CyberTrainTutoriel

Premiers pas : le blog

Calqué sur le guide Getting Started de Rails : un blog avec des articles et des commentaires. Vous allez générer le scaffold des articles, ajouter des validations, imbriquer les commentaires sous les articles, écrire un test qui pilote l’application dans le même processus, et compiler l’application dans un dossier dist/ déployable. L’application terminée est examples/blog, testée par la CI sous Linux et macOS.

Prérequis
Ruby 3.2+, cc, make, git, curl, en-têtes SQLite 3 (Spinel 2026.09.12 est installé pour vous) Étape 00
Point de départ
gem install cybertrain
Résultat
dist/blog

Vous êtes ici :Installation Ruby, cc, SQLite

Prérequis

PourquoiCyberTrain compile via Spinel vers du C : il lui faut donc une chaîne de compilation C et les en-têtes SQLite ; la commande cybertrain est une gem, il lui faut donc Ruby.

  • Ruby 3.2+ et RubyGems, uniquement pour la commande cybertrain ; les applications ne tournent jamais sur CRuby.
  • Spinel 2026.09.12, non testé avec d’autres versions. Vous ne l’installez pas à la main : la première commande qui en a besoin compile cette version dans ~/.cybertrain (quelques minutes, une seule fois), sauf si un spinel de cette version est déjà dans le PATH.
  • Une chaîne de compilation C (cc, ou le compilateur désigné par CC) et make, plus git et curl, pour compiler Spinel, qui compile chaque programme en C.
  • En-têtes et bibliothèque SQLite 3 : libsqlite3-dev sous Debian/Ubuntu ; fournis avec les Command Line Tools de Xcode sous macOS.
  • Les en-têtes OpenSSL (libssl-dev) sont facultatifs : sans eux, Spinel est compilé sans son paquet openssl, que CyberTrain n’utilise pas.

cybertrain doctor vérifie ces outils et en-têtes ainsi que l’installation de Spinel ; quand un élément requis manque, il affiche aussi la commande d’installation pour votre plateforme.

Comment CyberTrain installe SpinelCe qui se passe la première fois qu’une commande a besoin de spin

cybertrain new, db, server, build et spin cherchent d’abord Spinel 2026.09.12 : un spinel et un spin de cette version déjà dans le PATH, sinon la copie sous ~/.cybertrain/spinel/2026.09.12/, sinon ils compilent cette copie à partir du tag de version de Spinel (git clone, make deps, make -j, make install), une seule fois, en quelques minutes. Le journal est ~/.cybertrain/log/spinel-2026.09.12-build.log ; s’il manque un outil nécessaire à la compilation, cybertrain indique lequel et affiche la commande d’installation pour votre plateforme avant de commencer.

cybertrain setup fait la même chose à la demande (--force recompile la copie sous ~/.cybertrain) et, quand c’est sa propre copie qui est utilisée, affiche la ligne qui la place dans votre PATH, pour lancer spin vous-même :

terminal — n’importe quel répertoireshell
export PATH="$HOME/.cybertrain/bin:$PATH"

~/.cybertrain/bin contient des liens spinel et spin vers la version utilisée. S’il n’est pas dans le PATH, cybertrain spin ARGS... lance spin avec cette version depuis n’importe quel répertoire. CYBERTRAIN_HOME remplace ~/.cybertrain, et CYBERTRAIN_SPINEL_HOME=PREFIX utilise à la place le Spinel installé dans PREFIX (ce doit être cette version). Un spinel d’une autre version présent dans le PATH n’est jamais utilisé ni modifié.

Pas de machine sur laquelle installer ? Le bac à sable ouvre ce blog dans un GitHub Codespace avec les étapes 02, 03, 04 et 07 déjà faites.

Vous êtes ici :Installation la CLI cybertrain

Installer la CLI

Pourquoicybertrain est la commande qui crée les applications, génère les scaffolds de ressources, migre, sert et compile.

La première ligne l’installe depuis RubyGems. Le reste est le tutoriel entier en un coup d’œil ; les étapes ci-dessous le reprennent une commande à la fois.

