carton 0.2.0 → 0.3.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.
@@ -15,9 +15,10 @@ module Carton
15
15
  module GemActivation
16
16
  module_function
17
17
 
18
- # The non-default gem in `loaded_specs` that owns `feature`, plus its
19
- # activated runtime dependencies, dependencies first. Empty when no
20
- # activated gem owns the file.
18
+ # The gem in `loaded_specs` that owns `feature`, plus its activated
19
+ # runtime dependencies, dependencies first. Default gems count: a new Box
20
+ # would otherwise load the newest installed version, not the caller's.
21
+ # Empty when no activated gem owns the file.
21
22
  def closure(feature, loaded_specs)
22
23
  owner = owner_of(feature, loaded_specs.values)
23
24
  return [] unless owner
@@ -34,21 +35,52 @@ module Carton
34
35
  loaded_specs.values.select { |spec| shareable?(spec) }
35
36
  end
36
37
 
38
+ # The specs that own at least one of `features`, by require path. Sorted
39
+ # features let each directory check be a binary search, so a large bundle
40
+ # stays cheap.
41
+ def owning(specs, features)
42
+ sorted = features.sort
43
+ specs.select do |spec|
44
+ spec.full_require_paths.any? do |dir|
45
+ prefix = "#{dir}/"
46
+ sorted.bsearch { |feature| feature >= prefix }&.start_with?(prefix)
47
+ end
48
+ end
49
+ end
50
+
37
51
  # Rebuild specs in the current Box and activate them. With `locked`, the
38
52
  # Box is restricted to those specs and all of them are activated, as
39
53
  # `Bundler.setup` does, since RubyGems does not auto-activate under
40
54
  # Bundler's restricted view. Spec objects come from another Box, so their
41
55
  # own `activate` would update that Box; a YAML round trip builds this Box's
42
56
  # `Gem::Specification` without re-evaluating gemspec files.
57
+ #
58
+ # Default gems come first, from this Box's own default specs: the YAML
59
+ # round trip loads Psych, whose requires (date, for one) would otherwise
60
+ # activate the newest installed version before the caller's.
43
61
  def activate(specs, locked = nil)
44
- rebuilt = (locked || specs).map { |source| rebuild(source) }
62
+ defaults, others = (locked || specs).partition(&:default_gem?)
63
+ defaults.each { |source| activate_spec(default_spec(source)) }
64
+
65
+ rebuilt = others.map { |source| rebuild(source) }
45
66
  restrict_to(rebuilt) if locked
67
+ rebuilt.each { |spec| activate_spec(spec) }
68
+ end
46
69
 
47
- rebuilt.each do |spec|
48
- loaded = Gem.loaded_specs[spec.name]
49
- spec.activate unless loaded && loaded.version == spec.version
50
- end
70
+ def activate_spec(spec)
71
+ loaded = Gem.loaded_specs[spec.name]
72
+ spec.activate unless loaded && loaded.version == spec.version
73
+ end
74
+ private_class_method :activate_spec
75
+
76
+ def default_spec(source)
77
+ stub =
78
+ Gem::Specification.default_stubs(
79
+ "#{source.name}-#{source.version}.gemspec",
80
+ ).first
81
+ stub ? stub.to_spec : rebuild(source)
51
82
  end
83
+ private_class_method :default_spec
52
84
 
53
85
  def rebuild(source)
54
86
  spec = Gem::Specification.from_yaml(source.to_spec.to_yaml)
@@ -80,7 +112,7 @@ module Carton
80
112
 
81
113
  def owner_of(feature, specs)
82
114
  specs
83
- .select { |spec| shareable?(spec) }
115
+ .select { |spec| activatable?(spec) }
84
116
  .filter_map do |spec|
85
117
  dir =
86
118
  spec.full_require_paths.find do |path|
@@ -94,7 +126,7 @@ module Carton
94
126
  private_class_method :owner_of
95
127
 
96
128
  def visit(spec, loaded_specs, ordered, seen)
97
- return ordered if seen[spec.name] || !shareable?(spec)
129
+ return ordered if seen[spec.name] || !activatable?(spec)
98
130
 
99
131
  seen[spec.name] = true
100
132
  spec.runtime_dependencies.each do |dependency|
@@ -105,13 +137,15 @@ module Carton
105
137
  end
106
138
  private_class_method :visit
107
139
 
