carton 0.1.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -1
  3. data/README.md +103 -27
  4. data/lib/carton/activate_gems.rb +8 -0
  5. data/lib/carton/box.rb +162 -68
  6. data/lib/carton/bundler.rb +73 -0
  7. data/lib/carton/error.rb +24 -0
  8. data/lib/carton/gem_activation.rb +151 -0
  9. data/lib/carton/import_cache.rb +105 -0
  10. data/lib/carton/kernel_patch.rb +12 -8
  11. data/lib/carton/loaded_features.rb +10 -0
  12. data/lib/carton/namespace.rb +78 -0
  13. data/lib/carton/{export_methods.rb → namespace_methods.rb} +14 -3
  14. data/lib/carton/resolver.rb +149 -0
  15. data/lib/carton/runtime.rb +74 -59
  16. data/lib/carton/version.rb +1 -1
  17. data/lib/carton.rb +13 -22
  18. data/sig/carton.rbs +49 -0
  19. metadata +20 -34
  20. data/AGENTS.md +0 -47
  21. data/DESIGN.md +0 -84
  22. data/Rakefile +0 -52
  23. data/TODO.md +0 -33
  24. data/USAGE.md +0 -173
  25. data/examples/complex/README.md +0 -33
  26. data/examples/complex/cartons/adventure/.env +0 -1
  27. data/examples/complex/cartons/adventure/Gemfile +0 -5
  28. data/examples/complex/cartons/adventure/Gemfile.lock +0 -17
  29. data/examples/complex/cartons/adventure/lib/adventure.rb +0 -12
  30. data/examples/complex/cartons/loot/.env +0 -1
  31. data/examples/complex/cartons/loot/Gemfile +0 -5
  32. data/examples/complex/cartons/loot/Gemfile.lock +0 -17
  33. data/examples/complex/cartons/loot/lib/loot.rb +0 -47
  34. data/examples/complex/cartons/quest/lib/quest.rb +0 -6
  35. data/examples/complex/main.rb +0 -37
  36. data/examples/minimal/README.md +0 -16
  37. data/examples/minimal/bar.rb +0 -10
  38. data/examples/minimal/baz.rb +0 -7
  39. data/examples/minimal/foo.rb +0 -7
  40. data/examples/minimal/main.rb +0 -17
  41. data/images/carton.png +0 -0
  42. data/lib/carton/exports.rb +0 -35
@@ -4,79 +4,94 @@ module Carton
4
4
  module Runtime
5
5
  ENTRYPOINT = File.expand_path('../carton.rb', __dir__)
6
6
  private_constant :ENTRYPOINT
7
+ # Used only when this Box is the Main Box. Carton Boxes reuse the Main
8
+ # Box cache, so every Carton is built by the Main Box loader.
9
+ IMPORT_CACHE = ImportCache.new { |target| load_import(target) }
10
+ private_constant :IMPORT_CACHE
7
11
 
8
12
  module_function
9
13
 
10
- def import(path, base_dir:)
11
- box = build_import_box
12
- target = resolve_import_target(box, path, base_dir:)
13
- box.__send__(:activate_bundle_if_configured) if target == path
14
- box.__send__(:require_in_box, target)
15
- extract_export(box)
16
- end
14
+ # Load the target feature in a new box and return the exported value, or
15
+ # a `Carton::Namespace` of its definitions if it exports nothing. Each Entrypoint
16
+ # loads once per process; later imports return the same value.
17
+ # `base_dir` is given for `import_relative`; plain `import` searches the
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)
21
+ unless Ruby::Box.enabled?
22
+ raise ImportError, 'Carton needs Ruby::Box: run Ruby with RUBY_BOX=1'
23
+ end
17
24
 
18
- def export(args, kwargs)
19
- if args.any? || kwargs.empty?
20
- raise ArgumentError,
21
- 'export takes keyword arguments; use export_default for a single export'
25
+ caller_box = Ruby::Box.current
26
+ load_path = caller_box.eval('$LOAD_PATH.to_a')
27
+ target = Resolver.resolve(path, load_path:, base_dir:)
28
+ loaded_specs = caller_box.eval('defined?(Gem) ? Gem.loaded_specs : {}')
29
+ target.gems = GemActivation.closure(target.feature, loaded_specs)
30
+ unless target.gems.empty?
31
+ target.locked_gems = GemActivation.locked(caller_box, loaded_specs)
22
32
  end
