CyberTrainチュートリアル

はじめよう:ブログを作る

Rails 公式の Getting Started に沿って、記事とコメントのあるブログを作ります。記事をスキャフォールドし、バリデーションを追加し、記事の下にコメントをネストし、アプリをプロセス内で動かすテストを書き、最後にアプリをデプロイ可能な dist/ フォルダーにビルドします。完成したアプリは examples/blog にあり、Linux と macOS の CI で検証されています。

必要なもの
Ruby 3.2+、cc、make、git、curl、SQLite 3 のヘッダー(Spinel 2026.09.12 は自動でインストールされます) ステップ 00
出発点
gem install cybertrain
完成物
dist/blog

現在地:準備 Ruby、cc、SQLite

必要なもの

なぜCyberTrain は Spinel を通じて C にコンパイルされるため、C ツールチェーンと SQLite のヘッダーが必要です。cybertrain コマンドは gem なので、Ruby が必要です。

  • Ruby 3.2+ と RubyGems。cybertrain コマンドのためだけに必要で、アプリケーションが CRuby 上で動くことはありません。
  • Spinel 2026.09.12。他のバージョンでは未テストです。手動でインストールする必要はありません:Spinel を必要とする最初のコマンドが、そのリリースを ~/.cybertrain にビルドします(数分、初回のみ)。ただし、そのリリースの spinel がすでに PATH 上にある場合は除きます。
  • C ツールチェーン(cc、または CC で指定したコンパイラ)と make、さらに git と curl。すべてのプログラムを C にコンパイルする Spinel をビルドするために使います。
  • SQLite 3 のヘッダーとライブラリ:Debian/Ubuntu では libsqlite3-dev、macOS では Xcode のコマンドラインツールに含まれています。
  • OpenSSL のヘッダー(libssl-dev)は任意です:ない場合、Spinel は openssl パッケージなしでビルドされますが、CyberTrain はこのパッケージを使いません。

cybertrain doctor はこれらのツールとヘッダー、Spinel のインストールを確認します。必要なものが欠けている場合は、プラットフォームに応じたインストールコマンドも表示します。

CyberTrain が Spinel をインストールする仕組みコマンドが初めて spin を必要としたときに起きること

cybertrain new、db、server、build、spin は、まず Spinel 2026.09.12 を探します:そのリリースの spinel と spin がすでに PATH 上にあればそれを、なければ ~/.cybertrain/spinel/2026.09.12/ 以下のコピーを使い、それもなければ Spinel のリリースタグからそのコピーをビルドします(git clone、make deps、make -j、make install)。ビルドは一度だけで、数分かかります。ログは ~/.cybertrain/log/spinel-2026.09.12-build.log です。ビルドに必要なツールが欠けている場合、cybertrain はビルドを始める前に、どれが足りないかを伝え、プラットフォームに応じたインストールコマンドを表示します。

cybertrain setup は同じことを求めに応じて行います(--force は ~/.cybertrain 以下のコピーを再ビルドします)。さらに、自前のコピーが使われている場合は、それを PATH に加える行を表示します。spin を自分で実行するためのものです:

ターミナル — 任意のディレクトリshell
export PATH="$HOME/.cybertrain/bin:$PATH"

~/.cybertrain/bin には、使用中のリリースを指す spinel と spin のリンクがあります。これを PATH に入れなくても、cybertrain spin ARGS... を使えば、任意のディレクトリからそのリリースの spin を実行できます。CYBERTRAIN_HOME は ~/.cybertrain の代わりになり、CYBERTRAIN_SPINEL_HOME=PREFIX を指定すると、代わりに PREFIX にインストールされた Spinel を使います(そのリリースである必要があります)。PATH 上にある別リリースの spinel が使われたり変更されたりすることはありません。

インストールできるマシンがない場合は、プレイグラウンドを使えば、ステップ 02、03、04、07 を済ませたこのブログを GitHub Codespace で開けます。

現在地:準備 cybertrain CLI

CLI をインストールする