108
- # Default gems already load in every Box. Bundler is left out too: each
109
- # Box loads its own copy to apply the restriction, possibly a different
110
- # version than the caller's, and activating the caller's on top of it
111
- # raises Gem::LoadError.
112
- def shareable?(spec)
113
- !spec.default_gem? && spec.name != 'bundler'
114
- end
140
+ # Bundler is never shared: each Box loads its own copy to apply the
141
+ # restriction, possibly a different version than the caller's, and
142
+ # activating the caller's on top of it raises Gem::LoadError.
143
+ def activatable?(spec) = spec.name != 'bundler'
144
+ private_class_method :activatable?
145
+
146
+ # Bundler's restriction already lets default gems load, so the locked set
147
+ # leaves them out.
148
+ def shareable?(spec) = activatable?(spec) && !spec.default_gem?
115
149
  private_class_method :shareable?
116
150
  end
117
151
  end
@@ -10,61 +10,96 @@ module Carton
10
10
  # value or `Carton::Exports` from the Main Box regardless of which
11
11
  # Carton imported it first.
12
12
  class ImportCache
13
+ # Returned by `claim` when the calling thread should load the Entrypoint.
14
+ LOAD = Object.new.freeze
15
+ private_constant :LOAD
16
+
13
17
  def initialize(&loader)
14
18
  @loader = loader
15
19
  @values = {}
16
- @gem_versions = {}
20
+ @gem_usage = {}
21
+ # The thread loading each Entrypoint, while it loads.
17
22
  @loading = {}
18
23
  @lock = Thread::Mutex.new
24
+ @loaded = Thread::ConditionVariable.new
19
25
  end
20
26
 
21
27
  # Load `target` once per Entrypoint path. Targets without an absolute
22
- # path are loaded uncached. Nested imports on the owning thread re-enter
23
- # without waiting on the lock.
28
+ # path are loaded uncached.
29
+ #
30
+ # The lock only guards this bookkeeping and is never held while an
31
+ # Entrypoint runs, so an Entrypoint can start threads that import other
32
+ # Cartons. An import of an Entrypoint another thread is loading waits
33
+ # for that load; one the same thread is loading is a cycle.
24
34
  def import(target)
25
35
  return @loader.call(target) unless File.absolute_path?(target.feature)
26
36
 
27
37
  key = File.realpath(target.feature)
28
- return fetch(key, target) if @lock.owned?
38
+ cached = claim(key)
39
+ return load_entry(key, target) if cached.equal?(LOAD)
29
40
 
30
- @lock.synchronize { fetch(key, target) }
41
+ check_gem_versions(key, target)
42
+ cached
31
43
  end
32
44
 
33
45
  private
34
46
 
35
- def fetch(key, target)
36
- if @values.key?(key)
37
- check_gem_versions(key, target)
38
- return @values[key]
47
+ # The cached value, or LOAD once this thread owns loading `key`.
48
+ def claim(key)
49
+ @lock.synchronize do
50
+ loop do
51
+ return @values[key] if @values.key?(key)
52
+
53
+ owner = @loading[key]
54
+ if owner.nil?
55
+ @loading[key] = Thread.current
56
+ return LOAD
57
+ end
58
+ if owner == Thread.current
59
+ raise ImportError, "circular import: #{key}"
60
+ end
61
+
62
+ @loaded.wait(@lock)
63
+ end
39
64
  end
40
- raise ImportError, "circular import: #{key}" if @loading.key?(key)
65
+ end
41
66
 
42
- @loading[key] = true
43
- begin
44
- @values[key] = @loader.call(target)
45
- @gem_versions[key] = gem_versions(target)
46
- @values[key]
47
- ensure
67
+ # A failed load is not cached: the next import, or a thread waiting on
68
+ # this one, tries again.
69
+ def load_entry(key, target)
70
+ value = @loader.call(target)
71
+ @lock.synchronize do
72
+ @values[key] = value
73
+ @gem_usage[key] = target.gem_usage || -> { target.gems }
74
+ end
75
+ value
76
+ ensure
77
+ @lock.synchronize do
48
78
  @loading.delete(key)
79
+ @loaded.broadcast
49
80
  end
50
81
  end
51
82
 
52
- # A cached Carton keeps the gem versions its first importer activated.
53
- # Raise instead of silently sharing it with an importer that locks a
54
- # different version of one of those gems.
83
+ # A cached Carton keeps the gem versions its first importer gave it: its
84
+ # closure and the locked gems it has loaded, including any loaded lazily
85
+ # since. Raise instead of silently sharing it with an importer that
86
+ # activates or locks another version of one.
55
87
  def check_gem_versions(key, target)