33
+ begin
34
+ import_cache.import(target)
35
+ rescue LoadError => e
36
+ raise unless e.path == target.feature && base_dir.nil?
23
37
 
24
- current_import_box.__send__(:set_export, kwargs)
38
+ raise LoadError, missing_name_message(path, e.message, from)
39
+ end
25
40
  end
26
41
 
27
- def export_default(value)
28
- current_import_box.__send__(:set_export, value)
29
- end
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)
47
+ raise ExportError,
48
+ "export takes the value to export, not a name like #{value.inspect}: " \
49
+ 'export a module, or keywords for a Hash'
50
+ end
30
51
 
31
- def build_import_box
32
- parent_box = Ruby::Box.current
33
- box = Carton::Box.new
34
- bundle_gemfile = parent_box.eval("ENV['BUNDLE_GEMFILE']")
35
- box.__send__(
36
- :configure_for_import,
37
- parent_box:,
38
- entrypoint: ENTRYPOINT,
39
- bundle_gemfile:,
40
- )
41
- box
52
+ entrypoint_box(file)&.__send__(:declare_export, value)
53
+ value
42
54
  end
43
- private_class_method :build_import_box
44
-
45
- def extract_export(box)
46
- return box unless box.__send__(:export_set?)
47
55
 
48
- value = box.__send__(:export_value)
49
- value.is_a?(Hash) ? Carton::Exports.new(value) : 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
50
67
  end
51
- private_class_method :extract_export
52
-
53
- def current_import_box
54
- box = Ruby::Box.current
55
-
56
- unless box.respond_to?(:set_export, true)
57
- raise RuntimeError,
58
- 'export/export_default must be called from inside Carton.import/import_relative'
59
- end
60
-
61
- box
68
+ private_class_method :missing_name_message
69
+
70
+ def load_import(target)
71
+ Box
72
+ .new
73
+ .__send__(
74
+ :import_target,
75
+ target,
76
+ carton_entrypoint: ENTRYPOINT,
77
+ import_cache: IMPORT_CACHE,
78
+ ) { |methods, constants| Namespace.new(methods, constants) }
62
79
  end
63
- private_class_method :current_import_box
64
-
65
- def resolve_import_target(box, path, base_dir:)
66
- expanded = File.expand_path(path, base_dir)
67
- return expanded if File.file?(expanded)
68
-
69
- expanded_rb = "#{expanded}.rb"
70
- return expanded_rb if File.file?(expanded_rb)
80
+ private_class_method :load_import
71
81
 
72
- resolved = box.__send__(:resolve_feature_path_in_box, path)
73
- return path unless resolved
74
-
75
- type, resolved_path = resolved
76
- return resolved_path if type == :rb && File.file?(resolved_path)
77
-
78
- path
82
+ def import_cache
83
+ box = Box.for(Ruby::Box.current)
84
+ box ? box.__send__(:import_cache) : IMPORT_CACHE
85
+ end
86
+ private_class_method :import_cache
87
+
88
+ # The Carton Box whose Entrypoint is `file`, or nil. Outside that file,
89
+ # such as under a plain `require`, `export` is inert, so Carton-aware
90
+ # files and gems still load the ordinary way.
91
+ def entrypoint_box(file)
92
+ box = Box.for(Ruby::Box.current)
93
+ box if box&.__send__(:entrypoint?, file)
79
94
  end
80
- private_class_method :resolve_import_target
95
+ private_class_method :entrypoint_box
81
96
  end
