torikago 0.0.1 → 0.1.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.
@@ -1,31 +1,93 @@
1
1
  require "pathname"
2
+ require_relative "root_constant_resolver"
2
3
 
3
4
  module Torikago
4
5
  # Owns the runtime for one registered module. When Ruby::Box is available it
5
6
  # loads the module into an isolated Box; otherwise it falls back to the host
6
7
  # process for development and tests.
7
8
  class EngineContainer
8
- def initialize(name:, module_root:, entrypoint: nil, setup: nil, gemfile: nil, box_factory: nil, gemfile_dependency_loader: nil, gem_activator: nil)
9
+ BUNDLER_SETUP_ENV_MUTEX = Mutex.new
10
+ TORIKAGO_RUNTIME_CONSTANTS = %i[
11
+ Error
12
+ DependencyError
13
+ BoxUnavailableError
14
+ PublicApiError
15
+ GemfileOverrideError
16
+ Gateway
17
+ ].freeze
18
+ FRAMEWORK_CONSTANTS = %i[
19
+ Rails
20
+ ActionController
21
+ ActionDispatch
22
+ ActionView
23
+ ActiveModel
24
+ ActiveRecord
25
+ ActiveSupport
26
+ ].freeze
27
+
28
+ def initialize(name:, module_root:, entrypoint: nil, rails_engine: false, setup: nil, gemfile: nil, registered_roots: nil, box_factory: nil, gemfile_dependency_loader: nil, gem_activator: nil, root_constant_resolver: nil)
9
29
  @name = name
10
30
  @module_root = Pathname(module_root)
11
31
  @entrypoint = entrypoint
32
+ @rails_engine = rails_engine
12
33
  @setup = setup
13
34
  @gemfile = gemfile
35
+ @registered_roots = registered_roots || [@module_root]
14
36
  @box_factory = box_factory
15
37
  @gemfile_dependency_loader = gemfile_dependency_loader || method(:load_gemfile_dependencies)
16
38
  @gem_activator = gem_activator || method(:activate_gem_dependency)
39
+ @root_constant_resolver = root_constant_resolver
17
40
  end
18
41
 
19
- def call(public_api_class_name, *args, **kwargs)
42
+ def invoke(public_api_class_name, method_name, constructor_args:, constructor_kwargs:, method_args:, method_kwargs:)
20
43
  CurrentExecution.with_box(name) do
21
44
  public_api_class = resolve_public_api_class(public_api_class_name)
22
- public_api_class.new.call(*args, **kwargs)
45
+ instance = public_api_class.new(*constructor_args, **constructor_kwargs)
46
+ instance.public_send(method_name, *method_args, **method_kwargs)
47
+ end
48
+ end
49
+
50
+ # Dispatches a Rack request to a Rails::Engine owned by this module. Route
51
+ # recognition happens against the Box-local engine, then the Box-local
52
+ # controller class is invoked directly so Rails never constantizes the
53
+ # controller in the host application's Object namespace.
54
+ def call(env)
55
+ unless rails_engine?
56
+ raise ArgumentError, "module #{name} is not registered with rails_engine: true"
57
+ end
58
+
59
+ CurrentExecution.with_box(name) do
60
+ boot_runtime!
61
+ route, rack_response = recognize_route(env)
62
+ return rack_response if rack_response
63
+
64
+ controller = resolve_runtime_class("#{camelize_controller(route.fetch(:controller))}Controller")
65
+
66
+ dispatch_controller_action(env, controller, route.fetch(:action), route)
67
+ end
68
+ end
69
+
70
+ # Dispatches a host-owned route to a controller that belongs to this module.
71
+ # Unlike #call, route recognition has already happened in the host router, so
72
+ # this path does not require a Rails::Engine inside the module.
73
+ def dispatch_controller(env, controller_name:, action_name:)
74
+ CurrentExecution.with_box(name) do
75
+ boot_controller_runtime!
76
+ controller = resolve_runtime_class(controller_name)
77
+
78
+ dispatch_controller_action(
79
+ env,
80
+ controller,
81
+ action_name,
82
+ controller: controller_name,
83
+ action: action_name.to_s
84
+ )
23
85
  end
