[{"data":1,"prerenderedAt":40},["ShallowReactive",2],{"doc:fr:plugins\u002Frust":3},{"title":4,"lede":5,"html":6,"headings":7},"Le kit Rust","Un plugin en Rust est un module WASM, compilé par le store depuis ses sources, sur les mêmes méthodes qu’un plugin en TypeScript. Cette page dit quelles crates utiliser, comment une source, un hébergeur ou un harness s’écrit, et ce que la compilation refuse.","\u003Ch2 id=\"les-trois-crates\">Les trois crates\u003C\u002Fh2>\n\u003Cp>Elles sont sur crates.io en \u003Ccode class=\"key\">0.1.0\u003C\u002Fcode>, sous licence MIT : un plugin garde le choix de la sienne. Leur documentation est sur docs.rs, pour le natif et pour \u003Ccode class=\"key\">wasm32-wasip1\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Crate\u003C\u002Fth>\n\u003Cth>Ce qu’elle donne\u003C\u002Fth>\n\u003Cth>Quand t’en servir\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ca href=\"https:\u002F\u002Fdocs.rs\u002Fescouade-plugin-kit\" rel=\"noopener\">\u003Ccode class=\"key\">escouade-plugin-kit\u003C\u002Fcode>\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>La boucle (\u003Ccode class=\"key\">serve\u003C\u002Fcode>), le client http, \u003Ccode class=\"key\">sleep\u003C\u002Fcode> et \u003Ccode class=\"key\">timeout\u003C\u002Fcode>, le service des hébergeurs et des sources (\u003Ccode class=\"key\">hosts\u003C\u002Fcode>), les erreurs typées, la langue.\u003C\u002Ftd>\n\u003Ctd>Presque toujours.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"https:\u002F\u002Fdocs.rs\u002Fescouade-plugin-protocol\" rel=\"noopener\">\u003Ccode>escouade-plugin-protocol\u003C\u002Fcode>\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>Le protocole : ses types, \u003Ccode class=\"key\">turn\u003C\u002Fcode> et \u003Ccode class=\"key\">module!\u003C\u002Fcode>, le contrat des harness (\u003Ccode class=\"key\">engine\u003C\u002Fcode>). Le kit le réexporte (\u003Ccode>escouade_plugin_kit::protocol\u003C\u002Fcode>).\u003C\u002Ftd>\n\u003Ctd>Pour un harness, seul.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"https:\u002F\u002Fdocs.rs\u002Fescouade-acp\" rel=\"noopener\">\u003Ccode class=\"key\">escouade-acp\u003C\u002Fcode>\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>Le client ACP, une machine à états sans process.\u003C\u002Ftd>\n\u003Ctd>Pour un harness qui parle ACP et que le \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fharnesses\u002F\">manifeste seul\u003C\u002Fa> ne suffit pas à décrire.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Un plugin en dépend par version, avec son \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode> :\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002FCargo.toml\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-toml\">[lib]\nname = &quot;plugin&quot;\ncrate-type = [&quot;cdylib&quot;, &quot;rlib&quot;]\n\n[dependencies]\nescouade-plugin-kit = &quot;0.1.0&quot;\nserde_json = &quot;1&quot;\n\n# Only for `cargo test`, on your machine: the module has no tokio.\n[target.&#39;cfg(not(target_family = &quot;wasm&quot;))&#39;.dev-dependencies]\ntokio = { version = &quot;1&quot;, features = [&quot;io-util&quot;, &quot;rt&quot;] }\n\n# The plugin stands alone: a repository with several plugins does not make it a member of anything.\n[workspace]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>La même source compile deux fois : en process sur ta machine pour \u003Ccode>cargo test\u003C\u002Fcode>, avec tokio et reqwest, et pour \u003Ccode class=\"key\">wasm32-wasip1\u003C\u002Fcode> en module, sans eux.\u003C\u002Fp>\n\u003Ch2 id=\"un-plugin\">Un plugin\u003C\u002Fh2>\n\u003Cp>\u003Ccode class=\"key\">Plugin\u003C\u002Fcode> a une méthode, \u003Ccode class=\"key\">call\u003C\u002Fcode>, que le kit appelle pour tout ce qui arrive, sauf \u003Ccode class=\"key\">initialize\u003C\u002Fcode> et \u003Ccode class=\"key\">shutdown\u003C\u002Fcode>, qu’il répond lui-même :\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct Template;\n\nimpl Plugin for Template {\n    \u002F\u002F Every method of Escouade comes here but `initialize` and `shutdown`, which the kit answers. A notification too: what\n    \u002F\u002F it returns goes nowhere. Nothing may block here: an `.await` gives the turn back to Escouade.\n    fn call&lt;&#39;a&gt;(\n        &amp;&#39;a self,\n        cx: &amp;&#39;a Cx,\n        method: &amp;&#39;a str,\n        params: Value,\n    ) -&gt; BoxFuture&lt;&#39;a, Result&lt;Value, Refusal&gt;&gt; {\n        Box::pin(async move {\n            match method {\n                &quot;events\u002Ffact&quot; =&gt; {\n                    fact(cx, &amp;params);\n                    Ok(Value::Null)\n                }\n                &quot;permissions\u002Fdecide&quot; =&gt; Ok(decide(&amp;params)),\n                other =&gt; Err(Refusal::not_found(other)),\n            }\n        })\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode class=\"key\">Refusal::not_found\u003C\u002Fcode> répond \u003Ccode class=\"key\">-32601\u003C\u002Fcode> à ce que le plugin ne sert pas. Reste à dire ce qui tourne, et à en faire un module, en une ligne, qui ne produit rien en natif :\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F What the module runs at its first turn: the plugin, kept for the turns to come. Natively, the loop on stdin and stdout.\npub fn run() -&gt; i32 {\n    serve::run(&quot;plugin&quot;, Template)\n}\n\n\u002F\u002F The one line that makes this crate a module of Escouade. Natively, it is nothing.\nescouade_plugin_kit::protocol::module!(run);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>npm create escouade-plugin@latest mon-plugin -- --kind rust\u003C\u002Fcode> écrit tout cela, avec ses tests : voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fquick-start\u002F\">un premier plugin\u003C\u002Fa>. Chaque appel de l’hôte a son exemple dans \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F\">les appels\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"le-modele-par-tours\">Le modèle par tours\u003C\u002Fh2>\n\u003Cp>Une instance par plugin, sur un fil, réveillée par ce qui arrive : une ligne d’Escouade, la fin d’un \u003Ccode class=\"key\">net\u002Ffetch\u003C\u002Fcode>, un morceau de flux, un message de socket, la sortie d’un programme, un minuteur. Escouade écrit tout ce qui est arrivé sur l’entrée du module, lance \u003Cstrong>un\u003C\u002Fstrong> tour, puis sert ce que le module a écrit.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Rien n’attend dans un tour.\u003C\u002Fstrong> Une demande à l’hôte est une ligne, et sa réponse arrive à un tour suivant.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Un \u003Ccode class=\"key\">.await\u003C\u002Fcode> rend la main. Le kit garde la tâche et la reprend au tour où sa réponse arrive : plusieurs appels avancent à la fois, et un appel lent ne retient pas les autres.\u003C\u002Fli>\n\u003Cli>Ce qui bloque le fil bloque tout le plugin : une boucle, un calcul long, \u003Ccode class=\"key\">std::thread::sleep\u003C\u002Fcode>. Il n’y a pas de thread, et \u003Ccode class=\"key\">std::process\u003C\u002Fcode> échoue. \u003Ccode>escouade_plugin_kit::time::sleep\u003C\u002Fcode> remplace l’attente.\u003C\u002Fli>\n\u003Cli>Il n’y a pas d’environnement : la langue vient d’\u003Ccode class=\"key\">initialize\u003C\u002Fcode>, lue par \u003Ccode>escouade_plugin_kit::i18n\u003C\u002Fcode>, et le dossier d’un agent par \u003Ccode class=\"key\">agent\u002Fattach\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Le réseau passe par \u003Ccode class=\"key\">http::reqwest\u003C\u002Fcode>, la façade du kit : la surface de reqwest dont les plugins se servent, sur \u003Ccode class=\"key\">net\u002Ffetch\u003C\u002Fcode>. Le proxy, les certificats et le délai restent à l’hôte, et ce que le plugin en dit n’a pas d’effet.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Un tour ne garde rien pour lui : l’état vit dans le plugin, entre deux tours, comme dans un process. Il n’est pas gardé d’une instance à l’autre.\u003C\u002Fp>\n\u003Ch3 id=\"la-porte-synchrone\">La porte synchrone\u003C\u002Fh3>\n\u003Cp>Une machine à états qui doit lire un fichier avant de répondre, comme un harness, a un appel de plus : \u003Ccode class=\"key\">escouade.call\u003C\u002Fcode>, un import du module, servi sur le fil du plugin. \u003Ccode class=\"key\">turn::read_file\u003C\u002Fcode> lit par lui, et \u003Ccode class=\"key\">turn::host_call\u003C\u002Fcode> envoie \u003Ccode class=\"key\">fs\u002F*\u003C\u002Fcode> ou \u003Ccode class=\"key\">proc\u002F*\u003C\u002Fcode>. Il compte dans le temps du tour.\u003C\u002Fp>\n\u003Ch2 id=\"les-limites\">Les limites\u003C\u002Fh2>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Limite\u003C\u002Fth>\n\u003Cth>Valeur\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Mémoire\u003C\u002Ftd>\n\u003Ctd>64 Mio\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Un tour\u003C\u002Ftd>\n\u003Ctd>2 s, 500 ms quand il tient une décision\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">net\u002Ffetch\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>16 à la fois\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">net\u002Fstream\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>8 à la fois\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>WebSocket\u003C\u002Ftd>\n\u003Ctd>4 à la fois\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Minuteurs\u003C\u002Ftd>\n\u003Ctd>64 à la fois\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Programmes\u003C\u002Ftd>\n\u003Ctd>4 à la fois\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Le temps d’attente du réseau ne compte pas : il n’est pas dans un tour. Une instance qui dépasse, ou qui panique, est finie et refaite, avec les mêmes trois relances en cinq minutes qu’un process. Le journal dit pourquoi, et ce qu’elle tenait lui est redonné par \u003Ccode class=\"key\">agent\u002Fattach\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"ecrire-une-source\">Écrire une source\u003C\u002Fh2>\n\u003Cp>Une source du Kanban sert \u003Ccode class=\"key\">tickets\u002F*\u003C\u002Fcode>. Le kit fait presque tout : \u003Ccode class=\"key\">Hosts\u003C\u002Fcode> répond le protocole, demande le jeton du compte, garde un client par compte et met les erreurs dans la forme attendue. Un plugin donne sa sorte (\u003Ccode class=\"key\">SourceKind\u003C\u002Fcode>) et un client (\u003Ccode class=\"key\">TicketSource\u003C\u002Fcode>) :\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Flinear\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">impl SourceKind for LinearSource {\n    fn id(&amp;self) -&gt; &amp;str {\n        ID\n    }\n    fn name(&amp;self) -&gt; &amp;str {\n        S\n    }\n    fn client(&amp;self, http: reqwest::Client, account: &amp;Credentials) -&gt; Box&lt;dyn TicketSource&gt; {\n        let base = account.api.clone().filter(|a| !a.trim().is_empty());\n        Box::new(Linear::new(http, account, base.as_deref().unwrap_or(API)))\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>et les branche, dans \u003Ccode class=\"key\">run\u003C\u002Fcode> :\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Flinear\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub fn run() -&gt; i32 {\n    escouade_plugin_kit::serve::run(\n        &quot;linear&quot;,\n        escouade_plugin_kit::hosts::Hosts::new(vec![], vec![Box::new(LinearSource)]),\n    )\n}\n\nescouade_plugin_kit::protocol::module!(run);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Le manifeste déclare le compte et la source, avec \u003Ccode class=\"key\">network\u003C\u002Fcode> et \u003Ccode class=\"key\">account:&lt;nom&gt;\u003C\u002Fcode> dans \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode> :\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Flinear\u002Fescouade-plugin.json\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">    &quot;ticketSources&quot;: [{ &quot;id&quot;: &quot;linear&quot;, &quot;name&quot;: &quot;Linear&quot;, &quot;account&quot;: &quot;linear&quot; }]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Une erreur http reste une erreur : \u003Ccode class=\"key\">HttpError\u003C\u002Fcode> devient \u003Ccode>-(32000 + statut)\u003C\u002Fcode>, \u003Ccode class=\"key\">-32401\u003C\u002Fcode> pour un jeton refusé, et \u003Ccode class=\"key\">Refused\u003C\u002Fcode> devient \u003Ccode class=\"key\">-32010\u003C\u002Fcode>, un refus durable. Le code entier est dans \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fguillaume-gagnaire\u002Fescouade\u002Ftree\u002Fmain\u002Fplugins\u002Flinear\" rel=\"noopener\">\u003Ccode class=\"key\">plugins\u002Flinear\u003C\u002Fcode>\u003C\u002Fa>, avec Jira, Trello et ClickUp à côté.\u003C\u002Fp>\n\u003Ch2 id=\"ecrire-un-hebergeur\">Écrire un hébergeur\u003C\u002Fh2>\n\u003Cp>Un hébergeur de code sert \u003Ccode class=\"key\">forge\u002F*\u003C\u002Fcode>, de la même façon : \u003Ccode class=\"key\">ForgeKind\u003C\u002Fcode> et le client \u003Ccode class=\"key\">Forge\u003C\u002Fcode> du kit, dont chaque méthode répond \u003Ccode class=\"key\">Unsupported\u003C\u002Fcode> tant que tu ne l’écris pas. Le plugin Gitea, qui sert toutes les instances d’un service auto-hébergé, tient en quelques lignes :\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Fgitea\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">impl ForgeKind for GiteaForge {\n    fn id(&amp;self) -&gt; &amp;str {\n        &quot;gitea&quot;\n    }\n    fn name(&amp;self) -&gt; &amp;str {\n        &quot;Gitea \u002F Forgejo&quot;\n    }\n    fn client(&amp;self, http: reqwest::Client, account: &amp;Credentials) -&gt; Box&lt;dyn Forge&gt; {\n        let base = account\n            .api\n            .clone()\n            .filter(|a| !a.trim().is_empty())\n            .unwrap_or_else(|| api_base(&amp;account.url));\n        Box::new(pulls::Gitea::new(http, base, account.token.clone()))\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode class=\"key\">account.url\u003C\u002Fcode> est l’instance que l’utilisateur a saisie, d’où \u003Ccode class=\"key\">{account}\u003C\u002Fcode> dans \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode>. Un jeton refusé, une limite atteinte et le budget d’appels sont traités par \u003Ccode class=\"key\">Hosts\u003C\u002Fcode>. Le banc joue \u003Ccode class=\"key\">account\u002Fme\u003C\u002Fcode> et \u003Ccode>forge\u002FpullRequestsOfBranch\u003C\u002Fcode> dès que \u003Ccode class=\"key\">contributes.forges\u003C\u002Fcode> est déclaré.\u003C\u002Fp>\n\u003Ch2 id=\"ecrire-un-harness\">Écrire un harness\u003C\u002Fh2>\n\u003Cp>Pour un agent qui parle ACP, le \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fharnesses\u002F\">manifeste\u003C\u002Fa> suffit, sans une ligne de code. Sinon, un harness est une machine à états sans attente, qui ne dépend que du protocole : \u003Ccode class=\"key\">turn::run\u003C\u002Fcode> tient la boucle, sur les deux cibles.\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Fcodex\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub fn run() -&gt; i32 {\n    let mut plugin = Plugin::default();\n    turn::run(\n        &quot;codex&quot;,\n        json!({ &quot;harnesses&quot;: [&quot;codex&quot;] }),\n        move |m, params| plugin.handle(m, params),\n    )\n}\n\nescouade_plugin_protocol::module!(run);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Le contrat est dans \u003Ccode>escouade_plugin_protocol::engine\u003C\u002Fcode>. Le cœur lance le programme de l’agent et lit sa sortie ; le plugin dit comment le lancer, puis traduit ses lignes en éléments de conversation. \u003Ccode class=\"key\">std::process::id\u003C\u002Fcode> se remplace par \u003Ccode class=\"key\">turn::instance_id\u003C\u002Fcode>, et \u003Ccode class=\"key\">std::fs\u003C\u002Fcode> par \u003Ccode class=\"key\">turn::read_file\u003C\u002Fcode>, sous une racine de \u003Ccode class=\"key\">permissions.files\u003C\u002Fcode>. Un client ACP se branche par \u003Ccode class=\"key\">escouade_acp::Agent\u003C\u002Fcode>, dont \u003Ccode class=\"key\">frames\u003C\u002Fcode> lit les lignes de l’agent.\u003C\u002Fp>\n\u003Ch2 id=\"ce-que-la-compilation-refuse\">Ce que la compilation refuse\u003C\u002Fh2>\n\u003Cp>\u003Ccode>escouade-plugin build\u003C\u002Fcode> compile comme le store, et \u003Ccode>escouade-plugin validate\u003C\u002Fcode> dit la même chose sans rien lancer. Un plugin compile \u003Cstrong>seul dans son dossier\u003C\u002Fstrong>, sur une copie, avec rustc 1.98.0 et \u003Ccode class=\"key\">wasm32-wasip1\u003C\u002Fcode> que la commande télécharge elle-même, un profil imposé et \u003Ccode class=\"key\">--locked\u003C\u002Fcode> : pour les mêmes sources et le même \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode>, le module est le même à l’octet sous Windows et sous Linux.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Refusé\u003C\u002Fth>\n\u003Cth>Pourquoi\u003C\u002Fth>\n\u003Cth>Quoi faire\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Pas de \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Le store compile ce qu’il dit, et rien d’autre.\u003C\u002Ftd>\n\u003Ctd>Commite-le.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Un \u003Ccode class=\"key\">build.rs\u003C\u002Fcode> dans le plugin\u003C\u002Ftd>\n\u003Ctd>Il tournerait en natif sur le store.\u003C\u002Ftd>\n\u003Ctd>Génère le code avant et commite-le.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Une macro procédurale à toi\u003C\u002Ftd>\n\u003Ctd>La même raison.\u003C\u002Ftd>\n\u003Ctd>Une macro \u003Ccode class=\"key\">macro_rules!\u003C\u002Fcode>, ou un crate de la liste.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Un crate hors de la liste fermée qui tourne à la compilation\u003C\u002Ftd>\n\u003Ctd>Un script, une macro ou ce dont ils dépendent. Ce qui ne tourne qu’en WASM n’a pas besoin d’y être.\u003C\u002Ftd>\n\u003Ctd>Retire-le, ou reste sur les versions de la liste.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Une dépendance \u003Ccode class=\"key\">git\u003C\u002Fcode> ou d’un autre registre\u003C\u002Ftd>\n\u003Ctd>Le lock doit venir de crates.io seul.\u003C\u002Ftd>\n\u003Ctd>Publie-la sur crates.io.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Un \u003Ccode class=\"key\">path\u003C\u002Fcode> qui sort du dossier\u003C\u002Ftd>\n\u003Ctd>Le store ne lit que le dossier du plugin.\u003C\u002Ftd>\n\u003Ctd>Garde tes sous-crates dans le dossier.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">[patch]\u003C\u002Fcode>, \u003Ccode class=\"key\">[replace]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Le module ne serait plus ce que dit le lock.\u003C\u002Ftd>\n\u003Ctd>Retire-les.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">.cargo\u002Fconfig.toml\u003C\u002Fcode>, \u003Ccode class=\"key\">rust-toolchain\u003C\u002Fcode>, \u003Ccode class=\"key\">[profile.*.package]\u003C\u002Fcode>, \u003Ccode class=\"key\">build-override\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>La chaîne et le profil sont imposés.\u003C\u002Ftd>\n\u003Ctd>Retire-les. Un \u003Ccode class=\"key\">[profile]\u003C\u002Fcode> simple est permis, et ignoré.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Autre chose qu’un \u003Ccode class=\"key\">cdylib\u003C\u002Fcode> (une \u003Ccode class=\"key\">rlib\u003C\u002Fcode> à côté est permise) ; un \u003Ccode class=\"key\">[[bin]]\u003C\u002Fcode>, \u003Ccode class=\"key\">src\u002Fmain.rs\u003C\u002Fcode>, \u003Ccode class=\"key\">src\u002Fbin\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un plugin est un module, sans \u003Ccode class=\"key\">main\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>crate-type = [&quot;cdylib&quot;]\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">module!\u003C\u002Fcode> manquant\u003C\u002Ftd>\n\u003Ctd>Le module n’a pas l’export \u003Ccode class=\"key\">escouade-turn\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>La ligne de \u003Ccode class=\"key\">module!\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cdiv class=\"callout warning\">\u003Cp class=\"callout-title\">Attention\u003C\u002Fp>\u003Cp>La liste fermée nomme des versions : \u003Ccode class=\"key\">serde\u003C\u002Fcode> 1.0.229, \u003Ccode class=\"key\">syn\u003C\u002Fcode> 3.0.9… Un \u003Ccode>cargo update\u003C\u002Fcode> qui monte l’un de ces crates peut faire refuser le plugin (« … n’est pas sur la liste relue »). Lance \u003Ccode>escouade-plugin validate\u003C\u002Fcode> et \u003Ccode class=\"key\">build\u003C\u002Fcode> après chaque mise à jour du lock. Le modèle écrit un \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode> qui passe.\u003C\u002Fp>\n\u003C\u002Fdiv>\n\u003Ch2 id=\"ensuite\">Ensuite\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">Tester un plugin\u003C\u002Fa> : \u003Ccode>cargo test\u003C\u002Fcode>, le banc et le faux Escouade.\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fpublishing\u002F\">Publier sur le store\u003C\u002Fa> : l’étiquette, les contrôles, la revue.\u003C\u002Fli>\n\u003C\u002Ful>\n",[8,12,15,18,22,25,28,31,34,37],{"id":9,"text":10,"depth":11},"les-trois-crates","Les trois crates",2,{"id":13,"text":14,"depth":11},"un-plugin","Un plugin",{"id":16,"text":17,"depth":11},"le-modele-par-tours","Le modèle par tours",{"id":19,"text":20,"depth":21},"la-porte-synchrone","La porte synchrone",3,{"id":23,"text":24,"depth":11},"les-limites","Les limites",{"id":26,"text":27,"depth":11},"ecrire-une-source","Écrire une source",{"id":29,"text":30,"depth":11},"ecrire-un-hebergeur","Écrire un hébergeur",{"id":32,"text":33,"depth":11},"ecrire-un-harness","Écrire un harness",{"id":35,"text":36,"depth":11},"ce-que-la-compilation-refuse","Ce que la compilation refuse",{"id":38,"text":39,"depth":11},"ensuite","Ensuite",1793889379716]