82
97
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Carton
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/carton.rb CHANGED
@@ -1,31 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- raise 'Ruby 4.0+ is required for Carton' if RUBY_VERSION.to_f < 4.0
4
-
5
- module Carton
6
- module_function
7
-
8
- def with_bundle(gemfile)
9
- previous = ENV['BUNDLE_GEMFILE']
10
- ENV['BUNDLE_GEMFILE'] = gemfile
11
- yield
12
- ensure
13
- if previous
14
- ENV['BUNDLE_GEMFILE'] = previous
15
- else
16
- ENV.delete 'BUNDLE_GEMFILE'
17
- end
18
- end
19
- end
20
-
21
- require_relative 'carton/export_methods'
22
- require_relative 'carton/exports'
3
+ require_relative 'carton/error'
4
+ require_relative 'carton/bundler'
5
+ require_relative 'carton/namespace_methods'
6
+ require_relative 'carton/namespace'
23
7
  require_relative 'carton/box'
8
+ require_relative 'carton/gem_activation'
9
+ require_relative 'carton/import_cache'
10
+ require_relative 'carton/resolver'
24
11
  require_relative 'carton/runtime'
25
12
  require_relative 'carton/kernel_patch'
26
13
 
27
14
  module Carton
28
- private_constant :ExportMethods
29
- private_constant :Runtime
15
+ private_constant :Box
16
+ private_constant :GemActivation
17
+ private_constant :ImportCache
30
18
  private_constant :KernelPatch
19
+ private_constant :NamespaceMethods
20
+ private_constant :Resolver
21
+ private_constant :Runtime
31
22
  end
data/sig/carton.rbs ADDED
@@ -0,0 +1,49 @@
1
+ module Kernel
2
+ private
3
+
4
+ def import: (::String path) -> untyped
5
+ def import_relative: (::String path) -> untyped
6
+ def export: [T] (T value) -> T
7
+ end
8
+
9
+ module Carton
10
+ VERSION: ::String
11
+
12
+ def self.setup_bundle: (*::Symbol groups, ?gemfile: ::String?) -> untyped
13
+ end
14
+
15
+ class Carton::Error < ::StandardError
16
+ end
17
+
18
+ class Carton::ImportError < Carton::Error
19
+ end
20
+
21
+ class Carton::ExportError < Carton::Error
22
+ end
23
+
24
+ class Carton::GemfileNotFound < Carton::Error
25
+ end
26
+
27
+ # Lookup helpers on every `Carton::Namespace`. `carton rbs` extends its
28
+ # generated `Carton::Namespace::*` modules with these.
29
+ module Carton::NamespaceMethods
30
+ type key = ::String | ::Symbol
31
+
32
+ # Helpers come back as Method objects; constants as set.
33
+ def []: (key key) -> untyped
34
+ def deconstruct_keys: (nil keys) -> ::Hash[::Symbol, untyped]
35
+ | (::Array[::Symbol] keys) -> ::Hash[::Symbol, untyped]
36
+ def fetch: (key key) -> untyped
37
+ | (key key, untyped default) -> untyped
38
+ | [T] (key key) { (key key) -> T } -> (untyped | T)
39
+ def fetch_values: (*key keys) -> ::Array[untyped]
40
+ def keys: () -> ::Array[::Symbol]
41
+ def to_h: () -> ::Hash[::Symbol, untyped]
42
+ def has_key?: (key key) -> bool
43
+ def key?: (key key) -> bool
44
+ def values_at: (*key keys) -> ::Array[untyped]
45
+ end
46
+
47
+ class Carton::Namespace < ::Module
48
+ include ::Carton::NamespaceMethods
49
+ end
metadata CHANGED
@@ -1,63 +1,49 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carton
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cristofaro
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: Carton is a thin wrapper around Ruby::Box for safe, ergonomic modularization
13
- in Ruby 4+. It gives you imports and exports that feel like ES Modules while still
14
- reading like Ruby. Each carton can isolate constants, gems, and monkey patches behind
15
- a small public API, so large apps can keep clear boundaries.
12
+ description: The package system Ruby has been missing. Split code into Cartons that
13
+ import and export like modules. Each Carton runs in its own Ruby::Box, so only what
14
+ it exports is reachable, monkey patches stay where they are made, and two Cartons
15
+ can use different versions of the same gem. No manifest needed. Requires Ruby 4.0.7+.
16
16
  email:
17
17
  - david@dtcristo.com
18
18
  executables: []
19
19
  extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
- - AGENTS.md
23
22
  - CHANGELOG.md
24
- - DESIGN.md
25
23
  - LICENSE.txt