なぜcybertrain は、アプリの作成、リソースのスキャフォールド、マイグレーション、サーバーの起動、ビルドを行うコマンドです。

1 行目で RubyGems からインストールします。残りはチュートリアル全体を一望したもので、以降のステップでは 1 コマンドずつ進めます。

ターミナル — 任意のディレクトリ、その後 blog/shell
gem install cybertrain
cybertrain new blog          # 初回は Spinel 2026.09.12 をインストール(数分)
cd blog
cybertrain g scaffold article title:string body:text
cybertrain db migrate
cybertrain server            # http://127.0.0.1:3000
cybertrain build             # 本番用: dist/blog, dist/public/, dist/storage/, dist/tmp/

cybertrain コマンドは素の Ruby で書かれており、gem はこれを CRuby(3.2 以降)で実行します。フレームワークも Spinel も gem には含まれません。コマンドは次のとおりです(cybertrain help で表示されます):

  • cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin] は NAME にアプリケーションを作成し、その中で spin lock と spin run gen を実行します。必要なら先に Spinel をインストールします。
  • cybertrain generate scaffold NAME field:type ...(エイリアスは g scaffold)は、カレントディレクトリのアプリケーションにリソースを追加します。
  • cybertrain db migrate は生成を行い、未適用のマイグレーションを適用して、もう一度生成します(spin run gen; spin run db -- migrate; spin run gen)。cybertrain migration もエイリアスとして残されています。
  • cybertrain db COMMAND... は spin run gen の後に、その他のデータベースコマンドを実行します:status、rollback [N](実行後にもう一度生成します)、schema:dump、create。
  • cybertrain server [PORT] は生成を行ってから開発サーバーを起動します(spin run gen; spin run NAME、または spin run NAME -- PORT。デフォルトのポートは 3000)。
  • cybertrain build は app/views/ を埋め込んで NAME をビルドし、dist/(バイナリ、public/、storage/、tmp/)を組み立てます。
  • cybertrain spin ARGS... は、固定された Spinel で spin を任意のディレクトリから実行します:cybertrain spin test、cybertrain spin run gen -- --check。
  • cybertrain setup [--force] は Spinel 2026.09.12 を ~/.cybertrain にインストールします(そのリリースの spinel がすでに PATH 上にある場合を除く)。--force は ~/.cybertrain 以下のコピーを再ビルドします。
  • cybertrain doctor は C ツールチェーン、SQLite のヘッダー、Spinel のインストールを確認します。
  • cybertrain version はバージョン(cybertrain 0.2.1)を、cybertrain help は使い方を表示します。

db、server、build はアプリケーションの中で実行します。NAME はそのアプリケーションの spin.toml にある [package] name です。

現在地:自分で書く blog/

アプリケーションを作成する

なぜRails のディレクトリ規約に加えて、3 つの bin/ プログラムが手に入ります:bin/blog.rb(アプリ本体)、bin/gen.rb(ジェネレーター)、bin/db.rb(開発用のマイグレーション)。

ターミナル — 任意のディレクトリshell
cybertrain new blog
cd blog

cybertrain new はアプリケーションを書き出します。その spin.toml は CLI と一致するリリースを指しています:

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

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

次に Spinel がインストールされていることを確認し(初回は Spinel 2026.09.12 を ~/.cybertrain にビルドします:数分、初回のみ。ステップ 00 を参照)、spin lock(spin がフレームワークをキャッシュ ~/.cache/spin/packages/ に取得し、コミットを spin.lock に固定します)と spin run gen を実行します。rails new が bundle install を実行するのと同じようなもので、これで cybertrain server がすぐに動きます。最後に次の手順を表示して終わります。フレームワークのファイルはアプリに一切コピーされません。spin.toml と spin.lock をコミットしてください。

Spinel のインストールか、2 つの spin ステップのいずれかが失敗した場合、ファイルは残したまま new は終了コード 1 で終了し、原因を解消した後に実行すべきコマンドを表示します(Spinel の場合:cybertrain setup を実行し、続いて新しいディレクトリで cybertrain spin lock && cybertrain spin run gen)。--skip-spin はファイルを書き出すだけです。--path DIR は代わりにローカルのチェックアウトに依存し、--git URL [--ref R] は URL にあるフレームワーク(ブランチまたはタグ R)に、--version V は cybertrain が spin-index に登録された後、インデックスのバージョンに依存します。