24
86
  end
25
87
 
26
88
  private
27
89
 
28
- attr_reader :box_factory, :entrypoint, :gem_activator, :gemfile, :gemfile_dependency_loader, :module_root, :name, :setup
90
+ attr_reader :box_factory, :entrypoint, :gem_activator, :gemfile, :gemfile_dependency_loader, :module_root, :name, :registered_roots, :root_constant_resolver, :setup
29
91
 
30
92
  def boot_runtime!
31
93
  return if @booted
@@ -33,15 +95,23 @@ module Torikago
33
95
  files = runtime_files
34
96
  gemfile_dependencies
35
97
  if isolated_box_enabled?
36
- # The Box starts with an independent load path and constant table, so
37
- # boot has to copy enough host context before loading module code.
38
- prepare_box!
39
- prepend_gemfile_require_paths_to_box!
40
- load_setup_hook_into_box!
41
- ensure_root_namespace_in_box!
42
-
43
- files.each do |path|
44
- box.load(path)
98
+ without_bundler_setup_env do
99
+ # The Box starts with an independent load path and constant table, so
100
+ # boot has to copy enough host context before loading module code.
101
+ with_box_runtime_errors do
102
+ prepare_box!
103
+ prepend_gemfile_require_paths_to_box!
104
+ install_host_runtime_bridges!
105
+ load_setup_hook_into_box!
106
+ ensure_root_namespace_in_box!
107
+ prepare_rails_application_controller_in_box!
108
+
109
+ files.each do |path|
110
+ box.load(path)
111
+ normalize_rails_constant_name!(path)
112
+ end
113
+ attach_rails_helpers!
114
+ end
45
115
  end
46
116
  else
47
117
  # Non-Box mode preserves the same public behavior while giving up
@@ -52,12 +122,203 @@ module Torikago
52
122
 
53
123
  files.each do |path|
54
124
  load path
125
+ normalize_rails_constant_name!(path)
55
126
  end
127
+ attach_rails_helpers!
56
128
  end
57
129
 
58
130
  @booted = true
59
131
  end
60
132
 