terminal — n’importe quel répertoire, puis blog/shell
gem install cybertrain
cybertrain new blog          # installe Spinel 2026.09.12 la première fois (quelques minutes)
cd blog
cybertrain g scaffold article title:string body:text
cybertrain db migrate
cybertrain server            # http://127.0.0.1:3000
cybertrain build             # production: dist/blog, dist/public/, dist/storage/, dist/tmp/

La commande cybertrain est en Ruby pur et la gem l’exécute sous CRuby (3.2 ou plus récent) ; ni le framework ni Spinel ne sont dans la gem. Ses commandes (cybertrain help les affiche) :

  • cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin] crée une application dans NAME, puis y lance spin lock et spin run gen, en installant d’abord Spinel si nécessaire.
  • cybertrain generate scaffold NAME field:type ... (alias g scaffold) ajoute une ressource à l’application du répertoire courant.
  • cybertrain db migrate génère, applique les migrations en attente, puis génère à nouveau (spin run gen; spin run db -- migrate; spin run gen) ; cybertrain migration est conservé comme alias.
  • cybertrain db COMMAND... lance toute autre commande de base de données après spin run gen : status, rollback [N] (qui génère à nouveau ensuite), schema:dump, create.
  • cybertrain server [PORT] génère, puis démarre le serveur de développement (spin run gen; spin run NAME, ou spin run NAME -- PORT ; port 3000 par défaut).
  • cybertrain build compile NAME avec app/views/ intégré et assemble dist/ (le binaire, public/, storage/, tmp/).
  • cybertrain spin ARGS... lance spin avec le Spinel épinglé, depuis n’importe quel répertoire : cybertrain spin test, cybertrain spin run gen -- --check.
  • cybertrain setup [--force] installe Spinel 2026.09.12 dans ~/.cybertrain, sauf si un spinel de cette version est déjà dans le PATH ; --force recompile la copie sous ~/.cybertrain.
  • cybertrain doctor vérifie la chaîne de compilation C, les en-têtes SQLite et l’installation de Spinel.
  • cybertrain version affiche la version (cybertrain 0.2.1) ; cybertrain help affiche l’aide.

db, server et build s’exécutent dans une application ; NAME est le [package] name de son spin.toml.

Vous êtes ici :Vous écrivez blog/

Créer l’application

PourquoiVous obtenez les conventions de répertoires de Rails, plus trois programmes dans bin/ : bin/blog.rb (l’application), bin/gen.rb (le générateur) et bin/db.rb (les migrations en développement).

terminal — n’importe quel répertoireshell
cybertrain new blog
cd blog

cybertrain new écrit l’application avec un spin.toml qui pointe vers la version correspondant à la CLI :

spin.tomltoml
[package]
name = "blog"
version = "0.1.0"

[dependencies]
cybertrain = { git = "https://github.com/saeki-mototsune/cybertrain", ref = "v0.2.1" }

Ensuite, il s’assure que Spinel est installé (la première fois, cela signifie compiler Spinel 2026.09.12 dans ~/.cybertrain : quelques minutes, une seule fois ; voir l’étape 00) et lance spin lock (spin récupère le framework dans son cache, ~/.cache/spin/packages/, et épingle le commit dans spin.lock) et spin run gen, un peu comme rails new lance bundle install, pour que cybertrain server fonctionne tout de suite. Il termine en affichant les étapes suivantes. Rien du framework n’est copié dans l’application ; commitez spin.toml et spin.lock.

Si Spinel ou l’une des deux étapes spin échoue, les fichiers restent en place, new sort avec le code 1 et affiche ce qu’il faut lancer une fois la cause corrigée (pour Spinel : cybertrain setup, puis cybertrain spin lock && cybertrain spin run gen dans le nouveau répertoire). --skip-spin se contente d’écrire les fichiers ; --path DIR fait plutôt dépendre l’application d’une copie locale du dépôt, --git URL [--ref R] du framework situé à URL (à la branche ou au tag R), et --version V d’une version de l’index, une fois que cybertrain sera sur un spin-index.