アプリ名には英小文字、数字、_ を使い、先頭は英字にします。生成されるツリー:

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 はアプリケーション唯一のエントリーポイントで、spin build blog がコンパイルするプログラムです:./blog でサーバーを起動し、./blog migrate と ./blog db status でデータベースを管理します。bin/gen.rb はジェネレーターを実行し(spin run gen)、bin/db.rb は開発中にマイグレーターを実行します(cybertrain db migrate がこれを使います。最初のマイグレーションで gen/models/ が生成されるまで、bin/blog.rb はコンパイルできないためです)。gen/ にはすでに views.rb があります。これはコミットされる、空の埋め込みビューのテーブルです。

ApplicationController は最初は次のようになっており、レコードが見つからない場合は rescue_from によって 404 になります:

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

現在地:自分で書く app/, db/migrate/

記事をスキャフォールドする

なぜひとつのコマンドで、CRUD 一式を Rails 流に書き出します。

ターミナル — blog/shell
cybertrain generate scaffold article title:string body:text

マイグレーション、モデル、コントローラー、5 つのビュー(index、show、new、edit、_form)を書き出し、config/routes.rb に resources :articles を挿入します。フィールドの型は string、text、integer、float、boolean、date、datetime、references で、デフォルトは string です。クイックスタートの cybertrain g scaffold も同じコマンドです。

既存のファイルが上書きされることはありません:各ファイルは create、identical、exist のいずれかとして報告されます。

現在地:自分で書く config/routes.rb

root ルートを設定する

なぜ/ で記事一覧を表示するためです。

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

ルーティングはビルド時に読み込まれます:spin run gen がそれを gen/routes.rb に変換します。cybertrain server、cybertrain db migrate、cybertrain build がこれを代わりに実行します。

現在地:自分で書く app/models/

モデルにバリデーションを追加する

なぜRails ガイドと同じように、長さのバリデーションを手で追加します。

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 にはスーパークラスを書きません:スーパークラスの Cybertrain::Model、カラム、ファインダーは gen/models/article.rb から来ます。保存に失敗すると、「Title can't be blank」(タイトルは空にできません)や「Body is too short (minimum is 10 characters)」(本文が短すぎます。最小 10 文字)のようなメッセージが報告されます。

現在地:自分で書く app/controllers/

ArticlesController

なぜ完全な CRUD で、Strong Parameters やフラッシュを設定してからのリダイレクトに至るまで Rails 流です。ただし、Spinel がどこでも強制する 2 つの点だけは異なります。

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

スキャフォールドは index、show、edit、update、destroy も含めたコントローラー全体を書き出しますが、上の抜粋では省略しています。index では @articles = Article.all.to_a と書きます:この .to_a は必須です。

インスタンス変数は @ivar = の字句スキャンで見つけるため、追加したら cybertrain spin run gen を再実行してください。開発サーバーはこれを自動で行います。

現在地:ビルド時、次に実行時 gen/ → build/bin/blog

生成、マイグレーション、実行

なぜスキーマとルーティングを gen/ に変換し、SQLite データベースを作成して、サーバーを起動します。

ターミナル — blog/shell
cybertrain db migrate    # spin run gen; spin run db -- migrate; spin run gen
cybertrain server        # http://127.0.0.1:3000。Ruby を編集すると再ビルド、ビューは再ビルドなしで再読み込み