133
+ def boot_controller_runtime!
134
+ boot_runtime!
135
+ return if rails_engine?
136
+ return if @controller_runtime_booted
137
+
138
+ files = controller_runtime_files - runtime_files
139
+ if isolated_box_enabled?
140
+ without_bundler_setup_env do
141
+ with_box_runtime_errors do
142
+ install_rails_runtime_support!
143
+ prepare_rails_application_controller_in_box!
144
+ load_runtime_files_into_box!(files)
145
+ attach_rails_helpers!
146
+ end
147
+ end
148
+ else
149
+ files.each do |path|
150
+ load path
151
+ normalize_rails_constant_name!(path)
152
+ end
153
+ attach_rails_helpers!
154
+ end
155
+
156
+ @controller_runtime_booted = true
157
+ end
158
+
159
+ def load_runtime_files_into_box!(files)
160
+ files.each do |path|
161
+ box.load(path)
162
+ normalize_rails_constant_name!(path)
163
+ end
164
+ end
165
+
166
+ def dispatch_controller_action(env, controller, action_name, path_parameters)
167
+ current_parameters = env.fetch(
168
+ "action_dispatch.request.path_parameters",
169
+ Hash.new
170
+ )
171
+ request_env = env.merge(
172
+ "action_dispatch.request.path_parameters" =>
173
+ current_parameters.merge(path_parameters.transform_keys(&:to_sym))
174
+ )
175
+
176
+ controller.action(action_name).call(request_env)
177
+ end
178
+
179
+ def attach_rails_helpers!
180
+ helper_modules = Dir[module_root.join("app/helpers/**/*_helper.rb").to_s].sort.filter_map do |path|
181
+ constant_name = rails_constant_name_for(path)
182
+ resolve_runtime_class(constant_name) if constant_name
183
+ rescue NameError
184
+ nil
185
+ end
186
+ return if helper_modules.empty?
187
+
188
+ rails_controller_files.each do |path|
189
+ constant_name = rails_constant_name_for(path)
190
+ next unless constant_name
191
+
192
+ controller = resolve_runtime_class(constant_name)
193
+ next unless controller.respond_to?(:helper)
194
+
195
+ helper_modules.each { |helper_module| controller.helper(helper_module) }
196
+ rescue NameError
197
+ next
198
+ end
199
+ end
200
+
201
+ def prepare_rails_application_controller_in_box!
202
+ return unless Object.const_defined?(:ActionController, false)
203
+ return unless module_root.join("app/controllers/#{name}/application_controller.rb").exist?
204
+
205
+ # FIXME: This workaround only supports controllers under the namespace
206
+ # derived from the registered module name. Real top-level ActionController
207
+ # subclasses still expose Ruby::Box's internal prefix to Rails inherited
208
+ # hooks and helper lookup. Track support for non-namespaced controllers in:
209
+ # https://github.com/se4weed/torikago/issues/15
210
+ namespace_name = camelize(name.to_s)
211
+ namespace = box.const_get(namespace_name)
212
+ application_controller = if namespace.const_defined?(:ApplicationController, false)
213
+ namespace.const_get(:ApplicationController, false)
214
+ else
215
+ action_controller_base = Object.const_get(:ActionController).const_get(:Base)
216
+ namespace.const_set(:ApplicationController, Class.new(action_controller_base))
217
+ end
218
+
219
+ application_controller.define_singleton_method(:name) { "#{namespace_name}::ApplicationController" }
220
+ application_controller.define_singleton_method(:inherited) do |subclass|
221
+ native_name = Module.instance_method(:name).bind_call(subclass)
222
+ marker = "::#{namespace_name}::"
223
+ conventional_name = if native_name&.include?(marker)
224
+ "#{namespace_name}::#{native_name.split(marker, 2).last}"
225
+ else
226
+ native_name
227
+ end
228
+ # A class is still anonymous while inherited() runs. Do not let it
229
+ # inherit ApplicationController's singleton name implementation.
230
+ subclass.define_singleton_method(:name) { conventional_name }
231
+ # ActiveSupport's default module_parents implementation constantizes
232
+ # through the host Object. Return the already-resolved Box namespace.
233
+ subclass.define_singleton_method(:module_parents) { [namespace] }
234
+ # AbstractController tries to constantize "<Controller>Helper" in the
235
+ # host Object namespace during inherited(). Module helpers live in this
236
+ # Box, so they are attached explicitly after runtime files are loaded.
237
+ subclass.define_singleton_method(:default_helper_module!) {}
238
+ subclass.singleton_class.send(:private, :default_helper_module!)
239
+ super(subclass)
240
+ end
241
+ end
242
+
243
+ def normalize_rails_constant_name!(path)
244
+ constant_name = rails_constant_name_for(path)
245
+ return unless constant_name
246
+
247
+ constant = resolve_runtime_class(constant_name)
248
+ return unless constant.respond_to?(:define_singleton_method)
249
+
250
+ # Ruby::Box includes an internal Box prefix in Module#name. Rails derives
251
+ # helper and model constants from that value, so expose the conventional
252
+ # application constant name to Rails while keeping the constant itself in
253
+ # the Box.
254
+ constant.define_singleton_method(:name) { constant_name }
255
+ constant.remove_instance_variable(:@controller_path) if constant.instance_variable_defined?(:@controller_path)
256
+ constant.remove_instance_variable(:@controller_name) if constant.instance_variable_defined?(:@controller_name)
257
+ if path.include?("/app/controllers/") && constant.respond_to?(:prepend_view_path)
258
+ constant.prepend_view_path(module_root.join("app/views").to_s)
259
+ end
260
+ rescue NameError
261
+ # Namespace-only files and files that define a differently named helper
262
+ # do not need normalization.
263
+ nil
264
+ end
265
+
266
+ def rails_constant_name_for(path)
267
+ app_root = module_root.join("app").to_s
268
+ return unless path.start_with?("#{app_root}/")
269
+
270
+ relative_path = path.delete_prefix("#{app_root}/")
271
+ category, constant_path = relative_path.split("/", 2)
272
+ return unless %w[controllers models helpers].include?(category)
273
+ return unless constant_path&.end_with?(".rb")
274
+
275
+ constant_path.delete_suffix(".rb").split("/").map { |segment| camelize(segment) }.join("::")
276
+ end
277
+
278
+ def recognize_route(env)
279
+ engine = resolve_runtime_class("#{camelize(name.to_s)}::Engine")
280
+ routes = engine.routes
281
+ return recognize_rails_route(routes, env) if routes.respond_to?(:router) && Object.const_defined?(:ActionDispatch, false)
282
+
283
+ route = routes.recognize_path(
284
+ env.fetch("PATH_INFO", "/"),
285
+ method: env.fetch("REQUEST_METHOD", "GET").downcase.to_sym
286
+ )
287
+ [route.transform_keys(&:to_sym), nil]
288
+ end
289
+
290
+ def recognize_rails_route(routes, env)
291
+ action_dispatch = Object.const_get(:ActionDispatch)
292
+ request_class = action_dispatch.const_get(:Request)
293
+ cascade_header = action_dispatch.const_get(:Constants).const_get(:X_CASCADE)
294
+ request = request_class.new(env)
295
+ recognized = nil
296
+
297
+ routes.router.recognize(request) do |route, parameters|
298
+ next unless route.app.matches?(request)
299
+
300
+ request.path_parameters = parameters
301
+ if route.app.dispatcher?
302
+ recognized = [parameters.transform_keys(&:to_sym), nil]
303
+ else
304
+ response = route.app.serve(request)
305
+ next if response[1][cascade_header] == "pass"
306
+
307
+ recognized = [nil, response]
308
+ end
309
+ break
310
+ end
311
+
312
+ return recognized if recognized
313
+
314
+ routing_error = Object.const_get(:ActionController).const_get(:RoutingError)
315
+ raise routing_error, "No route matches #{env.fetch("PATH_INFO", "/").inspect}"
316
+ end
317
+
318
+ def camelize_controller(controller_path)
319
+ controller_path.to_s.split("/").map { |segment| camelize(segment) }.join("::")
320
+ end
321
+
61
322
  def load_setup_hook!