26
24
  - README.md
27
- - Rakefile
28
- - TODO.md
29
- - USAGE.md
30
- - examples/complex/README.md
31
- - examples/complex/cartons/adventure/.env
32
- - examples/complex/cartons/adventure/Gemfile
33
- - examples/complex/cartons/adventure/Gemfile.lock
34
- - examples/complex/cartons/adventure/lib/adventure.rb
35
- - examples/complex/cartons/loot/.env
36
- - examples/complex/cartons/loot/Gemfile
37
- - examples/complex/cartons/loot/Gemfile.lock
38
- - examples/complex/cartons/loot/lib/loot.rb
39
- - examples/complex/cartons/quest/lib/quest.rb
40
- - examples/complex/main.rb
41
- - examples/minimal/README.md
42
- - examples/minimal/bar.rb
43
- - examples/minimal/baz.rb
44
- - examples/minimal/foo.rb
45
- - examples/minimal/main.rb
46
- - images/carton.png
47
25
  - lib/carton.rb
26
+ - lib/carton/activate_gems.rb
48
27
  - lib/carton/box.rb
49
- - lib/carton/export_methods.rb
50
- - lib/carton/exports.rb
28
+ - lib/carton/bundler.rb
29
+ - lib/carton/error.rb
30
+ - lib/carton/gem_activation.rb
31
+ - lib/carton/import_cache.rb
51
32
  - lib/carton/kernel_patch.rb
33
+ - lib/carton/loaded_features.rb
34
+ - lib/carton/namespace.rb
35
+ - lib/carton/namespace_methods.rb
36
+ - lib/carton/resolver.rb
52
37
  - lib/carton/runtime.rb
53
38
  - lib/carton/version.rb
39
+ - sig/carton.rbs
54
40
  homepage: https://github.com/dtcristo/carton
55
41
  licenses:
56
42
  - MIT
57
43
  metadata:
58
44
  allowed_push_host: https://rubygems.org
45
+ rubygems_mfa_required: 'true'
59
46
  homepage_uri: https://github.com/dtcristo/carton
60
- source_code_uri: https://github.com/dtcristo/carton
61
47
  changelog_uri: https://github.com/dtcristo/carton/blob/main/CHANGELOG.md
62
48
  rdoc_options: []
63
49
  require_paths:
@@ -66,14 +52,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
52
  requirements:
67
53
  - - ">="
68
54
  - !ruby/object:Gem::Version
69
- version: 4.0.1
55
+ version: 4.0.7
70
56
  required_rubygems_version: !ruby/object:Gem::Requirement
71
57
  requirements:
72
58
  - - ">="
73
59
  - !ruby/object:Gem::Version
74
60
  version: '0'
75
61
  requirements: []
76
- rubygems_version: 4.0.10
62
+ rubygems_version: 4.0.20
77
63
  specification_version: 4
78
- summary: Easily box your Ruby
64
+ summary: Real boundaries for Ruby
79
65
  test_files: []
