cybertrain 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 213798cb904eb6eb4ba8f45d0ef3c4d89db13533689a40ca0faf8419514bdc25
4
- data.tar.gz: 4a975e911f2e3bbd3de9c3e5d126a5095a52a36bbe21365e97620c0b13b921d1
3
+ metadata.gz: 7660171ff57ac0e3f625dbffdef0a5a81de709bb80d2bcaf4e2b55be40034c24
4
+ data.tar.gz: 467b4243e1b26cfa42b64e276f6f859d01c1c7f9bd96df015e1222b66b31dd5d
5
5
  SHA512:
6
- metadata.gz: 44538d5ef1697095ca313333baaf0c7a47ece4ae98ed480b39da2553cb99f3da265862d24e921a7e60f8226bbb02a52b804cbc7f95b553152730b19f626772e9
7
- data.tar.gz: 7edfda7ced1ce88bea85fca1a45ced852ebdee842861fb3ebd6ee0cfe9bfdb317bdecce8e1f330a1095ba1712f5e6f122eb2e48e6b0f447849ea1a29db7b95fb
6
+ metadata.gz: f56d35ff51a73b15c54376c145197847760d0238cf6599c68559d1c82c84f27998d65b0e53fd594ed7951f7d1b9d75ed3da7895571b4746e3ccb3b58e11bb7d1
7
+ data.tar.gz: 49a0e1927cd913289307e6684065bf157a1c04c22a585bad2e38cd2826f523cf480aa8a2cea19cd2437fb21dd249dd21a4a3ad97e6c43e231a580f166c202aa6
data/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  cybertrain is a Rails-shaped web application framework written natively for
4
4
  [Spinel](https://github.com/matz/spinel), matz's ahead-of-time Ruby compiler.
5
5
  There is no Rack, no gems at runtime, and no Ruby interpreter in the deployed
6
- artifact: `spin build` compiles an application — controllers, models, views
7
- and the framework itself — into a single native binary. Where Rails leans on
6
+ artifact: `cybertrain build` compiles an application — controllers, models,
7
+ views and the framework itself — into a single native binary. Where Rails leans on
8
8
  runtime metaprogramming (`method_missing`, `define_method`, `eval`) for
9
9
  `before_action :set_post` or `Post.where(title: "x")`, cybertrain gets the
10
10
  same vocabulary by generating plain, committed Ruby from `db/schema.rb` and
@@ -48,7 +48,8 @@ Spinel, since they find test methods by reflection and an ahead-of-time
48
48
  compiler has nothing to reflect on at run time. `spin test --regen
49
49
  test/<name>.rb` rewrites a snapshot from CRuby's output; a few tests that
50
50
  touch SQLite through FFI take theirs from the compiled binary instead
51
- ([spikes/NOTES.md](spikes/NOTES.md), rule 23).
51
+ ([spikes/NOTES.md](spikes/NOTES.md), rule 23); `script/regen-snapshot
52
+ test/<name>.rb` does that build-then-capture step for you and prints the diff stat.
52
53
 
53
54
  ## Installing the CLI
54
55
 
@@ -57,13 +58,29 @@ gem install cybertrain
57
58
  ```
58
59
 
59
60
  The `cybertrain` command is plain Ruby and the gem runs it under CRuby
60
- (3.2 or newer); the framework itself is not in the gem. Two commands:
61
- `cybertrain new NAME` scaffolds an application, `cybertrain generate
62
- scaffold NAME field:type ...` (alias `g scaffold`) adds a resource to one.
61
+ (3.2 or newer); the framework itself is not in the gem. Its commands
62
+ (`cybertrain help` prints them):
63
+
64
+ - `cybertrain new NAME` creates an application in NAME, then runs `spin
65
+ lock` and `spin run gen` in it.
66
+ - `cybertrain generate scaffold NAME field:type ...` (alias `g scaffold`)
67
+ adds a resource to the application in the current directory.
68
+ - `cybertrain migration` generates, applies pending migrations, and
69
+ generates again (`spin run gen; spin run db -- migrate; spin run gen`).
70
+ - `cybertrain db COMMAND...` runs any database command: `status`,
71
+ `rollback [N]`, `schema:dump`, `create`.
72
+ - `cybertrain server [PORT]` generates, then starts the development server
73
+ (`spin run gen; spin run NAME`, or `spin run NAME -- PORT`; default port
74
+ 3000).
75
+ - `cybertrain build` builds NAME with `app/views/` embedded and assembles
76
+ `dist/` (the binary, `public/`, `storage/`, `tmp/`).
77
+
78
+ `migration`, `db`, `server` and `build` run inside an application; NAME is
79
+ the `[package] name` in its `spin.toml`.
63
80
 
64
81
  `cybertrain new` points the app's `spin.toml` at the release matching the
65
82
  CLI — `cybertrain = { git = "https://github.com/saeki-mototsune/cybertrain",
66
- ref = "v0.1.0" }` — then runs `spin lock` (spin fetches the framework into
83
+ ref = "v0.1.1" }` — then runs `spin lock` (spin fetches the framework into
67
84
  its cache, `~/.cache/spin/packages/`, and pins the commit in `spin.lock`)
68
85
  and `spin run gen`, much as `rails new` runs `bundle install`. Nothing of
69
86
  the framework is copied into the app; commit `spin.toml` and `spin.lock`.
@@ -87,7 +104,7 @@ articles and comments.
87
104
  ```sh
88
105
  cybertrain new blog
89
106
  cd blog
90
- spin build # works right away: `new` already locked the framework and ran `spin run gen`
107
+ cybertrain server # works right away: `new` already locked the framework and ran `spin run gen`
91
108
  ```
92
109
 
93
110
  ```
@@ -95,10 +112,22 @@ blog/
95
112
  spin.toml config/app.rb config/routes.rb
96
113
  db/schema.rb db/migrate/
97
114
  app/controllers/application_controller.rb app/models/ app/views/ app/helpers/
98
- public/ bin/server.rb bin/gen.rb bin/db.rb
115
+ public/ bin/blog.rb bin/gen.rb bin/db.rb
99
116
  gen/ storage/ tmp/ test/
100
117
  ```
101
118
 
119
+ `bin/blog.rb` is the application's one entry point, the program `spin build
120
+ blog` compiles: `./blog` serves, `./blog migrate` and `./blog db status`
121
+ manage the database. `bin/gen.rb` runs the generator (`spin run gen`), and
122
+ `bin/db.rb` runs the migrator during development (`cybertrain migration`
123
+ uses it, since `bin/blog.rb` cannot compile until the first migration has
124
+ generated `gen/models/`). `gen/` already holds `views.rb`, the committed,
125
+ empty table of embedded views (see "How it works").
126
+
127
+ Apps created before `cybertrain build` existed have `bin/server.rb` and
128
+ no `bin/<name>.rb`: copy `bin/<name>.rb` from a fresh `cybertrain new` (with
129
+ the same NAME), delete `bin/server.rb` and run `spin run gen`.
130
+
102
131
  `ApplicationController` starts with the one thing every controller inherits:
103
132
 
104
133
  ```ruby
@@ -181,11 +210,14 @@ end
181
210
  ### Generate, migrate, run
182
211
 
183
212
  ```sh
184
- spin run gen # reads db/schema.rb + config/routes.rb, scans app/, writes gen/
185
- spin run db -- migrate # applies db/migrate/*.rb, rewrites db/schema.rb (arguments go after --)
186
- spin run server # http://127.0.0.1:3000
213
+ cybertrain migration # spin run gen; spin run db -- migrate; spin run gen
214
+ cybertrain server # http://127.0.0.1:3000; rebuilds on Ruby edits, views reload without a rebuild
187
215
  ```
188
216
 
217
+ `spin run gen` reads `db/schema.rb` and `config/routes.rb`, scans `app/` and
218
+ writes `gen/`; `spin run db -- migrate` applies `db/migrate/*.rb` and
219
+ rewrites `db/schema.rb`.
220
+
189
221
  Re-run `spin run gen` (and commit `gen/`) after touching the schema, routes,
190
222
  or a controller/model's callbacks and ivars — the dev server does this for
191
223
  you automatically (see "How it works").
@@ -277,6 +309,27 @@ child routes to the nested collection (`article_comments_path`), a persisted
277
309
  one to the nested member (`article_comment_path`) — generated into
278
310
  `gen/routes.rb` from the nested `resources` block above.
279
311
 
312
+ ### Build for deployment
313
+
314
+ ```sh
315
+ cybertrain build
316
+ ```
317
+
318
+ writes `dist/`: `dist/blog`, the app compiled with every `app/views/`
319
+ template embedded (rendered from memory, error messages still say
320
+ `articles/show.html.erb:12`), `dist/public/`, and empty `dist/storage/` and
321
+ `dist/tmp/`. Copy `dist/` to a machine with the same OS and CPU (the binary
322
+ links the system SQLite) and run `./blog migrate && ./blog`; a built binary
323
+ is production by default (`CYBERTRAIN_ENV` overrides). Set
324
+ `CYBERTRAIN_SECRET_KEY_BASE`. Static files can also be served by the reverse
325
+ proxy from `dist/public/` — see [docs/deploy.md](docs/deploy.md).
326
+
327
+ A running `cybertrain server` skips its rebuilds while `cybertrain build` is
328
+ in progress (the build holds `tmp/cybertrain-build.lock`, since both write
329
+ `build/bin/blog`), logs the skip, and rebuilds as usual on the next save
330
+ once the build has finished. The lock records the build's PID; one left
331
+ behind by a killed build is ignored and removed.
332
+
280
333
  ## How it works
281
334
 
282
335
  **Build-time code generation.** `spin run gen` (`bin/gen.rb`) executes
@@ -289,17 +342,24 @@ zero-argument `def`s. From that it writes plain, readable Ruby under `gen/`:
289
342
  `gen/models/<name>.rb` (accessors, casts, finders, FK-derived associations),
290
343
  `gen/routes.rb` (route table, dispatcher, `*_path`/`*_url` helpers),
291
344
  `gen/controllers.rb` (`view_assigns` and `run_callback` per controller),
292
- `gen/migrations.rb` and `gen/app.rb` (the `require_relative` manifest).
345
+ `gen/migrations.rb`, `gen/views.rb` (the embedded views table, empty unless
346
+ building for deployment) and `gen/app.rb` (the `require_relative` manifest).
293
347
  `gen/` is committed, not gitignored — `spin build`/`spin test` have no hook
294
348
  to generate first, so the checked-in output has to already be what compiles.
295
349
  CI re-runs `spin run gen` and fails on a diff, so stale generated code can't
296
350
  merge.
297
351
 
298
352
  **Views are interpreted, not compiled.** `app/views/**/*.html.erb` files look
299
- like Rails ERB but are read from disk, parsed into an AST at request time
300
- (cached after first parse in production; re-parsed on change in
301
- development), then walked by a tree-walking interpreter — Spinel has no
302
- `eval`, so this is a closed grammar (literals, `@ivar`/local lookups, calls
353
+ like Rails ERB but are parsed into an AST at request time, then walked by a
354
+ tree-walking interpreter. Where the source comes from depends on the mode:
355
+ in development (and test) the engine reads `app/views/` from disk and
356
+ re-parses a template when it changes, so view edits need no rebuild.
357
+ `cybertrain build` runs `spin run gen -- --embed-views`, which writes every
358
+ template's source into `gen/views.rb` as a string table for that one `spin
359
+ build`, then runs `spin run gen` again to restore the empty table that is
360
+ committed; the production binary renders only that table (parsing each
361
+ template once) and refuses to boot when it is empty (`error: views are not
362
+ embedded in this binary`). Spinel has no `eval`, so this is a closed grammar (literals, `@ivar`/local lookups, calls
303
363
  through fixed per-type dispatch tables, `if`/`unless`,
304
364
  `each`/`each_with_index`/one helper block), not real Ruby. Full grammar,
305
365
  helpers and gaps: [docs/template-language.md](docs/template-language.md).
@@ -316,10 +376,11 @@ faster and stall-free for this I/O-bound shape at 100 concurrent connections.
316
376
  and foreign keys on; every query is bound, never interpolated. It's the only
317
377
  adapter today (PostgreSQL via `libpq` FFI is noted as possible future work).
318
378
 
319
- **The development loop.** `spin run server` in `development` (the default
320
- `CYBERTRAIN_ENV`) also polls `app/**/*.rb`, `config/**/*.rb`, `db/schema.rb`
379
+ **The development loop.** `cybertrain server` (`spin run gen`, then `spin run NAME`) in
380
+ `development` (the default `CYBERTRAIN_ENV`, except in a binary from
381
+ `cybertrain build`) also polls `app/**/*.rb`, `config/**/*.rb`, `db/schema.rb`
321
382
  and `gen/**/*.rb` every half second (views are excluded — the engine reloads
322
- those itself). A change runs `spin run gen && spin build server` in the
383
+ those itself — and so is `gen/views.rb`, empty in development). A change runs `spin run gen && spin build NAME` in the
323
384
  background; success sends the server `SIGHUP`, whose handler stops listening
324
385
  and `execv`s the new binary on the same port and PID, invisible to a client
325
386
  mid-session. A failed build keeps serving the old binary and banners the
@@ -330,7 +391,7 @@ compiler output on every HTML response. None of this loads in production.
330
391
  | Rails | cybertrain |
331
392
  | --- | --- |
332
393
  | `rails console` | No console — Spinel has no `eval` |
333
- | Edit code, the running app picks it up | Ruby needs a rebuild; `spin run server` in development does this for you (rebuild, `SIGHUP`, `execv`) |
394
+ | Edit code, the running app picks it up | Ruby needs a rebuild; `cybertrain server` in development does this for you (rebuild, `SIGHUP`, `execv`) |
334
395
  | Edit a view, no reload needed | Same — views are parsed from disk per request in development |
335
396
  | `def new` | `def new_action` (`new` would shadow `Klass.new(ctx)`) |
336
397
  | `before_action { do_thing }` (implicit `self`) | `before_action { \|c\| c.do_thing }` — no `instance_exec` on a stored block, so callbacks take the controller/record explicitly |
@@ -361,7 +422,7 @@ end
361
422
 
362
423
  | Variable | Attribute | Default |
363
424
  | --- | --- | --- |
364
- | `CYBERTRAIN_ENV` | `env` | `"development"` (also `"test"`, `"production"`) |
425
+ | `CYBERTRAIN_ENV` | `env` | `"development"`; `"production"` in a binary from `cybertrain build` (also `"test"`) |
365
426
  | `PORT` | `port` | `3000` |
366
427
  | `CYBERTRAIN_DATABASE` | `database_path` | `storage/#{env}.sqlite3` |
367
428
  | `CYBERTRAIN_SECRET_KEY_BASE` | `secret_key_base` | required in production; dev/test auto-generate one into `tmp/secret_key` |
@@ -374,10 +435,12 @@ Other attributes with fixed, overridable defaults: `host` (`"127.0.0.1"`),
374
435
  marks the session cookie `Secure`; `false` elsewhere), `pool_size` (4),
375
436
  `static_files`/`csrf` (`true`).
376
437
 
377
- **Deployment:** `spin build server` produces `build/bin/server`. Ship it with
378
- `app/views/` (read at request time, never compiled in), `public/` (static
379
- assets, or let a reverse proxy serve it) and `storage/` (the SQLite file),
380
- `CYBERTRAIN_ENV=production` and `CYBERTRAIN_SECRET_KEY_BASE` set. The binary
438
+ **Deployment:** `cybertrain build` produces `dist/`: the binary `dist/NAME`
439
+ with the views embedded, `dist/public/` (static assets, or let a reverse
440
+ proxy serve it), and `dist/storage/` (the SQLite file by default) and
441
+ `dist/tmp/`. Run it from `dist/` with `CYBERTRAIN_SECRET_KEY_BASE` set;
442
+ it is production unless `CYBERTRAIN_ENV` says otherwise, and `./NAME
443
+ migrate` applies the migrations compiled into it. The binary
381
444
  speaks plain HTTP/1.1 only; put nginx/Caddy in front for TLS and HTTP/2 (the
382
445
  session cookie is `Secure` in production, so serve it over HTTPS). In
383
446
  production an exception answers 500 and a bare error response (the router's
@@ -453,8 +516,8 @@ before the gem is pushed:
453
516
  1. Bump `Cybertrain::VERSION` (`cybertrain/version.rb`) and `version` in
454
517
  `spin.toml` together (CI checks they match); spin caches a git
455
518
  dependency by that version.
456
- 2. Merge to `main`, then tag and push: `git tag v0.1.0 && git push origin v0.1.0`.
457
- 3. `gem build cybertrain.gemspec && gem push cybertrain-0.1.0.gem`.
519
+ 2. Merge to `main`, then tag and push: `git tag v0.1.1 && git push origin v0.1.1`.
520
+ 3. `gem build cybertrain.gemspec && gem push cybertrain-0.1.1.gem`.
458
521
 
459
522
  ## License
460
523
 
@@ -0,0 +1,174 @@
1
+ # `cybertrain build`: dist/ = the app binary with app/views embedded, plus
2
+ # public/. Also the command lists `cybertrain migration` and `server` run. Plain Ruby:
3
+ # runs under CRuby (the gem) and compiles under Spinel (spin install).
4
+ module Cybertrain
5
+ module CLI
6
+ module Build
7
+ # The [package] name in root/spin.toml, or "" when there is none.
8
+ def self.app_name(root)
9
+ path = "#{root}/spin.toml"
10
+ return "" unless File.exist?(path)
11
+
12
+ name = ""
13
+ in_package = false
14
+ File.read(path).each_line do |line|
15
+ text = line.strip
16
+ in_package = text == "[package]" if text.start_with?("[")
17
+ next unless in_package && text.start_with?("name")
18
+
19
+ match = text.match(/\Aname\s*=\s*"([^"]*)"/)
20
+ unless match.nil?
21
+ name = match[1]
22
+ break
23
+ end
24
+ end
25
+ name
26
+ end
27
+
28
+ def self.commands(name)
29
+ ["spin run gen -- --embed-views", "spin build #{name}", "spin run gen"]
30
+ end
31
+
32
+ # Through bin/db.rb: the app binary cannot compile before the first
33
+ # migration has produced gen/models.
34
+ def self.migration_commands
35
+ ["spin run gen", "spin run db -- migrate", "spin run gen"]
36
+ end
37
+
38
+ # port is "" (the app's default, 3000) or a port? string.
39
+ def self.server_commands(name, port)
40
+ run = port == "" ? "spin run #{name}" : "spin run #{name} -- #{port}"
41
+ ["spin run gen", run]
42
+ end
43
+
44
+ # All digits, 1 to 65535 (the app reads 0 as "no port").
45
+ def self.port?(text)
46
+ return false if text.empty? || text.size > 5
47
+ return false unless text.bytes.all? { |b| b >= 48 && b <= 57 }
48
+
49
+ text.to_i >= 1 && text.to_i <= 65535
50
+ end
51
+
52
+ # Runs one build step (a spin command) in root; true when it succeeds.
53
+ # Build.run takes a Runner so tests can stand in for spin.
54
+ class Runner
55
+ def run_step(root, command)
56
+ Build.run_in(root, command)
57
+ end
58
+ end
59
+
60
+ # Holds this process's PID while the steps run and dist/ is assembled:
61
+ # the development server (Dev::Rebuilder) skips its rebuilds
62
+ # meanwhile, since both write build/bin/<name>, and treats a lock
63
+ # whose PID is gone (a killed build) as stale.
64
+ def self.lock_path(root)
65
+ "#{root}/tmp/cybertrain-build.lock"
66
+ end
67
+
68
+ # Runs the build in root and assembles dist/. Returns the exit code.
69
+ def self.run(root, name, runner = Runner.new)
70
+ steps = commands(name)
71
+ ok = false
72
+ restored = false
73
+ code = 1
74
+ Templates.mkdir_p("#{root}/tmp")
75
+ File.write(lock_path(root), Process.pid.to_s)
76
+ begin
77
+ begin
78
+ ok = runner.run_step(root, steps[0]) && runner.run_step(root, steps[1])
79
+ ensure
80
+ # Always put gen/views.rb back to the empty table, even after a
81
+ # failure or Ctrl-C.
82
+ restored = runner.run_step(root, steps[2])
83
+ puts "warning: gen/views.rb still holds the embedded views; run spin run gen" unless restored
84
+ end
85
+ if ok && restored
86
+ assemble(root, name)
87
+ code = 0
88
+ end
89
+ ensure
90
+ # Released only once dist/ holds the new binary: assemble reads
91
+ # build/bin/<name>, which a dev rebuild would be writing.
92
+ File.delete(lock_path(root)) if File.exist?(lock_path(root))
93
+ end
94
+ return 1 unless code == 0
95
+
96
+ puts ""
97
+ puts "dist/#{name} (production by default)"
98
+ puts "dist/public/"
99
+ puts "run: cd dist && ./#{name} migrate && ./#{name}"
100
+ 0
101
+ end
102
+
103
+ def self.run_in(root, command)
104
+ puts "run #{command}"
105
+ system("cd #{shell_quote(root)} && #{command}")
106
+ end
107
+
108
+ # Copies build/bin/<name> and public/ into dist/; storage/ and tmp/ are
109
+ # created when missing and never emptied (a database and the secret
110
+ # key can live there). Each copy lands in a dist/.*.tmp entry first and
111
+ # is renamed into place: a running dist/<name> keeps its old file
112
+ # (writing over a running binary fails with ETXTBSY on Linux), and the
113
+ # old dist/public/ steps aside to dist/.public.old between two renames
114
+ # (rename cannot replace a non-empty directory), so dist/public/ is
115
+ # missing only for that instant rather than for the whole copy.
116
+ def self.assemble(root, name)
117
+ binary = "#{root}/build/bin/#{name}"
118
+ raise InvalidArgument, "build/bin/#{name} is missing: `spin build #{name}` did not produce it" unless File.exist?(binary)
119
+
120
+ dist = "#{root}/dist"
121
+ Templates.mkdir_p(dist)
122
+ binary_tmp = "#{dist}/.#{name}.tmp"
123
+ public_tmp = "#{dist}/.public.tmp"
124
+ public_old = "#{dist}/.public.old"
125
+ # Leftovers of an interrupted earlier run.
126
+ rm_tree(binary_tmp)
127
+ rm_tree(public_tmp)
128
+ rm_tree(public_old)
129
+
130
+ copy_command = "cp #{shell_quote(binary)} #{shell_quote(binary_tmp)}"
131
+ raise InvalidArgument, "could not copy #{binary} to dist/" unless system(copy_command)
132
+
133
+ File.rename(binary_tmp, "#{dist}/#{name}")
134
+
135
+ if File.directory?("#{root}/public")
136
+ public_command = "cp -R #{shell_quote("#{root}/public")} #{shell_quote(public_tmp)}"
137
+ raise InvalidArgument, "could not copy public/ to dist/" unless system(public_command)
138
+ else
139
+ Dir.mkdir(public_tmp)
140
+ end
141
+ public_dir = "#{dist}/public"
142
+ File.rename(public_dir, public_old) if File.exist?(public_dir) || File.symlink?(public_dir)
143
+ File.rename(public_tmp, public_dir)
144
+ rm_tree(public_old)
145
+
146
+ Templates.mkdir_p("#{dist}/storage")
147
+ Templates.mkdir_p("#{dist}/tmp")
148
+ ["dist/#{name}", "dist/public/", "dist/storage/", "dist/tmp/"]
149
+ end
150
+
151
+ # A symlink is removed itself, never followed: its target may be
152
+ # dist/storage/ or outside dist/.
153
+ def self.rm_tree(path)
154
+ if File.symlink?(path)
155
+ File.delete(path)
156
+ return nil
157
+ end
158
+ return nil unless File.exist?(path)
159
+
160
+ if File.directory?(path)
161
+ Dir.children(path).each { |child| rm_tree("#{path}/#{child}") }
162
+ Dir.rmdir(path)
163
+ else
164
+ File.delete(path)
165
+ end
166
+ nil
167
+ end
168
+
169
+ def self.shell_quote(text)
170
+ "'#{text.gsub("'", "'\\\\''")}'"
171
+ end
172
+ end
173
+ end
174
+ end
@@ -18,7 +18,7 @@ module Cybertrain
18
18
  files = [
19
19
  ["spin.toml", Templates.spin_toml(package, framework_dep)],
20
20
  [".gitignore", Templates.gitignore],
21
- ["README.md", Templates.readme(title)],
21
+ ["README.md", Templates.readme(title, package)],
22
22
  ["config/app.rb", Templates.config_app],
23
23
  ["config/routes.rb", Templates.routes],
24
24
  ["db/schema.rb", Templates.schema],
@@ -30,7 +30,7 @@ module Cybertrain
30
30
  ["public/404.html", Templates.error_page("404", "Not Found")],
31
31
  ["public/500.html", Templates.error_page("500", "Internal Server Error")],
32
32
  ["public/style.css", Templates.style_css],
33
- ["bin/server.rb", Templates.bin_server],
33
+ ["bin/#{package}.rb", Templates.bin_app(package)],
34
34
  ["bin/gen.rb", Templates.bin_gen],
35
35
  ["bin/db.rb", Templates.bin_db],
36
36
  ["gen/.keep", ""],
@@ -76,6 +76,7 @@ module Cybertrain
76
76
  def self.gitignore
77
77
  <<~TEXT
78
78
  /build/
79
+ /dist/
79
80
  /storage/*.sqlite3*
80
81
  /tmp/*
81
82
  !/tmp/.keep
@@ -83,7 +84,7 @@ module Cybertrain
83
84
  TEXT
84
85
  end
85
86
 
86
- def self.readme(title)
87
+ def self.readme(title, package)
87
88
  <<~MARKDOWN
88
89
  # #{title}
89
90
 
@@ -91,16 +92,22 @@ module Cybertrain
91
92
 
92
93
  ```sh
93
94
  cybertrain generate scaffold post title:string body:text
94
- spin run gen # pick up the new migration (gen/migrations.rb)
95
- spin run db -- migrate # apply db/migrate, rewrite db/schema.rb
96
- spin run gen # regenerate gen/ from db/schema.rb, config/routes.rb and app/
97
- spin run server # http://127.0.0.1:3000
95
+ cybertrain migration # gen, apply db/migrate, gen again
96
+ cybertrain server # http://127.0.0.1:3000 (`cybertrain server 4000` for another port)
97
+ cybertrain build # dist/: the binary (views embedded) + public/
98
98
  ```
99
99
 
100
- Run `spin run gen` after changing the schema, the routes or a
101
- controller's instance variables and callbacks, and commit `gen/`.
102
- Views under `app/views/` are read at run time: edit them without
103
- rebuilding.
100
+ `spin run gen` (which `migration`, `server` and `build` run for you)
101
+ regenerates `gen/` from db/schema.rb, config/routes.rb and app/;
102
+ commit `gen/`. In development views under `app/views/` are read at
103
+ run time: edit them without rebuilding. `cybertrain db status` and
104
+ `cybertrain db rollback 1` reach the other database commands.
105
+ `cybertrain migration` runs migrations through `bin/db.rb`; a
106
+ deployed `dist/#{package}` runs them with `./#{package} migrate`.
107
+
108
+ To deploy, copy `dist/` to a machine with the same OS and CPU, then
109
+ `cd dist && ./#{package} migrate && ./#{package}` (production by default;
110
+ set CYBERTRAIN_SECRET_KEY_BASE).
104
111
  MARKDOWN
105
112
  end
106
113
 
@@ -204,17 +211,18 @@ module Cybertrain
204
211
  CSS
205
212
  end
206
213
 
207
- def self.bin_server
214
+ def self.bin_app(package)
208
215
  <<~RUBY
209
216
  require "cybertrain"
210
- require_relative "../config/app"
217
+ require_relative "../gen/views" # embedded build: sets CYBERTRAIN_ENV=production by default
218
+ require_relative "../config/app" # so it comes before the config
211
219
  require_relative "../gen/app"
220
+ require_relative "../gen/migrations"
212
221
 
213
- app = Cybertrain::Application.new(
214
- router: Gen::Routes.build(Cybertrain::Router.new),
215
- url_resolver: Gen::Routes.url_resolver
216
- )
217
- app.run
222
+ exit(Cybertrain::Main.run("#{package}", ARGV,
223
+ router: Gen::Routes.build(Cybertrain::Router.new),
224
+ url_resolver: Gen::Routes.url_resolver,
225
+ views: Gen::Views::SOURCES))
218
226
  RUBY
219
227
  end
220
228
 
@@ -228,6 +236,7 @@ module Cybertrain
228
236
  RUBY
229
237
  end
230
238
 
239
+ # Development only, so migrations run before gen/models exist; production uses `./NAME migrate`.
231
240
  def self.bin_db
232
241
  <<~RUBY
233
242
  require "cybertrain"
data/cybertrain/cli.rb CHANGED
@@ -3,11 +3,13 @@
3
3
  #
4
4
  # cybertrain new NAME [--path DIR | --version V] [--skip-spin]
5
5
  # cybertrain generate scaffold NAME field:type ... [parent:references]
6
+ # cybertrain migration | db COMMAND... | server [PORT] | build
6
7
  # cybertrain version | help
7
8
  require "cybertrain/version"
8
9
  require "cybertrain/cli/templates"
9
10
  require "cybertrain/cli/new_app"
10
11
  require "cybertrain/cli/scaffold"
12
+ require "cybertrain/cli/build"
11
13
 
12
14
  module Cybertrain
13
15
  module CLI
@@ -24,6 +26,17 @@ module Cybertrain
24
26
  cybertrain generate scaffold NAME field:type ... [parent:references]
25
27
  Add a resource to the application in the current directory.
26
28
  Types: #{Field::TYPES.join(", ")} (default string).
29
+ cybertrain migration
30
+ Generate, apply pending migrations, generate again
31
+ (spin run gen; spin run db -- migrate; spin run gen).
32
+ cybertrain db COMMAND...
33
+ Any database command: status, rollback [N], schema:dump, create.
34
+ cybertrain server [PORT]
35
+ Generate, then start the development server
36
+ (spin run gen; spin run NAME [-- PORT]; default port 3000).
37
+ cybertrain build
38
+ Build NAME with app/views embedded and assemble dist/
39
+ (the binary, public/, storage/, tmp/).
27
40
  cybertrain version
28
41
  cybertrain help
29
42
  TEXT
@@ -34,6 +47,10 @@ module Cybertrain
34
47
  case command
35
48
  when "new" then run_new(argv)
36
49
  when "generate", "g" then run_generate(argv)
50
+ when "migration" then run_in_app { |_name| run_all(Build.migration_commands) }
51
+ when "db" then run_in_app { |_name| run_all(["spin run db -- #{db_args(argv)}"]) }
52
+ when "server" then run_in_app { |name| run_server(name, argv) }
53
+ when "build" then run_in_app { |name| Build.run(".", name) }
37
54
  when "version", "--version", "-v"
38
55
  puts "cybertrain #{Cybertrain::VERSION}"
39
56
  0
@@ -88,6 +105,48 @@ module Cybertrain
88
105
  argv[i + 1]
89
106
  end
90
107
 
108
+ # Commands that need the app: its name comes from ./spin.toml.
109
+ def self.run_in_app
110
+ name = Build.app_name(".")
111
+ if name == ""
112
+ puts "error: no spin.toml with a [package] name here; run this inside a cybertrain application"
113
+ return 1
114
+ end
115
+ yield name
116
+ end
117
+
118
+ # Runs each command in turn; stops at the first failure.
119
+ def self.run_all(commands)
120
+ status = 0
121
+ commands.each do |command|
122
+ puts "run #{command}"
123
+ unless system(command)
124
+ status = 1
125
+ break
126
+ end
127
+ end
128
+ status
129
+ end
130
+
131
+ # `cybertrain server [PORT]`.
132
+ def self.run_server(name, argv)
133
+ port = argv.size > 1 ? argv[1] : ""
134
+ unless port == "" || Build.port?(port)
135
+ puts "error: PORT must be a number"
136
+ return 1
137
+ end
138
+ if argv.size > 2
139
+ puts "error: unexpected argument '#{argv[2]}'"
140
+ return 1
141
+ end
142
+ run_all(Build.server_commands(name, port))
143
+ end
144
+
145
+ # The words after `db`, each shell-quoted.
146
+ def self.db_args(argv)
147
+ argv[1, argv.size - 1].map { |arg| Build.shell_quote(arg) }.join(" ")
148
+ end
149
+
91
150
  def self.run_generate(argv)
92
151
  kind = argv.size > 1 ? argv[1] : ""
93
152
  raise InvalidArgument, "only `generate scaffold` is supported" unless kind == "scaffold"
@@ -1,5 +1,5 @@
1
1
  module Cybertrain
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
 
4
4
  # Where `cybertrain new` points an app's spin.toml by default: the git tag
5
5
  # "v#{VERSION}" of this repository. Kept here because the gem (CRuby) and
data/exe/cybertrain CHANGED
@@ -4,4 +4,12 @@
4
4
  # same entry point, compiled by spin).
5
5
  require "cybertrain/cli"
6
6
 
7
- exit(Cybertrain::CLI.run(ARGV))
7
+ # Ctrl-C reaches the whole process group: the child (`spin run NAME`, a
8
+ # build) stops itself, and CRuby raises Interrupt out of `system` here.
9
+ # Build.run's ensure has restored gen/views.rb by the time it arrives.
10
+ begin
11
+ status = Cybertrain::CLI.run(ARGV)
12
+ rescue Interrupt
13
+ status = 130
14
+ end
15
+ exit(status)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cybertrain
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saeki Mototsune
@@ -21,6 +21,7 @@ extra_rdoc_files: []
21
21
  files:
22
22
  - README.md
23
23
  - cybertrain/cli.rb
24
+ - cybertrain/cli/build.rb
24
25
  - cybertrain/cli/new_app.rb
25
26
  - cybertrain/cli/scaffold.rb
26
27
  - cybertrain/cli/templates.rb