62
323
  path = setup_path
63
324
  return unless path
@@ -88,30 +349,69 @@ module Torikago
88
349
 
89
350
  # Put module-specific gems ahead of the host load path so the Box resolves
90
351
  # dependency versions from the module Gemfile first.
91
- box.load_path.replace(paths.map(&:to_s) + box.load_path)
352
+ box.load_path.replace(paths.map { |path| path.to_s } + box.load_path)
92
353
  end
93
354
 
94
355
  def gemfile_dependencies
95
356
  path = gemfile_path
96
- return [] unless path
357
+ return Array.new unless path
97
358
 
98
359
  @gemfile_dependencies ||= gemfile_dependency_loader.call(path)
99
360
  end
100
361
 
101
362
  def resolve_public_api_class(class_name)
102
363
  boot_runtime!
364
+ resolve_runtime_class(class_name)
365
+ end
366
+
367
+ def resolve_runtime_class(class_name)
103
368
  root = isolated_box_enabled? ? box : Object
104
369
  # const_get is evaluated inside the Box object when isolation is enabled,
105
- # which keeps public API constants out of the host Object namespace.
370
+ # which keeps module-owned constants out of the host Object namespace.
106
371
  class_name.split("::").reduce(root) { |context, segment| context.const_get(segment) }
107
372
  end
108
373
 
109
374
  def runtime_files
