galaaz 2.0.0 → 2.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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -5
  3. data/README.md +200 -128
  4. data/Rakefile +17 -10
  5. data/bin/galaaz +10 -2
  6. data/bin/galaaz-jruby +3 -6
  7. data/bin/galaaz-ruby +16 -0
  8. data/bin/galaaz_jruby_env.inc.sh +19 -3
  9. data/bin/galaaz_ruby_env.inc.sh +38 -0
  10. data/bin/gbookdown +2 -3
  11. data/bin/gknit +2 -3
  12. data/bin/gstudio +4 -4
  13. data/bin/run_all_rspec +6 -5
  14. data/bin/run_example +10 -7
  15. data/bin/run_old_rspec +6 -5
  16. data/bin/run_rspec +9 -8
  17. data/bin/run_slow_rspec +6 -5
  18. data/blogs/R-on-Rails-Planning-Document.md +79 -112
  19. data/blogs/galaaz_ggplot/galaaz_ggplot_files/figure-gfm/midwest_rb.png +0 -0
  20. data/blogs/galaaz_ggplot/galaaz_ggplot_files/figure-gfm/scatter_plot_rb.png +0 -0
  21. data/blogs/gknit/gknit_files/figure-gfm/bubble-1.png +0 -0
  22. data/blogs/gknit/gknit_files/figure-gfm/diverging_bar.png +0 -0
  23. data/blogs/manual/manual.Rmd +22 -19
  24. data/blogs/manual/manual.md +200 -128
  25. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/dose_len.png +0 -0
  26. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/facet_by_delivery.png +0 -0
  27. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/facet_by_dose.png +0 -0
  28. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/facets_by_delivery_color.png +0 -0
  29. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/facets_by_delivery_color2.png +0 -0
  30. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/facets_with_decorations.png +0 -0
  31. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/facets_with_jitter.png +0 -0
  32. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/facets_with_points.png +0 -0
  33. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/final_box_plot.png +0 -0
  34. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/final_violin_plot.png +0 -0
  35. data/blogs/ruby_plot/ruby_plot_files/figure-gfm/violin_with_jitter.png +0 -0
  36. data/blogs/test/test.md +10 -0
  37. data/examples/50Plots_MasterList/ScatterPlot.rb +2 -1
  38. data/examples/50Plots_MasterList/scatter_plot.rb +1 -0
  39. data/examples/bioconductor_deseq2_airway/bench_galaaz_three_same_process.rb +1 -0
  40. data/examples/bioconductor_deseq2_airway/deseq2_airway_galaaz.rb +1 -0
  41. data/examples/bioconductor_deseq2_airway/deseq2_airway_galaaz_optimized.rb +1 -0
  42. data/examples/islr/all.rb +1 -0
  43. data/examples/islr/ch2.spec.rb +1 -0
  44. data/examples/islr/ch3.spec.rb +1 -0
  45. data/examples/islr/ch3_boston.rb +1 -0
  46. data/examples/islr/ch3_multiple_regression.rb +1 -0
  47. data/examples/islr/ch6.spec.rb +1 -0
  48. data/examples/islr/x_y_rnorm.jpg +0 -0
  49. data/examples/misc/ggplot.rb +2 -1
  50. data/examples/misc/moneyball.rb +1 -0
  51. data/examples/misc/subsetting.rb +1 -0
  52. data/examples/multithread_shards_to_r/shards_to_r.rb +1 -0
  53. data/examples/sthda_ggplot/all.rb +1 -0
  54. data/examples/sthda_ggplot/one_variable_continuous/density_gg.rb +1 -0
  55. data/examples/sthda_ggplot/one_variable_continuous/geom_area.rb +1 -0
  56. data/examples/sthda_ggplot/one_variable_continuous/geom_density.rb +1 -0
  57. data/examples/sthda_ggplot/one_variable_continuous/geom_dotplot.rb +1 -0
  58. data/examples/sthda_ggplot/one_variable_continuous/geom_freqpoly.rb +1 -0
  59. data/examples/sthda_ggplot/one_variable_continuous/geom_histogram.rb +1 -0
  60. data/examples/sthda_ggplot/one_variable_continuous/histogram_density.rb +1 -0
  61. data/examples/sthda_ggplot/one_variable_continuous/stat.rb +1 -0
  62. data/examples/sthda_ggplot/one_variable_discrete/bar.rb +1 -0
  63. data/examples/sthda_ggplot/qplots/box_violin_dot.rb +1 -0
  64. data/examples/sthda_ggplot/qplots/scatter_plots.rb +1 -0
  65. data/examples/sthda_ggplot/scatter_gg.rb +1 -0
  66. data/examples/sthda_ggplot/two_variables_cont_bivariate/geom_bin2d.rb +1 -0
  67. data/examples/sthda_ggplot/two_variables_cont_bivariate/geom_density2d.rb +1 -0
  68. data/examples/sthda_ggplot/two_variables_cont_bivariate/geom_hex.rb +1 -0
  69. data/examples/sthda_ggplot/two_variables_cont_cont/geom_point.rb +1 -0
  70. data/examples/sthda_ggplot/two_variables_cont_cont/geom_smooth.rb +1 -0
  71. data/examples/sthda_ggplot/two_variables_cont_cont/misc.rb +1 -0
  72. data/examples/sthda_ggplot/two_variables_cont_function/geom_area.rb +1 -0
  73. data/examples/sthda_ggplot/two_variables_disc_cont/geom_bar.rb +1 -0
  74. data/examples/sthda_ggplot/two_variables_disc_cont/geom_boxplot.rb +1 -0
  75. data/examples/sthda_ggplot/two_variables_disc_cont/geom_dotplot.rb +1 -0
  76. data/examples/sthda_ggplot/two_variables_disc_cont/geom_jitter.rb +1 -0
  77. data/examples/sthda_ggplot/two_variables_disc_cont/geom_line.rb +1 -0
  78. data/examples/sthda_ggplot/two_variables_disc_cont/geom_violin.rb +1 -0
  79. data/examples/sthda_ggplot/two_variables_disc_disc/geom_jitter.rb +1 -0
  80. data/examples/sthda_ggplot/two_variables_error/geom_crossbar.rb +1 -0
  81. data/lib/R_interface/r_arrow.rb +37 -0
  82. data/lib/R_interface/rvector.rb +15 -0
  83. data/lib/galaaz/arrow_ipc/java_arrow_backend.rb +250 -0
  84. data/lib/galaaz/arrow_ipc/red_arrow_backend.rb +126 -0
  85. data/lib/galaaz/arrow_ipc.rb +167 -0
  86. data/lib/galaaz/cli.rb +531 -0
  87. data/lib/galaaz.rb +6 -0
  88. data/lib/galaaz_jruby.rb +17 -2
  89. data/lib/galaaz_ruby.rb +34 -0
  90. data/lib/new_bridge/envelope.rb +2 -2
  91. data/new_bridge_specs/arrow_ipc_async_spec.rb +90 -0
  92. data/new_bridge_specs/arrow_ipc_export_async_spec.rb +68 -0
  93. data/r_requires/arrow.txt +3 -0
  94. data/r_requires/knit-extras.txt +4 -0
  95. data/r_requires/knit.txt +4 -0
  96. data/specs/arrow_ipc_export_spec.rb +62 -0
  97. data/specs/arrow_ipc_handoff_spec.rb +97 -0
  98. data/specs/gknit_include_engine_spec.rb +2 -2
  99. data/specs/gknit_install_timeout_report_spec.rb +2 -2
  100. data/specs/gknit_internal_error_report_spec.rb +2 -2
  101. data/specs/gknit_vector_map_output_spec.rb +2 -2
  102. data/version.rb +1 -1
  103. metadata +50 -13
