Accéder au contenu principal

9min.

Castor 1.8: Windows, self-update, et des binaires vérifiables

This blog post is also available in 🇬🇧 English: Castor 1.8: Windows, self-update, and binaries you can verify.

La dernière fois que nous avons parlé de Castor sur ce blog, c’était pour annoncer la sortie de la 1.0, en octobre 2025. Depuis, notre rongeur préféré a pris un rythme mensuel : une version mineure toutes les quatre à cinq semaines, avec à chaque fois quelques helpers et quelques correctifs, parfois une dépréciation pour préparer la 2.0.

La 1.8 casse un peu ce rythme : 81 commits et 33 pull requests depuis la 1.7.0 de début août, et presque aucun n’ajoute de helper. Le travail est parti dans les recoins de Castor que vous ne regardez jamais : comment il arrive sur votre machine, comment il se met à jour, et ce qui prouve que le binaire que vous lancez est bien celui que notre CI a construit. Ah, et il tourne sous Windows maintenant 🪟.

Section intitulée ce-que-vous-avez-peut-etre-rate-depuis-la-1–0–0Ce que vous avez peut-être raté depuis la 1.0.0

Sept versions mineures sont passées depuis l’article sur la 1.0, et certaines méritent mieux qu’une ligne dans le changelog. Au cas où vous en auriez sauté quelques-unes :

  • PHP 8.4 est le minimum depuis la 1.6, et les binaires statiques embarquent PHP 8.5 depuis la 1.3. Si vous êtes coincés en 8.2 ou 8.3, Castor vous le dit et vous oriente vers le binaire statique ;
  • #[AsArgsAfterOptionEnd] (1.2) donne à une tâche tout ce qui est tapé après --, tel quel. Pratique pour un castor phpunit -- --filter foobar qui transmet le reste à une autre CLI ;
  • Le contexte a appris deux choses en 1.4 : input, pour alimenter le stdin d’un process (un mot de passe, plutôt que de le mettre sur la ligne de commande), et supportsInteraction(), pour savoir si vous êtes dans un TTY ou dans une CI avant d’appeler toInteractive() ;
  • Un fichier .castor.context à la racine du projet définit le contexte par défaut, derrière --context et CASTOR_CONTEXT (1.6). Commitez-le, ou gitignorez-le pour un défaut personnel ;
  • L’autocomplétion des chemins est devenue plus maligne : #[AsPathArgument] et #[AsPathOption] acceptent un directory et un filter (1.4) ;
  • dispatch() et event_dispatcher() (1.7) laissent vos tâches dispatcher leurs propres événements, en plus de ceux que Castor émet déjà ;
  • Le repack n’a plus besoin de jolicode/castor dans votre composer.json (1.3), et --castor-file pointe Castor vers un fichier racine qui ne s’appelle pas castor.php (1.1) ;
  • Plus petits : terminal() pour la taille du terminal, slug() pour slugifier une chaîne, un lien vers la définition de la tâche dans castor help, et un en-tête silencieux quand Castor tourne dans un agent IA (moins de tokens gachés !).

Le changelog contient le reste. Passons à la 1.8.

Section intitulée castor-sous-windowsCastor sous Windows

Castor fournit un phar Windows depuis les premières releases. Mais un phar a besoin de PHP, et « installer PHP sous Windows » n’est pas aussi fluide que sur les autres plateformes. Les binaires statiques, ceux qui embarquent PHP, n’existaient que pour Linux et macOS.

La 1.8 ajoute castor.windows-amd64.exe à chaque release : un fichier, PHP 8.5 dedans, rien à installer. Déposez-le dans un dossier de votre PATH et c’est terminé :

curl.exe "https://github.com/jolicode/castor/releases/latest/download/castor.windows-amd64.exe" -Lso C:\<un dossier de votre PATH>\castor.exe

Info

Sous WSL, gardez le binaire statique Linux : même si ça fonctionne, le .exe tournerait comme un process Windows via l’interop, ce qui n’est pas ce que vous attendez d’un shell Linux.

Si vous distribuez votre propre CLI construite sur Castor (repack, puis compile), castor:compile a gagné une option --os=windows : vos tâches peuvent devenir un exécutable Windows elles aussi.

La première tentative pour supporter les binaires Windows remonte à octobre 2025, juste après la 1.0, et il a fallu quelques impasses avant d’aboutir à une solution fonctionnelle. Nous construisons les binaires statiques avec static-php-cli, un super outil sous Linux et macOS, mais moins évident sous Windows. Dans le désordre : le runner windows-latest embarque un Visual Studio que static-php-cli ne reconnaît pas (il veut le 2019 ou le 2022), le tar de git bash réécrit les chemins Windows dans votre dos, et MSVC abandonne sur les chemins de sources de plus de 260 caractères. Chacun de ces soucis se corrige en cinq minutes. Chacun coûte aussi un run de CI complet pour être découvert 😅. Si vous aimez ce genre d’archéologie, toute l’histoire est dans la pull request.

