Skip to content

Les macros en Common Lisp: fonctions anonymes raccourcies

Cet article a pour but de présenter les macros de Common Lisp au travers d'un exemple concret, qui permettra peut-être à certaines personnes de mieux comprendre quand les utiliser, ainsi que leur intérêt.

Si vous avez déjà touché à clojure ou à arc, vous aurez sûrement apprécié leurs raccourcis pour écrire des fonctions anonymes. Par exemple en Clojure, les deux s-exps suivantes sont équivalentes:

(map (fn [x] (println "Hello, ")) '("world!" "you!"))
(map #(println "Hello, " %) '("world!" "you!"))

Avec arc, c'est le même principe (ça sera [prn "Hello, " _]), sauf qu'on a pas la possiblité d'utiliser ce mécanisme avec plusieurs arguments, tandis qu'avec clojure il est possible d'utiliser %1, %2 pour indiquer le premier argument, le second argument, …

Comme cette fonctionnalitée n'est pas présente dans Common Lisp, nous allons donc l'ajouter grâce aux macros.

Titre

La première chose à faire avant d'écrire une macro est d'écrire un ou deux exemples d'utilisation de cette macro, afin de clarifier nos idées. Comme la syntaxe '#(...)' est déjà utilisée pour les vecteurs, nous allons utiliser des crochets pour délimiter les fonction anonymes raccourcies, comme dans arc, mais nous allons garder le symbole pourcent suivit d'un nombre de clojure pour identifier les paramètres. Ainsi, l'exemple précédent s'écrira comme suit en Common Lisp grâce à notre macro:

(mapcar [format t "Hello, ~a~%" %] '("world!" "you!"))

Et sera équivalent à:

(mapcar (lambda (x) (format t "Hello, ~a~%" x)) '("world!" "you!"))

Mais premièrement, on va laisser les crochets de côté et écrire une macros qui sera équivalente. On verra à la fin de cet article comment faire pour manipuler le code avant le reader et donc pouvoir utiliser les crochets. Contentons nous pour le moment de manipuler le code après le reader et avant l'évaluation.

Première version simplifiée

Dans le développement de macros, il est préférable de coder la macros en plusieurs étapes. Nous allons donc écrire une première version de fn (notre macro), qui ne prendra pas en compte le nombre et l'ordre de ses arguments (le premier qui apparaîtra dans le code sera le premier argument, peu importe son numéro, et (fn (1+ %3)) ne prendra qu'un argument plutôt que trois).

Pour récupérer tous les symboles commençant par un % au sein de notre macro, nous auront besoin des fonctions flatten (voir On Lisp, p. 49) et percent-symbol-p. La première, très utile pour les macros, « aplatit » une liste, par exemple:

> (flatten '(1 (2 (3 4) 5) 6))
(1 2 3 4 5 6)

La seconde permet simplement de savoir si un symbole commence par un %:

> (percent-symbol-p '%foo)
T
> (percent-symbol-p 'foo)
NIL

Voici leur définitions:

(defun flatten (x)
  (labels ((rec (x acc)
             (cond ((null x) acc)
                   ((atom x) (cons x acc))
                   (t (rec
                       (car x)
                       (rec (cdr x) acc))))))
    (rec x nil)))

(defun percent-symbol-p (sym)
  (string= (subseq (symbol-name sym) 0 1)
           "%"))

On peut alors définir fn assez simplement: on récupère tous les symboles commençant par %, et on encapsule le code dans un lambda qui prend les symboles récupérés comme arguments:

(defmacro fn (code)
  (let ((args
         (remove-duplicates
          (remove-if-not #'percent-symbol-p
                         (flatten code)))))
    `(lambda ,args
       ,code)))

Il est donc déjà possible d'utiliser cette macro:

> (mapcar (fn (1+ %)) '(1 2 3))
(2 3 4)

Une version plus correcte

Maintenant que l'on a une macro de base qui fonctionne, on peut commencer à régler ses défauts petit à petit. La façon la plus simple pour gérer l'ordre des symboles et prendre en argument des symboles qui n'apparaissent pas dans le code et de récupérer l'argument maximal et de générer la liste de tous les arguments inférieurs.

On va donc avoir besoin de quelques fonctions supplémentaires:

  • mkstr (On Lisp, p. 58) qui transforme tous ses arguments en string
  • symb (On Lisp, p. 58) qui convertit la chaîne de caracère formé par ses arguments en symbol
  • percent-value qui permet de connaître la valeur d'un symbole commençant par % (par exemple, % et %1 valent 1 et %3 vaut 3)
  • gen-percents-syms qui génère tous les symboles pourcents entre 1 et la valeur maximale
(defun mkstr (&rest args)
  (with-output-to-string (s)
    (dolist (a args) (princ a s))))

(defun symb (&rest args)
  (values (intern (apply #'mkstr args))))

(defun percent-value (sym)
  (if (symbolp sym)
    (let ((sym-name (symbol-name sym)))
      (handler-case
          (if (string= (subseq sym-name 0 1) "%")
              (if (= (length sym-name) 1)
                  1
                  (parse-integer (subseq sym-name 1)))
              0)
        (parse-error () 0)))
    0))

(defun gen-percents-syms (max)
  (loop for i from 1 to max
       collect (symb "%" i)))

La nouvelle macro se définit alors:

(defmacro fn (code)
  (let ((arg-max (loop for sym in (flatten code)
                      maximize (percent-value sym))))
    `(lambda ,(gen-percents-syms arg-max)
         ,code)))

Finalisation

Notre macro est presque complète, il ne nous reste plus que deux cas à gérer: - clojure permet aussi d'utiliser %& pour définir des arguments de type &rest - il faut que % soit équivalent à %1

Cela se fait assez facilement:

(defun replace-symbol (symbol1 symbol2 list)
  (typecase list
    (list (mapcar (lambda (x) (replace-symbol symbol1 symbol2 x))
                  list))
    (t (if (eq list symbol1)
           symbol2
           list))))

(defun gen-percents-and-rest (max restp)
  (if restp
    (append (gen-percents-syms max) '(&rest %&))
    (gen-percents-syms max)))

(defmacro fn (code)
  (let ((arg-max (loop for sym in (flatten code)
                      maximize (percent-value sym)))
        (restp (find '%& (flatten code))))
    `(lambda ,(gen-percents-and-rest arg-max restp)
       ,(replace-symbol '% '%1 code))))

Ajout des crochets

Nous allons maintenant avoir recours aux read-macros pour modifier le code avant que le reader ne le lise. Tout ce que notre read-macro a à faire ici, c'est de transformer une expression du type [...] en (fn (...)). On définit donc une fonction qui sera appelée chaque fois qu'un crochet ouvrant est rencontré dans le code, qui se charge de lire le code jusqu'au crochet fermant et de retourner le code lu. Le premier argument d'une telle fonction est le flux depuis lequel lire le code, et le second correspond au caractère qui a déclenché son appel. Comme nous ne l'utilisons que pour un [, on peut ignorer ce second argument.

(defun bracket-reader (stream c)
  (declare (ignore c))
  (let (chars)
    (do ((char (read-char stream) (read-char stream)))
        ((char= char #\]))
      (push char chars))
    (read-from-string
     (format nil "(fn (~a))" (coerce (nreverse chars) 'string)))))

Et on définit finalement [ comme macro-character:

(set-macro-character #\[ #'bracket-reader)

Conclusion

Voilà, au travers de cet exemple j'espère avoir permis à certaines personnes de mieux comprendre l'intérêt des macros et en quoi elles sont si pratique. Le code final pour cette macro est disponible ici

Notez que les read-macros ne sont pas tellement utilisées en pratiques (on aurait très bien pu se limiter à la forme (fn (…)), qui n'est pas tellement plus longue à taper). Néanmoins elles peuvent être assez utiles, par exemple dans CLSQL, où elles permettent de distinguer le code Lisp et les requêtes SQL.

Si vous souhaitez lire plus à propos des macros, je vous recommande la lecture de On Lisp de Paul Graham et de Let Over Lambda de Doug Hoyte.

Mieux gérer ses dépôts mercurial

Vous connaissez sûrement github et bitbucket, c'est très pratique pour hoster des projets qui sont gérés avec git ou mercurial. Mais peut-être que vous préférer hoster ça chez vous, parce que c'est plus marrant, plus souple, tout ça.

Pour ce qui est des projets qui sont gérés avec git, on peut utiliser gitosis (voir ce billet pour plus d'informations sur son installation et son utilisation) avec un cgit/gitweb, ou carrément récupérer le code source de gitorious (ici) et se référer au README.

Le billet n'a pas pour but de parler de ces méthodes, vu qu'il existe sufissament d'informations à propos de celles-ci. Nous allons plutôt voir trois méthodes pour faire de même avec Mercurial.

hg-git

La première, assez évidente, consiste à convertir le dépôt mercurial en un dépôt git, et simplement utiliser n'importe quelle méthode citée ci dessus pour gérer le dépôt git.

Pour cela, il faut utiliser hg-git qui vous permettra de garder votre dépôt mercurial, et de pusher/puller comme si c'était git. Les instructions sur le site de hg-git sont assez claires je pense et je ne vais pas les répéter ici.

hg-git nous permet aussi de convertir nos anciens dépôts git en dépôts mercurial si on le souhaite (il suffit de cloner le git), ce qui peut être assez pratique si souhaitez vous rattraper après avoir utiliser git pendant tout un temps.

hgwebdir et l'authentification http

Ici on va plutôt avoir recours à notre serveur web pour gérer l'authentification, et ça va nous permettre d'autoriser le push via le port 80, ce qui peut être pratique quand les autres ports sont bloqués.

La configuration se fait donc au niveau de votre serveur web, avec par exemple mod_auth pour lighttpd et auth_basic pour nginx.

Ainsi, la conf lighttpd ressemblera à ceci:

$HTTP["host"] == "hg.foo.bar" {
    server.document-root = "/usr/local/www/foo/hg"
    server.name = "%3%0"

    cgi.assign = ( ".cgi" => "/usr/local/bin/python" )
    url.rewrite-once = ("^(/hgwebdir.cgi/.*)$" => "$1", "^(/.*)$" => "/hgwebdir.cgi$1" )

    auth.backend = "htpasswd"
    auth.backend.htpasswd.userfile = "/usr/local/etc/hg.passwd"

    $HTTP["querystring"] =~ "cmd=unbundle" {
        auth.require = (   "" => (
            "method"  => "basic",
            "realm"   => "mercurial",
            "require" => "valid-user"
            )   
        )   
    }   
}

Pour modifier les options des repos, vous pouvez tout faire dans le fichier .hg/hgrc du repo. C'est par exemple là que vous pouvez indiquer qui aura le droit de push dans ce repo:

[web]
contact = foo
description = foo's repository
allow_push = foo, bar

Il vous reste alors ajouter vos repos dans la conf de hgwebdir.

hg-ssh

Si par contre on peut se passer du push via le port 80, il est possible d'imiter le comportement de gitosis avec hg-ssh, un script fourni avec mercurial (dans /usr/local/share/mercurial/contrib/hg-ssh sous FreeBSD, disponible ici si vous ne l'avez pas).

Ce script, combiné avec une bonne configuration du ~/.ssh/authorized_keys, vous permet de définir quel utilisateur peut pusher où, avec une authentification basée sur les clés ssh. C'est donc exactement le même principe que gitosis.

Il faut d'abord créer un utilisateur hg, qui ne va servir qu'à gérer nos repo avec hg-ssh. Il faut aussi que le script hg-ssh soit dans le path, si ce n'est pas le cas, placez le par exemple dans /home/hg/bin/ et ajoutez ce répertoire dans le path de l'utilisateur hg.

Pour permettre à un utilisateur de pusher sur un ou plusieurs repos on peut donc ajouter la ligne suivante dans le ~/.ssh/authorized_keys de l'user hg:

command="hg-ssh repo1 repo2",no-port-forwarding,no-X11-forwarding,no-agent-forwarding <la clé ssh de l'utilisateur>

Et comme on est des grosses feignasses et qu'on veut pas avoir besoin de gérer ce fichier à la main, rien de tel qu'un petit script qui fait le tout à notre place.

Par exemple, pour ajouter l'utilisateur foo et le repo bar, en écriture par foo, il suffit de faire:

% hg-ssh.sh add foo bar
% hg-ssh.sh add_key foo `cat foo.pub`
% hg-ssh.sh dump > ~/.ssh/authorized_keys

Vous pouvez aussi supprimer des utilisateurs, modifier les droits des utilisateurs sur les repos etc. Je pense que l'aide est assez complète pour cela:

% hg-ssh help
hg-ssh.sh command args
commands:
  list                  lists all the users and their repos
  add user [repos]      give write-access to the repos for user, create the 
                        user or the repos if needed
  dump                  dump the new authorized_keys file
  add_key user ssh-key  add the key to the user's keys
  del_user user         delete an user
  del_repos repos       remove repositories from hard drive and configuration
  del_write user repo   remove the write access of one user to a repo
  help:                 print this help screen

Une fois le tout bien configuré, vous pourrez pusher via ssh, même en n'ayant pas de compte sur le serveur:

% hg push ssh://hg@hg.foo.bar/repo

Pour rendre les repos visibles de l'extérieur, vous devrez utiliser hgwebdir, comme vu plus haut, et la conf au niveau des infos sur les repos se fera aussi dans le .hg/hgrc des repos dans le home de l'utilisateur hg.

Voilà donc trois façons de mieux gérer vos repos avec mercurial. Je n'ai pas vraiment été dans les détails pour ce billet, donc si vous avez des questions n'hésitez pas.

Découverte de Tinyscheme

Tinyscheme est une implémentation de Scheme qui implémente la plupart des fonctionnalitées du R5RS facilement embarquable dans des programmes en C, au même titre que Guile qui, lui, est beaucoup plus complet. Néanmoins on ne souhaite pas spécialement avoir toutes les fonctionnalitées que propose Guile et on peut souvent se limiter à Tinyscheme, qui est plus simple à utiliser.

Un autre avantage de Tinyscheme par rapport à Guile est qu'on peut simplement inclure Tinyscheme avec son programme plutôt que de l'ajouter en dépendance et devoir l'installer soi-même sur son système d'exploitation (il n'est notamment pas dans les ports FreeBSD)

Installation

Vous pouvez installer Tinyscheme via votre gestionnaire de paquet ou bien plus simplement le placer dans un sous dossier de votre projet par exemple. Vous pourrez alors linker le tout facilement grâce au paramètre -L de gcc:

gcc foo.c -o foo -Ltinyscheme-1.39/

Parcourez aussi les fichiers scheme.h et scheme-private.h (que vous incluerez bien évidemment dans chaque fichier utilisant tinyscheme), ils vous en apprendront bien plus sur tinyscheme que ce que billet vous apprendra.

Initialisation

Avant toute chose, il faut connaître les deux types utilisés par Tinyscheme:

  • un pointer représente un « objet » Scheme, c'est-à-dire n'importe quelle valeur manipulable en Scheme ;
  • un scheme représente l'environnement Scheme dans son état actuel.

L'initialisation de Tinyscheme se fait via la fonction scheme_init_new, qui retourne un nouveau scheme * que l'on passera par la suite en premier argument à toute fonction de Tinyscheme. Pour finaliser cette initialisation, il faut aussi charger le fichier init.scm qui définit une grosse partie du langage (en effet, il est beaucoup plus simple d'implémenter Scheme en Scheme qu'en C). Pour charger ce fichier on passe par scheme_load_file. Il est aussi utile de définir stdout comme port de sortie en passant par la fonction scheme_set_output_port.

On peut donc définir une fonction initialize_scheme qui fera tout ça pour nous:

scheme *
initialize_scheme()
{
        FILE *init;
        scheme *sc;

        sc = scheme_init_new();
        if ((init = fopen("init.scm", "r")) == NULL) {
                perror("fopen");
                return NULL;
        }
        scheme_load_file(sc, init);
        fclose(init);

        scheme_set_output_port_file(sc, stdout);
        return sc;
}

Il faudra aussi appeler scheme_deinit à la fin de notre programme pour arrêter proprement Tinyscheme.

Évaluer du code scheme

Pour charger un fichier Scheme il suffit d'utiliser la fonction scheme_load_file de la même façon qu'on l'a utilisée pour charger init.scm Pour directement évaluer du code Scheme depuis le C on utilisera scheme_load_string:

scheme_load_string(sc, "(display \"Hello, world!\") (newline)");

Manipuler des données

Il est très simple de créer des objets Scheme depuis le code C. Pour cela, regarder les fonctions du type mk_* dans scheme.h. On peut aussi définir des variables via scheme_define, une variable étant caractérisée par:

  • sa portée
  • son nom (un symbole)
  • sa valeur

Ainsi, pour faire (define foo 42):

pointer value = mk_integer(sc, 42);
scheme_define(sc, sc->global_env, mk_symbol(sc, "foo"), value);

Définir des fonctions

Une fonction se définit comme une variable en Scheme, par exemple:

(define 1+
  (lambda (x)
    (+ 1 x)))

On utilisera donc toujours scheme_define pour définir une fonction et il suffit d'utiliser mk_foreign_func pour récupérer un pointer à partir d'une fonction définie en C. Cette fonction doit retourner un pointer et prendre en argument un scheme * représentant l'état actuel de l'interpréteur ainsi qu'un pointer qui contient les arguments passés à cette fonction.

Grâce à cela, on peut donc ajouter de nouvelles fonctions qu'il serait impossibles de définir en Scheme, par exemple getenv (redéfinir 1+ en C n'aurait aucun intérêt vu la simplicité de cette définition en Scheme) :

pointer
scm_getenv(scheme *sc, pointer args)
{
        char *value;
        if (args == sc->NIL 
              || !is_string(pair_car(args))
              || (value = getenv(string_value(pair_car(args)))) == NULL)
                return sc->F;
        return mk_string(sc, value);
}

La valeur sc->NIL est assez explicite, et sc->F représente le booléen #f (sc->T représente quant à lui #t). Il faut bien évidemment informer Tinyscheme de l'existence de cette fonction:

scheme_define(sc, sc->global_env,
              mk_symbol(sc, "getenv"), mk_foreign_func(sc, scm_getenv))

Conclusion

Ce qui a été présenté ici couvre − je pense − la plupart des fonctionnalitées de Tinyscheme utilisées par des programmes tels que Tiny-Fu, le système de script-fu de GIMP.

Il est donc très facile d'embarquer Tinyscheme dans vos applications, et il est aussi simple d'écrire des extensions pour Tinyscheme (il suffira de définir un ensemble de fonctions et de compiler le tout en -shared). Pour des exemples concrets d'extensions, regardez TSX ainsi que l'extension regex.

La documentation de Tinyscheme étant inexistante (excepté les fichiers Manual.txt et hack.txt inclus avec Tinyscheme) si vous souhaitez plus d'exemples, il est essentiel de se plonger dans les sources de Tiny-Fu, de TSX ainsi que d'autres logiciels utilisant Tinyscheme si nécessaire.

FreeBSD ports on steroids -- part 1

Voici le premier billet d'une série sur les ports FreeBSD. Au sommaire aujourd'hui: l'amélioration des performances des ports avec fastest_sites, ccache et cpuflags.

fastest_sites: améliorer le téléchargement des fichiers

fastest_sites est un petit script python qui trie les MASTER_SITEs en fonction de leur temps de réponse, ce qui permet de redéfinir l'ordre d'utilisation des mirroirs pour utiliser les plus rapides en priorité. Comme il doit se connecter à chaque mirroir, son éxécution prend un certain temps, n'hésitez pas à continuer la lecture de cet article pendant ce temps.

cd /usr/ports/ports-mgmt/fastest_sites
make install clean
fastest_sites > /usr/local/etc/ports_sites.conf
echo '.include "/usr/locale/etc/ports_sites.conf"' >> /etc/make.conf

ccache: améliorer la compilation des ports

ccache, comme son nom l'indique, garde en cache les compilations de programmes C ou C++, et permet donc de ne pas recompiler quelque chose qui a déjà été compilé auparavant ce qui améliore grandement le temps de compilation.

Il est disponible dans le port devel/ccache. Une fois installé, on lit le fichier /usr/local/share/doc/ccache/ccache-howto-freebsd.txt comme indiqué. Dans mon cas, il faut donc ajouter ceci à /etc/make.conf:

.if (!empty(.CURDIR:M/usr/src*) || !empty(.CURDIR:M/usr/obj*)) && !defined(NOCCACHE)
  CC=/usr/local/libexec/ccache/world-cc
  CXX=/usr/local/libexec/ccache/world-c++
.endif

ainsi que ceci dans le /etc/profile (attention, j'utilise mksh comme shell root mais le shell par défaut pour est un csh, à adapter donc):

export PATH=/usr/local/libexec/ccache:$PATH
export CCACHE_PATH=/usr/bin:/usr/local/bin

On peut aussi adapter CCACHE_DIR qui contient le dossier où ccache stockera ses données (/root/.ccache/ par défaut), et CCACHE_LOGFILE qui contient le chemin vers le fichier de log. On peut finalement adapter la taille maximale du cache via l'option -M:

# ccache -M 5G
Set cache size limit to 5242880k

cpuflags: améliorer l'éxécution des ports

cpuflags est un script shell qui vous donne les CFLAGS adaptés à votre processeur. Cepandant il n'est pas encore disponible dans les ports FreeBSD, bien qu'il soit compatible avec FreeBSD. Néanmoins, il est disponible dans pkgsrc, voici donc une démarche plus ou moins propre pour l'installer via les ports.

Tout d'abord, on récupère les fichiers le concernant depuis le cvs de pkgsrc:

export CVSROOT=anoncvs@anoncvs.de.netbsd.org:/cvsroot
export CVS_RSH=ssh
cvs checkout -PA pkgsrc/devel/cpuflags

Il faut ensuite convertir ce pkgsrc en un port, pour pouvoir l'installer via les ports:

cd pkgsrc/devel/cpuflags
mv PLIST pkg-plist
mv DESCR pkg-descr
wget http://awesom.eu/~acieroid/files/ports/cpuflags/Makefile.diff
patch -p1 Makefile < Makefile.diff
rm Makefile.diff
make install clean

Il suffit ensuite d'ajouter ce que nous sort cpuflags aux CFLAGS, dans le make.conf:

echo "CFLAGS+=`cpuflags`" >> /etc/make.conf

Bépo et DragonFly BSD

La disposition de clavier bépo est disponible pour FreeBSD, mais elle n'est pas compatible avec DragonFly BSD, bien que cette dernière soit dérivée de FreeBSD. C'est en fait dû à kbdcontrol(1) qui n'est pas compatible à 100% entre FreeBSD et DragonFly. Les développeurs de FreeBSD ont continués à ajouter des nouvelles fonctionnalitées après le fork de DragonFly, qui n'ont pas été reprises chez DragonFly.

Néanmoins il reste possible de « convertir » le pilote bépo de FreeBSD dans quelque chose de compatible avec le kbdcontrol de DragonFly BSD. La seule incompatibilitée utilisée dans le driver bépo est l'utilisation de l'action paste, qui a été ajoutée dans FreeBSD 5.0. Un petit coup de sed pour réparer ça, et on peut installer et charger le driver:

wget http://download.tuxfamily.org/dvorak/devel/fr-dvorak-bepo-kbdmap-1.0rc2.tgz
tar xzvf fr-dvorak-bepo-kbdmap-1.0rc2.tgz
cd fr-dvorak-bepo-bkdmap-1.0rc2
sed -i -e 's/paste/nop/g' fr-dvorak-bepo.kbd
cp fr-dvorak-bepo.kbd /usr/share/syscons/keymaps/

On peut maintenant switcher au bépo:

kbdcontrol -l fr-dvorak-bepo.kbd

On modifie ensuite /etc/rc.conf pour avoir le bépo au démarrage:

keymap="fr-dvorak-bepo"

Par contre, même remarque que sur le wiki bépo, la console de FreeBSD ne supporte pas l'UTF-8, c'est donc aussi le cas de DragonFly BSD.

Vole libellule, vole !

J’ai enfin pu m’installer un BSD utilisable sur mon desktop ! Ça faisait longtemps que je le voulais, mais le problème venait de la carte wifi, sans drivers sous BSD (elle passait sous FreeBSD avec ndisgen, mais je préfère éviter ça). J’ai donc fait l’acquisition d’une nouvelle carte wifi censée être supportée par rum(4), et à part quelques problèmes, désormais résolus, ça fonctionne à merveille.

J’ai d’abord regardé du côté de NetBSD, qui fonctionnait plutôt bien excepté pour le wifi. J’obtenais par moment 50% voire 75% de paquets perdus lors d’un ping(8). Je me suis alors tourné vers DragonFly BSD, ceux qui ajouté le driver bwi(4) qui a ensuite été repris par OpenBSD (en 4.3), puis par FreeBSD (en 8.0) et il est toujours en -current chez NetBSD et devrait être inclut dans NetBSD 6.0.

USB

Après l’installation, l’USB n’était pas détecté par l’OS à cause du support Plug And Play du BIOS, il suffit donc le désactiver cette option ainsi que le support USB (sinon on se retrouve avec énormément d’interruptions, ce qui rends le système peu utilisable).

On peut se rendre compte de la non-détection des périphériques USB via usbdevs(8):

# avec le support PnP et USB activés dans le bios:
$ usbdevs -v
usbdevs: no USB controllers found

Par contre, en désactivant le support de l’USB dans le BIOS, on perd la possibilitée d’utiliser un clavier USB avec GRUB et avec le bootloader de DragonFly BSD.

Wifi

J’ai donc acheté une carte wifi USB pour pouvoir me passer de mes cartes Broadcom (BCM3418 sur le desktop et BCM4312 sur le netbook), même si la BCM4318 est supportée par bwi(4) (le driver fonctionne, mais est inutilisable, les vitesses de téléchargement sont de l’ordre du bit par seconde). La nouvelle carte est une TP-Link TL-WN321G, du chipset rum(4), et est détectée directement (à partir du moment où l’USB fonctionne). Un petit coup d’ifconfig(8) et de route(8), et hop, on est connecté. Le driver à l’air de fonctionner assez bien la plupart du temps, même si il lui arrive d’avoir un comportement bizarre (le débit tombe parfois aux alentours des 20kB/s, sans raison apparente)

Configuration

Je vais indiquer ici les étapes de la configuration qui ne sont pas mentionnées dans la documentation de DragonFly BSD. Lors d’une première installation, le handbook est très pratique mais il faut savoir chercher par soi même car il n’est pas totalement à jour (par exemple, depuis la version 2.4 DragonFly utilise un /dev dynamique, ce qui n’est mentionné nulle part dans le handbook où ils utilisent le vieux /dev/MAKEDEV pour créer les devices).

Bépo

Au niveau du bépo dans la console, le driver [pour FreeBSD] ne fonctionne pas. Je n’ai pas encore pris le temps de chercher plus loin de ce côté car dès que le réseau était fonctionnel, j’ai tout configuré via ssh depuis un autre ordinateur pourvu du bépo.

Pour ce qui est du driver dans X, il n’est pas à jour et il faut donc le mettre à jour soi-même (une fois X installé, bien sûr):

# cd /usr/pkg/share/X11/xkb/symbols
# mv fr fr.bak
# fetch -o fr http://www.clavier-dvorak.org/donnees/xkb/fr-bepo.1.0-rc1-hardy 

On ajoute ensuite ceci dans /etc/X11/xorg.conf, à la section InputDevice du clavier:

Option          "XkbRules"      "xorg"
Option          "XkbModel"      "pc105"
Option          "XkbLayout"     "fr"
Option          "XkbVariant"    "bepo"

La souris en tty

Ce n’est certainement pas la chose la plus utile, mais ça permet de voir si la souris fonctionne bien avant d’avoir installé X. On regarde d’abord sur quel device se trouve la souris (si elle est détectée):

$ grep Mouse /var/log/messages
Jan 31 17:46:51 newton kernel: ums0: <Logitech USB-PS/2 Optical Mouse, class 0/0, rev 2.00/20.00, addr 2> on uhub0

On ajoute ensuite ceci au rc.conf:

moused_enable="YES"
moused_port="/dev/ums0"

Et on peut démarrer moused:

rcstart moused

Mise en veille

Pour la mise en veille, il faut charger le module acpi.ko s’il n’est pas chargé par défaut:

# kldload acpi.ko

Ensuite, on utilise acpiconf(8) pour mettre l’ordinateur en veille, par exemple pour faire l’équivalent d’un s2ram:

# acpiconf -s 3

Hélas, le seul mode d’hibernation qui fonctionne sur mon ordinateur est le 1 (qui arrête juste l’horloge du CPU).

UTF–8

Pour avoir l’UTF–8, il suffit de rajouter ceci de modifier le /etc/login.conf pour que la fin de la section default: ressemble à ceci:

    :ignoretime@:\
    :umask=022:\
    :charset=UTF-8:\
    :lang=en_US.UTF-8:

Le changement sera donc fait pour tout le système et cela nous évite d’avoir à modifier nous-même les variables LC_* et LANG, ce qui peut varier selons les shells utilisés.

Désactiver le beep système

On peut désactiver ce beep à deux niveaux: dans le tty ou dans X:

# dans le tty
kbdcontrol -b off
# dans X
xset -b

La première commande est à placer dans /etc/profile et la seconde dans ~/.xinitrc si vous utilisez startx.

Moins de tty

Je n’ai jamais vraiment compris l’utilité d’avoir 7 ou 8 (TODO) ttys activés par défaut alors qu’un suffit. On va donc tous les désactiver sauf les deux premiers (au cas où on oublie de lancer tmux ou screen avant une commande qui met du temps à s’éxécuter). Pour cela, il suffit de commenter TODO dans le fichier /etc/tty.

Désactiver les messages du kernel (silent boot)

C’est très désagréables de se faire déranger par les messages du kernel quand on travaille en tty. Ça se désactive facilement en mettant l’option kern.consmute de sysctl(8) à 1. Cela peut se faire au niveau de /etc/sysctl.conf, qui est chargé après le chargement du kernel, donc vous verrez toujours les messages pendant le boot. Pour avoir un silent boot, c’est-à-dire un boot sans messages du kernel, il suffit simplement de changer cette option au niveau du fichier /boot/loader.conf.

La ligne à ajouter dans les deux cas est:

kern.consmute="1"

Carte son

Ma carte son étant une ATI IXP, il a suffit de charger le module snd_atiixp.ko et de l’ajouter au rc.conf:

# kldload snd_atiixp.ko
# echo 'snd_atiixp_load="YES"' >> /etc/rc.conf

Et pour tester le son, rien de tel qu’un petit

# cat /dev/urandom > /dev/audio

Logiciels utilisés

Pour finir, voici les principaux logiciels que j’utilise et le nom de leur pkgsrc:

UtilitéLogicielPort
Window Managerwmiiwm/wmii
Terminalrxvt-unicodex11/rxvt-unicode
Multiplexeur de terminauxtmuxmisc/tmux
Éditeur de texteVim, GNU Emacs1editors/vim, editors/emacs-snapshot
Navigateurfirefox2www/firefox
Lecteur audiompd et mpcaudio/musicpd et audio/mpc
Pagermostmisc/most
Shellmkshshells/mksh
Lecteur PDFxpdf3print/xpdf
Visionneur d’imagesxvgraphics/xv

To be continued…

Il reste encore beaucoup de choses à regarder de plus près (monter de l’ext4 et du reiserfs, le bépo dans la console, …), dont certaines propres à DragonFly BSD (hammer, vkernel). Cela fera peut-être l’objet de futurs billets.

J’aimerais aussi me pencher sur la création de pkgsrc, notamment pour installer proprement mupdf et pouvoir me passer de xpdf.


  1. Vim pour configurer et Emacs pour programmer.

  2. Avec le plugin vimperator.

  3. Regardez du côté de mupdf pour un autre lecteur intéressant (mais pas (encore) dans pkgsrc).

Des tips, enfin !

Ça fait un moment qu'on voulait un système de tips sur awesom qui nous permettrait d'avoir un endroit ou stocker des simples astuces, et qui pourrait par la même occasion être accessible à d'autres personnes.

On a tout d'abord pensé à un StatusNet, mais c'était peu adapté à ce qu'on voulait (mais pour une utilisation « normale », ça poutre). On a donc décidé qu'on le coderait nous même, ce qu'Izu a commencé avec web.py, et s'est arrêté une fois que ça affichait les tips, mais on n'avait pas de possibilitée d'en ajouter et quelques problèmes à le mettre en place proprement avec lighttpd. De mon côté, j'ai essayé dancer et nitrogen, mais c'était un peu trop chiant à utiliser pour faire un truc aussi simple que tips. Izu a ensuite fait quelque chose de pas mal avec Code Igniter, mais toujours sans la possibilitée d'ajouter des tips. J'ai finalement testé Arc et ça m'a semblé être un bon choix.

Arc, un langage orienté web

Arc est un langage de la famille des lisp, conçu par Paul Graham, un hacker Lisp assez renommé qui écrit beaucoup, aussi bien des essais que des bons livres sur le Common Lisp.

La particularité de Arc est qu'il est incroyablement bien adapté pour développer des applications web avec ou même n'importe quelle application qui utilise le réseau (la gestion des sockets est simple avec Arc contrairement aux sockets de Common Lisp qui s'utilisent de façon différente sur chaque implémentation, même s'il existe des bibliothèques plus ou moins portables). Arc a beau être léger, il implémente énormément de fonctions très utiles pour le développement web, ce qui permet de se concentrer sur l'application en soi plutôt que de coder encore et encore les même fonctions qui manquent dans le langage.

Évidemment, Arc étant un dérivé de Lisp, vous pouvez toujours définir des macros (lisez les chapitres 7 et 8 de Practical Common Lisp si vous débutez en Common Lisp et que vous ne conaissez pas les macros, pour ceux qui ont un niveau plus élevé, regardez du côté de On Lisp ou même de Let Over Lambda), ce qui simplifie grandement le développement.

Je ne vais pas parler plus en détail de Arc, si vous voulez plus d'informations, je vous recommande Design Philosophy (ou en français) et Why Arc Isn't Especially Object-Oriented qui peuvent vous donner une première impression sur la façon dont Arc est conçu. Pour avoir un aperçu d'Arc, regardez aussi Take the Arc Challenge et comparez le avec les solutions dans d'autres langages.

Installation d'Arc

Plusieurs implémentations d'Arc sont disponibles, dont les deux principales, implémentées en PLT-Scheme:

  • tout d'abord, la version « officielle » de Paul Graham, qui date un peu ;
  • ensuite, Anarki une version dérivée de la version officielle, qui est bien plus à jour et peut-être légèrement incompatible avec celle de Graham.

Il existe aussi d'autres versions moins actives, moins utilisées et surtout moins complètes, mais qui valent quand même le coup d'œuil:

  • un compilateur Arc vers C, arc2c ;
  • une implémentation en Java, rainbow ;
  • une implémentation en Javascript, arclite ;
  • une implémentation en .NET avec MBase ;
  • une implémentation en Common Lisp avec SBCL, arc-sbcl.

Personnellement j'utilise Anarki, qui est de loin la version la plus complète et active. Pour l'installation, un coup de

git clone http://github.com/nex3/arc
ln -s arc/arc.sh ~/bin

et c'est parti pour de folles heures de hack. N'oubliez pas d'installer les fichiers relatif à votre éditeur de texte, disponibles dans extras (il y a un arc-mode et un inferior-arc-mode pour GNU Emacs ainsi que les fichiers d'indentation, de syntaxe et une extension pour intégrer la REPL d'Arc à Vim)

Pour commencer à programmer avec Arc, lisez d'abord l'introduction de Paul Graham et utilisez la documentation disponible sur l'Arc Language Blog, qui est quasi complète, mais pas totalement, il vous restera donc à trouver votre documentation en lisant les sources d'Arc. Parcourez aussi les deux exemples d'applications disponibles avec Arc: un simple blog et le code source de Hacker News. N'hésitez pas non plus à jeter un œuil au code source de tips (qui s'inspire de ces deux exemples).

J'ai aussi mis en place une page regroupant les ressources francophones disponibles pour Arc, toute contribution est la bienvenue.

Mise en place sur awesom

Arc a été installé dans une jail et il a simplement suffit de récupérer tips dessus (via son dépôt mercurial (que je commence d'ailleurs à préférer à git, car plus simple et plus logique tout en restant puissant)), il ne restait donc plus qu'à configurer le serveur web (en l'occurrence, lighttpd).

Ce dernier point a été assez dur a mettre en place car le mod_proxy de lighttpd refusait simplement de marcher avec Arc (la seule réponse qu'on obtenait du serveur était une page blanche). On a donc mis en place un nginx (qui est très simple à configurer soit dit en passant) dans la jail qui écoute sur le port 80 et qui fait proxy vers tips (qui lui tourne sur le 8080).

Arc étant lancé dans une REPL, on peut facilement modifier les paramètres (url, etc.) et surtout, mettre à jour tips sans même devoir couper le service (en rechargeant simplement les fichiers sources).

Voilà, on a enfin quelque chose pour gérer nos tips, sur tips.awesom.eu, il faut juste espérer que ça se remplisse petit à petit.

Chronicle: suite & fin

Après l'écriture de mon premier post, j'ai fait part de mes modifications à Steve Kemp, l'auteur de Chronicle, qui a très vite répondu et a non seulement implémenté le support de la coloration (uniquement via Vim), mais a séparé la partie touchant au formattage des billets de la partie principale. Le formattage se fait donc maintenant grâce à bin/chronicle-entry-filter, qui permet de définir des pré/post filtres, qui seront appelés avant/après le formattage principal du texte (transformation en html et coloration) ce qui permet de pouvoir faire proprement ce que l'on veut au texte, sans devoir modifier le programme.

Le seul petit problème, c'est que la coloration ne s'effectue que sur les balises <code> pourvues d'un attribut lang, et il faut un moyen de spécifier à markdown/textile cet attribut. Pour textile, c'est très simple, il suffit de spécifier le langage entre crochet quand on utilise bc:

bc[perl]. print "Hello, world!";

Avec markdown (ce qui marche aussi avec textile), on peut remédier à ce problème en écrivant directement du code html (donc on fout notre code dans une balise <code> avec un attribut lang). Et si ça vous fait chier d'écrire ça à chaque bout de code, il suffit d'utiliser un pré-filtre qui matche par exemple "highlight:language" au début de chaque bout de code et qui le remplace par le bout de code html.

Bien évidemment, ce système de pré/post filtre ne se limite pas à la coloration syntaxique, mais permet de faire un peu ce qu'on veut avec les billets (l'exemple donné par Steve dans les sources est par exemple d'utiliser un tr [a-z] [A-Z] comme post-filtre :-').

Voilà, maintenant je ne peut pas ne pas vous recommander d'utiliser Chronicle comme moteur de blog, une nouvelle release devrait sortir d'ici quelques jours avec ces fonctionnalitées (et tout est déjà dans le dépôt mercurial).

Chronicle: générateur de blog statique

Voilà, ce premier billet va donc présenter le moteur de blog utilisé: Chronicle. En effet, je souhaitait un moteur de blog assez simple et qui génère du contenu statique (comme nanoblogger, utilisé par heaumer), et après de nombreuses recherches je suis resté sur Chronicle qui était le seul, excepté nanoblogger, à générer un blog statique (en plus y a même un script cgi si jamais je veux activer les commentaires).

Le principe est donc simple: vous écrivez vos billets dans un des langages de formattage disponibles, c'est-à-dire Markdown, Textile, ou directement du HTML. Une fois votre article écrit, vous lancez chronicle avec les options kivonbien, et votre blog est généré en html statique, et il ne reste plus qu'à le rendre accessible via n'importe que serveur web.

Néanmoins, il a fallu modifier quelques petites choses pour avoir le fonctionnement voulu. Heureusement c'est du perl et ça se laisse tripoter facilement.

Le thème

Les cinq thèmes fournis avec Chronicle ne sont pas parfait, en fait ils sont tous moches, sauf le thème « simple », mais qui a certains défauts (les liens en jaune clair quand on passe dessus, c'est immonde). Je suis donc parti de ce thème que j'ai légèrement modifié. La modification se fait très facilement, quelques valeurs à changer dans les CSS, quelques modifications dans les .templates utilisés pour générer l'html et le travail est fait. Le thème final est disponible ici.

Coloration syntaxique

Ensuite vient le problème de la coloration syntaxique, qui est assez indispensable au niveau esthétique pour un blog qui est destiné à parler programmation. Hélas, il n'y a pas de coloration syntaxique dans Chronicle, mais ça peut s'ajouter facilement.

Au niveau du logiciel utilisé pour la coloration, mon choix s'est d'abord porté vers Pygments via XML-RPC (le serveur XML-RPC doit avoir une fonction highlight_code qui prends en premier paramètre le code à colorer et en second paramètre le langage, ce qui est par exemple le cas du script écrit par delroth), mais finalement j'utilise la coloration syntaxique de Vim (via Text::VimColor), que j'avais d'abord éliminé car dans mes souvenirs ça ne convenait pas, ce qui était le cas pour paste.pl, qui devait éxécuter Vim pour colorer le texte (ce qui n'est pas génial niveau sécu pour un script CGI), mais dans ce cas-ci, le script étant éxécuté par l'utilisateur uniquement à la génération, ça ne pose aucun problème.

J'ai aussi ajouté la possibilitée d'utiliser Kate, un autre module de coloration syntaxique directement implémenté en Perl. L'utilisateur peut donc choisir la méthode qu'il préfère en l'indiquant dans le fichier de configuration (paramètre highlight qui est mis soit à kate, vim ou pygments).

Pour ma part, j'utilise donc la coloration avec Vim, car plus de langages sont supportés (520 fichiers dans /usr/local/share/vim/vim72/syntax/), et dans le cas de langages peu utilisés, il y a peu de chances d'avoir une coloration dans Pygments ou avec Kate, tandis que dans Vim, oui (c'est par exemple le cas de arc).

Pour spécifier du code à colorer, il doit commencer par "highlight:langage", avec langage qui est un langage supporté par la méthode de coloration que vous avez choisie.

Je mets à votre disposition le diff, pour que vous puissiez patcher Chronicle si jamais vous avez envie d'avoir une coloration syntaxique de dingue, et voilà le résultat:

(defun hello (who)
  (format t "Hello, ~a!~%" who))

Autres améliorations possibles

On pourrait aussi ajouter un paramètre qui permettrait à l'utilisateur d'utiliser une commande shell de son choix pour formatter le code (un script python pour utiliser Pygments par exemple, c'est plus simple que d'utiliser XML-RPC), et on pourrait faire de même pour le formattage du texte (pour utiliser mdown ou d'autres logiciels). Je le ferait peut être un jour si l'envie me vient.