56
- cached = @gem_versions.fetch(key)
57
- gem_versions(target).each do |name, version|
58
- next if cached.fetch(name, version) == version
88
+ wanted = versions([*target.locked_gems, *target.gems])
89
+ return if wanted.empty?
90
+
91
+ usage = @lock.synchronize { @gem_usage.fetch(key) }
92
+ versions(usage.call).each do |name, version|
93
+ next if wanted.fetch(name, version) == version
59
94
 
60
95
  raise ImportError,
61
- "#{key} was imported with #{name} #{cached[name]}, " \
62
- "but this import activates #{name} #{version}"
96
+ "#{key} was imported with #{name} #{version}, " \
97
+ "but this import activates #{name} #{wanted[name]}"
63
98
  end
64
99
  end
65
100
 
66
- def gem_versions(target)
67
- (target.gems || []).to_h { |spec| [spec.name, spec.version.to_s] }
101
+ def versions(specs)
102
+ (specs || []).to_h { |spec| [spec.name, spec.version.to_s] }
68
103
  end
69
104
  end
70
105
  end
@@ -7,7 +7,7 @@ module Carton
7
7
  # Import a carton by absolute path or by feature name from the caller box's
8
8
  # current `$LOAD_PATH`.
9
9
  def import(path)
10
- Runtime.import(path)
10
+ Runtime.import(path, from: caller_locations(1, 1).first.path)
11
11
  end
12
12
 
13
13
  # Import a carton relative to the calling file, like `require_relative`.
@@ -16,17 +16,11 @@ module Carton
16
16
  Runtime.import(path, base_dir: caller_dir)
17
17
  end
18
18
 
19
- # Export top-level methods by name plus keyword values from the current
20
- # carton. `self` is the carton's top-level object, which owns its methods.
21
- def export(*names, **values)
19
+ # Export one value from the current carton; `import` returns it as is.
20
+ # Keywords export a Hash, as in `export version: '1.0'`.
21
+ def export(value)
22
22
  file = caller_locations(1, 1).first.absolute_path
23
- Runtime.export(names, values, owner: self, file:)
24
- end
25
-
26
- # Export a single value that `import` returns as is.
27
- def export_default(value)
28
- file = caller_locations(1, 1).first.absolute_path
29
- Runtime.export_default(value, file:)
23
+ Runtime.export(value, file:)
30
24
  end
31
25
  end
32
26
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loaded inside a Carton Box by `Carton::Box#loaded_features`. Like
4
+ # `$LOAD_PATH`, `$LOADED_FEATURES` resolves to the Box of the file being
5
+ # loaded, so this file sees the Carton's own features even while another
6
+ # Carton is importing it.
7
+ Carton
8
+ .const_get(:Box)
9
+ .for(Ruby::Box.current)
10
+ .__send__(:receive_loaded_features, $LOADED_FEATURES.to_a)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Carton
4
+ # What importing a Bare Carton returns: a module of the constants and
5
+ # top-level helpers its Entrypoint defined. Helpers become singleton
6
+ # methods that forward to the Carton's method, and lookup helpers return
7
+ # the `Method` object or the constant.
8
+ class Namespace < ::Module
9
+ include NamespaceMethods
10
+
11
+ # A helper becomes a singleton method, so one named like a method every
12
+ # `Carton::Namespace` has, such as `fetch` or `name`, would hide it.
13
+ # Constants are capitalized and never collide.
14
+ def self.reserved?(name)
15
+ name = name.to_s
16
+ !name.match?(/\A[A-Z]/) && RESERVED.include?(name)
17
+ end
18
+
19
+ # Raise for the first reserved name in `names`.
20
+ def self.check_names(names)
21
+ name = names.find { |n| reserved?(n) } or return
22
+
23
+ raise ExportError,
24
+ "a top-level method named #{name} would hide " \
25
+ "Carton::Namespace##{name}: rename it, or export a module"
26
+ end
27
+
28
+ def initialize(methods = {}, constants = {})
29
+ super()
30
+ Namespace.check_names(methods.keys)
31
+ @lookup = {}
32
+ define_methods(methods)
33
+ define_constants(constants)
34
+ end
35
+
36
+ def deconstruct_keys(keys)
37
+ return super if keys
38
+
39
+ to_h
40
+ end
41
+
42
+ # `#<Carton::Namespace PI, add>`: its names, so a namespace is
43
+ # recognizable in a console.
44
+ def inspect
45
+ names = @lookup.keys.join(', ')
46
+ names.empty? ? '#<Carton::Namespace>' : "#<Carton::Namespace #{names}>"
47
+ end
48
+ alias to_s inspect
49
+
50
+ private
51
+
52
+ def define_methods(methods)
53
+ methods.each do |name, method|
54
+ @lookup[name.to_s] = method
55
+ define_singleton_method(name) do |*args, **kwargs, &block|
56
+ method.call(*args, **kwargs, &block)
57
+ end
58
+ end
59
+ end
60
+
61
+ def define_constants(constants)
62
+ constants.each do |name, value|
63
+ @lookup[name.to_s] = value
64
+ const_set(name, value)
65
+ end
66
+ end
67
+
68
+ def lookup_entry(key)
69
+ name = key.to_s
70
+ @lookup.key?(name) ? [true, @lookup[name]] : [false, nil]
71
+ end
72
+
73
+ def lookup_names = @lookup.keys
74
+
75
+ RESERVED = public_instance_methods.map(&:to_s).freeze
76
+ private_constant :RESERVED
77
+ end
78
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Carton
4
- # Hash-like lookup helpers for `Carton::Exports`.
5
- module ExportMethods
4
+ # Hash-like lookup helpers for `Carton::Namespace`.
5
+ module NamespaceMethods
6
6
  # Support destructuring like `import('math') => { add: }`.