data/AGENTS.md DELETED
@@ -1,47 +0,0 @@
1
- # Agents
2
-
3
- Guidance and memory for agents working on this repo.
4
-
5
- ## Memory bank
6
-
7
- - Goal: make Carton feel like part of Ruby: built-in, simple, streamlined, and not verbose.
8
- - Prefer simpler implementation over complexity.
9
- - Keep the public API small and Ruby-like.
10
- - The project should work with or without Bundler.
11
- - A package in this system is called a carton, though docs can still use "package" in plain English when it reads better.
12
- - Current reliable bundled-carton pattern: use `Carton.with_bundle(gemfile) { import ... }` for the entry import that should activate a carton-local bundle.
13
- - Conflicting bundles still need a subprocess workaround today.
14
- - Ruby::Box boxes start from root-box load paths/loaded features, so Carton must copy parent non-gem load paths forward itself.
15
- - `$LOAD_PATH` is box-local, but `Gem.loaded_specs` is shared across boxes in practice, so conflicting bundle activation still collides there.
16
- - Duplicating `Gem.loaded_specs` inside a box isolates a single bundled import from root state, but conflicting bundles still fail because Bundler also rewrites shared RubyGems entrypoints/spec state such as `Gem::Specification.all`.
17
- - Never push. Make local commits only; the user handles pushes.
18
-
19
- ## Working rules
20
-
21
- - Everything should have tests. Write tests for new behaviour, run targeted tests as you go, and `RUBY_BOX=1 bundle exec rake` before finishing.
22
- - If you only changed an example, run that example during iteration and the full suite before finishing.
23
- - Run `bundle exec rake format` after every change.
24
- - Keep implementation and docs concise.
25
- - Do not over-engineer.
26
- - Never use thread local variables in implementation.
27
- - Always review https://docs.ruby-lang.org/en/4.0/Ruby/Box.html on how `Ruby::Box` works.
28
- - Never bump the version or publish the gem.
29
- - When multiple implementation choices are viable, present a menu with your recommendation.
30
- - Commit as you go with descriptive messages and the required Co-authored-by trailer.
31
- - Never push; the user will do that.
32
-
33
- ## Docs
34
-
35
- - Keep `README.md`, `USAGE.md`, `DESIGN.md`, `TODO.md`, `AGENTS.md`, and example READMEs up to date when behavior, workflow, or plans change.
36
- - Keep the root `README.md` minimal; move detail into focused docs.
37
- - Put future work, open questions, and upstream ideas in `TODO.md`.
38
- - Update `AGENTS.md` whenever the user gives durable project guidance or you discover stable context worth remembering.
39
- - Use `AGENTS.md` as a concise memory bank, not a changelog.
40
-
41
- ## Commands
42
-
43
- - `RUBY_BOX=1 bundle exec rake` - full suite
44
- - `RUBY_BOX=1 bundle exec rake test` - unit tests only
45
- - `RUBY_BOX=1 bundle exec rake example:minimal` - minimal example
46
- - `RUBY_BOX=1 bundle exec rake example:complex` - complex example
47
- - `bundle exec rake format` - format code
data/DESIGN.md DELETED
@@ -1,84 +0,0 @@
1
- # Design
2
-
3
- ## Goal
4
-
5
- Keep the library small: a thin wrapper around `Ruby::Box` that makes isolated file-to-file imports feel like part of Ruby.
6
-
7
- ## Main pieces
8
-
9
- | File | Role |
10
- | --- | --- |
11
- | `lib/carton.rb` | Entry point, version guard, `Carton.with_bundle`, and internal wiring |
12
- | `lib/carton/kernel_patch.rb` | Adds global `import`, `import_relative`, `export_default`, and `export` |
13
- | `lib/carton/runtime.rb` | Builds boxes, resolves targets, runs imports, and extracts exports |
14
- | `lib/carton/box.rb` | Box-specific helpers for requiring files, managing export state, and inheriting safe load paths |
15
- | `lib/carton/exports.rb` | Wraps named exports in a module-like namespace |
16
- | `lib/carton/export_methods.rb` | Shared `[]`, `fetch`, `fetch_values`, `values_at`, `key?`, and destructuring support |
17
-
18
- ## Import flow
19
-
20
- 1. `import` or `import_relative` delegates to `Carton::Runtime.import`.
21
- 2. A fresh `Carton::Box` is created for that import.
22
- 3. The carton entrypoint (`lib/carton.rb`) is required inside the box so the box has `export`, `import`, and `import_relative`.
23
- 4. The target file is resolved either from the caller's base directory or from the current box's `$LOAD_PATH`.
24
- 5. The target file is required inside the box.
25
- 6. If the file called `export_default` or `export`, the exported value is returned.
26
- 7. If the export was a hash, it is wrapped in `Carton::Exports`.
27
- 8. If there was no export, the box itself is returned.
28
-
29
- Every import creates a fresh box. There is no module cache at the Carton layer.
30
-
31
- ## Export model
32
-
33
- There are three return shapes:
34
-
35
- - single export: the exported object itself
36
- - named exports: `Carton::Exports`
37
- - no export: `Carton::Box`
38
-
39
- `export_default` is the explicit single-export form. `export` is the named-export form and only accepts keyword arguments. `Carton::Exports` exposes capitalized keys as constants and lowercase keys as singleton methods. Both `Carton::Exports` and `Carton::Box` share the same small fetch/deconstruction API through `ExportMethods`.
40
-
41
- ## Load path model
42
-
43
- Ruby::Box creates new user boxes from the root box, not from the current caller box. In `box.c`, each new box starts by duplicating the root box's `load_path` and `loaded_features`, and `require` resolves against the loading box's local `$LOAD_PATH` / `$LOADED_FEATURES`.
44
-
45
- Carton therefore re-inherits the parent box's non-gem load paths manually so local carton directories can still be resolved by name across nested imports.
46
-
47
- The current filter deliberately tries to avoid leaking gem paths from a parent import chain into child boxes:
48
-
49
- - RubyGems paths are filtered
50
- - common Bundler vendor paths are filtered
51
- - local, non-gem carton paths are preserved
52
-
53
- This is why the examples add sibling `lib/` directories to `$LOAD_PATH` explicitly before using `import 'name'`.
54
-
55
- ## Bundler model
56
-
57
- The current implementation works best when a carton bundle is selected before its entry file is evaluated. In practice that means selecting `BUNDLE_GEMFILE` around the `import` call for that carton; `Carton.with_bundle` is the small wrapper for that handoff.
58
-
59
- This keeps one carton-local bundle isolated from the caller, but current limitations remain:
60
-
61
- - a parent already running under Bundler is not a reliable place to import a child carton with its own bundle
62
- - switching between conflicting bundles in one process is not reliable today
63
- - the complex example uses a subprocess for the conflicting `loot` bundle for that reason
64
-
65
- The problem is not that Bundler has special C code mutating every box at once. Bundler and RubyGems do their activation work in Ruby:
66
-
67
- - `Bundler::SharedHelpers#find_gemfile` reads `ENV['BUNDLE_GEMFILE']`
68
- - `Bundler::Runtime#setup` calls `clean_load_path`, `replace_entrypoints`, `mark_loaded`, and `Gem.add_to_load_path`
69
- - RubyGems activation checks and writes `Gem.loaded_specs`
70
-
71
- What matters under `Ruby::Box` is which of those pieces are box-local. In practice:
72
-
73
- - `$LOAD_PATH` stays box-local; activating a bundle in an imported box does not inject its gem lib paths into the caller box
74
- - `Gem.loaded_specs` is still shared across boxes in practice unless it is explicitly replaced inside the box
75
-
76
- Duplicating `Gem.loaded_specs` inside an imported box was enough to keep a single bundled import from leaking back into the caller box, but it was still not enough to make two conflicting bundles coexist. Bundler also rewrites RubyGems entrypoints and spec state through code paths such as `replace_entrypoints`, `stub_rubygems`, `Gem::Specification.all = ...`, and `Gem.clear_paths`.
77
-
78
- That shared activation state is why a second box can see a gem version activated by the first one even though its own `$LOAD_PATH` is separate. In local probes, activating `dotenv 3.2.0` in one box caused a second box targeting `dotenv ~> 2.0` to fail its Bundler activation and then crash under Ruby 4.0 + `Ruby::Box`.
79
-
80
- See [TODO.md](TODO.md) for the upstream work that would help.
81
-
82
- ## Why the complex example stays explicit
83
-
84
- The complex example keeps load-path setup and bundled imports visible in `main.rb` on purpose. It is meant to demonstrate Carton directly, not hide the mechanics behind another layer.
data/Rakefile DELETED
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- STREE_FILES = '"**/*.{rb,rake,gemspec}" "**/Rakefile" "**/Gemfile"'
4
- EXAMPLES = %w[minimal complex].freeze
5
-
6
- desc 'Run all tests'
7
- task :test do
8
- test_files =
9
- Dir.glob('test/**/*_test.rb').sort.map { |f| File.expand_path(f) }
10
- sh "RUBY_BOX=1 ruby -Ilib -Itest -e 'ARGV.each { |f| require f }' #{test_files.join(' ')}",
11
- verbose: false
12
- end
13
-
14
- namespace :example do
15
- EXAMPLES.each do |name|
16
- desc "Run the #{name} example"
17
- task name.to_sym do
18
- dir = File.join('examples', name)
19
-
20
- puts
21
- puts "== Running example/#{name} =="
22
- puts
23
-
24
- Bundler.with_unbundled_env do
25
- # Install gems for any carton/local example gemfile.
26
- Dir
27
- .glob(File.join(dir, '**/Gemfile'))
28
- .each do |gemfile|
29
- pkg_dir = File.dirname(gemfile)
30
- sh(
31
- "cd #{pkg_dir} && " \
32
- 'BUNDLE_GEMFILE=Gemfile bundle check >/dev/null 2>&1 || ' \
33
- 'BUNDLE_GEMFILE=Gemfile bundle install --quiet',
34
- verbose: false,
35
- )
36
- end
37
-
38
- sh "RUBY_BOX=1 ruby #{File.join(dir, 'main.rb')}", verbose: false
39
- end
40
- end
41
- end
42
- end
43
-
44
- desc 'Run all examples'
45
- task examples: EXAMPLES.map { |n| "example:#{n}" }
46
-
47
- desc 'Format code'
48
- task :format do
49
- sh "bundle exec stree write #{STREE_FILES}", verbose: false
50
- end
51
-
52
- task default: %i[test examples]
data/TODO.md DELETED
@@ -1,33 +0,0 @@
1
- # TODO
2
-
3
- ## Bundler / upstream
4
-
5
- - Add a Bundler API that can activate a specific Gemfile explicitly instead of relying on process-global `ENV['BUNDLE_GEMFILE']` and `bundler/setup`.
6
- - Make multiple bundle contexts safer for embedded runtimes. Today one carton-local bundle can work, but importing a second conflicting bundle in the same process still resolves against the first bundle's shared RubyGems activation state and can crash under `Ruby::Box`.
7
- - Expose resolved bundle load paths without forcing full `Bundler.setup` and RubyGems entrypoint replacement.
8
- - Make `Bundler.reset!`, `with_original_env`, and `with_unbundled_env` sufficient for re-entering Bundler with another Gemfile, or add a supported replacement for that workflow.
9
- - Improve support for a parent process already running under Bundler importing a child carton that needs a different bundle.
10
- - Make RubyGems activation state such as `Gem.loaded_specs` safely box-local or otherwise explicitly isolateable so two conflicting bundles can coexist under `Ruby::Box` without a subprocess.
11
- - Investigate whether `Bundler.rubygems.replace_entrypoints`, `stub_rubygems`, `Gem::Specification.all`, and `Gem.clear_paths` can be made box-local. Duplicating `Gem.loaded_specs` inside the box isolated a single bundled import from the root box, but it was not enough to make a second conflicting bundle work.
12
-
13
- ## Ruby::Box / upstream
14
-
15
- - Expose more public APIs or clearer guarantees around box-local `$LOAD_PATH`, `$LOADED_FEATURES`, and feature resolution so libraries like Carton do not need to manually copy parent load paths and call `resolve_feature_path`.
16
- - Investigate the crash path after conflicting bundled imports under Ruby 4.0 + `Ruby::Box`, even when the immediate conflict is already surfaced as a Bundler/RubyGems activation error.
17
-
18
- ## Library
19
-
20
- - Revisit `Carton::Box#gem_path?`; it still relies partly on path heuristics.
21
- - Add targeted tests for bundled carton imports instead of covering that behavior only through the complex example.
22
- - Remove the targeted Minitest `object_id` warning suppression once Ruby::Box or Minitest no longer emits it under Ruby 4.0.
23
- - Revisit whether `import` should always return a `Carton::Exports` wrapper with a `default` accessor, or whether the current duck-typed return values are the better long-term fit.
24
-
25
- ## Examples / docs
26
-
27
- - Keep the root README intentionally short and push detail into focused docs.
28
- - Keep documenting current Bundler constraints plainly until the runtime or Bundler story improves.
29
-
30
- ## Tooling ideas
31
-
32
- - Explore a Prism-based strict mode/linter that enforces `import`/`export` calls at file root and can build a static dependency graph without evaluating code.
33
- - If that exists, add graph validation ideas such as cycle detection, missing import targets, dead imports, and unused exports.