carton 0.1.0 → 0.2.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 +119 -1
- data/README.md +98 -26
- data/lib/carton/activate_gems.rb +8 -0
- data/lib/carton/box.rb +96 -69
- data/lib/carton/bundler.rb +73 -0
- data/lib/carton/error.rb +23 -0
- data/lib/carton/export_declaration.rb +72 -0
- data/lib/carton/export_methods.rb +7 -2
- data/lib/carton/exports.rb +27 -8
- data/lib/carton/gem_activation.rb +117 -0
- data/lib/carton/import_cache.rb +70 -0
- data/lib/carton/kernel_patch.rb +15 -5
- data/lib/carton/resolver.rb +120 -0
- data/lib/carton/runtime.rb +74 -53
- data/lib/carton/version.rb +1 -1
- data/lib/carton.rb +12 -19
- data/sig/carton.rbs +42 -0
- metadata +17 -31
- data/AGENTS.md +0 -47
- data/DESIGN.md +0 -84
- data/Rakefile +0 -52
- data/TODO.md +0 -33
- data/USAGE.md +0 -173
- data/examples/complex/README.md +0 -33
- data/examples/complex/cartons/adventure/.env +0 -1
- data/examples/complex/cartons/adventure/Gemfile +0 -5
- data/examples/complex/cartons/adventure/Gemfile.lock +0 -17
- data/examples/complex/cartons/adventure/lib/adventure.rb +0 -12
- data/examples/complex/cartons/loot/.env +0 -1
- data/examples/complex/cartons/loot/Gemfile +0 -5
- data/examples/complex/cartons/loot/Gemfile.lock +0 -17
- data/examples/complex/cartons/loot/lib/loot.rb +0 -47
- data/examples/complex/cartons/quest/lib/quest.rb +0 -6
- data/examples/complex/main.rb +0 -37
- data/examples/minimal/README.md +0 -16
- data/examples/minimal/bar.rb +0 -10
- data/examples/minimal/baz.rb +0 -7
- data/examples/minimal/foo.rb +0 -7
- data/examples/minimal/main.rb +0 -17
- data/images/carton.png +0 -0
data/lib/carton.rb
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
3
|
+
require_relative 'carton/error'
|
|
4
|
+
require_relative 'carton/bundler'
|
|
21
5
|
require_relative 'carton/export_methods'
|
|
22
6
|
require_relative 'carton/exports'
|
|
7
|
+
require_relative 'carton/export_declaration'
|
|
23
8
|
require_relative 'carton/box'
|
|
9
|
+
require_relative 'carton/gem_activation'
|
|
10
|
+
require_relative 'carton/import_cache'
|
|
11
|
+
require_relative 'carton/resolver'
|
|
24
12
|
require_relative 'carton/runtime'
|
|
25
13
|
require_relative 'carton/kernel_patch'
|
|
26
14
|
|
|
27
15
|
module Carton
|
|
16
|
+
private_constant :Box
|
|
17
|
+
private_constant :ExportDeclaration
|
|
28
18
|
private_constant :ExportMethods
|
|
29
|
-
private_constant :
|
|
19
|
+
private_constant :GemActivation
|
|
20
|
+
private_constant :ImportCache
|
|
30
21
|
private_constant :KernelPatch
|
|
22
|
+
private_constant :Resolver
|
|
23
|
+
private_constant :Runtime
|
|
31
24
|
end
|
data/sig/carton.rbs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Kernel
|
|
2
|
+
private
|
|
3
|
+
|
|
4
|
+
def import: (::String path) -> untyped
|
|
5
|
+
def import_relative: (::String path) -> untyped
|
|
6
|
+
def export: (*::Symbol names, **untyped values) -> ::Symbol?
|
|
7
|
+
def export_default: [T] (T value) -> T
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module Carton
|
|
11
|
+
VERSION: ::String
|
|
12
|
+
|
|
13
|
+
def self.setup_bundle: (*::Symbol groups, ?gemfile: ::String?) -> untyped
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Carton::Error < ::StandardError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Carton::ImportError < Carton::Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Carton::ExportError < Carton::Error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Carton::GemfileNotFound < Carton::Error
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Carton::Exports < ::Module
|
|
29
|
+
type key = ::String | ::Symbol
|
|
30
|
+
|
|
31
|
+
# Exported methods come back as Method objects; values as exported.
|
|
32
|
+
def []: (key key) -> untyped
|
|
33
|
+
def deconstruct_keys: (nil keys) -> ::Hash[::Symbol, untyped]
|
|
34
|
+
| (::Array[::Symbol] keys) -> ::Hash[::Symbol, untyped]
|
|
35
|
+
def fetch: (key key) -> untyped
|
|
36
|
+
| (key key, untyped default) -> untyped
|
|
37
|
+
| [T] (key key) { (key key) -> T } -> (untyped | T)
|
|
38
|
+
def fetch_values: (*key keys) -> ::Array[untyped]
|
|
39
|
+
def has_key?: (key key) -> bool
|
|
40
|
+
def key?: (key key) -> bool
|
|
41
|
+
def values_at: (*key keys) -> ::Array[untyped]
|
|
42
|
+
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cristofaro
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
a
|
|
12
|
+
description: Split a Ruby app into gem-sized units that import and export like modules.
|
|
13
|
+
Each Carton runs in its own Ruby::Box, so two parts of one app can use different
|
|
14
|
+
versions of the same gem, monkey patches stay where they are made, and only what
|
|
15
|
+
a Carton exports is reachable. 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
|
|
28
|
+
- lib/carton/bundler.rb
|
|
29
|
+
- lib/carton/error.rb
|
|
30
|
+
- lib/carton/export_declaration.rb
|
|
49
31
|
- lib/carton/export_methods.rb
|
|
50
32
|
- lib/carton/exports.rb
|
|
33
|
+
- lib/carton/gem_activation.rb
|
|
34
|
+
- lib/carton/import_cache.rb
|
|
51
35
|
- lib/carton/kernel_patch.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.
|
|
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.
|
|
62
|
+
rubygems_version: 4.0.20
|
|
77
63
|
specification_version: 4
|
|
78
64
|
summary: Easily box your 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.
|
data/USAGE.md
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
# Usage
|
|
2
|
-
|
|
3
|
-
## Requirements
|
|
4
|
-
|
|
5
|
-
- Ruby 4.0+
|
|
6
|
-
- `RUBY_BOX=1`
|
|
7
|
-
- `require 'carton'` before using `import`, `import_relative`, `export_default`, or `export`
|
|
8
|
-
|
|
9
|
-
In Carton, an importable package is called a carton. This guide still says "package" sometimes in plain English, but "carton" is the project term.
|
|
10
|
-
|
|
11
|
-
## Exporting
|
|
12
|
-
|
|
13
|
-
### Single export
|
|
14
|
-
|
|
15
|
-
Use a single export when the file represents one main thing.
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
# user.rb
|
|
19
|
-
class User
|
|
20
|
-
def initialize(name) = @name = name
|
|
21
|
-
|
|
22
|
-
def greet = "Hello, #{@name}!"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
export_default User
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Importing that file returns the exported object itself:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
User = import_relative 'user'
|
|
32
|
-
User.new('Alice').greet
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Named exports
|
|
36
|
-
|
|
37
|
-
Use keyword arguments when the file exports a small namespace.
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
# math.rb
|
|
41
|
-
def add(a, b) = a + b
|
|
42
|
-
|
|
43
|
-
export(
|
|
44
|
-
PI: 3.14159,
|
|
45
|
-
version: '1.0.0',
|
|
46
|
-
add: method(:add)
|
|
47
|
-
)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Importing named exports returns a `Carton::Exports` object, which behaves like a namespace module.
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
MathTools = import_relative 'math'
|
|
54
|
-
|
|
55
|
-
MathTools::PI
|
|
56
|
-
MathTools.version
|
|
57
|
-
MathTools.add(2, 3)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`Carton::Exports` and bare `Carton::Box` imports share a small lookup API: `[]`, `fetch`, `fetch_values`, `values_at`, and `key?`.
|
|
61
|
-
|
|
62
|
-
Only one export call is allowed per imported file. Use `export_default value` for single exports or `export foo:, bar:` for named exports.
|
|
63
|
-
|
|
64
|
-
## Importing
|
|
65
|
-
|
|
66
|
-
### `import_relative`
|
|
67
|
-
|
|
68
|
-
`import_relative path` resolves relative to the calling file, like `require_relative`.
|
|
69
|
-
|
|
70
|
-
```ruby
|
|
71
|
-
User = import_relative 'user'
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### `import`
|
|
75
|
-
|
|
76
|
-
`import path` resolves using the current box's `$LOAD_PATH` or an absolute path.
|
|
77
|
-
|
|
78
|
-
```ruby
|
|
79
|
-
Quest = import 'quest'
|
|
80
|
-
Widget = import '/absolute/path/to/widget.rb'
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
When you want `import 'name'` to work across local cartons, add their `lib/` directories to `$LOAD_PATH` first.
|
|
84
|
-
|
|
85
|
-
```ruby
|
|
86
|
-
cartons_dir = File.expand_path('cartons', __dir__)
|
|
87
|
-
|
|
88
|
-
Dir.glob(File.join(cartons_dir, '*/lib')).sort.each do |dir|
|
|
89
|
-
$LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
|
|
90
|
-
end
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
The complex example keeps that setup explicit in `examples/complex/main.rb`.
|
|
94
|
-
|
|
95
|
-
## Destructuring
|
|
96
|
-
|
|
97
|
-
Named exports support pattern matching through `deconstruct_keys`.
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
import_relative('math') => { add:, version: }
|
|
101
|
-
|
|
102
|
-
add.(2, 3)
|
|
103
|
-
version
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
You can rename during destructuring:
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
import_relative('math') => { add: sum }
|
|
110
|
-
sum.(10, 10)
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Constants
|
|
114
|
-
|
|
115
|
-
For exported constants, the simplest options are namespace access, `[]`, or `fetch`.
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
MathTools = import_relative 'math'
|
|
119
|
-
PI = MathTools::PI
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
PI = import('math')[:PI]
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
For multiple values:
|
|
127
|
-
|
|
128
|
-
```ruby
|
|
129
|
-
PI, version = import_relative('math').fetch_values(:PI, :version)
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Bare imports
|
|
133
|
-
|
|
134
|
-
If a file does not call `export`, importing it returns the `Carton::Box` itself.
|
|
135
|
-
|
|
136
|
-
```ruby
|
|
137
|
-
Toolbox = import 'some_script'
|
|
138
|
-
|
|
139
|
-
Toolbox::SomeConstant
|
|
140
|
-
Toolbox.fetch(:helper)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Bundler inside cartons
|
|
144
|
-
|
|
145
|
-
The library works with or without Bundler. Plain cartons need no extra setup. For a bundled carton, the reliable pattern today is still: select the Gemfile around the `import` that loads that carton.
|
|
146
|
-
|
|
147
|
-
```ruby
|
|
148
|
-
gemfile = File.expand_path('cartons/adventure/Gemfile', __dir__)
|
|
149
|
-
|
|
150
|
-
Adventure = Carton.with_bundle(gemfile) { import 'adventure' }
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
`Carton.with_bundle` is just a small wrapper around the same `BUNDLE_GEMFILE` handoff. The underlying constraint is still Bundler's: `bundler/setup` discovers the active Gemfile from env/process state, and RubyGems activation state (`Gem.loaded_specs`) is still shared across boxes in practice, so conflicting bundles still cannot be switched reliably in one process today.
|
|
154
|
-
|
|
155
|
-
Current limits:
|
|
156
|
-
|
|
157
|
-
- one carton-local bundle can be activated cleanly from an unbundled parent
|
|
158
|
-
- a bundled parent process is not a reliable place to import a child carton with its own bundle
|
|
159
|
-
- conflicting bundles in one process still need a subprocess workaround
|
|
160
|
-
|
|
161
|
-
See [TODO.md](TODO.md) for the upstream Bundler work that would make this cleaner.
|
|
162
|
-
|
|
163
|
-
## Examples
|
|
164
|
-
|
|
165
|
-
```sh
|
|
166
|
-
RUBY_BOX=1 ruby examples/minimal/main.rb
|
|
167
|
-
RUBY_BOX=1 bundle exec rake example:complex
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
See the example READMEs for details:
|
|
171
|
-
|
|
172
|
-
- [examples/minimal/README.md](examples/minimal/README.md)
|
|
173
|
-
- [examples/complex/README.md](examples/complex/README.md)
|
data/examples/complex/README.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Complex example
|
|
2
|
-
|
|
3
|
-
Multi-carton example showing the library directly: explicit load-path setup, a bundled single export loaded by name, a plain carton loaded by name, and a destructured import.
|
|
4
|
-
|
|
5
|
-
## Cartons
|
|
6
|
-
|
|
7
|
-
| Carton | Purpose |
|
|
8
|
-
| --- | --- |
|
|
9
|
-
| `main` | explicit feature tour of the library |
|
|
10
|
-
| `adventure` | bundled carton imported by name |
|
|
11
|
-
| `quest` | plain carton imported by name |
|
|
12
|
-
| `loot` | carton that demonstrates a conflicting `dotenv` version via subprocess |
|
|
13
|
-
|
|
14
|
-
`main.rb` keeps the load-path setup and bundled import explicit on purpose so the example reads like a direct Carton feature tour. `Carton.with_bundle` shortens the `BUNDLE_GEMFILE` handoff, but it is still the same Bundler constraint under the hood.
|
|
15
|
-
|
|
16
|
-
## Why `loot` still uses a subprocess
|
|
17
|
-
|
|
18
|
-
One carton-local bundle can be activated from an unbundled parent today. The trouble starts when another box tries to activate a conflicting version: box-local `$LOAD_PATH` is not enough because RubyGems activation state is still shared. `loot` therefore loads its `dotenv` data in a subprocess and selects its Gemfile there instead of using `Carton.with_bundle` during the import itself. That is tracked in [../../TODO.md](../../TODO.md).
|
|
19
|
-
|
|
20
|
-
## Run
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
RUBY_BOX=1 bundle exec rake example:complex
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Or manually:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
cd examples/complex/cartons/adventure && BUNDLE_GEMFILE=Gemfile bundle install
|
|
30
|
-
cd ../loot && BUNDLE_GEMFILE=Gemfile bundle install
|
|
31
|
-
cd ../../..
|
|
32
|
-
RUBY_BOX=1 ruby examples/complex/main.rb
|
|
33
|
-
```
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
BANNER=single export loaded from a bundled carton
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
GEM
|
|
2
|
-
remote: https://rubygems.org/
|
|
3
|
-
specs:
|
|
4
|
-
dotenv (3.2.0)
|
|
5
|
-
|
|
6
|
-
PLATFORMS
|
|
7
|
-
arm64-darwin-25
|
|
8
|
-
ruby
|
|
9
|
-
|
|
10
|
-
DEPENDENCIES
|
|
11
|
-
dotenv (~> 3.0)
|
|
12
|
-
|
|
13
|
-
CHECKSUMS
|
|
14
|
-
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
|
15
|
-
|
|
16
|
-
BUNDLED WITH
|
|
17
|
-
4.0.3
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'dotenv'
|
|
4
|
-
|
|
5
|
-
module Adventure
|
|
6
|
-
CONFIG = Dotenv.parse(File.expand_path('../.env', __dir__))
|
|
7
|
-
def self.banner = CONFIG.fetch('BANNER')
|
|
8
|
-
|
|
9
|
-
def self.dotenv_version = Gem.loaded_specs.fetch('dotenv').version.to_s
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
export_default Adventure
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
MESSAGE=loaded through a conflicting bundle in a subprocess
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
GEM
|
|
2
|
-
remote: https://rubygems.org/
|
|
3
|
-
specs:
|
|
4
|
-
dotenv (2.8.1)
|
|
5
|
-
|
|
6
|
-
PLATFORMS
|
|
7
|
-
arm64-darwin-25
|
|
8
|
-
ruby
|
|
9
|
-
|
|
10
|
-
DEPENDENCIES
|
|
11
|
-
dotenv (~> 2.0)
|
|
12
|
-
|
|
13
|
-
CHECKSUMS
|
|
14
|
-
dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
|
|
15
|
-
|
|
16
|
-
BUNDLED WITH
|
|
17
|
-
4.0.3
|