7
7
  def deconstruct_keys(keys)
8
8
  keys.each_with_object({}) do |key, hash|
@@ -11,20 +11,20 @@ module Carton
11
11
  end
12
12
  end
13
13
 
14
- # Return the exported value or `nil`.
14
+ # Return the member or `nil`.
15
15
  def [](key)
16
16
  found, value = lookup_entry(key)
17
17
  found ? value : nil
18
18
  end
19
19
 
20
- # Check whether the export lookup will succeed.
20
+ # Check whether the lookup will succeed.
21
21
  def key?(key)
22
22
  lookup_entry(key).first
23
23
  end
24
24
 
25
25
  alias has_key? key?
26
26
 
27
- # Fetch an exported value with the same default/block semantics as `Hash#fetch`.
27
+ # Fetch a member with the same default/block semantics as `Hash#fetch`.
28
28
  def fetch(key, *default)
29
29
  if default.size > 1
30
30
  raise ArgumentError,
@@ -39,12 +39,18 @@ module Carton
39
39
  raise KeyError, "key not found: #{key.inspect}"
40
40
  end
41
41
 
42
- # Fetch several exported values and raise if any are missing.
42
+ # Member names as Symbols: helpers first, then constants.
43
+ def keys = lookup_names.map(&:to_sym)
44
+
45
+ # Every member by name: helpers as `Method` objects, constants as set.
46
+ def to_h = keys.to_h { |key| [key, self[key]] }
47
+
48
+ # Fetch several members and raise if any are missing.
43
49
  def fetch_values(*keys)
44
50
  keys.map { |key| fetch(key) }
45
51
  end
46
52
 
47
- # Return several exported values, using `nil` for missing keys.
53
+ # Return several members, using `nil` for missing keys.
48
54
  def values_at(*keys)
49
55
  keys.map { |key| self[key] }
50
56
  end
@@ -9,8 +9,18 @@ module Carton
9
9
  # load-path entry that matched a name-based Import, if any. The runtime
10
10
  # fills `gems` with the caller specs to activate in the new Box, and
11
11
  # `locked_gems` with the caller's locked specs when it ran Bundler setup.
12
+ # After loading, the Carton Box sets `gem_usage`, a callable returning the
13
+ # gems the Carton depends on at that moment: its closure plus every locked
14
+ # gem whose files it has loaded so far.
12
15
  Target =
13
- Struct.new(:feature, :load_path, :gems, :locked_gems, keyword_init: true)
16
+ Struct.new(
17
+ :feature,
18
+ :load_path,
19
+ :gems,
20
+ :locked_gems,
21
+ :gem_usage,
22
+ keyword_init: true,
23
+ )
14
24
 
15
25
  EXTENSIONS = ['.rb', ".#{RbConfig::CONFIG['DLEXT']}"].freeze
16
26
  private_constant :EXTENSIONS
@@ -24,17 +34,22 @@ module Carton
24
34
  # as a native extension, comes back as the bare `path`. A file inside a
