cybertrain 0.1.0 → 0.2.0
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 +240 -73
- data/cybertrain/cli/build.rb +192 -0
- data/cybertrain/cli/new_app.rb +19 -14
- data/cybertrain/cli/scaffold.rb +7 -0
- data/cybertrain/cli/templates.rb +29 -20
- data/cybertrain/cli/toolchain.rb +787 -0
- data/cybertrain/cli.rb +151 -11
- data/cybertrain/generator/inflector.rb +151 -86
- data/cybertrain/version.rb +5 -1
- data/exe/cybertrain +9 -1
- metadata +7 -3
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# `cybertrain build`: dist/ = the app binary with app/views embedded, plus
|
|
2
|
+
# public/. Also the command lists `cybertrain db` 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
|
+
# `cybertrain db ARGS`, through bin/db.rb (the app binary cannot compile
|
|
33
|
+
# before the first migration has produced gen/models). bin/db.rb only
|
|
34
|
+
# knows the migrations gen/migrations.rb lists, so gen runs first;
|
|
35
|
+
# migrate and rollback rewrite db/schema.rb, which gen/models is
|
|
36
|
+
# derived from, so gen runs again after them.
|
|
37
|
+
def self.db_commands(args)
|
|
38
|
+
words = args.map { |arg| quote_arg(arg) }.join(" ")
|
|
39
|
+
commands = ["spin run gen", "spin run db -- #{words}"]
|
|
40
|
+
commands << "spin run gen" if args[0] == "migrate" || args[0] == "rollback"
|
|
41
|
+
commands
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `cybertrain migration`: the same as `cybertrain db migrate`.
|
|
45
|
+
def self.migration_commands
|
|
46
|
+
db_commands(["migrate"])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A word sh reads as itself stays bare; anything else is single-quoted.
|
|
50
|
+
def self.quote_arg(text)
|
|
51
|
+
return text if !text.empty? && text.match(/\A[A-Za-z0-9_:.\/-]+\z/)
|
|
52
|
+
|
|
53
|
+
shell_quote(text)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# port is "" (the app's default, 3000) or a port? string.
|
|
57
|
+
def self.server_commands(name, port)
|
|
58
|
+
run = port == "" ? "spin run #{name}" : "spin run #{name} -- #{port}"
|
|
59
|
+
["spin run gen", run]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# All digits, 1 to 65535 (the app reads 0 as "no port").
|
|
63
|
+
def self.port?(text)
|
|
64
|
+
return false if text.empty? || text.size > 5
|
|
65
|
+
return false unless text.bytes.all? { |b| b >= 48 && b <= 57 }
|
|
66
|
+
|
|
67
|
+
text.to_i >= 1 && text.to_i <= 65535
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Runs one build step (a spin command) in root; true when it succeeds.
|
|
71
|
+
# Build.run takes a Runner so tests can stand in for spin.
|
|
72
|
+
class Runner
|
|
73
|
+
def run_step(root, command)
|
|
74
|
+
Build.run_in(root, command)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Holds this process's PID while the steps run and dist/ is assembled:
|
|
79
|
+
# the development server (Dev::Rebuilder) skips its rebuilds
|
|
80
|
+
# meanwhile, since both write build/bin/<name>, and treats a lock
|
|
81
|
+
# whose PID is gone (a killed build) as stale.
|
|
82
|
+
def self.lock_path(root)
|
|
83
|
+
"#{root}/tmp/cybertrain-build.lock"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Runs the build in root and assembles dist/. Returns the exit code.
|
|
87
|
+
def self.run(root, name, runner = Runner.new)
|
|
88
|
+
steps = commands(name)
|
|
89
|
+
ok = false
|
|
90
|
+
restored = false
|
|
91
|
+
code = 1
|
|
92
|
+
Templates.mkdir_p("#{root}/tmp")
|
|
93
|
+
File.write(lock_path(root), Process.pid.to_s)
|
|
94
|
+
begin
|
|
95
|
+
begin
|
|
96
|
+
ok = runner.run_step(root, steps[0]) && runner.run_step(root, steps[1])
|
|
97
|
+
ensure
|
|
98
|
+
# Always put gen/views.rb back to the empty table, even after a
|
|
99
|
+
# failure or Ctrl-C.
|
|
100
|
+
restored = runner.run_step(root, steps[2])
|
|
101
|
+
puts "warning: gen/views.rb still holds the embedded views; run spin run gen" unless restored
|
|
102
|
+
end
|
|
103
|
+
if ok && restored
|
|
104
|
+
assemble(root, name)
|
|
105
|
+
code = 0
|
|
106
|
+
end
|
|
107
|
+
ensure
|
|
108
|
+
# Released only once dist/ holds the new binary: assemble reads
|
|
109
|
+
# build/bin/<name>, which a dev rebuild would be writing.
|
|
110
|
+
File.delete(lock_path(root)) if File.exist?(lock_path(root))
|
|
111
|
+
end
|
|
112
|
+
return 1 unless code == 0
|
|
113
|
+
|
|
114
|
+
puts ""
|
|
115
|
+
puts "dist/#{name} (production by default)"
|
|
116
|
+
puts "dist/public/"
|
|
117
|
+
puts "run: cd dist && ./#{name} migrate && ./#{name}"
|
|
118
|
+
0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.run_in(root, command)
|
|
122
|
+
puts "run #{command}"
|
|
123
|
+
system("cd #{shell_quote(root)} && #{command}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Copies build/bin/<name> and public/ into dist/; storage/ and tmp/ are
|
|
127
|
+
# created when missing and never emptied (a database and the secret
|
|
128
|
+
# key can live there). Each copy lands in a dist/.*.tmp entry first and
|
|
129
|
+
# is renamed into place: a running dist/<name> keeps its old file
|
|
130
|
+
# (writing over a running binary fails with ETXTBSY on Linux), and the
|
|
131
|
+
# old dist/public/ steps aside to dist/.public.old between two renames
|
|
132
|
+
# (rename cannot replace a non-empty directory), so dist/public/ is
|
|
133
|
+
# missing only for that instant rather than for the whole copy.
|
|
134
|
+
def self.assemble(root, name)
|
|
135
|
+
binary = "#{root}/build/bin/#{name}"
|
|
136
|
+
raise InvalidArgument, "build/bin/#{name} is missing: `spin build #{name}` did not produce it" unless File.exist?(binary)
|
|
137
|
+
|
|
138
|
+
dist = "#{root}/dist"
|
|
139
|
+
Templates.mkdir_p(dist)
|
|
140
|
+
binary_tmp = "#{dist}/.#{name}.tmp"
|
|
141
|
+
public_tmp = "#{dist}/.public.tmp"
|
|
142
|
+
public_old = "#{dist}/.public.old"
|
|
143
|
+
# Leftovers of an interrupted earlier run.
|
|
144
|
+
rm_tree(binary_tmp)
|
|
145
|
+
rm_tree(public_tmp)
|
|
146
|
+
rm_tree(public_old)
|
|
147
|
+
|
|
148
|
+
copy_command = "cp #{shell_quote(binary)} #{shell_quote(binary_tmp)}"
|
|
149
|
+
raise InvalidArgument, "could not copy #{binary} to dist/" unless system(copy_command)
|
|
150
|
+
|
|
151
|
+
File.rename(binary_tmp, "#{dist}/#{name}")
|
|
152
|
+
|
|
153
|
+
if File.directory?("#{root}/public")
|
|
154
|
+
public_command = "cp -R #{shell_quote("#{root}/public")} #{shell_quote(public_tmp)}"
|
|
155
|
+
raise InvalidArgument, "could not copy public/ to dist/" unless system(public_command)
|
|
156
|
+
else
|
|
157
|
+
Dir.mkdir(public_tmp)
|
|
158
|
+
end
|
|
159
|
+
public_dir = "#{dist}/public"
|
|
160
|
+
File.rename(public_dir, public_old) if File.exist?(public_dir) || File.symlink?(public_dir)
|
|
161
|
+
File.rename(public_tmp, public_dir)
|
|
162
|
+
rm_tree(public_old)
|
|
163
|
+
|
|
164
|
+
Templates.mkdir_p("#{dist}/storage")
|
|
165
|
+
Templates.mkdir_p("#{dist}/tmp")
|
|
166
|
+
["dist/#{name}", "dist/public/", "dist/storage/", "dist/tmp/"]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A symlink is removed itself, never followed: its target may be
|
|
170
|
+
# dist/storage/ or outside dist/.
|
|
171
|
+
def self.rm_tree(path)
|
|
172
|
+
if File.symlink?(path)
|
|
173
|
+
File.delete(path)
|
|
174
|
+
return nil
|
|
175
|
+
end
|
|
176
|
+
return nil unless File.exist?(path)
|
|
177
|
+
|
|
178
|
+
if File.directory?(path)
|
|
179
|
+
Dir.children(path).each { |child| rm_tree("#{path}/#{child}") }
|
|
180
|
+
Dir.rmdir(path)
|
|
181
|
+
else
|
|
182
|
+
File.delete(path)
|
|
183
|
+
end
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def self.shell_quote(text)
|
|
188
|
+
"'#{text.gsub("'", "'\\\\''")}'"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
data/cybertrain/cli/new_app.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# `cybertrain new blog`: writes the skeleton of an application package.
|
|
2
2
|
require "cybertrain/generator/inflector"
|
|
3
3
|
require "cybertrain/cli/templates"
|
|
4
|
+
require "cybertrain/cli/build"
|
|
5
|
+
require "cybertrain/cli/toolchain"
|
|
4
6
|
|
|
5
7
|
module Cybertrain
|
|
6
8
|
module CLI
|
|
7
9
|
module NewApp
|
|
8
|
-
KEEP_DIRS = ["db/migrate", "app/models", "app/helpers", "gen", "storage", "tmp", "test"]
|
|
9
|
-
|
|
10
10
|
# dir is the directory to create ("blog" or "path/to/blog"; its last
|
|
11
11
|
# component names the package). framework_dep is the TOML value that
|
|
12
12
|
# spin.toml's `cybertrain =` line gets: `{ path = "/abs/cybertrain" }` or
|
|
@@ -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", ""],
|
|
@@ -47,28 +47,33 @@ module Cybertrain
|
|
|
47
47
|
|
|
48
48
|
# What `rails new` does with `bundle install`: resolve and lock the
|
|
49
49
|
# framework (fetching it into spin's cache, ~/.cache/spin), then write
|
|
50
|
-
# gen/ so `spin build` works straight away.
|
|
51
|
-
#
|
|
50
|
+
# gen/ so `spin build` works straight away. Installs Spinel first when
|
|
51
|
+
# this machine has none of the pinned release (Toolchain.ensure!).
|
|
52
|
+
# Returns false when a step failed.
|
|
52
53
|
def self.bootstrap(dir)
|
|
53
|
-
unless
|
|
54
|
-
puts "skip spin lock / spin run gen:
|
|
55
|
-
puts "
|
|
56
|
-
return
|
|
54
|
+
unless Toolchain.ensure!
|
|
55
|
+
puts "skip spin lock / spin run gen: Spinel #{Cybertrain::SPINEL_TAG} is not available"
|
|
56
|
+
puts " fix the problem above (`cybertrain doctor` lists the checks), then: cybertrain setup && #{recovery_command(dir)}"
|
|
57
|
+
return false
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
puts "run spin lock && spin run gen"
|
|
60
61
|
return true if system(bootstrap_command(dir))
|
|
61
62
|
|
|
62
|
-
puts "error: bootstrapping #{dir} failed; fix the cause, then run: #{
|
|
63
|
+
puts "error: bootstrapping #{dir} failed; fix the cause, then run: #{recovery_command(dir)}"
|
|
63
64
|
false
|
|
64
65
|
end
|
|
65
66
|
|
|
67
|
+
# Run by bootstrap once Toolchain.ensure! has put spin on this
|
|
68
|
+
# process's PATH.
|
|
66
69
|
def self.bootstrap_command(dir)
|
|
67
|
-
"cd #{shell_quote(dir)} && spin lock && spin run gen"
|
|
70
|
+
"cd #{Build.shell_quote(dir)} && spin lock && spin run gen"
|
|
68
71
|
end
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
# The same steps for the user to run by hand: through `cybertrain
|
|
74
|
+
# spin`, since the Spinel cybertrain installed is not on their PATH.
|
|
75
|
+
def self.recovery_command(dir)
|
|
76
|
+
"cd #{Build.shell_quote(dir)} && cybertrain spin lock && cybertrain spin run gen"
|
|
72
77
|
end
|
|
73
78
|
end
|
|
74
79
|
end
|
data/cybertrain/cli/scaffold.rb
CHANGED
|
@@ -92,6 +92,13 @@ module Cybertrain
|
|
|
92
92
|
@fields = fields
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# The index route helper's name without _path, as Rails' scaffold
|
|
96
|
+
# names it: the plural, or "<plural>_index" for a word that is its own
|
|
97
|
+
# plural (sheep_index_path), matching the routes DSL.
|
|
98
|
+
def index_helper
|
|
99
|
+
@plural == @singular ? "#{@plural}_index" : @plural
|
|
100
|
+
end
|
|
101
|
+
|
|
95
102
|
# The column the model validates for presence ("" when there is none).
|
|
96
103
|
def first_string_field
|
|
97
104
|
@fields.each { |f| return f.field_name if f.field_type == "string" }
|
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 db migrate # 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 `db`, `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 db migrate` 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"
|
|
@@ -317,7 +326,7 @@ module Cybertrain
|
|
|
317
326
|
def destroy
|
|
318
327
|
@#{s}.destroy
|
|
319
328
|
flash[:notice] = "#{human} was successfully destroyed."
|
|
320
|
-
redirect_to #{
|
|
329
|
+
redirect_to #{res.index_helper}_path, status: :see_other
|
|
321
330
|
end
|
|
322
331
|
|
|
323
332
|
private
|
|
@@ -362,7 +371,7 @@ module Cybertrain
|
|
|
362
371
|
end
|
|
363
372
|
buf << "<p>\n"
|
|
364
373
|
buf << " <%= link_to \"Edit this #{words}\", edit_#{s}_path(@#{s}) %> |\n"
|
|
365
|
-
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.
|
|
374
|
+
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %>\n"
|
|
366
375
|
buf << "</p>\n\n"
|
|
367
376
|
buf << "<%= button_to \"Destroy this #{words}\", #{s}_path(@#{s}), method: :delete %>\n"
|
|
368
377
|
buf
|
|
@@ -373,7 +382,7 @@ module Cybertrain
|
|
|
373
382
|
buf = +""
|
|
374
383
|
buf << "<h1>New #{s.tr("_", " ")}</h1>\n\n"
|
|
375
384
|
buf << "<%= render \"form\", #{s}: @#{s} %>\n\n"
|
|
376
|
-
buf << "<p><%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.
|
|
385
|
+
buf << "<p><%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %></p>\n"
|
|
377
386
|
buf
|
|
378
387
|
end
|
|
379
388
|
|
|
@@ -384,7 +393,7 @@ module Cybertrain
|
|
|
384
393
|
buf << "<%= render \"form\", #{s}: @#{s} %>\n\n"
|
|
385
394
|
buf << "<p>\n"
|
|
386
395
|
buf << " <%= link_to \"Show this #{s.tr("_", " ")}\", #{s}_path(@#{s}) %> |\n"
|
|
387
|
-
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.
|
|
396
|
+
buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %>\n"
|
|
388
397
|
buf << "</p>\n"
|
|
389
398
|
buf
|
|
390
399
|
end
|