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 +4 -4
- data/README.md +91 -28
- data/cybertrain/cli/build.rb +174 -0
- data/cybertrain/cli/new_app.rb +2 -2
- data/cybertrain/cli/templates.rb +25 -16
- data/cybertrain/cli.rb +59 -0
- data/cybertrain/version.rb +1 -1
- data/exe/cybertrain +9 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7660171ff57ac0e3f625dbffdef0a5a81de709bb80d2bcaf4e2b55be40034c24
|
|
4
|
+
data.tar.gz: 467b4243e1b26cfa42b64e276f6f859d01c1c7f9bd96df015e1222b66b31dd5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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: `
|
|
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.
|
|
61
|
-
`cybertrain
|
|
62
|
-
|
|
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.
|
|
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
|
-
|
|
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/
|
|
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
|
|
185
|
-
|
|
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
|
|
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
|
|
300
|
-
|
|
301
|
-
development
|
|
302
|
-
|
|
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
|
|
320
|
-
`
|
|
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
|
|
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; `
|
|
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"
|
|
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:** `
|
|
378
|
-
`
|
|
379
|
-
|
|
380
|
-
`
|
|
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.
|
|
457
|
-
3. `gem build cybertrain.gemspec && gem push cybertrain-0.1.
|
|
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
|
data/cybertrain/cli/new_app.rb
CHANGED
|
@@ -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
|
|
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", ""],
|
data/cybertrain/cli/templates.rb
CHANGED
|
@@ -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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
214
|
+
def self.bin_app(package)
|
|
208
215
|
<<~RUBY
|
|
209
216
|
require "cybertrain"
|
|
210
|
-
require_relative "../
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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"
|
data/cybertrain/version.rb
CHANGED
data/exe/cybertrain
CHANGED
|
@@ -4,4 +4,12 @@
|
|
|
4
4
|
# same entry point, compiled by spin).
|
|
5
5
|
require "cybertrain/cli"
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
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
|