railst 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 +7 -0
- data/README.md +29 -0
- data/exe/railst +9 -0
- data/lib/railst/cli.rb +742 -0
- data/lib/railst/environment.rb +65 -0
- data/lib/railst/templates.rb +503 -0
- data/lib/railst/terminal.rb +80 -0
- data/lib/railst/version.rb +5 -0
- data/lib/railst.rb +10 -0
- metadata +50 -0
data/lib/railst/cli.rb
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "etc"
|
|
8
|
+
|
|
9
|
+
require_relative "version"
|
|
10
|
+
require_relative "terminal"
|
|
11
|
+
require_relative "environment"
|
|
12
|
+
require_relative "templates"
|
|
13
|
+
|
|
14
|
+
module Railst
|
|
15
|
+
class CLI
|
|
16
|
+
def self.start(args = ARGV)
|
|
17
|
+
new(args).run
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(args)
|
|
21
|
+
@args = args.dup
|
|
22
|
+
@command = @args.shift || "help"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
case @command
|
|
27
|
+
when "s", "server", "run"
|
|
28
|
+
run_server
|
|
29
|
+
when "c", "console"
|
|
30
|
+
run_console
|
|
31
|
+
when "g", "generate"
|
|
32
|
+
run_generate
|
|
33
|
+
when "d", "destroy"
|
|
34
|
+
run_destroy
|
|
35
|
+
when "new"
|
|
36
|
+
run_new
|
|
37
|
+
when "t", "test"
|
|
38
|
+
run_test
|
|
39
|
+
when "test:rust", "t:rust"
|
|
40
|
+
run_rust_test
|
|
41
|
+
when "test:all", "t:all"
|
|
42
|
+
run_all_tests
|
|
43
|
+
when "r", "runner"
|
|
44
|
+
run_runner
|
|
45
|
+
when "routes"
|
|
46
|
+
run_routes
|
|
47
|
+
when "db"
|
|
48
|
+
run_db
|
|
49
|
+
when /\Adb:/
|
|
50
|
+
run_db_task
|
|
51
|
+
when "build", "rust:build"
|
|
52
|
+
run_build
|
|
53
|
+
when "clean", "rust:clean"
|
|
54
|
+
run_clean
|
|
55
|
+
when "doctor", "check"
|
|
56
|
+
run_doctor
|
|
57
|
+
when "bench", "benchmark"
|
|
58
|
+
run_benchmark
|
|
59
|
+
when "stats"
|
|
60
|
+
run_stats
|
|
61
|
+
when "ci"
|
|
62
|
+
run_ci
|
|
63
|
+
when "setup"
|
|
64
|
+
run_setup
|
|
65
|
+
when "install"
|
|
66
|
+
run_install_cli
|
|
67
|
+
when "version", "--version", "-v"
|
|
68
|
+
show_version
|
|
69
|
+
when "help", "--help", "-h"
|
|
70
|
+
show_help
|
|
71
|
+
else
|
|
72
|
+
run_passthrough(@command)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def project_root
|
|
79
|
+
@project_root ||= begin
|
|
80
|
+
current = Pathname.pwd
|
|
81
|
+
loop do
|
|
82
|
+
return current if current.join("bin/rails").exist? && current.join("ext/rust_backend").exist?
|
|
83
|
+
break if current.root?
|
|
84
|
+
|
|
85
|
+
current = current.parent
|
|
86
|
+
end
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def require_project_root!
|
|
92
|
+
unless project_root
|
|
93
|
+
puts ""
|
|
94
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} You must run this command inside a Rust on Rails project directory."
|
|
95
|
+
puts ""
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def system_in_root(*cmd, exit_on_fail: true)
|
|
101
|
+
require_project_root!
|
|
102
|
+
success = false
|
|
103
|
+
exit_code = 0
|
|
104
|
+
|
|
105
|
+
Dir.chdir(project_root) do
|
|
106
|
+
success = Kernel.system(Environment.toolchains_env, *cmd)
|
|
107
|
+
exit_code = $?.exitstatus || (success ? 0 : 1)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
exit(exit_code) if exit_on_fail && !success
|
|
111
|
+
success
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def exec_in_root(*cmd)
|
|
115
|
+
require_project_root!
|
|
116
|
+
Dir.chdir(project_root) do
|
|
117
|
+
Kernel.exec(Environment.toolchains_env, *cmd)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def rust_backend_stale?
|
|
122
|
+
return false unless project_root
|
|
123
|
+
|
|
124
|
+
so_path = project_root.join("lib/rust_backend/rust_backend.so")
|
|
125
|
+
return true unless so_path.exist?
|
|
126
|
+
|
|
127
|
+
so_mtime = so_path.mtime
|
|
128
|
+
src_pattern = project_root.join("ext/rust_backend/**/*.{rs,toml,rb}").to_s
|
|
129
|
+
Dir[src_pattern].any? do |f|
|
|
130
|
+
next false if f.include?("/target/")
|
|
131
|
+
|
|
132
|
+
File.mtime(f) > so_mtime
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ensure_rust_built!
|
|
137
|
+
require_project_root!
|
|
138
|
+
return unless rust_backend_stale?
|
|
139
|
+
|
|
140
|
+
puts ""
|
|
141
|
+
puts " #{Terminal.badge("INFO", bg: :cyan, fg: :black)} Rust extension is missing or modified. Recompiling..."
|
|
142
|
+
puts ""
|
|
143
|
+
|
|
144
|
+
success = system_in_root("bin/rake", "rust:build", exit_on_fail: false)
|
|
145
|
+
unless success
|
|
146
|
+
puts ""
|
|
147
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Rust compilation failed."
|
|
148
|
+
puts ""
|
|
149
|
+
exit 1
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# --- Server Command ---
|
|
154
|
+
|
|
155
|
+
def parse_server_options(args)
|
|
156
|
+
options = {
|
|
157
|
+
host: "127.0.0.1",
|
|
158
|
+
port: 3000,
|
|
159
|
+
environment: ENV.fetch("RAILS_ENV", "development"),
|
|
160
|
+
find_port: false,
|
|
161
|
+
passthrough: []
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
i = 0
|
|
165
|
+
while i < args.length
|
|
166
|
+
arg = args[i]
|
|
167
|
+
case arg
|
|
168
|
+
when "-p", "--port"
|
|
169
|
+
i += 1
|
|
170
|
+
options[:port] = args[i].to_i
|
|
171
|
+
when /\A-p(\d+)\z/, /\A--port=(\d+)\z/
|
|
172
|
+
options[:port] = $1.to_i
|
|
173
|
+
when "-b", "-h", "--binding", "--host"
|
|
174
|
+
i += 1
|
|
175
|
+
options[:host] = args[i]
|
|
176
|
+
when /\A-b(.+)\z/, /\A--binding=(.+)\z/, /\A--host=(.+)\z/
|
|
177
|
+
options[:host] = $1
|
|
178
|
+
when "-e", "--environment"
|
|
179
|
+
i += 1
|
|
180
|
+
options[:environment] = args[i]
|
|
181
|
+
when /\A-e(.+)\z/, /\A--environment=(.+)\z/
|
|
182
|
+
options[:environment] = $1
|
|
183
|
+
when "-f", "--find-port"
|
|
184
|
+
options[:find_port] = true
|
|
185
|
+
else
|
|
186
|
+
options[:passthrough] << arg
|
|
187
|
+
end
|
|
188
|
+
i += 1
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
options
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def port_in_use?(host, port)
|
|
195
|
+
check_hosts = [ host ]
|
|
196
|
+
check_hosts << "127.0.0.1" if host == "0.0.0.0" || host == "localhost"
|
|
197
|
+
check_hosts << "0.0.0.0" if host == "127.0.0.1"
|
|
198
|
+
|
|
199
|
+
check_hosts.uniq.each do |h|
|
|
200
|
+
server = TCPServer.new(h, port)
|
|
201
|
+
server.close
|
|
202
|
+
rescue Errno::EADDRINUSE, Errno::EACCES
|
|
203
|
+
return true
|
|
204
|
+
rescue StandardError
|
|
205
|
+
# ignore other connection errors during check
|
|
206
|
+
end
|
|
207
|
+
false
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def find_available_port(host, starting_port, max_tries = 20)
|
|
211
|
+
candidate = starting_port
|
|
212
|
+
max_tries.times do
|
|
213
|
+
return candidate unless port_in_use?(host, candidate)
|
|
214
|
+
|
|
215
|
+
candidate += 1
|
|
216
|
+
end
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def run_server
|
|
221
|
+
require_project_root!
|
|
222
|
+
ensure_rust_built!
|
|
223
|
+
|
|
224
|
+
opts = parse_server_options(@args)
|
|
225
|
+
host = opts[:host]
|
|
226
|
+
port = opts[:port]
|
|
227
|
+
env_name = opts[:environment]
|
|
228
|
+
|
|
229
|
+
if port_in_use?(host, port)
|
|
230
|
+
if opts[:find_port]
|
|
231
|
+
next_port = find_available_port(host, port + 1)
|
|
232
|
+
if next_port
|
|
233
|
+
puts ""
|
|
234
|
+
puts " #{Terminal.badge("WARN", bg: :yellow, fg: :black)} Port #{port} is occupied. Using port #{next_port} instead."
|
|
235
|
+
port = next_port
|
|
236
|
+
else
|
|
237
|
+
print_port_error(host, port)
|
|
238
|
+
exit 1
|
|
239
|
+
end
|
|
240
|
+
else
|
|
241
|
+
suggested_port = find_available_port(host, port + 1) || (port + 1)
|
|
242
|
+
print_port_error(host, port, suggested_port)
|
|
243
|
+
exit 1
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
display_host = (host == "0.0.0.0") ? "127.0.0.1" : host
|
|
248
|
+
puts ""
|
|
249
|
+
puts " #{Terminal.badge("INFO", bg: :green, fg: :black)} Server running on #{Terminal.bold_cyan("[http://#{display_host}:#{port}]")}."
|
|
250
|
+
puts ""
|
|
251
|
+
puts " #{Terminal.dim("Press")} #{Terminal.bold("Ctrl+C")} #{Terminal.dim("to stop the server")}"
|
|
252
|
+
puts ""
|
|
253
|
+
puts " #{Terminal.dim("Environment")} #{Terminal.dim(".........................................")} #{env_name}"
|
|
254
|
+
puts " #{Terminal.dim("Rust Backend")} #{Terminal.dim(".......................")} #{Terminal.green("Rayon + Magnus 0.9 (active)")}"
|
|
255
|
+
puts ""
|
|
256
|
+
|
|
257
|
+
exec_in_root("bin/rails", "server", "-b", host, "-p", port.to_s, "-e", env_name, *opts[:passthrough])
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def print_port_error(host, port, suggested_port = nil)
|
|
261
|
+
puts ""
|
|
262
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Failed to listen on #{host}:#{port} (Address already in use)."
|
|
263
|
+
puts ""
|
|
264
|
+
puts " #{Terminal.dim("Port")} #{Terminal.bold(port)} #{Terminal.dim("is currently occupied by another process.")}"
|
|
265
|
+
if suggested_port
|
|
266
|
+
puts " #{Terminal.dim("Try specifying a different port:")}"
|
|
267
|
+
puts " #{Terminal.bold_yellow("railst s -p #{suggested_port}")}"
|
|
268
|
+
puts ""
|
|
269
|
+
puts " #{Terminal.dim("Or run with automatic port selection:")}"
|
|
270
|
+
puts " #{Terminal.bold_yellow("railst s --find-port")}"
|
|
271
|
+
end
|
|
272
|
+
puts ""
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# --- Core Commands ---
|
|
276
|
+
|
|
277
|
+
def run_console
|
|
278
|
+
require_project_root!
|
|
279
|
+
ensure_rust_built!
|
|
280
|
+
exec_in_root("bin/rails", "console", *@args)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def run_generate
|
|
284
|
+
require_project_root!
|
|
285
|
+
ensure_rust_built!
|
|
286
|
+
if @args.empty?
|
|
287
|
+
exec_in_root("bin/rails", "generate", "--help")
|
|
288
|
+
else
|
|
289
|
+
system_in_root("bin/rails", "generate", *@args)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def run_destroy
|
|
294
|
+
require_project_root!
|
|
295
|
+
system_in_root("bin/rails", "destroy", *@args)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def run_runner
|
|
299
|
+
require_project_root!
|
|
300
|
+
ensure_rust_built!
|
|
301
|
+
exec_in_root("bin/rails", "runner", *@args)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def run_routes
|
|
305
|
+
require_project_root!
|
|
306
|
+
system_in_root("bin/rails", "routes", *@args)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def run_db
|
|
310
|
+
require_project_root!
|
|
311
|
+
sub = @args.shift || "migrate"
|
|
312
|
+
system_in_root("bin/rails", "db:#{sub}", *@args)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def run_db_task
|
|
316
|
+
require_project_root!
|
|
317
|
+
system_in_root("bin/rails", @command, *@args)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def run_test
|
|
321
|
+
require_project_root!
|
|
322
|
+
if @args.empty?
|
|
323
|
+
run_all_tests
|
|
324
|
+
elsif @args.first == "rust"
|
|
325
|
+
@args.shift
|
|
326
|
+
run_rust_test
|
|
327
|
+
elsif @args.first == "all"
|
|
328
|
+
@args.shift
|
|
329
|
+
run_all_tests
|
|
330
|
+
else
|
|
331
|
+
ensure_rust_built!
|
|
332
|
+
system_in_root("bin/rails", "test", *@args)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def run_rust_test
|
|
337
|
+
require_project_root!
|
|
338
|
+
puts ""
|
|
339
|
+
puts " #{Terminal.badge("INFO", bg: :cyan, fg: :black)} Running native Rust crate unit tests..."
|
|
340
|
+
puts ""
|
|
341
|
+
system_in_root("bin/rake", "rust:test")
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def run_all_tests
|
|
345
|
+
require_project_root!
|
|
346
|
+
puts ""
|
|
347
|
+
puts " #{Terminal.badge("INFO", bg: :cyan, fg: :black)} Running native Rust tests..."
|
|
348
|
+
puts ""
|
|
349
|
+
rust_ok = system_in_root("bin/rake", "rust:test", exit_on_fail: false)
|
|
350
|
+
|
|
351
|
+
puts ""
|
|
352
|
+
puts " #{Terminal.badge("INFO", bg: :green, fg: :black)} Running Rails test suite..."
|
|
353
|
+
puts ""
|
|
354
|
+
rails_ok = system_in_root("bin/rails", "test", exit_on_fail: false)
|
|
355
|
+
|
|
356
|
+
exit((rust_ok && rails_ok) ? 0 : 1)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def run_build
|
|
360
|
+
require_project_root!
|
|
361
|
+
puts ""
|
|
362
|
+
puts " #{Terminal.badge("INFO", bg: :cyan, fg: :black)} Compiling Magnus Rust backend extension..."
|
|
363
|
+
puts ""
|
|
364
|
+
ok = system_in_root("bin/rake", "rust:build", exit_on_fail: false)
|
|
365
|
+
if ok
|
|
366
|
+
puts ""
|
|
367
|
+
puts " #{Terminal.badge("INFO", bg: :green, fg: :black)} Rust backend compiled successfully."
|
|
368
|
+
puts ""
|
|
369
|
+
else
|
|
370
|
+
puts ""
|
|
371
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Rust compilation failed."
|
|
372
|
+
puts ""
|
|
373
|
+
exit 1
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def run_clean
|
|
378
|
+
require_project_root!
|
|
379
|
+
puts ""
|
|
380
|
+
puts " #{Terminal.badge("INFO", bg: :cyan, fg: :black)} Cleaning Rust build artifacts..."
|
|
381
|
+
puts ""
|
|
382
|
+
system_in_root("bin/rake", "rust:clean")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def run_setup
|
|
386
|
+
require_project_root!
|
|
387
|
+
system_in_root("bin/setup", *@args)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def run_ci
|
|
391
|
+
require_project_root!
|
|
392
|
+
system_in_root("bin/ci", *@args)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def run_passthrough(cmd)
|
|
396
|
+
require_project_root!
|
|
397
|
+
exec_in_root("bin/rails", cmd, *@args)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def run_new
|
|
401
|
+
app_name = @args.shift
|
|
402
|
+
if app_name.nil? || app_name.empty? || app_name.start_with?("-")
|
|
403
|
+
puts ""
|
|
404
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Missing application name."
|
|
405
|
+
puts ""
|
|
406
|
+
puts " Usage:"
|
|
407
|
+
puts " #{Terminal.bold_cyan("railst new <app_name> [rails_options]")}"
|
|
408
|
+
puts ""
|
|
409
|
+
puts " Examples:"
|
|
410
|
+
puts " #{Terminal.bold_yellow("railst new my_app")}"
|
|
411
|
+
puts " #{Terminal.bold_yellow("railst new analytics_service -d postgresql")}"
|
|
412
|
+
puts ""
|
|
413
|
+
exit 1
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
target_dir = Pathname.pwd.join(app_name)
|
|
417
|
+
if target_dir.exist?
|
|
418
|
+
puts ""
|
|
419
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Destination directory '#{app_name}' already exists."
|
|
420
|
+
puts ""
|
|
421
|
+
exit 1
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
puts ""
|
|
425
|
+
puts " #{Terminal.badge("INFO", bg: :cyan, fg: :black)} Creating new Rails application: #{Terminal.bold(app_name)}..."
|
|
426
|
+
puts ""
|
|
427
|
+
|
|
428
|
+
rails_cmd = [ "rails", "new", app_name, *@args ]
|
|
429
|
+
rails_success = Kernel.system(Environment.toolchains_env, *rails_cmd)
|
|
430
|
+
unless rails_success
|
|
431
|
+
puts ""
|
|
432
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Failed to create base Rails application."
|
|
433
|
+
puts ""
|
|
434
|
+
exit 1
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
puts ""
|
|
438
|
+
puts " #{Terminal.badge("INFO", bg: :cyan, fg: :black)} Injecting Rust on Rails architecture (Magnus 0.9 + Rayon + GVL)..."
|
|
439
|
+
puts ""
|
|
440
|
+
|
|
441
|
+
Templates.inject_into(target_dir, project_root)
|
|
442
|
+
|
|
443
|
+
# Append rb_sys into Gemfile if not present
|
|
444
|
+
gemfile = target_dir.join("Gemfile")
|
|
445
|
+
if gemfile.exist?
|
|
446
|
+
gem_content = gemfile.read
|
|
447
|
+
unless gem_content.include?("rb_sys")
|
|
448
|
+
File.open(gemfile, "a") do |f|
|
|
449
|
+
f.puts "\n# Rust C-extension build tooling\ngroup :development, :test do\n gem \"rb_sys\", \"~> 0.9\"\nend\n"
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Copy railst CLI executable into bin/railst
|
|
455
|
+
cli_source = File.expand_path($PROGRAM_NAME)
|
|
456
|
+
dest_cli = target_dir.join("bin/railst")
|
|
457
|
+
FileUtils.cp(cli_source, dest_cli)
|
|
458
|
+
FileUtils.chmod(0755, dest_cli)
|
|
459
|
+
|
|
460
|
+
puts ""
|
|
461
|
+
puts " #{Terminal.badge("INFO", bg: :green, fg: :black)} Compiling native Rust backend in #{app_name}..."
|
|
462
|
+
puts ""
|
|
463
|
+
|
|
464
|
+
Dir.chdir(target_dir) do
|
|
465
|
+
Kernel.system(Environment.toolchains_env, "bundle", "install")
|
|
466
|
+
Kernel.system(Environment.toolchains_env, "bin/rake", "rust:build")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
puts ""
|
|
470
|
+
puts " #{Terminal.badge("SUCCESS", bg: :green, fg: :black)} #{Terminal.bold("Rust on Rails application created successfully!")}"
|
|
471
|
+
puts ""
|
|
472
|
+
puts " #{Terminal.dim("Get started:")}"
|
|
473
|
+
puts " #{Terminal.bold_yellow("cd #{app_name}")}"
|
|
474
|
+
puts " #{Terminal.bold_yellow("railst s")}"
|
|
475
|
+
puts ""
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def run_doctor
|
|
479
|
+
puts ""
|
|
480
|
+
puts " #{Terminal.bold_cyan("=== Rust on Rails System Diagnostics ===")}"
|
|
481
|
+
puts ""
|
|
482
|
+
|
|
483
|
+
# 1. Ruby & C Headers
|
|
484
|
+
ruby_ver = "#{RUBY_VERSION} (#{RUBY_PLATFORM})"
|
|
485
|
+
hdr_dir = RbConfig::CONFIG["rubyhdrdir"]
|
|
486
|
+
hdr_file = hdr_dir ? File.join(hdr_dir, "ruby.h") : nil
|
|
487
|
+
hdr_ok = hdr_file && File.exist?(hdr_file)
|
|
488
|
+
|
|
489
|
+
if hdr_ok
|
|
490
|
+
puts " #{Terminal.green("✓")} #{Terminal.bold("Ruby")}: #{ruby_ver}"
|
|
491
|
+
puts " #{Terminal.green("✓")} #{Terminal.bold("Ruby C-Headers")}: #{hdr_file}"
|
|
492
|
+
else
|
|
493
|
+
puts " #{Terminal.yellow("!")} #{Terminal.bold("Ruby")}: #{ruby_ver}"
|
|
494
|
+
puts " #{Terminal.red("✗")} #{Terminal.bold("Ruby C-Headers")}: Not found in #{hdr_dir || 'unknown'}"
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# 2. Rust & Cargo
|
|
498
|
+
env = Environment.toolchains_env
|
|
499
|
+
cargo_ver = `cargo --version 2>/dev/null`.strip
|
|
500
|
+
rustc_ver = `rustc --version 2>/dev/null`.strip
|
|
501
|
+
|
|
502
|
+
if !cargo_ver.empty? && !rustc_ver.empty?
|
|
503
|
+
puts " #{Terminal.green("✓")} #{Terminal.bold("Rust Toolchain")}: #{rustc_ver}, #{cargo_ver}"
|
|
504
|
+
else
|
|
505
|
+
puts " #{Terminal.red("✗")} #{Terminal.bold("Rust Toolchain")}: cargo/rustc not found in PATH."
|
|
506
|
+
puts " #{Terminal.dim("Hint: Install Rust via: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh")}"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# 3. LLVM / Clang
|
|
510
|
+
libclang = env["LIBCLANG_PATH"]
|
|
511
|
+
libclang_so = libclang ? Dir[File.join(libclang, "libclang.so*")].first : nil
|
|
512
|
+
|
|
513
|
+
if libclang_so
|
|
514
|
+
puts " #{Terminal.green("✓")} #{Terminal.bold("LLVM / Clang")}: #{libclang_so}"
|
|
515
|
+
else
|
|
516
|
+
puts " #{Terminal.yellow("!")} #{Terminal.bold("LLVM / Clang")}: libclang.so not detected in standard paths."
|
|
517
|
+
puts " #{Terminal.dim("Hint: Install libclang-dev (e.g. sudo apt install libclang-dev)")}"
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# 4. Multi-Core Hardware Detection (Rayon)
|
|
521
|
+
cores = Etc.nprocessors
|
|
522
|
+
puts " #{Terminal.green("✓")} #{Terminal.bold("Multi-Core Hardware")}: #{cores} physical/logical CPU cores (Rayon ready)"
|
|
523
|
+
|
|
524
|
+
# 5. Project-Specific Status (if inside project)
|
|
525
|
+
if project_root
|
|
526
|
+
puts ""
|
|
527
|
+
puts " #{Terminal.bold_cyan("Project Status:")} #{project_root}"
|
|
528
|
+
|
|
529
|
+
so_path = project_root.join("lib/rust_backend/rust_backend.so")
|
|
530
|
+
if so_path.exist?
|
|
531
|
+
stale = rust_backend_stale?
|
|
532
|
+
size_kb = (so_path.size / 1024.0).round(1)
|
|
533
|
+
if stale
|
|
534
|
+
puts " #{Terminal.yellow("!")} #{Terminal.bold("Rust Backend")}: Compiled (#{size_kb} KB) but #{Terminal.yellow("source files are newer")}."
|
|
535
|
+
puts " #{Terminal.dim("Hint: Run 'railst build' to recompile.")}"
|
|
536
|
+
else
|
|
537
|
+
puts " #{Terminal.green("✓")} #{Terminal.bold("Rust Backend")}: Compiled and up-to-date (#{size_kb} KB)"
|
|
538
|
+
end
|
|
539
|
+
else
|
|
540
|
+
puts " #{Terminal.red("✗")} #{Terminal.bold("Rust Backend")}: Not yet compiled."
|
|
541
|
+
puts " #{Terminal.dim("Hint: Run 'railst build' to compile.")}"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
schema_file = project_root.join("db/schema.rb")
|
|
545
|
+
if schema_file.exist?
|
|
546
|
+
puts " #{Terminal.green("✓")} #{Terminal.bold("Database Schema")}: Present (#{schema_file})"
|
|
547
|
+
end
|
|
548
|
+
else
|
|
549
|
+
puts ""
|
|
550
|
+
puts " #{Terminal.dim("(Run inside a Rust on Rails project directory for local extension diagnostics)")}"
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
puts ""
|
|
554
|
+
puts " #{Terminal.dim("System ready for high-performance Rust on Rails computing.")}"
|
|
555
|
+
puts ""
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def run_benchmark
|
|
559
|
+
require_project_root!
|
|
560
|
+
ensure_rust_built!
|
|
561
|
+
|
|
562
|
+
count = (@args.first || 25_000).to_i.clamp(1_000, 200_000)
|
|
563
|
+
|
|
564
|
+
puts ""
|
|
565
|
+
puts " #{Terminal.badge("BENCH", bg: :magenta, fg: :white)} #{Terminal.bold("Running Rust on Rails In-Process Performance Benchmark")}"
|
|
566
|
+
puts " Dataset: #{Terminal.bold(count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse)} in-memory record hashes"
|
|
567
|
+
puts ""
|
|
568
|
+
|
|
569
|
+
runner_code = <<~RUBY
|
|
570
|
+
count = #{count}
|
|
571
|
+
data = Array.new(count) { |i| { "id" => i, "value" => (i * 1.5).round(2) } }
|
|
572
|
+
cfg = { "multiplier" => 1.25, "compute_field" => "value" }
|
|
573
|
+
|
|
574
|
+
# 1. Pure Ruby Sequential Batch
|
|
575
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
576
|
+
ruby_res = data.map do |d|
|
|
577
|
+
d.merge("value_computed" => d["value"] * 1.25, "processed_by" => "ruby")
|
|
578
|
+
end
|
|
579
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
580
|
+
ruby_batch_ms = ((t1 - t0) * 1000.0).round(2)
|
|
581
|
+
|
|
582
|
+
# 2. Rust Multi-Core Rayon Batch
|
|
583
|
+
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
584
|
+
rust_res = RustBackend.process_batch(data, cfg)
|
|
585
|
+
t3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
586
|
+
rust_batch_ms = ((t3 - t2) * 1000.0).round(2)
|
|
587
|
+
|
|
588
|
+
# 3. Rust Parallel Reduction Metrics
|
|
589
|
+
t4 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
590
|
+
metrics = RustBackend.aggregate_metrics(data, "value")
|
|
591
|
+
t5 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
592
|
+
rust_metric_ms = ((t5 - t4) * 1000.0).round(2)
|
|
593
|
+
|
|
594
|
+
cores = Etc.nprocessors
|
|
595
|
+
ruby_thru = (count / [ruby_batch_ms / 1000.0, 0.001].max).round
|
|
596
|
+
rust_thru = (count / [rust_batch_ms / 1000.0, 0.001].max).round
|
|
597
|
+
|
|
598
|
+
puts "RESULTS:"
|
|
599
|
+
puts "\#{ruby_batch_ms}|\#{rust_batch_ms}|\#{rust_metric_ms}|\#{ruby_thru}|\#{rust_thru}|\#{cores}"
|
|
600
|
+
RUBY
|
|
601
|
+
|
|
602
|
+
out = nil
|
|
603
|
+
Dir.chdir(project_root) do
|
|
604
|
+
out = `bin/rails runner '#{runner_code}' 2>/dev/null`
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
if out && out.include?("RESULTS:")
|
|
608
|
+
line = out.lines.find { |l| l.include?("|") }&.strip
|
|
609
|
+
if line
|
|
610
|
+
ruby_ms, rust_ms, metric_ms, ruby_thru, rust_thru, cores = line.split("|")
|
|
611
|
+
|
|
612
|
+
puts " +---------------------------+----------------+----------------+"
|
|
613
|
+
puts " | Operation | Pure Ruby | Rust (Rayon) |"
|
|
614
|
+
puts " +---------------------------+----------------+----------------+"
|
|
615
|
+
puts " | Batch Compute Latency | #{ruby_ms.rjust(11)} ms | #{Terminal.bold_green(rust_ms.rjust(11) + ' ms')} |"
|
|
616
|
+
puts " | Parallel Metrics (5 ops) | N/A | #{Terminal.bold_cyan(metric_ms.rjust(11) + ' ms')} |"
|
|
617
|
+
puts " | Processing Throughput | #{ruby_thru.rjust(9)} rec/s | #{Terminal.bold_green(rust_thru.rjust(9) + ' rec/s')} |"
|
|
618
|
+
puts " +---------------------------+----------------+----------------+"
|
|
619
|
+
puts ""
|
|
620
|
+
puts " #{Terminal.dim("Hardware Acceleration:")} #{Terminal.bold(cores)} CPU cores #{Terminal.dim("(Rayon parallel pool, Ruby GVL released)")}"
|
|
621
|
+
puts ""
|
|
622
|
+
end
|
|
623
|
+
else
|
|
624
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Failed to execute benchmark runner."
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def run_stats
|
|
629
|
+
require_project_root!
|
|
630
|
+
system_in_root("bin/rails", "stats")
|
|
631
|
+
|
|
632
|
+
puts ""
|
|
633
|
+
puts " #{Terminal.bold_cyan("Rust Native Code Statistics:")}"
|
|
634
|
+
crate_dir = project_root.join("ext/rust_backend")
|
|
635
|
+
if crate_dir.exist?
|
|
636
|
+
rs_files = Dir[crate_dir.join("src/**/*.rs")]
|
|
637
|
+
total_lines = 0
|
|
638
|
+
test_count = 0
|
|
639
|
+
|
|
640
|
+
puts " +-------------------------------+-------+-------+"
|
|
641
|
+
puts " | Rust Module | Lines | Tests |"
|
|
642
|
+
puts " +-------------------------------+-------+-------+"
|
|
643
|
+
|
|
644
|
+
rs_files.sort.each do |f|
|
|
645
|
+
name = Pathname.new(f).relative_path_from(crate_dir).to_s
|
|
646
|
+
content = File.read(f)
|
|
647
|
+
lines = content.lines.count
|
|
648
|
+
tests = content.scan(/#\[test\]/).count
|
|
649
|
+
total_lines += lines
|
|
650
|
+
test_count += tests
|
|
651
|
+
puts " | #{name.ljust(29)} | #{lines.to_s.rjust(5)} | #{tests.to_s.rjust(5)} |"
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
puts " +-------------------------------+-------+-------+"
|
|
655
|
+
puts " | #{Terminal.bold('Total Rust Backend')} | #{Terminal.bold(total_lines.to_s.rjust(5))} | #{Terminal.bold(test_count.to_s.rjust(5))} |"
|
|
656
|
+
puts " +-------------------------------+-------+-------+"
|
|
657
|
+
puts ""
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def run_install_cli
|
|
662
|
+
user_gem_bin = `ruby -r rubygems -e "puts Gem.user_dir" 2>/dev/null`.strip
|
|
663
|
+
dest_dirs = []
|
|
664
|
+
dest_dirs << File.join(user_gem_bin, "bin") if !user_gem_bin.empty? && File.directory?(user_gem_bin)
|
|
665
|
+
dest_dirs << File.expand_path("~/.local/bin")
|
|
666
|
+
|
|
667
|
+
source_script = File.expand_path($PROGRAM_NAME)
|
|
668
|
+
|
|
669
|
+
dest_dirs.uniq.each do |dest_dir|
|
|
670
|
+
FileUtils.mkdir_p(dest_dir)
|
|
671
|
+
dest_path = File.join(dest_dir, "railst")
|
|
672
|
+
FileUtils.cp(source_script, dest_path)
|
|
673
|
+
File.chmod(0755, dest_path)
|
|
674
|
+
puts " #{Terminal.badge("INFO", bg: :green, fg: :black)} Installed railst CLI to: #{dest_path}"
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
puts ""
|
|
678
|
+
puts " #{Terminal.dim("You can now run")} #{Terminal.bold_cyan("railst <command>")} #{Terminal.dim("from any directory!")}"
|
|
679
|
+
puts ""
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def show_version
|
|
683
|
+
puts "Rust on Rails CLI v#{VERSION} (Rails 8 + Magnus 0.9 + Rayon)"
|
|
684
|
+
if project_root
|
|
685
|
+
rails_ver = `bin/rails --version 2>/dev/null`.strip
|
|
686
|
+
rust_ver = `rustc --version 2>/dev/null`.strip
|
|
687
|
+
puts " Rails: #{rails_ver}" unless rails_ver.empty?
|
|
688
|
+
puts " Rust: #{rust_ver}" unless rust_ver.empty?
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def show_help
|
|
693
|
+
puts <<~HELP
|
|
694
|
+
|
|
695
|
+
Usage: railst <command> [options]
|
|
696
|
+
|
|
697
|
+
Core Rails Commands (with Rust Acceleration):
|
|
698
|
+
railst s, server Start the Rails server (auto-builds Rust backend)
|
|
699
|
+
railst c, console Start the interactive Rails console
|
|
700
|
+
railst g, generate Run Rails & Rust code generators
|
|
701
|
+
railst d, destroy Remove generated code
|
|
702
|
+
railst t, test [path] Run test suite (Rust crate tests + Rails tests)
|
|
703
|
+
railst r, runner <code> Run Ruby code in application context
|
|
704
|
+
railst new <app_name> Create a new Rust on Rails application
|
|
705
|
+
|
|
706
|
+
Database Commands:
|
|
707
|
+
railst db [action] Run database task (migrate, rollback, prepare, seed, reset)
|
|
708
|
+
railst db:migrate Run database migrations (supports all db:* tasks)
|
|
709
|
+
|
|
710
|
+
Rust on Rails Tooling:
|
|
711
|
+
railst build Compile the native Rust crate (cargo build --release)
|
|
712
|
+
railst clean Clean Rust build artifacts (cargo clean)
|
|
713
|
+
railst doctor Diagnose Ruby, Rust, LLVM/Clang, and Magnus environment
|
|
714
|
+
railst bench [count] Run CLI multi-core vs Ruby benchmark (default: 25,000)
|
|
715
|
+
railst ci Run the full Continuous Integration verification suite
|
|
716
|
+
railst routes List all application routes
|
|
717
|
+
railst stats Report code statistics (Ruby + Rust lines of code)
|
|
718
|
+
railst install Install railst globally into user's PATH
|
|
719
|
+
railst version, -v Display framework version
|
|
720
|
+
railst help, -h Show this help message
|
|
721
|
+
|
|
722
|
+
Options for 'railst server':
|
|
723
|
+
-p, --port <port> Specify server port (default: 3000)
|
|
724
|
+
-b, --binding <host> Specify IP binding (default: 127.0.0.1)
|
|
725
|
+
-e, --environment <env> Specify environment (default: development)
|
|
726
|
+
-f, --find-port Auto-select next available port if default is occupied
|
|
727
|
+
|
|
728
|
+
Examples:
|
|
729
|
+
railst s
|
|
730
|
+
railst s -p 3001 --find-port
|
|
731
|
+
railst g rust:resource Transaction amount:decimal status:string
|
|
732
|
+
railst c
|
|
733
|
+
railst t
|
|
734
|
+
railst t:rust
|
|
735
|
+
railst doctor
|
|
736
|
+
railst bench 50000
|
|
737
|
+
railst db:migrate
|
|
738
|
+
|
|
739
|
+
HELP
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end
|