spin run gen は db/schema.rb と config/routes.rb を読み、app/ をスキャンして gen/ を書き出します。spin run db -- migrate は db/migrate/*.rb を適用して db/schema.rb を書き換えます。順序が重要です:1 回目の spin run gen は新しいマイグレーションを gen/migrations.rb に入れ(これを bin/db.rb が require します)、2 回目は書き換えられた db/schema.rb からモデルを構築します。

cybertrain server は実行する spin コマンドをひとつずつ表示し、続いてアプリの起動バナーを表示します。初回の起動ではアプリをコンパイルするため、しばらく時間がかかります:

出力 — 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
  • スキーマ、ルーティング、またはコントローラーやモデルのコールバックとインスタンス変数を変更したら、cybertrain spin run gen を再実行して gen/ をコミットしてください。開発サーバーはこれを自動で行います。
  • ポートは cybertrain server 4000(または PORT=4000)で変更できます。
  • cybertrain db は、--(spin run db が引数の前に必要とするもの)を補います。cybertrain db status などのデータベースコマンドも同じように動きます。

現在地:自分で書く db/migrate/, app/

コメントをスキャフォールドする

なぜreferences 型によって、関連付けの元になる外部キーが作られます。

ターミナル — blog/shell
cybertrain generate scaffold comment commenter:string body:text article:references
cybertrain db migrate    # スキャフォールドが comments テーブルのマイグレーションを書き出している

article:references は article_id カラム、インデックス、外部キーを追加します — Article/Comment の関連付けにはこれで十分です。has_many/belongs_to を書く必要はなく、spin run gen が外部キーから読み取ります。生成されるスキーマ:

db/schema.rb(抜粋)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"

スキャフォールドは独立した resources :comments の行を挿入しますが、これは次のステップで置き換えます。

現在地:自分で書く config/routes.rb

ルーティングをネストする

なぜRails ガイドではコメントを記事の下にネストしています。

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

現在地:自分で書く app/controllers/

CommentsController

なぜ到達できるアクションは 2 つだけです。親の記事は URL(params[:article_id])から取り出し、: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

次に、コメントのモデルにバリデーションを追加します:

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

ここでの Article#comments は Array なので、Rails の @article.comments.find(params[:id]) は Comment.where(article_id: @article.id).find(params[:id]) になります。別の記事に属するコメントを指定すると 404 が返ります。

現在地:実行時 app/views/(開発環境ではディスクから読み込み、cybertrain build で埋め込み)

ビューとパーシャル

なぜ記事のページにはコメントの一覧を表示し、最後に [@article, @comment] に結び付いたフォームを置きます。

まず、もう到達できなくなったコメントのビュー index、show、new、edit を削除し、ArticlesController#show に空の @comment を用意します:

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

次に、記事のページでコメントとフォームをレンダリングします:

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 %>

サンプルに含まれているとおりのコメントのパーシャル:

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] と form_with(model: [@article, @comment]) はどちらも同じ [parent, child] のルールで解決されます:新しい子はネストしたコレクション(article_comments_path)に、保存済みの子はネストしたメンバー(article_comment_path)にルーティングされます — これらは上のネストした resources ブロックから gen/routes.rb に生成されます。

現在地:ビルド時 cybertrain spin test

テストする

なぜminitest や RSpec はリフレクションでテストを見つけますが、AOT コンパイラではそれができません。そこで CyberTrain は Cybertrain::Test を同梱しています。

モデルのテスト

test/articles.rb(スニペット)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

アサーション:assert、refute、assert_equal、assert_nil、assert_includes、assert_raises("Class") { }、flunk。サンプルでは assert_response と assert_redirected_to も使っています。

HTTP の一連の流れ

test/articles.rb(スニペット)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 は cookie jar を持ち、アプリをプロセス内で(ソケットなしで)動かすので、セッションと CSRF はブラウザ越しと同じように振る舞います。トークンのないリクエストには 403 が返ります。

テストを実行する

ターミナル — blog/shell
cybertrain spin test

cybertrain spin test は固定された Spinel で spin test を実行します。ツールチェーンが PATH 上にあれば(ステップ 00)、spin test でも同じです。各 test/*.rb をそれぞれ独立したプログラムにコンパイルし、その出力(stdout と stderr をまとめたもの)を、コミット済みのスナップショット test/<name>.rb.expected と比較します。test/support/ 内のファイルは実行されず、require されるだけです。スナップショットを書き出すには:

ターミナル — 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 は CRuby での出力からスナップショットを書き出します。FFI 経由で SQLite に触れるテストは CRuby では実行できないため、そのスナップショットはコンパイル済みバイナリから作ります(上の 2 行目)。スナップショットが古い間は cybertrain spin test が 0 以外で終了するため、後続は ; でつなぎ、&& は使いません。[ -x ] のチェックにより、コンパイルに失敗したときは出力の取り込みをスキップするので、古いスナップショットが残ります。CRuby で実行できないテストは、このスナップショットがないと cybertrain spin test が通りません。スナップショットのないその他のテストは、CRuby での出力と比較されます。

現在地:実行時 dist/blog

デプロイ用にビルドする

なぜひとつのコマンドで、すべてのビューを埋め込んだ、デプロイ可能な dist/ フォルダーにアプリをビルドします。

ターミナル — blog/shell
cybertrain build

これは dist/ を書き出します:すべてのビューを埋め込んだアプリのバイナリと、その横に置かれる、バイナリが使うファイルです。spin run gen -- --embed-views、spin build blog、spin run gen(失敗や Ctrl-C の後でも、コミット済みの空の gen/views.rb を元に戻します)を実行してから、次のものを組み立てます:

  • dist/blogコンパイルされたフレームワーク、コントローラー、モデル、テンプレートインタプリタ。すべてのビューを埋め込み済み
  • dist/public/静的アセットとエラーページ。リバースプロキシに配信させてもよい
  • dist/storage/空の状態で作成。デフォルトでは SQLite のファイルを置く(再ビルドで空にされることはない)
  • dist/tmp/空の状態で作成(再ビルドで空にされることはない)

dist/ を同じ OS と CPU のマシンにコピーし(バイナリはシステムの SQLite にリンクします)、そこで実行します。ビルドしたバイナリはデフォルトで本番環境として動きます(CYBERTRAIN_ENV で上書きできます)。CYBERTRAIN_SECRET_KEY_BASE を設定してください。例:

ターミナル — 本番ホストshell
cd dist
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)"
./blog migrate && ./blog

./blog migrate はバイナリに組み込まれたマイグレーションを適用し、./blog db status はその状態を表示します。ポートは PORT で設定します。シークレットは再起動をまたいで同じ値に保ってください:変更するとすべてのセッションが終了します。

Cybertrain::Config はまず環境変数を読み、config/app.rb がそれを上書きします:

変数属性デフォルト
CYBERTRAIN_ENVenv"development"。cybertrain build で作ったバイナリでは "production"(ほかに "test")
PORTport3000
CYBERTRAIN_DATABASEdatabase_pathstorage/#{env}.sqlite3
CYBERTRAIN_SECRET_KEY_BASEsecret_key_base本番環境では必須。dev/test では tmp/secret_key に自動生成される
SPINEL_WORKERSworkers1
CYBERTRAIN_HOSThost"127.0.0.1"。0.0.0.0 にするとすべてのインターフェースで待ち受ける(コンテナ向け)
CYBERTRAIN_SESSION_SAME_SITEsession_same_site"Lax"。Strict または None も可(None は常に Secure を付ける)。それ以外の値では起動時にサーバーが停止する
CYBERTRAIN_SESSION_PARTITIONEDsession_partitionedfalse。1 または true で Partitioned(と Secure)を付ける
config/app.rbruby
# config/app.rb
Cybertrain.configure do |c|
  c.port = 3000
  c.workers = 1
end
  • サーバーが話すのは素の HTTP/1.1 です:TLS と HTTP/2 には前段に nginx か Caddy を置いてください。本番環境ではセッション cookie が Secure になるので、HTTPS で配信してください(本番環境が本当に素の http である場合に限り、c.session_secure = false を config/app.rb で設定します)。
  • 本番環境では、CYBERTRAIN_SECRET_KEY_BASE がないと起動を拒否します。
  • 本番環境では例外に対して 500 を返し、素のエラーレスポンス(ルーターのプレーンテキストの 404、head :not_found、render plain:)は、public/<status>.html が存在すればそれに置き換えられます。cybertrain new は 404.html と 500.html を書き出します。
  • SIGTERM を受けると新たな受け付けを止め、処理中のリクエストを完了させ、それらが終わった時点か、サーバーの drain_timeout(10 秒)が過ぎた時点で終了します。
  • ファイル監視と再ビルドのループが本番環境で動くことはありません。実行中の cybertrain server は、cybertrain build が tmp/cybertrain-build.lock を保持している間、再ビルドをスキップします。

知っておくこと

なぜ思わぬところでつまずく前に知っておきたい、制限とクセ。

  • セッションに保持できるのは String のみで、HMAC 署名付きの cookie に入ります。
  • データベースは SQLite のみで、Spinel の FFI を経由します。
  • バックトレースはありません:エラーにはクラス、メッセージ、リクエスト行、テンプレート名と行番号が表示されます。
  • ビュー内で属性名を書き間違えると、例外にならず空としてレンダリングされます。これは既知の課題です。
  • 関連付けはスキーマの外部キーからのみ作られます。has_many :through、includes、pluck、dependent:、enum、STI、ポリモーフィック関連付けはありません。
  • namespace、respond_to/フォーマット、constraints、mount はありません。render json: は最小限です。
  • スキャフォールドで生成したモデルでは、最初の string フィールドにだけデフォルトで presence バリデーションが付きます。
  • ジェネレーターは app/controllers と app/models に対して、パーサーではなく字句スキャン(正規表現)を使います。
  • ビューはビルド時に埋め込まれます:ビューだけの変更でも、本番環境に反映するには cybertrain build と再起動が必要です。
  • dist/ は、実行するマシンと同じ OS と CPU でビルドしてください:バイナリはシステムの SQLite にリンクします。
  • SPINEL_WORKERS のデフォルトは 1 です。
  • テスト済みなのは Spinel 2026.09.12 だけです。

Rails との違い

なぜREADME の完全な表を、ここにまとめて。

Rails と CyberTrain の違い(README の完全な表)
RailsCyberTrain
rails consoleコンソールなし — Spinel には eval がない
コードを編集すると、実行中のアプリに反映されるRuby の変更には再ビルドが必要。開発環境では cybertrain server が代わりに行う(再ビルド、停止、execv)
ビューを編集しても、リロードは不要同じ — 開発環境ではビューをリクエストごとにディスクからパースする
def newdef new_action (new だと Klass.new(ctx) を覆い隠してしまう)
before_action { do_thing }(暗黙の self)before_action { |c| c.do_thing } — 保存したブロックに対する instance_exec がないため、コールバックはコントローラー/レコードを明示的に受け取る
resources :posts do resources :comments endresources :posts do |posts| posts.resources :comments end — ネストしたブロックはマッパーを明示的に受け取る
ERB はメソッドにコンパイルされる。任意のオブジェクト、任意のメソッドを使える固定の文法/ディスパッチテーブルに従って解釈される
セッションにはマーシャリングした任意のオブジェクトを保持できるセッションの値は String のみ、HMAC 署名付き cookie
多数のデータベースアダプターSQLite のみ、FFI 経由
has_many :through、includes、pluck、dependent:、enum、STI、ポリモーフィック関連付け未実装。関連付けはスキーマの外部キーからのみ作られる
namespace、フォーマット/respond_to、constraints、mount未実装 — フラットな名前、render json: のみ
例外時には完全なバックトレースクラス、メッセージ、リクエスト行、テンプレート名と行番号 — Spinel はバックトレースを公開しない
Rack とそのミドルウェア、Gemfile に書いた任意の gemRack 互換性なし。小さな固定のミドルウェアセット。Spinel 独自の spin-index(その Ruby サブセットでコンパイルできるものに限る)
minitest / RSpecCybertrain::Test — リフレクションに基づくテストランナーは AOT では動かない
認証、メーラー、ジョブ、ActionCable、アセットパイプライン、i18n未構築(MVP の範囲外)

すべてを網羅してはいません。

次に読むもの