110
- @runtime_files ||= [
375
+ @runtime_files ||= if rails_engine?
376
+ rails_runtime_files
377
+ else
378
+ [
379
+ *library_files,
380
+ *gateway_model_files,
381
+ *Dir[public_api_root.join("**/*.rb").to_s].sort
382
+ ]
383
+ end
384
+ end
385
+
386
+ def rails_runtime_files
387
+ [
388
+ *rails_engine_entrypoint_files,
111
389
  *library_files,
112
- *gateway_model_files,
390
+ *Dir[module_root.join("app/models/**/*.rb").to_s].sort,
391
+ *Dir[module_root.join("app/helpers/**/*.rb").to_s].sort,
392
+ *rails_controller_files,
393
+ *Dir[public_api_root.join("**/*.rb").to_s].sort,
394
+ *Dir[module_root.join("config/routes.rb").to_s].sort
395
+ ].uniq
396
+ end
397
+
398
+ def controller_runtime_files
399
+ [
400
+ *library_files,
401
+ *Dir[module_root.join("app/models/**/*.rb").to_s].sort,
402
+ *Dir[module_root.join("app/helpers/**/*.rb").to_s].sort,
403
+ *rails_controller_files,
113
404
  *Dir[public_api_root.join("**/*.rb").to_s].sort
114
- ]
405
+ ].uniq
406
+ end
407
+
408
+ def rails_controller_files
409
+ files = Dir[module_root.join("app/controllers/**/*.rb").to_s].sort
410
+ application_controllers, other_controllers = files.partition do |path|
411
+ File.basename(path) == "application_controller.rb"
412
+ end
413
+
414
+ application_controllers + other_controllers
115
415
  end
116
416
 
117
417
  def public_api_root
@@ -161,8 +461,24 @@ module Torikago
161
461
  @box ||= if box_factory
162
462
  box_factory.call
163
463
  else
164
- Ruby::Box.new
464
+ without_bundler_setup_env { Ruby::Box.new }
165
465
  end
466
+ rescue BoxUnavailableError
467
+ raise
468
+ rescue StandardError => e
469
+ raise unless ENV["RUBY_BOX"] == "1"
470
+
471
+ raise BoxUnavailableError, "Ruby::Box is unavailable for module #{name}: #{e.message}"
472
+ end
473
+
474
+ def with_box_runtime_errors
475
+ yield
476
+ rescue BoxUnavailableError
477
+ raise
478
+ rescue StandardError => e
479
+ raise unless ENV["RUBY_BOX"] == "1"
480
+
481
+ raise BoxUnavailableError, "Ruby::Box is unavailable for module #{name}: #{e.message}"
166
482
  end
167
483
 
168
484
  def prepare_box!
@@ -172,9 +488,65 @@ module Torikago
172
488
  if box.respond_to?(:require)
173
489
  box.require("torikago/current_execution")
174
490
  end
491
+ install_torikago_runtime_bridges!
492
+ install_root_constant_fallback!
175
493
  @box_prepared = true
176
494
  end
177
495
 
496
+ def install_torikago_runtime_bridges!
497
+ box_torikago = box.const_get(:Torikago)
498
+
499
+ TORIKAGO_RUNTIME_CONSTANTS.each do |constant_name|
500
+ next if box_torikago.const_defined?(constant_name, false)
501
+
502
+ box_torikago.const_set(constant_name, Torikago.const_get(constant_name, false))
503
+ end
504
+ end
505
+
506
+ def install_root_constant_fallback!
507
+ return unless box.respond_to?(:eval)
508
+
509
+ resolver = root_constant_resolver || RootConstantResolver.new(registered_roots: registered_roots)
510
+ box_torikago = box.const_get(:Torikago)
511
+ box_torikago.const_set(:ROOT_CONSTANT_RESOLVER, resolver)
512
+ box_torikago.const_set(:ROOT_CONSTANT_UNRESOLVED, resolver.unresolved)
513
+ box.eval(<<~RUBY)
514
+ module Torikago
515
+ module RootConstantFallback
516
+ def const_missing(name)
517
+ resolved = ROOT_CONSTANT_RESOLVER.resolve(name)
518
+ return resolved unless resolved.equal?(ROOT_CONSTANT_UNRESOLVED)
519
+
520
+ super
521
+ end
522
+ end
523
+ end
524
+
525
+ Object.singleton_class.prepend(Torikago::RootConstantFallback)
526
+ RUBY
527
+ end
528
+
529
+ def install_host_runtime_bridges!
530
+ FRAMEWORK_CONSTANTS.each { |constant_name| install_host_constant_bridge!(constant_name) }
531
+ install_rails_runtime_support! if rails_engine?
532
+ end
533
+
534
+ def install_rails_runtime_support!
535
+ install_host_constant_bridge!(:ApplicationController)
536
+ install_host_constant_bridge!(:ApplicationRecord)
537
+ return unless Object.const_defined?(:ActiveSupport, false)
538
+ return unless box.respond_to?(:require)
539
+
540
+ box.require("active_support/core_ext/object/blank")
541
+ end
542
+
543
+ def install_host_constant_bridge!(constant_name)
544
+ return unless Object.const_defined?(constant_name, false)
545
+ return if box.const_defined?(constant_name, false)
546
+
547
+ box.const_set(constant_name, Object.const_get(constant_name, false))
548
+ end
549
+
178
550
  def gateway_model_files