25
35
  # Carton raises ImportError: only its Entrypoint can be imported.
26
36
  def resolve(path, load_path:, base_dir: nil)
27
- path = path.to_s
28
- target =
29
- if base_dir || File.absolute_path?(path)
30
- resolve_file(File.expand_path(path, base_dir))
31
- else
32
- resolve_name(path, load_path) || Target.new(feature: path)
33
- end
34
- reject_inside_carton(path, target)
37
+ target = locate(path, load_path:, base_dir:)
38
+ reject_inside_carton(path.to_s, target)
35
39
  target
36
40
  end
37
41
 
42
+ # Like `resolve`, but a file inside a Carton comes back as is, as
43
+ # `require` would load it. `carton check` uses this for `require` calls.
44
+ def locate(path, load_path:, base_dir: nil)
45
+ path = path.to_s
46
+ if base_dir || File.absolute_path?(path)
47
+ resolve_file(File.expand_path(path, base_dir))
48
+ else
49
+ resolve_name(path, load_path) || Target.new(feature: path)
50
+ end
51
+ end
52
+
38
53
  # A Carton is its Entrypoint `x.rb` plus the `x/` directory beside it,
39
54
  # the shape of a gem's `lib/` and of a Zeitwerk explicit namespace.
40
55
  # Directories without their own `.rb` file, like Zeitwerk implicit
@@ -87,10 +102,24 @@ module Carton
87
102
  return Target.new(feature: entrypoint, load_path: lib)
88
103
  end
89
104
 
90
- raise LoadError, "cannot load such file -- #{expanded}"
105
+ raise LoadError, missing_file_message(expanded)
91
106
  end
92
107
  private_class_method :resolve_file
93
108
 
109
+ # Name the files tried, relative to the directory they were tried in.
110
+ def missing_file_message(expanded)
111
+ name = File.basename(expanded)
112
+ tried =
113
+ if EXTENSIONS.include?(File.extname(name))
114
+ [name]
115
+ else
116
+ ["#{name}.rb", "#{name}/lib/#{name}.rb"]
117
+ end
118
+ "cannot load such file -- #{expanded} " \
119
+ "(tried #{tried.join(' and ')} in #{File.dirname(expanded)})"
120
+ end
121
+ private_class_method :missing_file_message
122
+
94
123
  # Like Ruby, check each load-path entry in order and try each extension
95
124
  # within it, so an earlier native extension shadows a later `.rb`.
96
125
  def resolve_name(path, load_path)
@@ -12,11 +12,12 @@ module Carton
12
12
  module_function
13
13
 
14
14
  # Load the target feature in a new box and return the exported value, or
15
- # the box itself if the target does not export anything. Each Entrypoint
15
+ # a `Carton::Namespace` of its definitions if it exports nothing. Each Entrypoint
16
16
  # loads once per process; later imports return the same value.
17
17
  # `base_dir` is given for `import_relative`; plain `import` searches the
18
- # caller Box's `$LOAD_PATH`.
19
- def import(path, base_dir: nil)
18
+ # caller Box's `$LOAD_PATH`, and `from` names the calling file for its
19
+ # error message.
20
+ def import(path, base_dir: nil, from: nil)
20
21
  unless Ruby::Box.enabled?
21
22
  raise ImportError, 'Carton needs Ruby::Box: run Ruby with RUBY_BOX=1'
22
23
  end
@@ -29,45 +30,42 @@ module Carton
29
30
  unless target.gems.empty?
30
31
  target.locked_gems = GemActivation.locked(caller_box, loaded_specs)
31
32
  end
32
- import_cache.import(target)
33
- end
33
+ begin
34
+ import_cache.import(target)
35
+ rescue LoadError => e
36
+ raise unless e.path == target.feature && base_dir.nil?
34
37
 
35
- # Add to the current carton's Export Namespace. A capitalized Symbol names
36
- # a top-level constant, any other Symbol a top-level method; keyword
37
- # arguments are plain values. Like `private`, a single Symbol is returned
38
- # so `export def name ...` works.
39
- def export(names, values, owner:, file:)
40
- if (names.empty? && values.empty?) ||
41
- names.any? { |name| !name.is_a?(Symbol) }
42
- raise ArgumentError,
43
- 'export takes Symbols naming methods or constants, and keyword values'
38
+ raise LoadError, missing_name_message(path, e.message, from)
44
39
  end
40
+ end
45
41
 