data/lib/galaaz/cli.rb ADDED
@@ -0,0 +1,531 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'rbconfig'
5
+ require 'open3'
6
+ require 'time'
7
+
8
+ module Galaaz
9
+ module CLI
10
+ BLOG_NAMES = %w[oh_my gknit galaaz_ggplot manual nse_dplyr ruby_plot].freeze
11
+ PROFILES = %w[knit arrow tex bio examples ledger demo].freeze
12
+ CONFIG_DIR = File.join(Dir.home, '.config', 'galaaz')
13
+ PROFILES_DIR = File.join(CONFIG_DIR, 'profiles')
14
+ DEFAULT_BLOGS_DIR = File.join(Dir.home, 'galaaz-blogs')
15
+ DEFAULT_EXAMPLES_DIR = File.join(Dir.home, 'galaaz-examples')
16
+ DEFAULT_LEDGER_DIR = File.join(Dir.home, 'r_on_rails_ledger')
17
+ LEDGER_REPO = 'https://github.com/rbotafogo/r_on_rails_ledger.git'
18
+ BLOGS_MARKER = '.galaaz-blogs'
19
+ EXAMPLES_MARKER = '.galaaz-examples'
20
+ CRAN = 'https://cloud.r-project.org'
21
+
22
+ module_function
23
+
24
+ def root
25
+ return ENV['GALAAZ_ROOT'] if ENV['GALAAZ_ROOT'] && !ENV['GALAAZ_ROOT'].empty?
26
+
27
+ File.expand_path('../..', __dir__)
28
+ end
29
+
30
+ def run(argv)
31
+ cmd = argv[0]
32
+ case cmd
33
+ when nil, '-h', '--help', 'help'
34
+ print_help
35
+ 0
36
+ when 'setup'
37
+ cmd_setup
38
+ when 'blogs'
39
+ cmd_blogs(argv[1..])
40
+ when 'doctor'
41
+ cmd_doctor
42
+ when 'add'
43
+ cmd_add(argv[1..])
44
+ else
45
+ # Legacy: `galaaz some:rake_task` forwarded to rake (see README / blogs).
46
+ Dir.chdir(root) do
47
+ ok = system('rake', *argv)
48
+ ok ? 0 : 1
49
+ end
50
+ end
51
+ rescue CliError => e
52
+ warn "galaaz: #{e.message}"
53
+ 1
54
+ rescue SystemCallError => e
55
+ warn "galaaz: #{e.class}: #{e.message}"
56
+ 1
57
+ end
58
+
59
+ def print_help
60
+ puts <<~HELP
61
+ Usage: galaaz <command> [args]
62
+
63
+ Commands:
64
+ setup Build the NewBridge gatekeeper; ensure Rcpp
65
+ blogs init [DIR] Copy blog sources (default: ~/galaaz-blogs)
66
+ Options: --force
67
+ doctor Report Ruby, R, gatekeeper, Rcpp, profiles
68
+ add PROFILE Install an add-on (idempotent)
69
+ Profiles: #{PROFILES.join(', ')}
70
+
71
+ Legacy: any other argument is passed to rake in the Galaaz root
72
+ (e.g. galaaz master_list:scatter_plot).
73
+ HELP
74
+ end
75
+
76
+ # ---- setup ----
77
+
78
+ def cmd_setup
79
+ need_cmd!('R')
80
+ need_cmd!('Rscript')
81
+ need_cmd!('make')
82
+ ensure_rcpp!
83
+ bridge = File.join(root, 'ext', 'new_bridge')
84
+ abort_unless(File.directory?(bridge), "missing #{bridge}")
85
+ puts "galaaz setup: make -C #{bridge} all"
86
+ ok = system('make', '-C', bridge, 'all')
87
+ abort_unless(ok, 'make failed building ext/new_bridge')
88
+ so = File.join(bridge, 'galaaz_gatekeeper.so')
89
+ abort_unless(File.file?(so), "gatekeeper missing after build: #{so}")
90
+ puts 'galaaz setup: OK'
91
+ puts "You can now run: galaaz doctor"
92
+ 0
93
+ end
94
+
95
+ # ---- blogs ----
96
+
97
+ def cmd_blogs(argv)
98
+ sub = argv[0]
99
+ case sub
100
+ when 'init'
101
+ blogs_init(argv[1..])
102
+ when nil, '-h', '--help'
103
+ puts 'Usage: galaaz blogs init [DIR] [--force]'
104
+ 0
105
+ else
106
+ raise CliError, "unknown blogs subcommand: #{sub.inspect} (try: init)"
107
+ end
108
+ end
109
+
110
+ def blogs_init(argv)
111
+ force = argv.delete('--force')
112
+ dest = File.expand_path(argv[0] || DEFAULT_BLOGS_DIR)
113
+ src_root = File.join(root, 'blogs')
114
+ abort_unless(File.directory?(src_root), "blogs not found in #{root}")
115
+
116
+ if File.exist?(dest)
117
+ entries = Dir.children(dest).reject { |n| n.start_with?('.') }
118
+ if !entries.empty? && !force
119
+ raise CliError, "#{dest} exists and is not empty (use --force to replace)"
120
+ end
121
+ FileUtils.rm_rf(dest) if force
122
+ end
123
+ FileUtils.mkdir_p(dest)
124
+
125
+ BLOG_NAMES.each do |name|
126
+ src = File.join(src_root, name)
127
+ abort_unless(File.directory?(src), "missing blog source: #{src}")
128
+ FileUtils.cp_r(src, File.join(dest, name))
129
+ rmd = File.join(dest, name, "#{name}.Rmd")
130
+ abort_unless(File.file?(rmd), "expected #{rmd}")
131
+ end
132
+
133
+ File.write(File.join(dest, BLOGS_MARKER), "galaaz blogs init\n")
134
+ puts "galaaz blogs init: #{dest}"
135
+ puts "You can now knit with gknit (after: galaaz add knit)"
136
+ 0
137
+ end
138
+
139
+ # ---- doctor ----
140
+
141
+ def cmd_doctor
142
+ puts "galaaz doctor"
143
+ puts " root: #{root}"
144
+ print_ruby_engines
145
+
146
+ r_ok = command_present?('R')
147
+ rs_ok = command_present?('Rscript')
148
+ puts " R: #{r_ok ? `R --version 2>/dev/null`.lines.first.to_s.strip : 'MISSING'}"
149
+ puts " Rscript: #{rs_ok ? 'OK' : 'MISSING'}"
150
+
151
+ so = File.join(root, 'ext', 'new_bridge', 'galaaz_gatekeeper.so')
152
+ puts " gatekeeper: #{File.file?(so) ? so : 'MISSING (run: galaaz setup)'}"
153
+
154
+ rcpp = rs_ok && r_namespace?('Rcpp')
155
+ puts " Rcpp: #{rcpp ? 'OK' : 'MISSING'}"
156
+
157
+ blogs = detect_blogs_dir
158
+ puts " blogs: #{blogs || '(not initialized)'}"
159
+
160
+ profiles = listed_profiles
161
+ puts " profiles: #{profiles.empty? ? '(none)' : profiles.join(', ')}"
162
+
163
+ setup_ok = r_ok && rs_ok && File.file?(so) && rcpp
164
+ puts setup_ok ? 'galaaz doctor: setup OK' : 'galaaz doctor: setup INCOMPLETE'
165
+ setup_ok ? 0 : 1
166
+ end
167
+
168
+ def print_ruby_engines
169
+ puts " this: #{RUBY_DESCRIPTION}"
170
+ puts " engine: #{RUBY_ENGINE} (interpreter running doctor)"
171
+
172
+ cruby = probe_cruby
173
+ jruby = probe_jruby
174
+ puts " cruby: #{cruby || 'MISSING (Omarchy / mise: ruby@3.3+)'}"
175
+ puts " jruby: #{jruby || 'MISSING'}"
176
+ if RUBY_ENGINE != 'ruby' && cruby
177
+ puts " tip: run under CRuby with: GALAAZ_RUBY=ruby bin/galaaz-ruby …"
178
+ elsif RUBY_ENGINE == 'ruby' && jruby
179
+ puts " tip: run under JRuby with: GALAAZ_RUBY=jruby bin/galaaz-ruby …"
180
+ end
181
+ end
182
+
183
+ # MRI description if available (PATH ruby may be JRuby via mise).
184
+ def probe_cruby
185
+ return RUBY_DESCRIPTION if RUBY_ENGINE == 'ruby'
186
+
187
+ if ENV['GALAAZ_RUBY'] && !ENV['GALAAZ_RUBY'].empty?
188
+ desc = ruby_description_for(ENV['GALAAZ_RUBY'])
189
+ return desc if desc && desc_engine(desc) == 'ruby'
190
+ end
191
+
192
+ if command_present?('mise')
193
+ mise_ruby_versions.each do |ver|
194
+ next if ver.include?('jruby')
195
+ desc = ruby_description_for('mise', 'exec', "ruby@#{ver}", '--', 'ruby')
196
+ return desc if desc
197
+ end
198
+ end
199
+
200
+ # Last resort: a binary named something other than the jruby-backed `ruby` shim.
201
+ %w[ruby3.3 ruby3.2 ruby3.4].each do |bin|
202
+ desc = ruby_description_for(bin)
203
+ return desc if desc && desc_engine(desc) == 'ruby'
204
+ end
205
+ nil
206
+ end
207
+
208
+ def probe_jruby
209
+ return RUBY_DESCRIPTION if RUBY_ENGINE == 'jruby'
210
+
211
+ desc = ruby_description_for('jruby')
212
+ return desc if desc
213
+
214
+ if command_present?('mise')
215
+ mise_ruby_versions.each do |ver|
216
+ next unless ver.include?('jruby')
217
+ desc = ruby_description_for('mise', 'exec', "ruby@#{ver}", '--', 'ruby')
218
+ return desc if desc
219
+ end
220
+ end
221
+ nil
222
+ end
223
+
224
+ def mise_ruby_versions
225
+ out, status = Open3.capture2e('mise', 'ls', 'ruby')
226
+ return [] unless status.success?
227
+
228
+ out.lines.filter_map do |line|
229
+ # e.g. "ruby 3.3.12" or "ruby jruby-10.1.1.0 …"
230
+ m = line.strip.match(/\Aruby\s+(\S+)/)
231
+ m && m[1]
232
+ end.uniq
233
+ end
234
+
235
+ def ruby_description_for(*cmd)
236
+ out, status = Open3.capture2e(*cmd, '-e', 'print RUBY_DESCRIPTION')
237
+ return nil unless status.success?
238
+ s = out.to_s.strip
239
+ s.empty? ? nil : s
240
+ rescue Errno::ENOENT
241
+ nil
242
+ end
243
+
244
+ def desc_engine(description)
245
+ return 'jruby' if description.downcase.start_with?('jruby')
246
+ return 'truffleruby' if description.downcase.start_with?('truffleruby')
247
+ 'ruby'
248
+ end
249
+
250
+ # ---- add ----
251
+
252
+ def cmd_add(argv)
253
+ profile = argv[0]
254
+ raise CliError, "usage: galaaz add <#{PROFILES.join('|')}>" if profile.nil? || profile.empty?
255
+ raise CliError, "unknown profile: #{profile.inspect}" unless PROFILES.include?(profile)
256
+
257
+ require_core_for_add! unless profile == 'demo'
258
+ return add_demo if profile == 'demo'
259
+
260
+ if profile_marked?(profile) && profile != 'demo'
261
+ puts "galaaz add #{profile}: already installed (marker present)"
262
+ return 0
263
+ end
264
+
265
+ case profile
266
+ when 'knit' then add_knit
267
+ when 'arrow' then add_arrow
268
+ when 'tex' then add_tex
269
+ when 'bio' then add_bio
270
+ when 'examples' then add_examples
271
+ when 'ledger' then add_ledger
272
+ end
273
+ end
274
+
275
+ def add_demo
276
+ %w[knit arrow ledger].each do |p|
277
+ code = cmd_add([p])
278
+ return code unless code.zero?
279
+ end
280
+ mark_profile!('demo')
281
+ puts 'galaaz add demo: OK'
282
+ 0
283
+ end
284
+
285
+ def add_knit
286
+ pkgs = read_pkg_list('knit.txt')
287
+ install_cran!(pkgs)
288
+ extras_path = File.join(root, 'r_requires', 'knit-extras.txt')
289
+ if File.file?(extras_path)
290
+ extras = read_pkg_list('knit-extras.txt')
291
+ unless install_cran_soft!(extras)
292
+ warn 'galaaz add knit: some optional packages are missing (see R messages above)'
293
+ end
294
+ end
295
+ mark_profile!('knit')
296
+ puts 'galaaz add knit: OK'
297
+ puts 'You can now run: gknit ~/galaaz-blogs/oh_my/oh_my.Rmd'
298
+ 0
299
+ end
300
+
301
+ def add_arrow
302
+ pkgs = read_pkg_list('arrow.txt')
303
+ install_cran!(pkgs)
304
+ if RUBY_ENGINE == 'ruby'
305
+ puts 'galaaz add arrow: gem install red-arrow (CRuby Stage B writer)'
306
+ unless system('gem', 'install', 'red-arrow', '--no-document')
307
+ warn 'galaaz add arrow: red-arrow gem install failed (need Apache Arrow GLib / libarrow-glib). R arrow is installed; Ruby IPC writer may be unavailable.'
308
+ end
309
+ else
310
+ warn 'galaaz add arrow: on JRuby, set GALAAZ_ARROW_JARS (or ~/arrow_jars) and JAVA_OPTS nio opens for Arrow Java'
311
+ end
312
+ mark_profile!('arrow')
313
+ puts 'galaaz add arrow: OK'
314
+ 0
315
+ end
316
+
317
+ def add_tex
318
+ unless command_present?('pandoc')
319
+ warn 'galaaz add tex: pandoc not on PATH — install via your package manager (e.g. pacman -S pandoc)'
320
+ end
321
+ script = File.join(root, 'bin', 'install-tinytex')
322
+ abort_unless(File.file?(script), "missing #{script}")
323
+ puts 'galaaz add tex: running bin/install-tinytex'
324
+ ok = system(script)
325
+ abort_unless(ok, 'TinyTeX install failed')
326
+ mark_profile!('tex')
327
+ puts 'galaaz add tex: OK'
328
+ 0
329
+ end
330
+
331
+ def add_bio
332
+ warn 'galaaz add bio: Bioconductor DESeq2 + airway — this can take a long time'
333
+ script = <<~R
334
+ repos <- '#{CRAN}'
335
+ if (!requireNamespace('BiocManager', quietly = TRUE))
336
+ install.packages('BiocManager', repos = repos)
337
+ BiocManager::install(c('DESeq2', 'airway'), update = FALSE, ask = FALSE)
338
+ stopifnot(requireNamespace('DESeq2', quietly = TRUE))
339
+ stopifnot(requireNamespace('airway', quietly = TRUE))
340
+ R
341
+ ok = system('Rscript', '-e', script)
342
+ abort_unless(ok, 'Bioconductor install failed')
343
+ mark_profile!('bio')
344
+ puts 'galaaz add bio: OK'
345
+ 0
346
+ end
347
+
348
+ def add_examples
349
+ dest = DEFAULT_EXAMPLES_DIR
350
+ src = File.join(root, 'examples')
351
+ abort_unless(File.directory?(src), "examples not found in #{root}")
352
+ if File.exist?(dest) && !Dir.children(dest).reject { |n| n.start_with?('.') }.empty?
353
+ if profile_marked?('examples')
354
+ puts "galaaz add examples: already at #{dest}"
355
+ return 0
356
+ end
357
+ raise CliError, "#{dest} exists and is not empty"
358
+ end
359
+ FileUtils.mkdir_p(File.dirname(dest))
360
+ FileUtils.rm_rf(dest) if File.exist?(dest)
361
+ FileUtils.cp_r(src, dest)
362
+ File.write(File.join(dest, EXAMPLES_MARKER), "galaaz add examples\n")
363
+ mark_profile!('examples')
364
+ puts "galaaz add examples: #{dest}"
365
+ 0
366
+ end
367
+
368
+ def add_ledger
369
+ unless profile_marked?('arrow') || r_namespace?('arrow')
370
+ puts 'galaaz add ledger: installing arrow profile first'
371
+ code = add_arrow
372
+ return code unless code.zero?
373
+ end
374
+
375
+ dest = DEFAULT_LEDGER_DIR
376
+ if File.directory?(dest) && File.file?(File.join(dest, 'Gemfile'))
377
+ puts "galaaz add ledger: using existing #{dest}"
378
+ else
379
+ need_cmd!('git')
380
+ abort_unless(!File.exist?(dest) || Dir.empty?(dest), "#{dest} exists but is not an empty/ledger dir")
381
+ FileUtils.rm_rf(dest) if File.exist?(dest)
382
+ puts "galaaz add ledger: git clone #{LEDGER_REPO}"
383
+ ok = system('git', 'clone', '--depth', '1', LEDGER_REPO, dest)
384
+ abort_unless(ok, 'git clone failed (is the ledger repo public/reachable?)')
385
+ end
386
+
387
+ gemfile = File.join(dest, 'Gemfile')
388
+ text = File.read(gemfile)
389
+ rewritten = text.gsub(/gem\s+["']galaaz["']\s*,\s*path:\s*["'][^"']+["']/, 'gem "galaaz"')
390
+ if rewritten != text
391
+ File.write(gemfile, rewritten)
392
+ puts 'galaaz add ledger: Gemfile now uses RubyGems galaaz (no path:)'
393
+ end
394
+
395
+ Dir.chdir(dest) do
396
+ need_cmd!('bundle')
397
+ abort_unless(system('bundle', 'install'), 'bundle install failed')
398
+ # Ensure gatekeeper for the bundled/installed gem path when possible.
399
+ system('galaaz', 'setup') || warn('galaaz add ledger: galaaz setup returned non-zero; check gatekeeper')
400
+ abort_unless(system('bin/rails', 'db:prepare'), 'rails db:prepare failed')
401
+ abort_unless(system({ 'SEED_PROFILE' => 'fast' }, 'bin/rails', 'db:seed'), 'rails db:seed failed')
402
+ end
403
+
404
+ mark_profile!('ledger')
405
+ puts 'galaaz add ledger: OK'
406
+ puts "You can now run: cd #{dest} && bin/dev"
407
+ puts 'Then open http://localhost:3000 — portfolio → Run stress test'
408
+ 0
409
+ end
410
+
411
+ # ---- helpers ----
412
+
413
+ class CliError < StandardError; end
414
+
415
+ def abort_unless(cond, msg)
416
+ raise CliError, msg unless cond
417
+ end
418
+
419
+ def need_cmd!(name)
420
+ abort_unless(command_present?(name), "missing command: #{name}")
421
+ end
422
+
423
+ def command_present?(name)
424
+ system('bash', '-c', "command -v #{shell_escape(name)} >/dev/null 2>&1")
425
+ end
426
+
427
+ def shell_escape(s)
428
+ s.gsub("'", "'\\''")
429
+ end
430
+
431
+ def ensure_rcpp!
432
+ return if r_namespace?('Rcpp')
433
+
434
+ puts 'galaaz setup: installing Rcpp from CRAN'
435
+ install_cran!(['Rcpp'])
436
+ end
437
+
438
+ def r_namespace?(pkg)
439
+ out, status = Open3.capture2e(
440
+ 'Rscript', '-e', "quit(status=if (requireNamespace('#{pkg}', quietly=TRUE)) 0 else 1)"
441
+ )
442
+ status.success?
443
+ rescue Errno::ENOENT
444
+ false
445
+ end
446
+
447
+ def install_cran!(pkgs)
448
+ list = pkgs.map { |p| "'#{p}'" }.join(', ')
449
+ script = <<~R
450
+ repos <- '#{CRAN}'
451
+ pkgs <- c(#{list})
452
+ inst <- rownames(installed.packages())
453
+ need <- pkgs[!pkgs %in% inst]
454
+ if (length(need)) {
455
+ message('Installing: ', paste(need, collapse=', '))
456
+ install.packages(need, repos = repos)
457
+ }
458
+ missing <- pkgs[!pkgs %in% rownames(installed.packages())]
459
+ if (length(missing)) {
460
+ message('Still missing: ', paste(missing, collapse=', '))
461
+ quit(status = 1)
462
+ }
463
+ R
464
+ ok = system('Rscript', '-e', script)
465
+ abort_unless(ok, "failed to install CRAN packages: #{pkgs.join(', ')}")
466
+ end
467
+
468
+ def install_cran_soft!(pkgs)
469
+ return if pkgs.empty?
470
+
471
+ list = pkgs.map { |p| "'#{p}'" }.join(', ')
472
+ script = <<~R
473
+ repos <- '#{CRAN}'
474
+ pkgs <- c(#{list})
475
+ inst <- rownames(installed.packages())
476
+ need <- pkgs[!pkgs %in% inst]
477
+ if (length(need)) {
478
+ message('Installing (optional): ', paste(need, collapse=', '))
479
+ tryCatch(
480
+ install.packages(need, repos = repos),
481
+ error = function(e) message(e)
482
+ )
483
+ }
484
+ missing <- pkgs[!pkgs %in% rownames(installed.packages())]
485
+ if (length(missing)) {
486
+ message('Optional still missing: ', paste(missing, collapse=', '))
487
+ quit(status = 2)
488
+ }
489
+ quit(status = 0)
490
+ R
491
+ status = system('Rscript', '-e', script)
492
+ status
493
+ end
494
+
495
+ def read_pkg_list(name)
496
+ path = File.join(root, 'r_requires', name)
497
+ abort_unless(File.file?(path), "missing #{path}")
498
+ File.readlines(path).map(&:strip).reject { |l| l.empty? || l.start_with?('#') }
499
+ end
500
+
501
+ def require_core_for_add!
502
+ so = File.join(root, 'ext', 'new_bridge', 'galaaz_gatekeeper.so')
503
+ unless File.file?(so) && r_namespace?('Rcpp')
504
+ raise CliError, 'core Galaaz incomplete (need gatekeeper + Rcpp). Run: galaaz setup'
505
+ end
506
+ end
507
+
508
+ def profile_marked?(name)
509
+ File.file?(File.join(PROFILES_DIR, name))
510
+ end
511
+
512
+ def mark_profile!(name)
513
+ FileUtils.mkdir_p(PROFILES_DIR)
514
+ File.write(File.join(PROFILES_DIR, name), "#{Time.now.utc.iso8601}\n")
515
+ end
516
+
517
+ def listed_profiles
518
+ return [] unless File.directory?(PROFILES_DIR)
519
+
520
+ Dir.children(PROFILES_DIR).sort
521
+ end
522
+
523
+ def detect_blogs_dir
524
+ candidates = [DEFAULT_BLOGS_DIR]
525
+ candidates.each do |d|
526
+ return d if File.file?(File.join(d, BLOGS_MARKER))
527
+ end
528
+ nil
529
+ end
530
+ end
531
+ end
data/lib/galaaz.rb CHANGED
@@ -23,6 +23,12 @@
23
23
 
24
24
  $LOAD_PATH << File.dirname(File.expand_path('..', __FILE__)) + "/r_requires"
25
25
 
26
+ if RUBY_ENGINE == 'jruby'
27
+ require_relative 'galaaz_jruby'
28
+ GalaazJRuby.apply_java_opts_env!
29
+ end
30
+
31
+ require_relative 'galaaz/arrow_ipc'
26
32
  require_relative 'R_interface/r'
27
33
  require_relative 'util/exec_ruby'
28
34
  require_relative 'util/inline_file'
data/lib/galaaz_jruby.rb CHANGED
@@ -3,14 +3,20 @@
3
3
  # JVM arguments required for every JRuby process that loads Galaaz (Apache Arrow memory
4
4
  # on Java 9+). Rake and Ruby launchers should use GalaazJRuby.shell_j_arg_string.
5
5
  #
6
+ # The opens flag must be on the *JVM at startup*. `jruby -J--add-opens=... -S bundle exec`
7
+ # does not pass it to the child rspec JVM — export JAVA_OPTS instead (see
8
+ # +apply_java_opts_env!+ and bin/galaaz_jruby_env.inc.sh).
9
+ #
6
10
  # Optional extra flags (e.g. -J-Xmx4g): set environment variable GALAAZ_JRUBY_OPTS
7
11
  # (space-separated; passed through to the shell unquoted).
8
12
  #
9
13
  # Bash entrypoints (bin/run_rspec, etc.) source bin/galaaz_jruby_env.inc.sh — keep the
10
14
  # -J list there identical to REQUIRED_JRUBY_J_ARGS below.
11
15
  module GalaazJRuby
12
- REQUIRED_JRUBY_J_ARGS = %w[
13
- -J--add-opens=java.base/java.nio=ALL-UNNAMED
16
+ JAVA_NIO_ADD_OPENS = '--add-opens=java.base/java.nio=ALL-UNNAMED'
17
+
18
+ REQUIRED_JRUBY_J_ARGS = %W[
19
+ -J#{JAVA_NIO_ADD_OPENS}
14
20
  ].freeze
15
21
 
16
22
  OPTIONAL_ENV = 'GALAAZ_JRUBY_OPTS'
@@ -19,4 +25,13 @@ module GalaazJRuby
19
25
  extra = ENV[OPTIONAL_ENV].to_s.strip
20
26
  [REQUIRED_JRUBY_J_ARGS.join(' '), extra].reject(&:empty?).join(' ')
21
27
  end
28
+
29
+ # For subprocess JVMs (and so launchers that exec JRuby after requiring this file
30
+ # pick up the flag). Does not change the already-running JVM.
31
+ def self.apply_java_opts_env!
32
+ opts = ENV['JAVA_OPTS'].to_s
33
+ return if opts.split.include?(JAVA_NIO_ADD_OPENS)
34
+
35
+ ENV['JAVA_OPTS'] = [JAVA_NIO_ADD_OPENS, opts].reject(&:empty?).join(' ')
36
+ end
22
37
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ # Interpreter selection for Ruby launchers (bin/gknit, bin/gbookdown, bin/gstudio, Rake).
6
+ # Default is `ruby` on PATH (JRuby or CRuby). Override with GALAAZ_RUBY=jruby / ruby / path.
7
+ # JVM flags are applied only when the selected interpreter is JRuby (see galaaz_jruby.rb).
8
+ module GalaazRuby
9
+ def self.interpreter
10
+ bin = ENV['GALAAZ_RUBY'].to_s.strip
11
+ bin.empty? ? 'ruby' : bin
12
+ end
13
+
14
+ def self.jruby?(bin = interpreter)
15
+ base = File.basename(bin.to_s)
16
+ return true if base == 'jruby' || base.start_with?('jruby.')
17
+
18
+ engine = `#{Shellwords.escape(bin)} -e 'print RUBY_ENGINE' 2>/dev/null`.to_s.strip
19
+ engine == 'jruby'
20
+ rescue StandardError
21
+ false
22
+ end
23
+
24
+ # Shell prefix: "<bin> [jvm-args] -I<lib_path>"
25
+ def self.shell_invocation(lib_path)
26
+ bin = interpreter
27
+ if jruby?(bin)
28
+ require_relative 'galaaz_jruby'
29
+ "#{bin} #{GalaazJRuby.shell_j_arg_string} -I#{lib_path}"
30
+ else
31
+ "#{bin} -I#{lib_path}"
32
+ end
33
+ end
34
+ end
@@ -22,7 +22,7 @@ module NewBridge
22
22
  raise InvalidEnvelope, "unknown keys: #{extra.join(', ')}" unless extra.empty?
23
23
 
24
24
  MessagePack.pack(h)
25
- rescue MessagePack::MalformedFormatError, MessagePack::UnpackError => e
25
+ rescue MessagePack::MalformedFormatError, MessagePack::UnpackError, EOFError => e
26
26
  raise Error, "MsgPack pack failed: #{e.message}"
27
27
  end
28
28
 
@@ -39,7 +39,7 @@ module NewBridge
39
39
  raise InvalidEnvelope, "missing keys: #{missing.join(', ')}" unless missing.empty?
40
40
 
41
41
  h
42
- rescue MessagePack::MalformedFormatError, MessagePack::UnpackError => e
42
+ rescue MessagePack::MalformedFormatError, MessagePack::UnpackError, EOFError => e
43
43
  raise Error, "invalid MsgPack envelope: #{e.message}"
44
44
  end
45
45