galaaz 2.1.0 → 2.1.2
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/CHANGELOG.md +38 -4
- data/README.md +79 -32
- data/bin/galaaz +10 -2
- data/bin/galaaz-jruby +0 -0
- data/bin/galaaz-ruby +0 -0
- data/bin/galaaz_jruby_env.inc.sh +19 -3
- data/bin/galaaz_ruby_env.inc.sh +2 -0
- data/bin/gbookdown +0 -0
- data/blogs/R-on-Rails-Planning-Document.md +79 -112
- data/blogs/manual/manual.md +79 -32
- data/ext/new_bridge/Makefile +10 -10
- data/lib/R_interface/r_arrow.rb +37 -0
- data/lib/galaaz/arrow_ipc/java_arrow_backend.rb +250 -0
- data/lib/galaaz/arrow_ipc/red_arrow_backend.rb +126 -0
- data/lib/galaaz/arrow_ipc.rb +167 -0
- data/lib/galaaz/cli.rb +552 -0
- data/lib/galaaz.rb +6 -0
- data/lib/galaaz_jruby.rb +17 -2
- data/new_bridge_specs/arrow_ipc_async_spec.rb +90 -0
- data/new_bridge_specs/arrow_ipc_export_async_spec.rb +68 -0
- data/r_requires/arrow.txt +3 -0
- data/r_requires/knit-extras.txt +4 -0
- data/r_requires/knit.txt +4 -0
- data/specs/arrow_ipc_export_spec.rb +62 -0
- data/specs/arrow_ipc_handoff_spec.rb +97 -0
- data/version.rb +1 -1
- metadata +17 -9
data/lib/galaaz/cli.rb
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
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
|
+
mark_profile!('core')
|
|
91
|
+
puts 'galaaz setup: OK'
|
|
92
|
+
puts "You can now run: galaaz doctor"
|
|
93
|
+
puts 'Omarchy menu: Install → Development → Galaaz (add-ons appear after core)'
|
|
94
|
+
0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ---- blogs ----
|
|
98
|
+
|
|
99
|
+
def cmd_blogs(argv)
|
|
100
|
+
sub = argv[0]
|
|
101
|
+
case sub
|
|
102
|
+
when 'init'
|
|
103
|
+
blogs_init(argv[1..])
|
|
104
|
+
when nil, '-h', '--help'
|
|
105
|
+
puts 'Usage: galaaz blogs init [DIR] [--force]'
|
|
106
|
+
0
|
|
107
|
+
else
|
|
108
|
+
raise CliError, "unknown blogs subcommand: #{sub.inspect} (try: init)"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def blogs_init(argv)
|
|
113
|
+
force = argv.delete('--force')
|
|
114
|
+
dest = File.expand_path(argv[0] || DEFAULT_BLOGS_DIR)
|
|
115
|
+
src_root = File.join(root, 'blogs')
|
|
116
|
+
abort_unless(File.directory?(src_root), "blogs not found in #{root}")
|
|
117
|
+
|
|
118
|
+
if File.exist?(dest)
|
|
119
|
+
entries = Dir.children(dest).reject { |n| n.start_with?('.') }
|
|
120
|
+
if !entries.empty? && !force
|
|
121
|
+
raise CliError, "#{dest} exists and is not empty (use --force to replace)"
|
|
122
|
+
end
|
|
123
|
+
FileUtils.rm_rf(dest) if force
|
|
124
|
+
end
|
|
125
|
+
FileUtils.mkdir_p(dest)
|
|
126
|
+
|
|
127
|
+
BLOG_NAMES.each do |name|
|
|
128
|
+
src = File.join(src_root, name)
|
|
129
|
+
abort_unless(File.directory?(src), "missing blog source: #{src}")
|
|
130
|
+
FileUtils.cp_r(src, File.join(dest, name))
|
|
131
|
+
rmd = File.join(dest, name, "#{name}.Rmd")
|
|
132
|
+
abort_unless(File.file?(rmd), "expected #{rmd}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
File.write(File.join(dest, BLOGS_MARKER), "galaaz blogs init\n")
|
|
136
|
+
puts "galaaz blogs init: #{dest}"
|
|
137
|
+
puts "You can now knit with gknit (after: galaaz add knit)"
|
|
138
|
+
0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ---- doctor ----
|
|
142
|
+
|
|
143
|
+
def cmd_doctor
|
|
144
|
+
puts "galaaz doctor"
|
|
145
|
+
puts " root: #{root}"
|
|
146
|
+
print_ruby_engines
|
|
147
|
+
|
|
148
|
+
r_ok = command_present?('R')
|
|
149
|
+
rs_ok = command_present?('Rscript')
|
|
150
|
+
puts " R: #{r_ok ? `R --version 2>/dev/null`.lines.first.to_s.strip : 'MISSING'}"
|
|
151
|
+
puts " Rscript: #{rs_ok ? 'OK' : 'MISSING'}"
|
|
152
|
+
|
|
153
|
+
so = File.join(root, 'ext', 'new_bridge', 'galaaz_gatekeeper.so')
|
|
154
|
+
puts " gatekeeper: #{File.file?(so) ? so : 'MISSING (run: galaaz setup)'}"
|
|
155
|
+
|
|
156
|
+
rcpp = rs_ok && r_namespace?('Rcpp')
|
|
157
|
+
puts " Rcpp: #{rcpp ? 'OK' : 'MISSING'}"
|
|
158
|
+
|
|
159
|
+
blogs = detect_blogs_dir
|
|
160
|
+
puts " blogs: #{blogs || '(not initialized)'}"
|
|
161
|
+
|
|
162
|
+
profiles = listed_profiles
|
|
163
|
+
puts " profiles: #{profiles.empty? ? '(none)' : profiles.join(', ')}"
|
|
164
|
+
|
|
165
|
+
setup_ok = r_ok && rs_ok && File.file?(so) && rcpp
|
|
166
|
+
puts setup_ok ? 'galaaz doctor: setup OK' : 'galaaz doctor: setup INCOMPLETE'
|
|
167
|
+
setup_ok ? 0 : 1
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def print_ruby_engines
|
|
171
|
+
puts " this: #{RUBY_DESCRIPTION}"
|
|
172
|
+
puts " engine: #{RUBY_ENGINE} (interpreter running doctor)"
|
|
173
|
+
|
|
174
|
+
cruby = probe_cruby
|
|
175
|
+
jruby = probe_jruby
|
|
176
|
+
puts " cruby: #{cruby || 'MISSING (Omarchy / mise: ruby@3.3+)'}"
|
|
177
|
+
puts " jruby: #{jruby || 'MISSING'}"
|
|
178
|
+
if RUBY_ENGINE != 'ruby' && cruby
|
|
179
|
+
puts " tip: run under CRuby with: GALAAZ_RUBY=ruby bin/galaaz-ruby …"
|
|
180
|
+
elsif RUBY_ENGINE == 'ruby' && jruby
|
|
181
|
+
puts " tip: run under JRuby with: GALAAZ_RUBY=jruby bin/galaaz-ruby …"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# MRI description if available (PATH ruby may be JRuby via mise).
|
|
186
|
+
def probe_cruby
|
|
187
|
+
return RUBY_DESCRIPTION if RUBY_ENGINE == 'ruby'
|
|
188
|
+
|
|
189
|
+
if ENV['GALAAZ_RUBY'] && !ENV['GALAAZ_RUBY'].empty?
|
|
190
|
+
desc = ruby_description_for(ENV['GALAAZ_RUBY'])
|
|
191
|
+
return desc if desc && desc_engine(desc) == 'ruby'
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if command_present?('mise')
|
|
195
|
+
mise_ruby_versions.each do |ver|
|
|
196
|
+
next if ver.include?('jruby')
|
|
197
|
+
desc = ruby_description_for('mise', 'exec', "ruby@#{ver}", '--', 'ruby')
|
|
198
|
+
return desc if desc
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Last resort: a binary named something other than the jruby-backed `ruby` shim.
|
|
203
|
+
%w[ruby3.3 ruby3.2 ruby3.4].each do |bin|
|
|
204
|
+
desc = ruby_description_for(bin)
|
|
205
|
+
return desc if desc && desc_engine(desc) == 'ruby'
|
|
206
|
+
end
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def probe_jruby
|
|
211
|
+
return RUBY_DESCRIPTION if RUBY_ENGINE == 'jruby'
|
|
212
|
+
|
|
213
|
+
desc = ruby_description_for('jruby')
|
|
214
|
+
return desc if desc
|
|
215
|
+
|
|
216
|
+
if command_present?('mise')
|
|
217
|
+
mise_ruby_versions.each do |ver|
|
|
218
|
+
next unless ver.include?('jruby')
|
|
219
|
+
desc = ruby_description_for('mise', 'exec', "ruby@#{ver}", '--', 'ruby')
|
|
220
|
+
return desc if desc
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def mise_ruby_versions
|
|
227
|
+
out, status = Open3.capture2e('mise', 'ls', 'ruby')
|
|
228
|
+
return [] unless status.success?
|
|
229
|
+
|
|
230
|
+
out.lines.filter_map do |line|
|
|
231
|
+
# e.g. "ruby 3.3.12" or "ruby jruby-10.1.1.0 …"
|
|
232
|
+
m = line.strip.match(/\Aruby\s+(\S+)/)
|
|
233
|
+
m && m[1]
|
|
234
|
+
end.uniq
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def ruby_description_for(*cmd)
|
|
238
|
+
out, status = Open3.capture2e(*cmd, '-e', 'print RUBY_DESCRIPTION')
|
|
239
|
+
return nil unless status.success?
|
|
240
|
+
s = out.to_s.strip
|
|
241
|
+
s.empty? ? nil : s
|
|
242
|
+
rescue Errno::ENOENT
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def desc_engine(description)
|
|
247
|
+
return 'jruby' if description.downcase.start_with?('jruby')
|
|
248
|
+
return 'truffleruby' if description.downcase.start_with?('truffleruby')
|
|
249
|
+
'ruby'
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# ---- add ----
|
|
253
|
+
|
|
254
|
+
def cmd_add(argv)
|
|
255
|
+
profile = argv[0]
|
|
256
|
+
raise CliError, "usage: galaaz add <#{PROFILES.join('|')}>" if profile.nil? || profile.empty?
|
|
257
|
+
raise CliError, "unknown profile: #{profile.inspect}" unless PROFILES.include?(profile)
|
|
258
|
+
|
|
259
|
+
require_core_for_add! unless profile == 'demo'
|
|
260
|
+
return add_demo if profile == 'demo'
|
|
261
|
+
|
|
262
|
+
if profile_marked?(profile) && profile != 'demo'
|
|
263
|
+
puts "galaaz add #{profile}: already installed (marker present)"
|
|
264
|
+
return 0
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
case profile
|
|
268
|
+
when 'knit' then add_knit
|
|
269
|
+
when 'arrow' then add_arrow
|
|
270
|
+
when 'tex' then add_tex
|
|
271
|
+
when 'bio' then add_bio
|
|
272
|
+
when 'examples' then add_examples
|
|
273
|
+
when 'ledger' then add_ledger
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def add_demo
|
|
278
|
+
%w[knit arrow ledger].each do |p|
|
|
279
|
+
code = cmd_add([p])
|
|
280
|
+
return code unless code.zero?
|
|
281
|
+
end
|
|
282
|
+
mark_profile!('demo')
|
|
283
|
+
puts 'galaaz add demo: OK'
|
|
284
|
+
0
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def add_knit
|
|
288
|
+
pkgs = read_pkg_list('knit.txt')
|
|
289
|
+
install_cran!(pkgs)
|
|
290
|
+
extras_path = File.join(root, 'r_requires', 'knit-extras.txt')
|
|
291
|
+
if File.file?(extras_path)
|
|
292
|
+
extras = read_pkg_list('knit-extras.txt')
|
|
293
|
+
unless install_cran_soft!(extras)
|
|
294
|
+
warn 'galaaz add knit: some optional packages are missing (see R messages above)'
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
mark_profile!('knit')
|
|
298
|
+
puts 'galaaz add knit: OK'
|
|
299
|
+
puts 'You can now run: gknit ~/galaaz-blogs/oh_my/oh_my.Rmd'
|
|
300
|
+
0
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def add_arrow
|
|
304
|
+
pkgs = read_pkg_list('arrow.txt')
|
|
305
|
+
install_cran!(pkgs)
|
|
306
|
+
if RUBY_ENGINE == 'ruby'
|
|
307
|
+
puts 'galaaz add arrow: gem install red-arrow (CRuby Stage B writer)'
|
|
308
|
+
unless system('gem', 'install', 'red-arrow', '--no-document')
|
|
309
|
+
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.'
|
|
310
|
+
end
|
|
311
|
+
else
|
|
312
|
+
warn 'galaaz add arrow: on JRuby, set GALAAZ_ARROW_JARS (or ~/arrow_jars) and JAVA_OPTS nio opens for Arrow Java'
|
|
313
|
+
end
|
|
314
|
+
mark_profile!('arrow')
|
|
315
|
+
puts 'galaaz add arrow: OK'
|
|
316
|
+
0
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def add_tex
|
|
320
|
+
unless command_present?('pandoc')
|
|
321
|
+
warn 'galaaz add tex: pandoc not on PATH — install via your package manager (e.g. pacman -S pandoc)'
|
|
322
|
+
end
|
|
323
|
+
script = File.join(root, 'bin', 'install-tinytex')
|
|
324
|
+
abort_unless(File.file?(script), "missing #{script}")
|
|
325
|
+
puts 'galaaz add tex: running bin/install-tinytex'
|
|
326
|
+
ok = system(script)
|
|
327
|
+
abort_unless(ok, 'TinyTeX install failed')
|
|
328
|
+
mark_profile!('tex')
|
|
329
|
+
puts 'galaaz add tex: OK'
|
|
330
|
+
0
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def add_bio
|
|
334
|
+
warn 'galaaz add bio: Bioconductor DESeq2 + airway — this can take a long time'
|
|
335
|
+
script = <<~R
|
|
336
|
+
repos <- '#{CRAN}'
|
|
337
|
+
if (!requireNamespace('BiocManager', quietly = TRUE))
|
|
338
|
+
install.packages('BiocManager', repos = repos)
|
|
339
|
+
BiocManager::install(c('DESeq2', 'airway'), update = FALSE, ask = FALSE)
|
|
340
|
+
stopifnot(requireNamespace('DESeq2', quietly = TRUE))
|
|
341
|
+
stopifnot(requireNamespace('airway', quietly = TRUE))
|
|
342
|
+
R
|
|
343
|
+
ok = system('Rscript', '-e', script)
|
|
344
|
+
abort_unless(ok, 'Bioconductor install failed')
|
|
345
|
+
mark_profile!('bio')
|
|
346
|
+
puts 'galaaz add bio: OK'
|
|
347
|
+
0
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def add_examples
|
|
351
|
+
dest = DEFAULT_EXAMPLES_DIR
|
|
352
|
+
src = File.join(root, 'examples')
|
|
353
|
+
abort_unless(File.directory?(src), "examples not found in #{root}")
|
|
354
|
+
if File.exist?(dest) && !Dir.children(dest).reject { |n| n.start_with?('.') }.empty?
|
|
355
|
+
if profile_marked?('examples')
|
|
356
|
+
puts "galaaz add examples: already at #{dest}"
|
|
357
|
+
return 0
|
|
358
|
+
end
|
|
359
|
+
raise CliError, "#{dest} exists and is not empty"
|
|
360
|
+
end
|
|
361
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
362
|
+
FileUtils.rm_rf(dest) if File.exist?(dest)
|
|
363
|
+
FileUtils.cp_r(src, dest)
|
|
364
|
+
File.write(File.join(dest, EXAMPLES_MARKER), "galaaz add examples\n")
|
|
365
|
+
mark_profile!('examples')
|
|
366
|
+
puts "galaaz add examples: #{dest}"
|
|
367
|
+
0
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def add_ledger
|
|
371
|
+
unless profile_marked?('arrow') || r_namespace?('arrow')
|
|
372
|
+
puts 'galaaz add ledger: installing arrow profile first'
|
|
373
|
+
code = add_arrow
|
|
374
|
+
return code unless code.zero?
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
dest = DEFAULT_LEDGER_DIR
|
|
378
|
+
if File.directory?(dest) && File.file?(File.join(dest, 'Gemfile'))
|
|
379
|
+
puts "galaaz add ledger: using existing #{dest}"
|
|
380
|
+
else
|
|
381
|
+
need_cmd!('git')
|
|
382
|
+
abort_unless(!File.exist?(dest) || Dir.empty?(dest), "#{dest} exists but is not an empty/ledger dir")
|
|
383
|
+
FileUtils.rm_rf(dest) if File.exist?(dest)
|
|
384
|
+
puts "galaaz add ledger: git clone #{LEDGER_REPO}"
|
|
385
|
+
ok = system('git', 'clone', '--depth', '1', LEDGER_REPO, dest)
|
|
386
|
+
abort_unless(ok, 'git clone failed (is the ledger repo public/reachable?)')
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
gemfile = File.join(dest, 'Gemfile')
|
|
390
|
+
text = File.read(gemfile)
|
|
391
|
+
rewritten = text.gsub(/gem\s+["']galaaz["']\s*,\s*path:\s*["'][^"']+["']/, 'gem "galaaz"')
|
|
392
|
+
if rewritten != text
|
|
393
|
+
File.write(gemfile, rewritten)
|
|
394
|
+
puts 'galaaz add ledger: Gemfile now uses RubyGems galaaz (no path:)'
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
Dir.chdir(dest) do
|
|
398
|
+
need_cmd!('bundle')
|
|
399
|
+
abort_unless(system('bundle', 'install'), 'bundle install failed')
|
|
400
|
+
# Ensure gatekeeper for the bundled/installed gem path when possible.
|
|
401
|
+
system('galaaz', 'setup') || warn('galaaz add ledger: galaaz setup returned non-zero; check gatekeeper')
|
|
402
|
+
abort_unless(system('bin/rails', 'db:prepare'), 'rails db:prepare failed')
|
|
403
|
+
abort_unless(system({ 'SEED_PROFILE' => 'fast' }, 'bin/rails', 'db:seed'), 'rails db:seed failed')
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
mark_profile!('ledger')
|
|
407
|
+
puts 'galaaz add ledger: OK'
|
|
408
|
+
puts "You can now run: cd #{dest} && bin/dev"
|
|
409
|
+
puts 'Then open http://localhost:3000 — portfolio → Run stress test'
|
|
410
|
+
0
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# ---- helpers ----
|
|
414
|
+
|
|
415
|
+
class CliError < StandardError; end
|
|
416
|
+
|
|
417
|
+
def abort_unless(cond, msg)
|
|
418
|
+
raise CliError, msg unless cond
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def need_cmd!(name)
|
|
422
|
+
abort_unless(command_present?(name), "missing command: #{name}")
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def command_present?(name)
|
|
426
|
+
system('bash', '-c', "command -v #{shell_escape(name)} >/dev/null 2>&1")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def shell_escape(s)
|
|
430
|
+
s.gsub("'", "'\\''")
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def ensure_rcpp!
|
|
434
|
+
return if r_namespace?('Rcpp')
|
|
435
|
+
|
|
436
|
+
puts 'galaaz setup: installing Rcpp from CRAN'
|
|
437
|
+
install_cran!(['Rcpp'])
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Omarchy/Arch: /usr/lib/R/library is not writable for normal users.
|
|
441
|
+
def r_lib_bootstrap
|
|
442
|
+
<<~R
|
|
443
|
+
lib <- Sys.getenv("R_LIBS_USER", unset = "")
|
|
444
|
+
if (!nzchar(lib)) {
|
|
445
|
+
lib <- file.path(Sys.getenv("HOME"), ".local", "lib", "R", "library")
|
|
446
|
+
Sys.setenv(R_LIBS_USER = lib)
|
|
447
|
+
}
|
|
448
|
+
dir.create(lib, recursive = TRUE, showWarnings = FALSE)
|
|
449
|
+
.libPaths(c(lib, .libPaths()))
|
|
450
|
+
renviron <- file.path(Sys.getenv("HOME"), ".Renviron")
|
|
451
|
+
line <- paste0("R_LIBS_USER=", lib)
|
|
452
|
+
if (!file.exists(renviron) || !any(grepl("^R_LIBS_USER=", readLines(renviron, warn = FALSE)))) {
|
|
453
|
+
cat(line, "\\n", file = renviron, append = TRUE)
|
|
454
|
+
}
|
|
455
|
+
R
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def r_namespace?(pkg)
|
|
459
|
+
script = "#{r_lib_bootstrap}\nquit(status=if (requireNamespace('#{pkg}', quietly=TRUE)) 0 else 1)"
|
|
460
|
+
_out, status = Open3.capture2e('Rscript', '-e', script)
|
|
461
|
+
status.success?
|
|
462
|
+
rescue Errno::ENOENT
|
|
463
|
+
false
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def install_cran!(pkgs)
|
|
467
|
+
list = pkgs.map { |p| "'#{p}'" }.join(', ')
|
|
468
|
+
script = <<~R
|
|
469
|
+
#{r_lib_bootstrap}
|
|
470
|
+
repos <- '#{CRAN}'
|
|
471
|
+
pkgs <- c(#{list})
|
|
472
|
+
inst <- rownames(installed.packages())
|
|
473
|
+
need <- pkgs[!pkgs %in% inst]
|
|
474
|
+
if (length(need)) {
|
|
475
|
+
message('Installing into ', lib, ': ', paste(need, collapse=', '))
|
|
476
|
+
install.packages(need, lib = lib, repos = repos)
|
|
477
|
+
}
|
|
478
|
+
missing <- pkgs[!pkgs %in% rownames(installed.packages())]
|
|
479
|
+
if (length(missing)) {
|
|
480
|
+
message('Still missing: ', paste(missing, collapse=', '))
|
|
481
|
+
quit(status = 1)
|
|
482
|
+
}
|
|
483
|
+
R
|
|
484
|
+
ok = system('Rscript', '-e', script)
|
|
485
|
+
abort_unless(ok, "failed to install CRAN packages: #{pkgs.join(', ')}")
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def install_cran_soft!(pkgs)
|
|
489
|
+
return if pkgs.empty?
|
|
490
|
+
|
|
491
|
+
list = pkgs.map { |p| "'#{p}'" }.join(', ')
|
|
492
|
+
script = <<~R
|
|
493
|
+
#{r_lib_bootstrap}
|
|
494
|
+
repos <- '#{CRAN}'
|
|
495
|
+
pkgs <- c(#{list})
|
|
496
|
+
inst <- rownames(installed.packages())
|
|
497
|
+
need <- pkgs[!pkgs %in% inst]
|
|
498
|
+
if (length(need)) {
|
|
499
|
+
message('Installing (optional) into ', lib, ': ', paste(need, collapse=', '))
|
|
500
|
+
tryCatch(
|
|
501
|
+
install.packages(need, lib = lib, repos = repos),
|
|
502
|
+
error = function(e) message(e)
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
missing <- pkgs[!pkgs %in% rownames(installed.packages())]
|
|
506
|
+
if (length(missing)) {
|
|
507
|
+
message('Optional still missing: ', paste(missing, collapse=', '))
|
|
508
|
+
quit(status = 2)
|
|
509
|
+
}
|
|
510
|
+
quit(status = 0)
|
|
511
|
+
R
|
|
512
|
+
status = system('Rscript', '-e', script)
|
|
513
|
+
status
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def read_pkg_list(name)
|
|
517
|
+
path = File.join(root, 'r_requires', name)
|
|
518
|
+
abort_unless(File.file?(path), "missing #{path}")
|
|
519
|
+
File.readlines(path).map(&:strip).reject { |l| l.empty? || l.start_with?('#') }
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def require_core_for_add!
|
|
523
|
+
so = File.join(root, 'ext', 'new_bridge', 'galaaz_gatekeeper.so')
|
|
524
|
+
unless File.file?(so) && r_namespace?('Rcpp')
|
|
525
|
+
raise CliError, 'core Galaaz incomplete (need gatekeeper + Rcpp). Run: galaaz setup'
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def profile_marked?(name)
|
|
530
|
+
File.file?(File.join(PROFILES_DIR, name))
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def mark_profile!(name)
|
|
534
|
+
FileUtils.mkdir_p(PROFILES_DIR)
|
|
535
|
+
File.write(File.join(PROFILES_DIR, name), "#{Time.now.utc.iso8601}\n")
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def listed_profiles
|
|
539
|
+
return [] unless File.directory?(PROFILES_DIR)
|
|
540
|
+
|
|
541
|
+
Dir.children(PROFILES_DIR).sort
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def detect_blogs_dir
|
|
545
|
+
candidates = [DEFAULT_BLOGS_DIR]
|
|
546
|
+
candidates.each do |d|
|
|
547
|
+
return d if File.file?(File.join(d, BLOGS_MARKER))
|
|
548
|
+
end
|
|
549
|
+
nil
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
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
|
-
|
|
13
|
-
|
|
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,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Async synchronization for Stage B1 Arrow IPC handoff.
|
|
4
|
+
# Run: bin/run_all_rspec new_bridge_specs/arrow_ipc_async_spec.rb
|
|
5
|
+
|
|
6
|
+
require 'thread'
|
|
7
|
+
require 'galaaz'
|
|
8
|
+
|
|
9
|
+
RSpec.describe 'Arrow IPC async handoff' do
|
|
10
|
+
before(:all) do
|
|
11
|
+
skip 'R not on PATH' unless system('command -v R >/dev/null 2>&1')
|
|
12
|
+
skip 'Galaaz::ArrowIpc writer backend not available' unless Galaaz::ArrowIpc.available?
|
|
13
|
+
ok = R::Support.eval("requireNamespace('arrow', quietly=TRUE) && requireNamespace('dplyr', quietly=TRUE)")
|
|
14
|
+
skip 'R packages arrow/dplyr are not available' unless ok == true
|
|
15
|
+
@paths = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after(:each) do
|
|
19
|
+
Array(@paths).each { |p| Galaaz::ArrowIpc.release(p) }
|
|
20
|
+
@paths = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def track(path)
|
|
24
|
+
@paths << path
|
|
25
|
+
path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'happens-before: write+fsync then async open returns correct nrow' do
|
|
29
|
+
values = [1.0, 2.0, 3.0, 4.0]
|
|
30
|
+
path = track(Galaaz::ArrowIpc.write(value: values))
|
|
31
|
+
|
|
32
|
+
done = Queue.new
|
|
33
|
+
# Path-only over the bridge; R reads the IPC file.
|
|
34
|
+
R.eval_r_async("nrow(arrow::read_ipc_file(#{path.inspect}))", timeout: 30) do |result|
|
|
35
|
+
done.push(result)
|
|
36
|
+
end
|
|
37
|
+
r = done.pop
|
|
38
|
+
expect(r).to be_ok
|
|
39
|
+
expect(r.value).to match(/4/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'async open then sync R reduction on the proxy' do
|
|
43
|
+
path = track(Galaaz::ArrowIpc.write(value: [10.0, 20.0, 30.0]))
|
|
44
|
+
|
|
45
|
+
done = Queue.new
|
|
46
|
+
R::Async.arrow___read_ipc_file(path, as_data_frame: false, timeout: 30) do |result|
|
|
47
|
+
done.push(result)
|
|
48
|
+
end
|
|
49
|
+
r = done.pop
|
|
50
|
+
expect(r).to be_ok
|
|
51
|
+
expect(r.value).to be_a(R::Object)
|
|
52
|
+
|
|
53
|
+
tbl = r.value
|
|
54
|
+
mean = R.mean(R.dplyr___pull(tbl, :value))
|
|
55
|
+
mean_v = mean.respond_to?(:>>) ? (mean >> 0) : mean
|
|
56
|
+
expect(mean_v).to eq(20.0)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'two concurrent handoffs do not cross-talk' do
|
|
60
|
+
path_a = track(Galaaz::ArrowIpc.write(tag: ['A'], n: [1]))
|
|
61
|
+
path_b = track(Galaaz::ArrowIpc.write(tag: ['B', 'B'], n: [2, 2]))
|
|
62
|
+
|
|
63
|
+
done = Queue.new
|
|
64
|
+
R.eval_r_async("nrow(arrow::read_ipc_file(#{path_a.inspect}))", timeout: 30) do |result|
|
|
65
|
+
done.push([:a, result])
|
|
66
|
+
end
|
|
67
|
+
R.eval_r_async("nrow(arrow::read_ipc_file(#{path_b.inspect}))", timeout: 30) do |result|
|
|
68
|
+
done.push([:b, result])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
results = {}
|
|
72
|
+
2.times do
|
|
73
|
+
key, res = done.pop
|
|
74
|
+
expect(res).to be_ok
|
|
75
|
+
results[key] = res.value
|
|
76
|
+
end
|
|
77
|
+
expect(results[:a]).to match(/1/)
|
|
78
|
+
expect(results[:b]).to match(/2/)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'missing path fails without hanging' do
|
|
82
|
+
done = Queue.new
|
|
83
|
+
missing = File.join(Galaaz::ArrowIpc.scratch_dir, 'galaaz_missing_async.arrow')
|
|
84
|
+
R.eval_r_async("arrow::read_ipc_file(#{missing.inspect})", timeout: 10) do |result|
|
|
85
|
+
done.push(result)
|
|
86
|
+
end
|
|
87
|
+
r = done.pop
|
|
88
|
+
expect(r).not_to be_ok
|
|
89
|
+
end
|
|
90
|
+
end
|