46
- duplicate = (names + values.keys).tally.find { |_, count| count > 1 }
47
- if duplicate
42
+ # Make `value` the current carton's export, which `import` returns as
43
+ # is. A Symbol is refused even outside the Entrypoint: `export def name`
44
+ # and `export :Name` would export the Symbol itself.
45
+ def export(value, file:)
46
+ if value.is_a?(Symbol)
48
47
  raise ExportError,
49
- "export name given more than once: #{duplicate.first}"
48
+ "export takes the value to export, not a name like #{value.inspect}: " \
49
+ 'export a module, or keywords for a Hash'
50
50
  end
51
51
 
52
- constants, method_names =
53
- names.partition { |name| name.match?(/\A[A-Z]/) }
54
- methods = method_names.to_h { |name| [name, owner.method(name)] }
55
- constants =
56
- constants.to_h { |name| [name, Object.const_get(name, false)] }
57
- entrypoint_box(file)&.__send__(
58
- :declare_export,
59
- methods,
60
- values.merge(constants),
61
- )
62
- names.first if names.size == 1 && values.empty?
52
+ entrypoint_box(file)&.__send__(:declare_export, value)
53
+ value
63
54
  end
64
55
 
65
- # Make `value` the current carton's Default Export, which `import`
66
- # returns as is. It cannot be combined with `export`.
67
- def export_default(value, file:)
68
- entrypoint_box(file)&.__send__(:declare_default, value)
69
- value
56
+ # A name not on `$LOAD_PATH` is the usual mistake of `import` for a file
57
+ # beside the caller, so point at `import_relative` when one is there.
58
+ def missing_name_message(path, message, from)
59
+ message = "#{message}: import searches $LOAD_PATH, like require"
60
+ dir = from && File.dirname(File.expand_path(from))
61
+ return message unless dir
62
+
63
+ file = Resolver.locate(path, load_path: [], base_dir: dir).feature
64
+ "#{message}; use import_relative #{path.to_s.inspect} for #{file}"
65
+ rescue LoadError
66
+ message
70
67
  end
68
+ private_class_method :missing_name_message
71
69
 
72
70
  def load_import(target)
73
71
  Box
@@ -77,15 +75,13 @@ module Carton
77
75
  target,
78
76
  carton_entrypoint: ENTRYPOINT,
79
77
  import_cache: IMPORT_CACHE,
80
- ) { |methods, values| Exports.new(methods, values) }
78
+ ) { |methods, constants| Namespace.new(methods, constants) }
81
79
  end
82
80
  private_class_method :load_import
83
81
 
84
82
  def import_cache
85
- box = Ruby::Box.current
86
- return IMPORT_CACHE unless box.respond_to?(:import_cache, true)
87
-
88
- box.__send__(:import_cache)
83
+ box = Box.for(Ruby::Box.current)
84
+ box ? box.__send__(:import_cache) : IMPORT_CACHE
89
85
  end
90
86
  private_class_method :import_cache
91
87
 
@@ -93,10 +89,8 @@ module Carton
93
89
  # such as under a plain `require`, `export` is inert, so Carton-aware
94
90
  # files and gems still load the ordinary way.
95
91
  def entrypoint_box(file)
96
- box = Ruby::Box.current
97
- return unless box.respond_to?(:entrypoint?, true)
98
-
99
- box if box.__send__(:entrypoint?, file)
92
+ box = Box.for(Ruby::Box.current)
93
+ box if box&.__send__(:entrypoint?, file)
100
94
  end
101
95
  private_class_method :entrypoint_box
102
96
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Carton
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/carton.rb CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  require_relative 'carton/error'
4
4
  require_relative 'carton/bundler'
5
- require_relative 'carton/export_methods'
6
- require_relative 'carton/exports'
7
- require_relative 'carton/export_declaration'
5
+ require_relative 'carton/namespace_methods'
6
+ require_relative 'carton/namespace'
8
7
  require_relative 'carton/box'
9
8
  require_relative 'carton/gem_activation'
10
9
  require_relative 'carton/import_cache'
@@ -14,11 +13,10 @@ require_relative 'carton/kernel_patch'
14
13
 
15
14
  module Carton
16
15
  private_constant :Box
17
- private_constant :ExportDeclaration
18
- private_constant :ExportMethods
19
16
  private_constant :GemActivation
20
17
  private_constant :ImportCache
21
18
  private_constant :KernelPatch
19
+ private_constant :NamespaceMethods
22
20
  private_constant :Resolver
23
21
  private_constant :Runtime
24
22
  end