Section intitulée installer-et-mettre-a-jour-castorInstaller et mettre à jour Castor

Section intitulée code-castor-self-update-codecastor self-update

Jusqu’ici, mettre à jour Castor voulait dire relancer l’installeur, ou aller chercher le nouveau phar à la main sur la page des releases. Castor se met maintenant à jour tout seul, comme le font Composer et PHPStan :

castor self-update

Il retrouve comment il a été installé, télécharge le phar ou le binaire statique qui correspond à votre plateforme, et se remplace lui-même. La version précédente est conservée à côté : un --rollback la remet en place si la nouvelle se comporte mal. Vous avez installé Castor globalement avec Composer ? La commande lance composer global update pour vous.

Castor vous disait « une nouvelle version est disponible » depuis deux ans. Il peut enfin y faire quelque chose.

Section intitulée snapshotsSnapshots

Chaque push sur main publie maintenant une pre-release snapshot. C’est le chemin le plus court pour essayer un correctif avant sa sortie :

castor self-update --snapshot
# ou, en partant de zéro
curl "https://castor.jolicode.com/install" | bash -s -- --version=snapshot

Un snapshot affiche une version du type v1.7.0-14-g4531440 : la dernière release, le nombre de commits depuis, et le commit à partir duquel il a été construit. castor self-update sans l’option vous ramène sur la dernière stable.

Égoïstement, c’est aussi pour nous : « tu peux essayer le snapshot ? » se demande bien plus facilement dans une issue que « tu peux cloner le dépôt et construire le phar ? ».

Section intitulée des-binaires-verifiablesDes binaires vérifiables

Un task runner tourne sur votre laptop et dans votre CI, et pour certains d’entre vous sur des serveurs de production. Un task runner qui se met à jour tout seul télécharge du code et l’exécute. Ça mérite mieux qu’un curl | bash et un acte de foi.

Chaque phar et chaque binaire statique est maintenant publié avec une attestation d’artefact GitHub : une déclaration signée que ce fichier précis a été construit par le workflow GitHub Actions de Castor, à partir de ce commit. Chaque release contient aussi un fichier SHA256SUMS, attesté lui aussi.

Ensuite, tout ce qui télécharge Castor les vérifie :

  • l’installeur et self-update comparent le checksum du binaire avec SHA256SUMS ;
  • si la CLI GitHub (2.49 ou plus) est installée et connectée, les deux lancent en plus gh attestation verify dessus ;
  • castor:repack refuse un phar Castor sans attestation (il existe un --allow-unattested pour les releases publiées avant les attestations) ;
  • castor:compile vérifie le checksum de l’archive static-php-cli qu’il télécharge.

Vous pouvez aussi le faire à la main, sur n’importe quel fichier d’une release :

gh attestation verify castor.linux-amd64 --repo jolicode/castor

Astuce

Le script d’installation a eu droit au même traitement : il télécharge dans un fichier temporaire privé, nettoie quoi qu’il arrive, et se lit en entier avant d’exécuter quoi que ce soit. Une connexion coupée en plein milieu ne peut plus lancer la moitié d’un script.

Section intitulée dans-vos-tachesDans vos tâches

Section intitulée des-helpers-plus-sursDes helpers plus sûrs

Tant que nous étions dans un mood sécurité, nous sommes repassé sur les helpers avec une seule question : et si cette valeur venait d’une saisie utilisateur ? Une poignée de comportements ont changé. Aucun ne devrait casser une tâche existante, et ils sont tous dans le changelog, mais quelques-uns valent le coup d’être mis en avant :

  • http_download() ne garde que le dernier segment du nom de fichier envoyé par le serveur : un téléchargement atterrit toujours dans le dossier de votre projet, quoi qu’en dise le serveur ;
  • ssh_run() échappe le chemin distant, et refuse un hôte, un utilisateur ou un chemin de clé contenant un métacaractère shell ;
  • zip() avec un mot de passe préfère l’extension PHP zip au binaire zip : le binaire prend le mot de passe sur sa ligne de commande, lisible par tous les utilisateurs de la machine pendant la création de l’archive. zip_binary() le fait toujours, et prévient désormais ;
  • run_php() passe son script au process enfant en argument, au lieu d’une variable d’environnement que n’importe quel process Castor aurait ramassée sans se poser de question ;
  • le dossier de cache est créé en 0700, respecte XDG_CACHE_HOME, et le binaire du watcher derrière watch() y est extrait plutôt que dans un chemin fixe de /tmp partagé avec tous les utilisateurs de la machine ;
  • encrypt_with_password() dérive sa clé avec les limites « moderate » de libsodium (Argon2id, 3 passes, 256 Mio) au lieu des « interactive ». Ce qui a été chiffré par un Castor plus ancien se déchiffre toujours. Ce qui est chiffré par la 1.8 demande la 1.8 ou plus.