Les noms d’application utilisent des lettres minuscules, des chiffres et _, et commencent par une lettre. L’arborescence générée :

blog/text
blog/
  spin.toml  config/app.rb  config/routes.rb
  db/schema.rb  db/migrate/
  app/controllers/application_controller.rb  app/models/  app/views/  app/helpers/
  public/  bin/blog.rb  bin/gen.rb  bin/db.rb
  gen/  storage/  tmp/  test/

bin/blog.rb est l’unique point d’entrée de l’application, le programme que compile spin build blog : ./blog sert l’application, ./blog migrate et ./blog db status gèrent la base de données. bin/gen.rb lance le générateur (spin run gen), et bin/db.rb lance le migrateur pendant le développement (cybertrain db migrate l’utilise, car bin/blog.rb ne peut pas compiler tant que la première migration n’a pas généré gen/models/). gen/ contient déjà views.rb, la table des vues intégrées, versionnée et vide.

ApplicationController commence ainsi, pour qu’un enregistrement introuvable donne une 404 via rescue_from :

app/controllers/application_controller.rbruby
class ApplicationController < Cybertrain::Controller
  rescue_from Cybertrain::RecordNotFound, with: :record_not_found

  private

  def record_not_found
    render plain: "Not Found", status: :not_found
  end
end

Vous êtes ici :Vous écrivez app/, db/migrate/

Scaffold d’un article

PourquoiUne seule commande écrit toute la tranche CRUD, à la Rails.

terminal — blog/shell
cybertrain generate scaffold article title:string body:text

Elle écrit une migration, un modèle, un contrôleur et cinq vues (index, show, new, edit, _form), et insère resources :articles dans config/routes.rb. Les types de champ sont string, text, integer, float, boolean, date, datetime et references ; le type par défaut est string. Le cybertrain g scaffold du démarrage rapide est la même commande.

Les fichiers existants ne sont jamais écrasés : chaque fichier est signalé comme create, identical ou exist.

Vous êtes ici :Vous écrivez config/routes.rb

Définir la route racine

Pourquoi/ doit afficher la liste des articles.

config/routes.rbruby
# config/routes.rb
Cybertrain::Routes.draw do
  root "articles#index"
  resources :articles
end

Les routes sont lues à la compilation : spin run gen les transforme en gen/routes.rb. cybertrain server, cybertrain db migrate et cybertrain build le lancent pour vous.

Vous êtes ici :Vous écrivez app/models/

Ajouter des validations au modèle

PourquoiComme dans le guide Rails, ajoutez à la main une validation de longueur.

app/models/article.rbruby
# app/models/article.rb -- only the first string field gets a default
# presence validation; add anything past that yourself.
class Article
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

class Article n’a pas de superclasse écrite : la superclasse Cybertrain::Model, les colonnes et les finders viennent de gen/models/article.rb. Un enregistrement qui échoue signale des messages tels que « Title can't be blank » et « Body is too short (minimum is 10 characters) ».

Vous êtes ici :Vous écrivez app/controllers/

Le contrôleur ArticlesController

PourquoiUn CRUD complet, à la Rails jusqu’aux strong params et au flash suivi d’une redirection, à deux choses près que Spinel impose partout.

app/controllers/articles_controller.rbruby
# app/controllers/articles_controller.rb (index/show/edit are as plain as
# Rails' own scaffold; update mirrors create)
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]

  def new_action   # a `new` method would shadow ArticlesController.new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      flash[:notice] = "Article was successfully created."
      redirect_to article_path(@article), status: :see_other
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end

  def article_params
    params.require(:article).permit(:title, :body)
  end
end

Le scaffold écrit le contrôleur complet, avec aussi index, show, edit, update et destroy ; l’extrait ci-dessus les omet. Dans index, @articles = Article.all.to_a : le .to_a est obligatoire.

Les variables d’instance sont trouvées par un balayage lexical des @ivar = : relancez donc cybertrain spin run gen après en avoir ajouté une ; le serveur de développement le fait pour vous.

Vous êtes ici :Compilation, puis exécution gen/ → build/bin/blog

Générer, migrer, lancer

