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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -102
- data/README.md +50 -46
- data/lib/carton/activate_gems.rb +1 -1
- data/lib/carton/box.rb +89 -22
- data/lib/carton/error.rb +3 -2
- data/lib/carton/gem_activation.rb +51 -17
- data/lib/carton/import_cache.rb +61 -26
- data/lib/carton/kernel_patch.rb +5 -11
- data/lib/carton/loaded_features.rb +10 -0
- data/lib/carton/namespace.rb +78 -0
- data/lib/carton/{export_methods.rb → namespace_methods.rb} +13 -7
- data/lib/carton/resolver.rb +39 -10
- data/lib/carton/runtime.rb +36 -42
- data/lib/carton/version.rb +1 -1
- data/lib/carton.rb +3 -5
- data/sig/carton.rbs +11 -4
- metadata +9 -9
- data/lib/carton/export_declaration.rb +0 -72
- data/lib/carton/exports.rb +0 -54
|
@@ -15,9 +15,10 @@ module Carton
|
|
|
15
15
|
module GemActivation
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
|
-
# The
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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|
|
|
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] || !
|
|
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
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
data/lib/carton/import_cache.rb
CHANGED
|
@@ -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
|
-
@
|
|
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.
|
|
23
|
-
#
|
|
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
|
-
|
|
38
|
+
cached = claim(key)
|
|
39
|
+
return load_entry(key, target) if cached.equal?(LOAD)
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
check_gem_versions(key, target)
|
|
42
|
+
cached
|
|
31
43
|
end
|
|
32
44
|
|
|
33
45
|
private
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
65
|
+
end
|
|
41
66
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
53
|
-
#
|
|
54
|
-
#
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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} #{
|
|
62
|
-
"but this import activates #{name} #{
|
|
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
|
|
67
|
-
(
|
|
101
|
+
def versions(specs)
|
|
102
|
+
(specs || []).to_h { |spec| [spec.name, spec.version.to_s] }
|
|
68
103
|
end
|
|
69
104
|
end
|
|
70
105
|
end
|
data/lib/carton/kernel_patch.rb
CHANGED
|
@@ -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
|
|
20
|
-
#
|
|
21
|
-
def export(
|
|
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(
|
|
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::
|
|
5
|
-
module
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
data/lib/carton/resolver.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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,
|
|
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)
|
data/lib/carton/runtime.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
33
|
+
begin
|
|
34
|
+
import_cache.import(target)
|
|
35
|
+
rescue LoadError => e
|
|
36
|
+
raise unless e.path == target.feature && base_dir.nil?
|
|
34
37
|
|
|
35
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
#
|
|
66
|
-
#
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/carton/version.rb
CHANGED
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/
|
|
6
|
-
require_relative 'carton/
|
|
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
|