Section intitulée intercepter-les-signauxIntercepter les signaux

Retour au quotidien. Un classique : une tâche démarre un serveur, et vous aimeriez que CTRL+C arrête le serveur, pas la tâche. Jusqu’ici, le SIGINT arrêtait Castor lui-même, et ce que vous aviez prévu après le serveur ne s’exécutait jamais.

Le contexte a gagné withTrappedSignals(), qui transmet le signal au process en cours à la place :

use Castor\Attribute\AsTask;

use function Castor\context;
use function Castor\run;

#[AsTask()]
function serve(): void
{
    // A CTRL+C stops the server, but not the task
    run('./my-server', context: context()->withTrappedSignals()->withAllowFailure());

    run('echo "the server has been stopped"');
}

Sans argument, il intercepte SIGINT et SIGTERM. Passez votre propre liste si besoin, withTrappedSignals([\SIGINT, \SIGQUIT]) par exemple. Les détails sont dans la documentation des signaux.

Section intitulée monter-un-paquet-distantMonter un paquet distant

Castor a deux façons d’amener des tâches externes dans un projet. import() ajoute des fonctions et des tâches dans votre application. mount() branche un sous-projet entier, avec son propre préfixe de namespace et son propre dossier de travail. Seul import() connaissait les paquets Composer, mount() ne fonctionnait qu’avec des dossiers locaux. Ils partagent maintenant la même résolution :

use function Castor\mount;

mount('composer://vendor/package', 'project:package');

Les tâches du paquet apparaissent sous project:package: et s’exécutent depuis le dossier du paquet installé, exactement comme un mount local. C’est le montage que nous voulions pour une boîte à outils qu’une équipe maintient dans son propre dépôt (un kit de déploiement, une stack Docker) et monte dans chaque projet.

Un correctif est tombé de ce chantier : un import() distant ne change plus le dossier de travail des tâches qu’il définit. Il n’aurait jamais dû. Seul un mount() explicite fait ça.

Section intitulée une-depreciation-pour-preparer-la-2–0Une dépréciation pour préparer la 2.0

Celle-ci mérite une minute, parce que vous dépendez peut-être de l’ancien comportement sans le savoir.

Aujourd’hui, seul run() s’exécute dans le dossier de travail du contexte. Tout le reste, fs(), finder() et les fonctions PHP natives comme mkdir() ou file_get_contents(), résout les chemins relatifs depuis l’endroit où vous avez tapé castor. Lancez castor build depuis un sous-dossier : file_get_contents('composer.json') cherche au mauvais endroit alors que run('cat composer.json') non. Pas terrible.

Castor 2.0 changera son propre dossier courant pour le dossier de travail du contexte : un chemin relatif voudra dire la même chose partout. Il suit aussi les blocs with(workingDirectory: ...), et restaure le dossier précédent quand ils se terminent.

Vous pouvez l’activer dès aujourd’hui avec une ligne en haut de votre castor.php :

defined('CASTOR_USE_CHDIR') || define('CASTOR_USE_CHDIR', true);

Ledefined() n’est pas décoratif : un projet monté ou un paquet importé peut définir la constante lui aussi, et un second define() nu lève une erreur.

Ne pas définir la constante est déprécié en 1.8, et le nouveau comportement devient le défaut en 2.0. Pour garder l’ancien comportement encore un moment sans la dépréciation, définissez-la à false.

Avertissement

Une fois activée, un chemin relatif donné en ligne de commande (castor castor:compile foo.phar, un argument de tâche) se résout depuis la racine du projet, pas depuis le dossier où vous avez invoqué castor. Ça ne compte que si vous lancez Castor depuis un sous-dossier, mais vérifiez vos tâches avant de basculer.

Section intitulée le-mot-de-la-finLe mot de la fin

Si vous ne retenez qu’une chose de la 1.8 : le binaire que vous lancez est le nôtre, preuve à l’appui, et il se met à jour tout seul. Vos collègues sous Windows ont le même outil que tout le monde. En bonus, CTRL+C fait enfin ce que vous attendez devant un serveur.

La mise à jour tient désormais en une commande :

castor self-update

La liste complète des changements est dans le changelog. Et si l’histoire de static-php-cli vous a donné envie de savoir comment sont construits les binaires Linux, macOS et Windows, dites-le-nous : ça pourrait être le prochain article 🦫

Commentaires et discussions

Nos articles sur le même sujet