179
551
  # Package APIs may depend on small PORO-style models. Active Record models
180
552
  # are skipped because Rails owns their loading and database connection.
@@ -205,22 +577,22 @@ module Torikago
205
577
 
206
578
  # Monkey patches are only loaded through the explicit setup hook so a
207
579
  # module has to opt in to global-ish runtime changes.
208
- all_files - monkey_patch_files
580
+ all_files - monkey_patch_files - rails_engine_entrypoint_files
209
581
  end
210
582
 
211
- def ruby_box_runtime_available?
212
- return @ruby_box_runtime_available unless @ruby_box_runtime_available.nil?
583
+ def rails_engine_entrypoint_files
584
+ return Array.new unless rails_engine?
585
+
586
+ path = module_root.join("lib/#{name}.rb")
587
+ path.exist? ? [path.to_s] : Array.new
588
+ end
213
589
 
214
- @ruby_box_runtime_available = if ENV["RUBY_BOX"] == "1"
215
- begin
216
- Ruby::Box.new
217
- true
218
- rescue RuntimeError
219
- false
220
- end
221
- else
222
- false
223
- end
590
+ def rails_engine?
591
+ @rails_engine
592
+ end
593
+
594
+ def ruby_box_runtime_available?
595
+ ENV["RUBY_BOX"] == "1"
224
596
  end
225
597
 
226
598
  def camelize(segment)
@@ -240,33 +612,15 @@ module Torikago
240
612
  installed_gem_dependencies = load_installed_gem_dependencies(path)
241
613
  return installed_gem_dependencies unless installed_gem_dependencies.empty?
242
614
 
243
- require "bundler"
244
-
245
- lockfile = Pathname("#{path}.lock")
246
- definition = Bundler::Definition.build(path.to_s, lockfile.exist? ? lockfile.to_s : nil, nil)
247
-
248
- specs_by_name = definition.specs.each_with_object({}) do |spec, specs|
249
- specs[spec.name] ||= spec
250
- end
251
-
252
- definition.dependencies.filter_map do |dependency|
253
- spec = specs_by_name.fetch(dependency.name, nil)
254
- next unless spec
255
-
256
- requirement = dependency.requirement.to_s
257
- {
258
- name: dependency.name,
259
- requirement: requirement,
260
- require_paths: spec.full_require_paths
261
- }
262
- end
263
- rescue Bundler::BundlerError => e
264
- raise GemfileOverrideError, "failed to load gemfile for #{name}: #{e.message}"
615
+ load_bundler_gem_dependencies(path)
265
616
  end
266
617
 
267
618
  def load_path_gem_dependencies(path)
268
619
  path.dirname.then do |gemfile_root|
269
620
  path.read.scan(/^\s*gem\s+["']([^"']+)["']\s*,\s*path:\s*["']([^"']+)["']/).filter_map do |gem_name, relative_path|
621
+ next unless relative_path
622
+
623
+ relative_path = relative_path.to_s
270
624
  gem_root = gemfile_root.join(relative_path)
271
625
  gemspec_path = gem_root.join("#{gem_name}.gemspec")
