Empaqueter un démon pour macOS
Vincent Bernat
Il existe trois façons de distribuer un démon pour macOS :
- Distribuer le code source accompagné d’instructions de compilation.
- Utiliser un système de paquets tiers comme Homebrew.
- Fournir un paquet pour l’installeur.
Homebrew#
Homebrew est un système de gestion de paquets plutôt populaire. Il fonctionne à l’image des ports BSD : téléchargement, compilation et installation tout en gérant les éventuelles dépendances automatiquement. La création d’un nouveau paquet est plutôt simple et il y a beaucoup d’exemples.
Cependant, il y a quelques limitations à cette approche :
- Il n’y a pas réellement construction d’un paquet, mais exécution d’une recette pour installer localement le logiciel.
- Il est nécessaire de disposer d’outils de développement, comme par exemple une installation complète de Xcode1.
- S’il est nécessaire d’effectuer certaines étapes en tant que
root, comme l’ajout d’un utilisateur dédié ou l’intégration dans
launchd
, celles-ci devront être expliquées à l’utilisateur qui devra les exécuter manuellement.
Si ces limitations vous paraissent importantes, la création d’un paquet pour l’installeur est une bonne alternative.
Création d’un paquet pour l’installeur#
OX X est livré avec un installeur graphique et un autre en ligne de commande. La version graphique s’exécute en ouvrant un paquet depuis le Finder. L’utilisateur est alors confronté à un assistant d’installation standard.
La documentation pour construire un tel paquet est souvent obsolète ou peu adaptée à une utilisation hors de Xcode. Je vais tenter de fournir ici des informations relativement précises s’appliquant dans le contexte suivant :
- Pas d’interface graphique.
- Construction basée sur Autoconf et Automake.
- Support de plusieurs architectures.
- Support des versions précédentes de macOS.
Création manuelle d’un paquet#
Par le passé, l’outil pour créer un paquet était PackageMaker. Celui-ci n’est plus disponible et est remplacé par le tandem pkgbuild/productbuild. Un article plutôt complet sur Stackoverflow détaille le fonctionnement de ces deux outils.
Un paquet se construit en deux étapes :
- Création des composants (component packages).
- Combinaison de ceux-ci en un produit (product archive).
Un composant contient un ensemble de fichiers et de scripts à
exécuter à différentes étapes de l’installation du logiciel. Il est
possible d’avoir plusieurs composants : par exemple, un composant pour
le démon et un composant pour le client. Ces composants sont
construits avec pkgbuild
.
Un produit est la combinaison de composants ainsi que d’un fichier qui décrit différentes facettes de l’installeur (composants facultatifs, licences, texte d’introduction, etc.).
Pour créer un composant, les fichiers adéquats doivent être installés dans un répertoire cible :
$ ./configure --prefix=/usr/local --sysconfdir=/private/etc $ make $ make install DESTDIR=$PWD/osx-pkg
Mise à jour (08.2022)
Depuis macOS 10.11, il n’est plus possible
d’installer dans /usr
. Il faut utiliser /usr/local
. Ce document a
été mis à jour dans ce sens.
Le contenu du répertoire osx-pkg
peut alors être transformé en un
composant avec pkgbuild
:
$ mkdir pkg1 $ pkgbuild --root osx-pkg \ > --identifier org.someid.daemon \ > --version 0.47 \ > --ownership recommended \ > pkg1/output.pkg pkgbuild: Inferring bundle components from contents of osx-pkg pkgbuild: Wrote package to output.pkg
Attention au choix de l’identifiant. Il doit permettre d’identifier à
la fois le logiciel dans son ensemble ainsi que le composant. Un
fichier XML décrivant l’installeur est ensuite nécessaire pour créer
le produit. Par convention, il est appelé distribution.xml
:
<?xml version="1.0" encoding="utf-8" standalone="no"?> <installer-gui-script minSpecVersion="1"> <title>Some daemon</title> <organization>org.someid</organization> <domains enable_localSystem="true"/> <options customize="never" require-scripts="true" rootVolumeOnly="true" /> <!-- Define documents displayed at various steps --> <welcome file="welcome.html" mime-type="text/html" /> <license file="license.html" mime-type="text/html" /> <conclusion file="conclusion.html" mime-type="text/html" /> <!-- List all component packages --> <pkg-ref id="org.someid.daemon" version="0" auth="root">output.pkg</pkg-ref> <!-- List them again here. They can now be organized as a hierarchy if you want. --> <choices-outline> <line choice="org.someid.daemon"/> </choices-outline> <!-- Define each choice above --> <choice id="org.someid.daemon" visible="false" title="some daemon" description="The daemon" start_selected="true"> <pkg-ref id="org.someid.daemon"/> </choice> </installer-gui-script>
Ce fichier est
documenté dans la bibliothèque du développeur Apple. Comme
nous n’avons qu’un seul composant, nous demandons à l’installeur de ne
pas proposer le choix des composants avec l’option
customize="never"
. L’attribut rootVolumeOnly="true"
indique quant
à lui que le logiciel ne peut être installé que pour tout le
système. Il est marqué comme déprécié dans la documentation mais
fonctionne toujours et est préféré à son remplaçant qui affiche des
une interface déroutante pour l’utilisateur.
Les documents HTML2 sont à placer dans un répertoire
resources
. Il est aussi possible de choisir une image de fond avec
la balise <background/>
.
La commande suivante se charge de construire le produit :
$ productbuild --distribution distribution.xml \ > --resources resources \ > --package-path pkg1 \ > --version 0.47 \ > ../final.pkg productbuild: Wrote product to ../final.pkg
Scripts#
Il est possible d’exécuter des scripts à l’installation du
paquet. Disons que nous voulons enregistrer le démon auprès de
launchd
. Pour cela, il faut installer dans /Library/LaunchDaemons
un fichier org.someid.plist
avec un contenu similaire à ceci :
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>org.someid</string> <key>ProgramArguments</key> <array> <string>/usr/local/sbin/mydaemon</string> <string>-d</string> </array> <key>RunAtLoad</key><true/> <key>KeepAlive</key><true/> </dict> </plist>
L’enregistrement se fait à l’aide du script scripts/postinstall
:
#!/bin/sh set -e /bin/launchctl load "/Library/LaunchDaemons/org.someid.plist"
Pour les mises à jour, il semble d’usage de se désinscrire de
launchd
, via le script scripts/preinstall
:
#!/bin/bash set -e if /bin/launchctl list "org.someid" &> /dev/null; then /bin/launchctl unload "/Library/LaunchDaemons/org.someid.plist" fi
Ces deux scripts doivent être exécutables et signalés à pkgbuild
via
l’option --scripts scripts
.
La commande dscl permet de manipuler les utilisateurs et les groupes du système.
Dépendances#
L’installeur de macOS n’effectue aucune gestion des dépendances. Il faut
donc s’assurer que le paquet ainsi distribué contient tout le
nécessaire et n’utilise que des bibliothèques du système de base. Par
exemple, lldpd
fait usage de libevent, une bibliothèque de
gestion des évènements, qui n’est pas fournie avec macOS. Quand elle
est absente, ./configure
utilisera une copie embarquée. Toutefois,
si vous avez installé libevent via Homebrew, lldpd
se trouvera
lié à cette installation locale de libevent et ne fonctionnera pas
sur un autre système.
La sortie de otool -L
permet de détecter les dépendances
superflues :
$ otool -L build/usr/local/sbin/lldpd build/usr/local/sbin/lldpd: /usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0) /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0) /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 744.19.0) /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 945.18.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)
Compilation pour des anciennes versions de macOS#
Le paquet ainsi construit ci-dessus ne fonctionnera que sur une version de macOS identique ou plus récente que celle utilisée lors de la compilation. Pour pouvoir fonctionner sur une version antérieure, il faut télécharger le SDK approprié3.
Il suffit ensuite d’indiquer au compilateur la version cible de macOS
ainsi que l’emplacement du SDK. Cela peut se faire via les variables
CFLAGS
et LDFLAGS
:
$ SDK=/Developer/SDKs/MacOSX10.6.sdk $ ./configure --prefix=/usr/local --sysconfdir=/private/etc \ > CFLAGS="-mmacosx-version-min=10.6 -isysroot $SDK" \ > LDFLAGS="-mmacosx-version-min=10.6 -isysroot $SDK"
Le drapeau -mmacosx-version-min
est utilisé par diverses macros dans
les fichiers d’entête pour indiquer que telle ou telle fonction est
disponible ou obsolète pour telle version de macOS. Il semble également
avoir une influence sur l’éditeur de liens.
L’option -isysroot
indique l’emplacement du SDK. Les fichiers
d’entête et les bibliothèques seront d’abord recherchés à cet endroit
Mise à jour (08.2022)
Depuis le SDK 10.9, il n’est plus
nécessaire de spécifier -isysroot
.
Binaires universels#
Mac OS X 10.6 est disponible à la fois pour les architectures IA-32 et x86-64. Pour permettre à un même paquet de fonctionner sur les deux architectures, il est possible de construire des binaires universels. Le format objet Mach, utilisé par macOS, permet de stocker dans un même fichier plusieurs versions de l’exécutable. Le système d’exploitation sélectionnera la version la plus appropriée.
Une façon simple de générer de tels fichiers est de passer les options
-arch i386 -arch x86_64
au compilateur :
$ ./configure --prefix=/usr/local --sysconfdir=/private/etc \ > CC="gcc -arch i386 -arch x86_64" \ > CPP="gcc -E"
Toutefois, c’est une approche plutôt déconseillée. Supposons que
./configure
détermine lors de son exécution la valeur de certains
paramètres dépendant de l’architecture (par exemple, avec la macro
AC_CHECK_SIZEOF
). La valeur correspondante sera celle de
l’architecture hôte. L’exécutable généré pour l’autre architecture
va alors utiliser une valeur erronée.
La meilleure façon de construire des binaires universels est d’effectuer deux compilations séparées et de construire le binaire avec lipo :
$ for arch in i386 x86_64; do > mkdir $arch ; cd $arch > ../configure --prefix=/usr/local --sysconfdir=/private/etc \ > CC="gcc -arch $ARCH" \ > CPP="gcc -E" > make > make install DESTDIR=$PWD/../target-$ARCH > cd .. > done […] $ lipo -create -output daemon i386/usr/local/sbin/daemon x86_64/usr/local/sbin/daemon
Étant donné que lipo
ne fonctionne que sur des fichiers, j’ai écrit
un script Python permettant
d’appliquer lipo
récursivement sur un ensemble de répertoires.
Mise à jour (08.2022)
De nos jours, vous devriez remplacer
i386
par arm64
. Apple recommande d’utiliser -target
pour
combiner à la fois l’architecture et le SDK cible. Toutefois, le
drapeau -arch
continue de fonctionner et il est possible de le
combiner avec -mmacosx-version-min
si besoin. À noter que cela
nécessite de compiler sur une plateforme capable d’exécuter les deux
architectures (comme un Apple M1).
Mise en place#
Pour automatiser le tout, il est possible de fournir un simple
script. Toutefois, pour une meilleure intégration dans le système de
construction, un fichier Makefile.am
semble plus approprié. Il
permettra d’exploiter des variables les variables issues de
./configure
, telles que @VERSION@
et de générer des versions
adaptées de distribution.xml
et du fichier de configuration de
launchd
. La première étape est d’ajouter dans le fichier
configure.ac
ces instructions :
AC_CONFIG_FILES([osx/Makefile osx/distribution.xml osx/im.bernat.lldpd.plist]) AC_CONFIG_FILES([osx/scripts/preinstall], [chmod +x osx/scripts/preinstall]) AC_CONFIG_FILES([osx/scripts/postinstall], [chmod +x osx/scripts/postinstall]) AC_SUBST([CONFIGURE_ARGS], [$ac_configure_args])
Regardons maintenant le fichier osx/Makefile.am
. Tout
d’abord, nous définissons quelques variables :
PKG_NAME=@PACKAGE@-@VERSION@.pkg PKG_TITLE=@PACKAGE@ @VERSION@ PKG_DIR=@PACKAGE@-@VERSION@ ARCHS=@host_cpu@
Si nous désirons construire un paquet pour plusieurs architectures, il
suffira d’utiliser la commande make ARCHS="x86_64 i386"
.
La cible install-data-local
permet d’installer les fichiers
spécifiques à macOS :
install-data-local: install -m 0755 -d $(DESTDIR)/Library/LaunchDaemons install -m 0644 im.bernat.@PACKAGE@.plist $(DESTDIR)/Library/LaunchDaemons uninstall-local: rm -f $(DESTDIR)/Library/LaunchDaemons/im.bernat.@PACKAGE@.plist
La cible principe est le produit, construit avec productbuild
:
../$(PKG_NAME): pkg.1/$(PKG_NAME) distribution.xml resources $(PRODUCTBUILD) \ --distribution distribution.xml \ --resources resources \ --package-path pkg.1 \ --version @VERSION@ \ $@
Sa principale dépendance est le composant, construit avec pkgbuild
:
pkg.1/$(PKG_NAME): $(PKG_DIR) scripts [ -d pkg.1 ] || mkdir pkg.1 $(PKGBUILD) \ --root $(PKG_DIR) \ --identifier im.bernat.@PACKAGE@.daemon \ --version @VERSION@ \ --ownership recommended \ --scripts scripts \ $@
Il nous faut maintenant construire $(PKG_DIR)
:
$(PKG_DIR): stamp-$(PKG_DIR) stamp-$(PKG_DIR): $(ARCHS:%=%/$(PKG_DIR)) $(srcdir)/lipo $(PKG_DIR) $^ touch $@
$(ARCHS:%=%/$(PKG_DIR))
est interprété comme x86_64/$(PKG_DIR)
i386/$(PKG_DIR)
. C’est une fonctionnalité
ajoutée récemment dans POSIX mais supportée
depuis longtemps par GNU make.
Avant l’application du script basé sur lipo
, nous devons construire
les répertoires contenant les versions spécifiques à chaque
architecture du composant :
pkg_curarch = $(@:stamp-%=%) $(ARCHS:%=%/$(PKG_DIR)): %/$(PKG_DIR): stamp-% $(ARCHS:%=stamp-%): stamp-%: im.bernat.lldpd.plist [ -d $(pkg_curarch) ] || mkdir -p $(pkg_curarch) (cd $(pkg_curarch) && \ $(abs_top_srcdir)/configure @CONFIGURE_ARGS@ \ CC="@CC@ -arch $(pkg_curarch)" \ CPP="@CPP@") (cd $(pkg_curarch) && \ $(MAKE) install DESTDIR=$(abs_builddir)/$(pkg_curarch)/$(PKG_DIR)) touch $@
C’est un peu intimidant, mais cela correspond exactement à ce qui a
été décrit auparavant. Notez l’utilisation de la variable
@CONFIGURE_ARGS
qui a été définie dans configure.ac
au début de
cette section.
Voici comment un utilisateur s’y prendrait pour créer un paquet pour macOS avec ce système de construction :
$ SDK=/Developer/SDKs/MacOSX10.6.sdk $ mkdir build && cd build $ ../configure --prefix=/usr/local --sysconfdir=/private/etc --with-embedded-libevent \ > CFLAGS="-mmacosx-version-min=10.6 -isysroot $SDK" \ > LDFLAGS="-mmacosx-version-min=10.6 -isysroot $SDK" […] $ make -C osx pkg ARCHS="i386 x86_64" […] productbuild: Wrote product to ../lldpd-0.7.5-21-g5b90c4f-dirty.pkg The package has been built in ../lldpd-0.7.5-21-g5b90c4f-dirty.pkg.
-
Bien que Xcode soit disponible gratuitement, il est nécessaire de s’enregistrer auprès d’Apple pour le télécharger. Cela nécessite de fournir un numéro de carte de crédit. Plutôt perturbant. ↩︎
-
Les textes peuvent être rédigés dans d’autres formats (tels que RTF) mais il me paraît beaucoup plus aisé de manipuler du HTML. ↩︎
-
Malheureusement, Apple rend l’accès aux SDKs précédents difficile. La façon la plus simple de récupérer le SDK pour Mac OS X 10.6 est de télécharger Xcode 4.3.3 et d’extraire de l’image le SDK approprié pour le placer dans
/Developer/SDKs
. ↩︎