PourquoiTransformer le schéma et les routes en gen/, créer la base SQLite, démarrer le serveur.

terminal — blog/shell
cybertrain db migrate    # spin run gen; spin run db -- migrate; spin run gen
cybertrain server        # http://127.0.0.1:3000; recompile après une modification Ruby, les vues se rechargent sans recompilation

spin run gen lit db/schema.rb et config/routes.rb, analyse app/ et écrit gen/ ; spin run db -- migrate applique db/migrate/*.rb et réécrit db/schema.rb. L’ordre compte : le premier spin run gen place la nouvelle migration dans gen/migrations.rb, que requiert bin/db.rb, et le second construit les modèles à partir du db/schema.rb réécrit.

cybertrain server affiche chaque commande spin qu’il lance, puis la bannière de démarrage de l’application. Le premier démarrage compile l’application, il prend donc un moment :

sortie — cybertrain servertext
=> Booting cybertrain 0.2.1
=> development environment (1 worker)
=> Watching app/, config/ and db/schema.rb for changes
* Listening on http://127.0.0.1:3000
Use Ctrl-C to stop
  • Relancez cybertrain spin run gen et commitez gen/ après avoir modifié le schéma, les routes, ou les callbacks et ivars d’un contrôleur ou d’un modèle. Le serveur de développement le fait pour vous.
  • Changez le port avec cybertrain server 4000 (ou PORT=4000).
  • cybertrain db ajoute le -- dont spin run db a besoin avant ses arguments ; cybertrain db status et les autres commandes de base de données fonctionnent de la même façon.

Vous êtes ici :Vous écrivez db/migrate/, app/

Scaffold des commentaires

PourquoiLe type references vous donne la clé étrangère dont l’association est déduite.

terminal — blog/shell
cybertrain generate scaffold comment commenter:string body:text article:references
cybertrain db migrate    # le scaffold a écrit une migration pour la table comments

article:references ajoute une colonne article_id, un index et une clé étrangère — de quoi donner à Article/Comment leur association ; il n’y a pas de has_many/belongs_to à écrire, spin run gen la lit dans la clé étrangère. Le schéma obtenu :

db/schema.rb (extrait)ruby
  s.create_table "comments" do |t|
    t.string "commenter"
    t.text "body"
    t.integer "article_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  s.add_index "comments", ["article_id"], name: "index_comments_on_article_id"

  s.add_foreign_key "comments", "articles", column: "article_id"

Le scaffold insère une ligne resources :comments indépendante, que l’étape suivante remplace.

Vous êtes ici :Vous écrivez config/routes.rb

Imbriquer les routes

PourquoiLe guide Rails imbrique les commentaires sous les articles.

config/routes.rbruby
Cybertrain::Routes.draw do
  root "articles#index"

  # A nested block takes the mapper explicitly (Spinel's instance_eval
  # trampoline only rewires `self` for the outermost block): `articles.resources`.
  resources :articles do |articles|
    articles.resources :comments, only: [:create, :destroy]
  end
end

Vous êtes ici :Vous écrivez app/controllers/

Le contrôleur CommentsController

PourquoiSeules deux actions sont accessibles, et l’article parent vient de l’URL (params[:article_id]), pas de :id.

app/controllers/comments_controller.rbruby
class CommentsController < ApplicationController
  before_action :set_article

  def create
    @comment = Comment.new(comment_params)
    @comment.article_id = @article.id
    if @comment.save
      flash[:notice] = "Comment was successfully created."
    else
      flash[:alert] = "Comment could not be saved: #{@comment.errors.full_messages.join(", ")}"
    end
    redirect_to article_path(@article), status: :see_other
  end

  def destroy
    @comment = Comment.where(article_id: @article.id).find(params[:id])
    @comment.destroy
    flash[:notice] = "Comment was successfully destroyed."
    redirect_to article_path(@article), status: :see_other
  end

  private

  def set_article
    @article = Article.find(params[:article_id])
  end

  def comment_params
    params.require(:comment).permit(:commenter, :body)
  end
end

Ajoutez ensuite des validations au modèle de commentaire :

app/models/comment.rbruby
class Comment
  validates :commenter, presence: true
  validates :body, presence: true
end

Article#comments est ici un Array : le @article.comments.find(params[:id]) de Rails devient donc Comment.where(article_id: @article.id).find(params[:id]). Un commentaire placé sous le mauvais article renvoie une 404.

Vous êtes ici :Exécution app/views/, lues depuis le disque en développement, intégrées par cybertrain build

Vues et partiels

PourquoiLa page d’un article liste ses commentaires et se termine par un formulaire lié à [@article, @comment].

Supprimez d’abord les vues de commentaire index, show, new et edit, désormais inaccessibles, et donnez à ArticlesController#show un @comment vide :

app/controllers/articles_controller.rb (extrait)ruby
  def show
    @comment = Comment.new
  end

Affichez ensuite les commentaires et le formulaire sur la page de l’article :

app/views/articles/show.html.erb · app/views/comments/_form.html.erberb
<%# app/views/articles/show.html.erb %>
<% @article.comments.each do |comment| %>
  <%= render "comments/comment", comment: comment %>
<% end %>
<%= render "comments/form" %>

<%# app/views/comments/_form.html.erb %>
<%= form_with(model: [@article, @comment]) do |f| %>
  <%= f.label :commenter %> <%= f.text_field :commenter %>
  <%= f.label :body %> <%= f.text_area :body %>
  <%= f.submit %>
<% end %>

Le partiel de commentaire, tel qu’il est livré dans l’exemple :

app/views/comments/_comment.html.erberb
<%# locals: (comment:) %>
<div class="comment">
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <%= button_to "Destroy comment", [@article, comment], method: :delete %>
</div>

button_to ... [@article, comment] et form_with(model: [@article, @comment]) se résolvent tous deux selon la même règle [parent, child] : un nouvel enfant est routé vers la collection imbriquée (article_comments_path), un enfant persisté vers le membre imbriqué (article_comment_path) — générés dans gen/routes.rb à partir du bloc resources imbriqué ci-dessus.

Vous êtes ici :Compilation cybertrain spin test

Tester

Pourquoiminitest et RSpec trouvent les tests par réflexion, ce qu’un compilateur AOT ne peut pas faire ; CyberTrain fournit donc Cybertrain::Test.

Un test de modèle

test/articles.rb (fragment)ruby
require_relative "support/blog_test"   # the app, BLOG and Cybertrain::Test

test "title must be present" do
  article = Article.new(body: "a body long enough to pass length")
  refute article.save
  assert_includes article.errors.full_messages, "Title can't be blank"
end

Assertions : assert, refute, assert_equal, assert_nil, assert_includes, assert_raises("Class") { }, flunk. L’exemple utilise aussi assert_response et assert_redirected_to.

Un parcours HTTP complet

test/articles.rb (fragment)ruby
require_relative "support/blog_test"   # the app, BLOG and Cybertrain::Test

test "POST /articles creates an article and redirects to it" do
  client = Cybertrain::Test::Client.new(BLOG)
  client.get("/articles/new")
  res = client.post("/articles", { "authenticity_token" => BlogTest.form_token(client),
                                   "article[title]" => "Hello Rails",
                                   "article[body]" => "I am on Rails! This is my first article." })
  assert_redirected_to res, BlogTest.article_path(Article.last)
  assert_response res, :see_other
end

Cybertrain::Test::Client pilote une application dans le même processus (sans socket) avec une cookie jar, de sorte que les sessions et le CSRF se comportent comme derrière un navigateur. Les requêtes sans le jeton reçoivent une 403.

Lancer les tests

terminal — blog/shell
cybertrain spin test

cybertrain spin test lance spin test avec le Spinel épinglé ; avec la chaîne d’outils dans le PATH (étape 00), spin test fait la même chose. Il compile chaque test/*.rb en un programme distinct et compare (diff) sa sortie (stdout et stderr ensemble) à un snapshot test/<name>.rb.expected versionné. Les fichiers de test/support/ sont requis, pas exécutés. Pour écrire un snapshot :

terminal — blog/shell
cybertrain spin test --regen test/<name>.rb
rm -f build/test/<name>; cybertrain spin test test/<name>.rb; [ -x build/test/<name> ] && ./build/test/<name> > test/<name>.rb.expected 2>&1

--regen écrit le snapshot à partir de la sortie de CRuby. Les tests qui accèdent à SQLite via FFI ne peuvent pas tourner sous CRuby : leur snapshot vient donc du binaire compilé (la deuxième ligne ci-dessus). cybertrain spin test sort avec un code non nul tant qu’un snapshot est périmé, il est donc suivi de ; et non de && ; la vérification [ -x ] saute la capture si la compilation a échoué, ce qui conserve l’ancien snapshot. Les tests qui ne peuvent pas tourner sous CRuby ont besoin de ce snapshot avant que cybertrain spin test puisse réussir ; les autres tests sans snapshot sont comparés à la sortie de CRuby.

Vous êtes ici :Exécution dist/blog

Compiler pour le déploiement

PourquoiUne seule commande compile l’application dans un dossier dist/ déployable, avec toutes les vues intégrées.

terminal — blog/shell
cybertrain build

Elle écrit dist/ : le binaire de l’application avec toutes les vues intégrées, plus les fichiers qu’il utilise, à côté de lui. Elle lance spin run gen -- --embed-views, spin build blog et spin run gen (qui remet en place le gen/views.rb versionné et vide, même après un échec ou un Ctrl-C), puis assemble :

  • dist/blogle framework compilé, les contrôleurs, les modèles et l’interpréteur de templates, avec toutes les vues intégrées
  • dist/public/ressources statiques et pages d’erreur, ou laissez un reverse proxy le servir
  • dist/storage/créé vide ; contient le fichier SQLite par défaut (une recompilation ne le vide jamais)
  • dist/tmp/créé vide (une recompilation ne le vide jamais)

Copiez dist/ sur une machine avec le même OS et le même CPU (le binaire est lié à la SQLite du système) et lancez-le là-bas. Un binaire compilé est en mode production par défaut (CYBERTRAIN_ENV permet de le changer). Définissez CYBERTRAIN_SECRET_KEY_BASE ; à titre d’illustration :

terminal — hôte de productionshell
cd dist
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)"
./blog migrate && ./blog

./blog migrate applique les migrations compilées dans le binaire, et ./blog db status affiche leur état ; PORT définit le port. Gardez le même secret d’un redémarrage à l’autre : le changer met fin à toutes les sessions.

Cybertrain::Config lit d’abord l’environnement ; config/app.rb a le dernier mot :

VariableAttributPar défaut
CYBERTRAIN_ENVenv"development" ; "production" dans un binaire issu de cybertrain build (aussi "test")
PORTport3000
CYBERTRAIN_DATABASEdatabase_pathstorage/#{env}.sqlite3
CYBERTRAIN_SECRET_KEY_BASEsecret_key_baserequis en production ; en dev/test, un secret est généré automatiquement dans tmp/secret_key
SPINEL_WORKERSworkers1
CYBERTRAIN_HOSThost"127.0.0.1" ; 0.0.0.0 écoute sur toutes les interfaces (conteneurs)
CYBERTRAIN_SESSION_SAME_SITEsession_same_site"Lax" ; aussi Strict ou None (None ajoute toujours Secure) ; toute autre valeur arrête le serveur au démarrage
CYBERTRAIN_SESSION_PARTITIONEDsession_partitionedfalse ; 1 ou true ajoute Partitioned (et Secure)
config/app.rbruby
# config/app.rb
Cybertrain.configure do |c|
  c.port = 3000
  c.workers = 1
end
  • Le serveur parle du HTTP/1.1 simple : placez nginx ou Caddy devant pour le TLS et HTTP/2. Le cookie de session est Secure en production, servez donc l’application en HTTPS (ne définissez c.session_secure = false dans config/app.rb que si la production est vraiment en http simple).
  • En production, l’application refuse de démarrer sans CYBERTRAIN_SECRET_KEY_BASE.
  • En production, une exception renvoie une 500, et une réponse d’erreur brute (la 404 en texte brut du routeur, head :not_found, render plain:) est remplacée par public/<status>.html quand ce fichier existe ; cybertrain new écrit 404.html et 500.html.
  • SIGTERM cesse d’accepter des connexions, laisse se terminer les requêtes en cours et quitte quand elles sont terminées, ou après le drain_timeout du serveur (10 s).
  • La surveillance des fichiers et la boucle de recompilation ne tournent jamais en production. Un cybertrain server en cours d’exécution saute ses recompilations tant que cybertrain build détient tmp/cybertrain-build.lock.

Bon à savoir

PourquoiDes limites et particularités à connaître avant qu’elles ne vous surprennent.

  • Les sessions ne contiennent que des String, dans un cookie signé HMAC.
  • SQLite uniquement, via la FFI de Spinel.
  • Pas de backtraces : les erreurs affichent la classe, le message, la ligne de requête et le nom/la ligne du template.
  • Un attribut mal orthographié dans une vue s’affiche vide au lieu de lever une erreur ; c’est une lacune connue.
  • Les associations viennent uniquement des clés étrangères du schéma ; pas de has_many :through, includes, pluck, dependent:, d’enums, de STI ni d’associations polymorphes.
  • Pas de namespace, de respond_to/formats, de constraints ni de mount ; render json: est minimal.
  • Dans un modèle généré par scaffold, seul le premier champ string reçoit une validation de présence par défaut.
  • Le générateur utilise un balayage lexical (des regex), pas un parseur, sur app/controllers et app/models.
  • Les vues sont intégrées à la compilation : une modification qui ne touche que les vues nécessite cybertrain build et un redémarrage avant d’atteindre la production.
  • Compilez dist/ sur le même OS et le même CPU que la machine qui l’exécute : le binaire est lié à la SQLite du système.
  • SPINEL_WORKERS vaut 1 par défaut.
  • Seul Spinel 2026.09.12 est testé.

Différences avec Rails

PourquoiLe tableau complet du README, en un seul endroit.

Différences entre Rails et CyberTrain (tableau complet du README)
RailsCyberTrain
rails consolePas de console — Spinel n’a pas d’eval
Modifiez le code, l’application en cours d’exécution le prend en compteLe Ruby demande une recompilation ; en développement, cybertrain server s’en charge pour vous (recompilation, arrêt, execv)
Modifiez une vue, aucun rechargement nécessairePareil — en développement, les vues sont analysées depuis le disque à chaque requête
def newdef new_action (new masquerait Klass.new(ctx))
before_action { do_thing } (self implicite)before_action { |c| c.do_thing } — pas d’instance_exec sur un bloc stocké, donc les callbacks reçoivent explicitement le contrôleur/l’enregistrement
resources :posts do resources :comments endresources :posts do |posts| posts.resources :comments end — les blocs imbriqués reçoivent le mapper explicitement
ERB est compilé en méthode ; n’importe quel objet, n’importe quelle méthodeInterprété selon une grammaire/table de dispatch fixe
La session contient n’importe quel objet sérialisé (marshal)Valeurs de session de type String uniquement, cookie signé HMAC
De nombreux adaptateurs de base de donnéesSQLite uniquement, via FFI
has_many :through, includes, pluck, dependent:, enums, STI, associations polymorphesNon implémenté ; les associations viennent uniquement des clés étrangères du schéma
namespace, format/respond_to, constraints, mountNon implémenté — noms plats, render json: uniquement
Backtrace complète lors d’une exceptionClasse, message, ligne de requête, nom/ligne du template — Spinel n’expose pas de backtraces
Rack, ses middlewares, et n’importe quelle gem d’un GemfilePas de compatibilité Rack ; un petit ensemble fixe de middlewares ; le spin-index propre à Spinel, limité à ce qui compile avec son sous-ensemble de Ruby
minitest / RSpecCybertrain::Test — les runners fondés sur la réflexion ne peuvent pas fonctionner avec une compilation AOT
Authentification, mailers, jobs, ActionCable, asset pipeline, i18nNon développé (hors du MVP)

Liste non exhaustive.

Pour aller plus loin