272
626
  gemspec_path = Dir[gem_root.join("*.gemspec").to_s].sort.first unless gemspec_path.exist?
@@ -284,9 +638,38 @@ module Torikago
284
638
  end
285
639
  end
286
640
 
641
+ def load_bundler_gem_dependencies(path)
642
+ require "bundler"
643
+
644
+ lockfile = Pathname("#{path}.lock")
645
+ definition = Bundler::Definition.build(path.to_s, lockfile.exist? ? lockfile.to_s : nil, nil)
646
+
647
+ specs_by_name = definition.specs.each_with_object(Hash.new) do |spec, specs|
648
+ specs[spec.name] ||= spec
649
+ end
650
+
651
+ definition.dependencies.filter_map do |dependency|
652
+ spec = specs_by_name.fetch(dependency.name, nil)
653
+ next unless spec
654
+
655
+ requirement = dependency.requirement.to_s
656
+ {
657
+ name: dependency.name,
658
+ requirement: requirement,
659
+ require_paths: spec.full_require_paths
660
+ }
661
+ end
662
+ rescue LoadError => e
663
+ raise GemfileOverrideError, "failed to load bundler for #{name}: #{e.message}"
664
+ rescue StandardError => e
665
+ raise unless defined?(Bundler::BundlerError) && e.is_a?(Bundler::BundlerError)
666
+
667
+ raise GemfileOverrideError, "failed to load gemfile for #{name}: #{e.message}"
668
+ end
669
+
287
670
  def load_installed_gem_dependencies(path)
288
671
  dependencies = exact_version_gemfile_dependencies(path)
289
- return [] if dependencies.empty?
672
+ return Array.new if dependencies.empty?
290
673
 
291
674
  dependencies.filter_map do |dependency|
292
675
  specs = installed_specs_for(dependency.fetch(:name), dependency.fetch(:requirement))
@@ -326,9 +709,13 @@ module Torikago
326
709
  match = line.match(/^\s*gem\s+["']([^"']+)["']\s*,\s*["']=\s*([^"']+)["']/)
327
710
  next unless match
328
711
 
712
+ gem_name = match[1]
713
+ version = match[2]
714
+ next unless gem_name && version
715
+
329
716
  {
330
- name: match[1],
331
- requirement: "= #{match[2]}"
717
+ name: gem_name,
718
+ requirement: "= #{version}"
332
719
  }
333
720
  end
334
721
  end
@@ -336,5 +723,45 @@ module Torikago
336
723
  def activate_gem_dependency(dependency)
337
724
  Kernel.send(:gem, dependency.fetch(:name), dependency.fetch(:requirement))
338
725
  end
726
+
727
+ def without_bundler_setup_env
728
+ if BUNDLER_SETUP_ENV_MUTEX.owned?
729
+ without_bundler_setup_env_unsynchronized { yield }
730
+ else
731
+ BUNDLER_SETUP_ENV_MUTEX.synchronize do
732
+ without_bundler_setup_env_unsynchronized { yield }
733
+ end
734
+ end
735
+ end
736
+
737
+ def without_bundler_setup_env_unsynchronized
738
+ rubyopt = ENV["RUBYOPT"]
739
+ bundler_setup = ENV["BUNDLER_SETUP"]
740
+
741
+ if rubyopt&.include?("bundler/setup")
742
+ options = rubyopt.split.reject { |option| option.include?("bundler/setup") }
743
+ if options.empty?
744
+ ENV.delete("RUBYOPT")
745
+ else
746
+ ENV["RUBYOPT"] = options.join(" ")
747
+ end
748
+ end
749
+
750
+ ENV.delete("BUNDLER_SETUP")
751
+
752
+ yield
753
+ ensure
754
+ if rubyopt
755
+ ENV["RUBYOPT"] = rubyopt
756
+ else
757
+ ENV.delete("RUBYOPT")
758
+ end
759
+
760
+ if bundler_setup
761
+ ENV["BUNDLER_SETUP"] = bundler_setup
762
+ else
763
+ ENV.delete("BUNDLER_SETUP")
764
+ end
765
+ end
339
766
  end
340
767
  end