Jeudi dernier, je me suis lancé un petit défi perso.
La veille, on avait décidé d’ajouter une fonctionnalité importante dans un projet : Pouvoir sauvegarder et reprendre la conversation d’un chatbot codé avec ellmer
.
Le challenge ? On voit le client à 10h pour un point de suivi.
Bon, pas de pression, ce n’est pas vital pour le rendez-vous, mais c’est toujours sympa de pouvoir dire « Oui, c’est déjà prêt ».
Je me chauffe, je branche Spotify, et je fonce dans le code.
J’avais déjà repéré une Merge Request toute fraîche dans le package qui faisait plus ou moins ce qu’il me fallait.
Je l’installe, je bricole, je teste… et ça marche.
Alors je commit, je pousse, la pipeline passe, et je file en visio, tout content de mon « exploit ».
Le call se passe bien, je pars faire mon sport, et je reviens l’après-midi, prêt à continuer.
Et là… message de Léo sur notre Mattermost :
COMMENT ÇA ÇA MARCHE PAS ?
Je vérifie chez moi, tout roule.
« Ça marche sur ma machine », que je lui dis.
« OK, mais sur la mienne, non. »
Je commence à m’énerver, je soulève mon bureau (╯°□°)╯︵ ┻━┻, et là je réalise :
On n’a plus la même version du package ellmer
.
On est exactement tombés dans le piège des gens qui n’utilisent pas renv
. Dans mon rush, j’avais oublié la mini étape de mettre à jour le renv.lock
.
Résultat : chez moi ça marche, mais chez Léo, rien ne passe.
Comment ça, la même chose vous est déjà arrivé ? Et vous n’utilisez pas renv
??
Ou alors, vous l’avez déjà installé mais vous ne savez pas trop à quoi il sert, ni comment l’utiliser au quotidien ?
Bon, OK, c’est justement le sujet de cet article. On va voir :
- Pourquoi
renv
est indispensable pour tout projet R un minimum sérieux (et pas seulement en équipe) - Comment il fonctionne concrètement
- Et comment résoudre tous les problèmes que vous pourriez avoir avec
C’est parti !
À quoi ça sert renv
?
Avoir les mêmes versions des packages sur tous vos environnements
C’est LA base.
renv
permet de garantir que tout le monde travaille avec exactement les mêmes versions de packages : vous, vos collègues, la machine de prod, la machine de test, etc.
Concrètement, ça veut dire quoi ?
- Plus de « ça marche sur ma machine » (mais pas sur celle du voisin)
- Sur l’onboarding d’un nouveau collègue, il a juste à cloner le dépôt de code, lancer
renv::restore()
, et c’est tout. Il a exactement le même environnement que tout le monde, sans prise de tête - Pour le déploiement en prod, même chose : le code tournera exactement de la même manière qu’en local
- Et surtout, pour de l’ancien code, qu’il soit vieux de 6 mois ou 6 ans, vous repartez d’exactement le même endroit qu’il y a 6 ans.
Bref : On élimine tous les bugs invisibles qui nous font perdre des heures et qui sont bêtement liés à des différences de versions de packages.
Utiliser des versions différentes selon vos projets
Sur une machine, tous les projets R partagent le même ensemble de packages par défaut.
C’est pratique… jusqu’au jour où c’est pas pratique. Vous mettez à jour un package pour un projet, et ça casse un autre projet à côté.
Chez nous à Data Champ’, on travaille sur des dizaines de projets en parallèle toute l’année. Ce serait impossible de retester toutes les applis dès qu’on met à jour un package.
Avec renv
, chaque projet a son propre environnement isolé :
- On peut bosser sur un vieux projet qui tourne avec R 3.6.3 et les packages d’époque, tout en profitant des dernières versions sur les autres projets.
- On évite les conflits entre projets. On n’a pas toujours le luxe de choisir de travailler avec la dernière version de R et des packages.
- Pour n’importe quel projet, on sait qu’on a juste à le cloner, à restaurer l’environnement, et on est sûr que tout fonctionnera comme prévu.
Ce n’est même pas une question d’être en équipe. C’est un outil de productivité qui facilite la vie en général pour tous les utilisateurs de R.
Et en plus, c’est très simple à utiliser !
Comment utiliser renv
?
Il y a trois verbes fondamentaux à connaître. C’est tout.
Initialiser renv
avec renv::init()
Quand un client m’envoie le code d’un projet, c’est la toute première chose que je fais.
Si le projet est un package, alors renv
vous propose d’utiliser le fichier DESCRIPTION
pour installer les packages.
> renv::init()
This project contains a DESCRIPTION file.
Which files should renv use for dependency discovery in this project?
1: Use only the DESCRIPTION file. (explicit mode)
2: Use all files in this project. (implicit mode)
Selection:
Nous on utilise rarement une structure de package pour nos applis Shiny, et en plus rien ne nous dit que le fichier DESCRIPTION
est bien à jour, donc j’ai plutôt envie de privilégier l’option 2 (mode implicite).
L’option 2 va scanner l’ensemble des fichiers du projets, et détecter automatiquement tous les packages utilisés :
- Quand vous utilisez
library(package)
- Ou bien l’écriture
package::function_name()
Les packages vont ensuite être téléchargés, et installés, automatiquement. Vous n’avez rien à faire de plus.
Un message de succès va s’afficher :
The version of R recorded in the lockfile will be updated:
- R [* -> 4.4.3]
- Lockfile written to "~/path/renv.lock".
- renv activated -- please restart the R session.
Deux informations sont importantes ici :
renv
a enregistré la version de R utilisée (la 4.4.3)renv
vous informe qu’il faut redémarrer la session R. C’est nécessaire après l’activation derenv
.
Lors de son initialisation, renv
a ajouté quelques fichiers dans votre environnement :
- Le fichier
renv.lock
est le plus important. Il contient la liste exhaustive de tous les packages nécessaires à votre code et les versions utilisées. - Une ligne dans le fichier
.Rprofile
(qu’il crée s’il n’existe pas) permettant d’activerrenv
automatiquement au démarrage de la session. - Le dossier
renv
contient des fichiers techniques et les packages installés. Il contient aussi un fichier.gitignore
qui exclut automatiquement ce qu’il faut.
Il n’est pas nécessaire de rajouter le dossier renv
dans le .gitignore
. Vous n’avez rien à faire de spécial à ce niveau.
Tout ce qui vous avez à faire est de lancer renv::init()
, c’est tout.
Sauvegarder les nouveaux packages avec renv::snapshot()
Au fil de votre développement, vous allez rajouter ou enlever des dépendances à certains packages. Ou bien vous pouvez aussi décider de mettre à jour certains packages.
Dans ce cas, pour que l’information du nouveau package soit bien enregistrée, il faut utiliser la commande renv::snapshot()
renv
va vous présenter les changements qui vont avoir lieu :
- Les packages ajoutés
- Les packages dont la version change
- Les packages enlevés
> renv::snapshot()
The following package(s) will be updated in the lockfile:
# CRAN -----------------------------------------------------------------------
- anytime [0.3.9 -> *]
- BH [1.81.0-1 -> *]
- bookdown [0.36 -> 0.43]
- brio [1.1.3 -> 1.1.5]
...
- xfun [0.41 -> 0.52]
- XML [3.99-0.15 -> 3.99-0.18]
- zip [2.3.0 -> 2.3.3]
- textshaping [* -> 1.0.1]
Do you want to proceed? [Y/n]:
Une fois que vous validez, le fichier renv.lock
est mis à jour. Il peut alors être ajouté dans un commit pour que les collègues le récupèrent.
C’est exactement cette étape que j’avais oublié dans mon histoire en début d’article !
Et du coup, comment ils font les collègues pour installer les packages ?
Restaurer l’environnement avec renv::restore()
Lorsque vous récupérez un projet qui utilise renv
, il va s’activer tout seul dès le lancement de la session :
# Bootstrapping renv 1.1.4 ---------------------------------------------------
- Downloading renv ... OK
- Installing renv ... OK
ℹ Using R 4.4.3 (lockfile was generated with R 4.1.2)
- Project '~/path' loaded. [renv 1.1.4]
- One or more packages recorded in the lockfile are not installed.
- Use `renv::status()` for more details.
R version 4.4.3 (2025-02-28) -- "Trophy Case"
Platform: x86_64-pc-linux-gnu (64-bit)
Plusieurs informations importantes sont affichées ici :
- Il a automatiquement installé
renv
en version 1.1.4. Très bien. - On m’informe que j’utilise R 4.4.3 alors que le lockfile (c’est-à-dire le fichier
renv.lock
) a été généré avec R 4.1.2. Là j’ai un problème, puisque je n’ai pas la bonne version de R. - On me dit aussi que certains packages enregistrés dans le lockfile ne sont pas installés. C’est normal, puisque je viens juste de lancer le projet.
- Finalement, on m’invite à lancer
renv::status()
pour en savoir plus.
Je relance le projet avec la bonne version de R cette fois-ci, puis je lance un renv::status()
:
> renv::status()
The following package(s) are in an inconsistent state:
package installed recorded used
anytime n y ?
AsioHeaders n y ?
askpass n y ?
backports n y ?
...
xtable n y ?
yaml n y ?
zip n y ?
zoo n y ?
See `?renv::status` for advice on resolving these issues.
Le renv::status()
m’affiche la liste des packages “problématiques” avec trois colonnes :
- Le package est-il installé ?
- Le package est-il sauvegardé dans le lockfile ?
- Le package est-il utilisé dans le projet ?
Dans mon cas ici, je restaure un projet. Donc tous les projets sont sauvegardés dans le lockfile et ne sont pas installés.
Pour les installer, rien de plus simple : renv::restore()
Mettre à jour tous les packages avec renv::update()
Dernière petite astuce. Si vous avez lu notre article « Quelle version de R faut-il utiliser en production ? », vous savez que c’est pas mal de régulièrement mettre à jour les packages. Disons une fois par an ou tous les deux ans.
Rien de plus simple : renv::update()
.
C’est tout.
La documentation
Cet article ne traite que d’un usage basique du package renv
(et honnêtement il n’y a pas grand chose de plus à savoir pour 99% du temps).
Si jamais vous avez besoin de creuser davantage, la documentation est par ici : Introduction to renv
Les problèmes et les solutions aux problèmes
renv
est vraiment un super outil.
Mais… il faut croire que gérer des arbres de dépendances dans tous les sens, ce n’est pas si évident.
Au-delà de la simplicité apparente des trois fonctions basiques pour un usage quotidien, vous risquez parfois de vous retrouver dans des situations un peu complexes.
C’est pourquoi j’ai rajouté cette section, avec tous les problèmes que j’ai moi-même rencontrés. J’entends enrichir cette section au fil des nouveaux problèmes qui apparaissent.
Et si vous-même avez un problème qui n’est pas traité ici, on se retrouve en bas dans la section Commentaires.
J’ai installé un package et renv
ne l’inclut pas dans le renv.lock
Vous avez fait :
install.packages("packagename")
- Puis :
renv::snapshot()
Et renv
ne vous propose pas de rajouter packagename
dans le lockfile.
C’est normal : renv
n’enregistre pas TOUS les packages que vous installez. Il enregistre seulement les packages qui sont utilisés dans l’application.
Donc si vous installez microbenchmark
pour vous aider à optimiser vos traitements de calcul, le package ne va pas être ajouté.
C’est une bonne chose : Inutile de surcharger les environnements de vos collègues ou en production avec des packages non utilisés.
Et si vous voulez VRAIMENT imposer à renv
d’ajouter le package ?
Alors je vous conseille de le faire de cette manière :
if (FALSE) library(microbenchmark) # Necessary to add in renv.lock
Le simple fait d’avoir écrit library(microbenchmark)
suffit à renv
pour inclure le package.
Mais : Le package n’est jamais chargé. Vous économisez ces précieuses millisecondes de chargement.
Et le commentaire est utile pour vos collègues qui ne comprendront peut-être pas pourquoi cette ligne existe et pourraient être tentés de la supprimer.
Les packages utilisés dans mes fichiers R Markdown ne sont pas détectés
Ce problème survient si le package yaml
n’est pas installé. D’ailleurs, vous avez peut-être reçu le warning suivant :
Warning message:
The 'yaml' package is required to parse dependencies within R Markdown files
Consider installing it with `install.packages("yaml")`.
Dans ce cas, installez le package yaml
.
Erreur : package ‘name’ is not available
En général, cette erreur survient lors d’un renv::init()
. Votre code utilise certains packages que renv
ne sait pas où aller chercher parce qu’ils ne sont pas sur le CRAN.
J’ai par exemple eu le cas avec le package polars
, qui est accessible depuis le R-multiverse :
The following package(s) were not installed successfully:
- [polars]: package 'polars' is not available
Dans ce cas, installez le package depuis la source où il est accessible, en général Github ou un autre repository que le CRAN.
Warning : renv took longer than expected to activate the sandbox
Un matin, vous ouvrez votre projet, comme d’habitude. Et bizarrement… La session R est très longue à démarrer.
Après un moment interminable, le message d’avertissement suivant s’affiche :
Warning message:
renv took longer than expected (37 seconds) to activate the sandbox.
The sandbox can be disabled by setting:
RENV_CONFIG_SANDBOX_ENABLED = FALSE
within an appropriate start-up .Renviron file.
Vous avez sans doute quelques questions…
C’est quoi la sandbox ?
L’objectif de la sandbox est d’éviter d’utiliser un package dans sa mauvaise version.
Vous savez peut-être que lorsque vous chargez un package, R ne regarde pas nécessairement dans un seul dossier. R peut aller voir dans une liste de dossiers, dans un certain ordre, et dès qu’il trouve le package désiré, il arrête sa recherche.
On peut le voir si vous tapez .libPaths()
dans votre console. Voici ce que j’obtiens sur mon système :
> .libPaths()
[1] "/home/charles/repos/myproject/renv/library/linux-ubuntu-noble/R-4.4/x86_64-pc-linux-gnu"
[2] "/home/charles/.cache/R/renv/sandbox/linux-ubuntu-noble/R-4.4/x86_64-pc-linux-gnu/17ee7825"
La première ligne correspond à mon projet (/home/charles/repos/myproject
), où renv
est activé et les packages sont stockés dans un sous-dossier.
Et on y trouve une deuxième ligne, c’est la sandbox. Ça c’est le comportement par défaut de renv
.
Supposons à présent que je désactive la sandbox renv
. Alors j’obtiens :
> .libPaths()
[1] "/home/charles/repos/myproject/renv/library/linux-ubuntu-noble/R-4.4/x86_64-pc-linux-gnu"
[2] "/opt/R/4.4.3/lib/R/library"
La première ligne est toujours la même, mais pas la deuxième. La deuxième ligne va directement chercher dans le dossier d’installation de R.
Et finalement, imaginez la situation suivante : Mon projet utilise le package dplyr
en version 1.2.3 mais :
- Je ne l’ai pas encore installé au niveau du projet avec
renv::restore()
- J’ai installé
dplyr
sur mon système, en dehors du projet, en version 0.9.8.
Lorsque R va faire sa recherche, il ne va pas trouver le package dans le premier dossier (puisque je n’ai pas lancé renv::restore()
), mais il va le trouver dans le deuxième. Et il va donc charger dplyr
en version 0.9.8.
Résultat ? Je vais avoir des surprises, sans comprendre ce qui se passe.
La solution est simple : renv::restore()
.
Mais renv
nous protège déjà contre ce problème grâce à la sandbox. La sandbox va remplacer le dossier “système” pour éviter de contenir des packages qui ne font pas partie de R base. Aucune chance que dplyr
se retrouve dans la sandbox. On va seulement y retrouver les packages de base qui viennent lorsqu’on installe R : base
, stats
, utils
, class
, etc.
Pourquoi ça arrive ?
Alors là, honnêtement, je ne sais pas.
Ça m’arrive parfois. Pas systématiquement. Des fois ça arrive, je redémarre la session R, et ça ne se répète pas. Je n’ai pas non plus passé des heures à décrypter les rouages internes de renv
pour aller comprendre, donc je reste ignorant.
Comment résoudre ce problème ?
Pour ma part, j’estime que la sandbox
crée plus de problèmes qu’elle n’en résout. Un oubli de renv::restore()
est de toute façon sanctionné au démarrage de la session par un message The project is out-of-sync
, qui devrait être suffisant.
Comme suggéré dans le message d’avertissement, j’ai rajouté RENV_CONFIG_SANDBOX_ENABLED = FALSE
dans le .Renviron
à l’échelle du système, et je n’ai plus ce problème.
Cette résolution est aussi particulièrement importante à rajouter dans vos Dockerfile
, puisque ça peut considérablement ralentir le démarrage d’un container.
Error in dyn.load() : unable to load shared object
Je crois que cette erreur est celle qui m’a rendu le plus fou ces dernières années. Oui parce que j’ai mis LONGTEMPS à comprendre d’où ça venait, pourquoi ça arrivait, et surtout comment résoudre ce problème.
Typiquement, là je suis en train de taper un renv::restore()
sur le serveur en production. Tout se passait bien en local, en développement, mais le serveur de prod est un peu différent. Voici ce que j’obtiens :
Error in dyn.load(file, DLLpath = DLLpath, ...) :
unable to load shared object '/home/charles/myproject/renv/staging/1/RcppArmadillo/libs/RcppArmadillo.so':
liblapack.so.3: cannot open shared object file: No such file or directory
Calls: loadNamespace -> library.dynam -> dyn.load
Execution halted
Error: error testing if 'RcppArmadillo' can be loaded [error code 1]
Ce qu’il faut savoir, c’est que renv
utilise par défaut le Posit Public Package Manager (P3M) depuis la version 1.0.0.
Le P3M peut être super pratique : En effet, le CRAN ne propose des versions binaires des packages uniquement pour Windows, et uniquement pour la dernière version de chaque package.
Ça veut dire que si vous utilisez Linux (pour un serveur de production par exemple), ou même Windows mais avec des versions qui ne sont pas les toutes dernières (ce qui va forcément arriver si vous utilisez renv
), alors vous devez recompiler les packages depuis la source.
Pas forcément un problème en soi… si vous avez du temps devant vous.
Ça devient vite un problème quand on manipule des images Docker, qu’on ré-installe les packages systématiquement dans une pipeline de CI/CD, et c’est juste pénible pour n’importe qui en fait.
Et le P3M propose juste des binaires pour tout le monde : Quelque soit la version d’un package, et quelque soit la distribution de Linux utilisée.
MAIS…
Ce n’est pas si simple.
Vous voyez le message d’erreur cryptique que j’ai obtenu plus tôt ?
Il vient précisément de ce comportement : J’ai téléchargé le binaire depuis le P3M, mais… impossible de charger le package, parce qu’il me manque une dépendance importante, nommée liblapack.so.3
.
Si j’avais essayé d’installer le package de manière classique, c’est-à-dire en compilant la source, j’aurais eu un message d’erreur explicite, du type :
--------------------------------------------------------------------------------
Configuration failed because libudunits2.so was not found. Try installing:
* deb: libudunits2-dev (Debian, Ubuntu, ...)
* rpm: udunits2-devel (Fedora, EPEL, ...)
* brew: udunits (OSX)
If udunits2 is already installed in a non-standard location, use:
--configure-args='--with-udunits2-lib=/usr/local/lib'
if the library was not found, and/or:
--configure-args='--with-udunits2-include=/usr/include/udunits2'
if the header was not found, replacing paths with appropriate values.
You can alternatively set UDUNITS2_INCLUDE and UDUNITS2_LIBS manually.
--------------------------------------------------------------------------------
Ici on voit clairement la dépendance système que je dois installer.
Mais sur le message d’erreur plus haut, on ne comprend pas : L’installation s’est en fait bien déroulée, le fichier binaire a été téléchargé et placé dans le bon dossier. C’est le chargement du dossier qui coince, et R qui se plaint de ne pas trouver une dépendance.
Vous devez donc deviner la dépendance système qu’il vous manque.
La plupart du temps, voici comment faire :
- Allez sur le Posit Public Package Manager
- Cliquez sur SETUP
- Choisissez votre OS, et la distribution Linux si vous utilisez Linux.
- Dans le champ de recherche en haut, tapez le nom du package.
- Scrollez en bas de la page jusqu’à Install system prerequisites
Voici un exemple pour le package sf
, un package populaire pour la manipulation d’objets spatiaux et qui nécessite beaucoup de dépendances :
Il ne reste plus qu’à installer la ou les dépendances nécessaires, et vous êtes bon.
…
Ou presque !
Que se passe-t-il si vous utilisez un système qui n’est pas supporté par le P3M ?
Au hasard, Debian 13 est sorti il y a plusieurs semaines, mais il n’est toujours pas sélectionnable dans le P3M. Pour nous (et pour nos clients), c’est un point bloquant pour faire la montée de version.
D’autres situations improbables se cachent encore.
Retournons sur notre message d’erreur :
Error in dyn.load(file, DLLpath = DLLpath, ...) :
unable to load shared object '/home/charles/myproject/renv/staging/1/RcppArmadillo/libs/RcppArmadillo.so':
/lib64/libm.so.6: version `GLIBC_2.29' not found (required by /home/charles/myproject/renv/staging/1/RcppArmadillo/libs/RcppArmadillo.so)
Calls: loadNamespace -> library.dynam -> dyn.load
Execution halted
Error: error testing if 'RcppArmadillo' can be loaded [error code 1]
Le message m’informe que glibc
est manquant. Je vais me renseigner et je trouve la dépendance à installer :
[charles@RH8 myproject]$ sudo yum install glibc
Failed to set locale, defaulting to C.UTF-8
Updating Subscription Management repositories.
Red Hat CodeReady Linux Builder for RHEL 8 x86_64 (RPMs) 65 kB/s | 4.5 kB 00:00
Package glibc-2.28-251.el8_10.25.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
Aie.
La dépendance est DÉJÀ installée, mais RHEL 8 (la distribution de Linux que j’utilise pour ce projet) me sert la version 2.28 par défaut, alors que la version distribuée par le P3M a été compilée avec la version 2.29.
Dans ce cas, il n’y a que deux solutions :
- Soit vous tordez le bras à RHEL pour installer la version 2.29. Sauf que… d’autres packages pourraient nécessiter encore une autre version. Quel enfer !
- Soit vous compilez le package depuis sa source.
La vraie solution, c’est la deuxième.
Comment faire ?
Il faut installer le package depuis un miroir du CRAN, par exemple https://cloud.r-project.org
.
Vérifiez la version enregistrée dans le renv.lock
, puis utilisez la commande suivante :
renv::install("[email protected]", repos = "https://cloud.r-project.org")
N’utilisez pas install.packages()
puisque cette fonction va installer la toute dernière version.
Et n’utilisez pas non plus renv::restore(repos = "https://cloud.r-project.org")
, puisque renv
va ignorer votre argument et utiliser le P3M.
Pour toutes ces galères, je trouve que la décision forcer l’utilisation de P3M par l’équipe de développement de renv
est vraiment une mauvaise idée.
L’objectif initial de renv
n’est pas d’accélérer l’installation des packages, même si ça pourrait être un effet secondaire sympa. L’objectif est de garantir la reproductibilité, et là on est perdant sur cette décision.
Certes, dans 95% des cas ça se passe bien, mais ces 5% restants font perdre énormément de temps aux équipes et créent une résistance forte à l’utilisation du package.
Il est heureusement toujours possible de désactiver ce comportement par défaut, en activant l’option suivante :
options(renv.config.ppm.default = FALSE) # New projects initialized with renv::init() will not use P3M
options(renv.config.ppm.enabled = FALSE) # Use source-only URLs
Si vous utilisez des environnements trop différents entre le développement et la production (Windows vs Linux, ou différentes distributions Linux), je vous conseille de désactiver le P3M avec ces deux options.
Commentaires