namespaced-gem 0.1.0.pre
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 +7 -0
- data/.idea/.gitignore +46 -0
- data/.idea/git_toolbox_prj.xml +15 -0
- data/.idea/modules.xml +8 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/HOT_HOOK.md +160 -0
- data/HOT_LOAD_ANALYSIS.md +325 -0
- data/ISSUE.md +198 -0
- data/README.md +369 -0
- data/Rakefile +12 -0
- data/WORKAROUND.md +43 -0
- data/lib/example_hooks.rb +57 -0
- data/lib/namespaced/gem/api_spec_patch.rb +106 -0
- data/lib/namespaced/gem/bundler_integration.rb +103 -0
- data/lib/namespaced/gem/bundler_resolver_patch.rb +121 -0
- data/lib/namespaced/gem/dependency_patch.rb +42 -0
- data/lib/namespaced/gem/download_patch.rb +49 -0
- data/lib/namespaced/gem/gem_resolver_patch.rb +108 -0
- data/lib/namespaced/gem/metadata_deps_hook.rb +168 -0
- data/lib/namespaced/gem/namespace_source_registry.rb +62 -0
- data/lib/namespaced/gem/uri_dependency.rb +177 -0
- data/lib/namespaced/gem/version.rb +7 -0
- data/lib/namespaced/gem.rb +26 -0
- data/lib/rubygems_plugin.rb +73 -0
- data/sig/namespaced/gem.rbs +87 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b72c048d303c91b9d305add224623f46281534c5fa44c44b866a2ac3809cf40c
|
|
4
|
+
data.tar.gz: 7cd113090909aded453c6e04ebf5bfe7603a223311ec1125a5910c3190a41130
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c7e733db83c4900d393ea6b80e32dda174b8d8db7f77fdd9533de40d79526f9f270b865be00c6f9c3715df342fe467433d21eace29cabb5144815cc4aa393d2b
|
|
7
|
+
data.tar.gz: 92d026b40e287882e94e54dc29398ec4f48e39cdf94e05ffad72cb6b5ef972d03971e21d86850a0160e5afbf3e076bfb8fc2f5a452f7c990df8169aa2062174e
|
data/.idea/.gitignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Default ignored files
|
|
2
|
+
/shelf/
|
|
3
|
+
/workspace.xml
|
|
4
|
+
# Ignored default folder with query files
|
|
5
|
+
/queries/
|
|
6
|
+
# Datasource local storage ignored files
|
|
7
|
+
/dataSources/
|
|
8
|
+
/dataSources.local.xml
|
|
9
|
+
# Editor-based HTTP Client requests
|
|
10
|
+
/httpRequests/
|
|
11
|
+
|
|
12
|
+
# Zencoder local files
|
|
13
|
+
/zencoder/chats
|
|
14
|
+
/zencoder-chat-index.xml
|
|
15
|
+
/zencoder-chats-dedicated.xml
|
|
16
|
+
# Local project config
|
|
17
|
+
*.iml
|
|
18
|
+
|
|
19
|
+
# Added: Ignore plugin state artifacts (machine-specific)
|
|
20
|
+
/copilot*.xml
|
|
21
|
+
/GitLink.xml
|
|
22
|
+
|
|
23
|
+
# Added: Ignore task & usage statistics (contain local timestamps / paths)
|
|
24
|
+
/tasks.xml
|
|
25
|
+
/usage.statistics.xml
|
|
26
|
+
|
|
27
|
+
# Added: Local inspection profiles (developer-specific customizations)
|
|
28
|
+
/inspectionProfiles/
|
|
29
|
+
|
|
30
|
+
# Added: Library & index caches (regenerated per machine)
|
|
31
|
+
/libraries/
|
|
32
|
+
/indexLayout.xml
|
|
33
|
+
/indexes/
|
|
34
|
+
|
|
35
|
+
# Added: Terminal, SSH, and other per-user runtime config
|
|
36
|
+
/terminal/
|
|
37
|
+
/sshConfigs/
|
|
38
|
+
|
|
39
|
+
# Added: RubyMine misc SDK version drift (optional). Comment to share SDK config.
|
|
40
|
+
# /misc.xml
|
|
41
|
+
|
|
42
|
+
# Commit: VCS local mappings can be regenerated (optional). Comment to share settings.
|
|
43
|
+
/vcs.xml
|
|
44
|
+
|
|
45
|
+
# Commit: Dictionary files (local spellcheck customizations)
|
|
46
|
+
# /dictionaries/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="GitToolBoxProjectSettings">
|
|
4
|
+
<option name="commitMessageIssueKeyValidationOverride">
|
|
5
|
+
<BoolValueOverride>
|
|
6
|
+
<option name="enabled" value="true" />
|
|
7
|
+
</BoolValueOverride>
|
|
8
|
+
</option>
|
|
9
|
+
<option name="commitMessageValidationEnabledOverride">
|
|
10
|
+
<BoolValueOverride>
|
|
11
|
+
<option name="enabled" value="true" />
|
|
12
|
+
</BoolValueOverride>
|
|
13
|
+
</option>
|
|
14
|
+
</component>
|
|
15
|
+
</project>
|
data/.idea/modules.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/namespaced-gem.iml" filepath="$PROJECT_DIR$/.idea/namespaced-gem.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"namespaced-gem" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["peter.boling@gmail.com"](mailto:"peter.boling@gmail.com").
|
data/HOT_HOOK.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Hookah::Gem
|
|
2
|
+
|
|
3
|
+
Investigation into RubyGems plugin hot-loading — specifically whether a
|
|
4
|
+
`rubygems_plugin.rb` carried by a **previously-uninstalled dependency gem** can
|
|
5
|
+
be live-loaded into the running `gem install` process.
|
|
6
|
+
|
|
7
|
+
**TL;DR — Yes, it works, via `Gem::Installer#load_plugin`.**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## How RubyGems Loads Plugins
|
|
12
|
+
|
|
13
|
+
### Path 1 — at `gem` startup (already-installed gems only)
|
|
14
|
+
|
|
15
|
+
`GemRunner#run` calls two methods before executing any command:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
Gem.load_env_plugins # scans $LOAD_PATH for rubygems_plugin.rb
|
|
19
|
+
Gem.load_plugins # scans $GEM_HOME/plugins/ for *_plugin.rb stubs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
These only reach gems that are **already on disk**.
|
|
23
|
+
|
|
24
|
+
### Path 2 — hot-load during `gem install` (the interesting one)
|
|
25
|
+
|
|
26
|
+
`Gem::Installer#install` runs this sequence for **every gem** it installs:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pre_install_checks
|
|
30
|
+
run_pre_install_hooks ← Gem.pre_install hooks fire here
|
|
31
|
+
extract_files
|
|
32
|
+
build_extensions
|
|
33
|
+
run_post_build_hooks ← Gem.post_build hooks fire here
|
|
34
|
+
generate_plugins ← writes $GEM_HOME/plugins/<name>_plugin.rb stub
|
|
35
|
+
write_spec
|
|
36
|
+
load_plugin ← HOT-LOADS the plugin into the running process
|
|
37
|
+
run_post_install_hooks ← Gem.post_install hooks fire here
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`load_plugin` (installer.rb ~line 986):
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
def load_plugin
|
|
44
|
+
specs = Gem::Specification.find_all_by_name(spec.name)
|
|
45
|
+
# Only hot-load on first install — avoids loading two versions at once.
|
|
46
|
+
return unless specs.size == 1
|
|
47
|
+
|
|
48
|
+
plugin_files = spec.plugins.map do |plugin|
|
|
49
|
+
File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}")
|
|
50
|
+
end
|
|
51
|
+
Gem.load_plugin_files(plugin_files)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`spec.plugins` (basic_specification.rb) discovers files via:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
def plugins
|
|
59
|
+
matches_for_glob("rubygems#{Gem.plugin_suffix_pattern}")
|
|
60
|
+
# expands to lib/rubygems_plugin{,.rb,.so,...}
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
So any gem with `lib/rubygems_plugin.rb` in its `require_paths` is a plugin gem.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## The Dependency Hot-Load Trick
|
|
69
|
+
|
|
70
|
+
`RequestSet#install` (request_set.rb) installs gems in **topological order**
|
|
71
|
+
(`sorted_requests` uses `TSort` / `strongly_connected_components`):
|
|
72
|
+
dependencies are installed **before** the gems that require them.
|
|
73
|
+
|
|
74
|
+
This creates the following opportunity:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
gem install hookah-gem
|
|
78
|
+
│
|
|
79
|
+
├─ 1. Resolve graph: hookah-gem → hookah-core (has rubygems_plugin.rb)
|
|
80
|
+
│
|
|
81
|
+
├─ 2. Install hookah-core first (leaf node)
|
|
82
|
+
│ └─ Gem::Installer#load_plugin fires
|
|
83
|
+
│ └─ hookah-core's rubygems_plugin.rb is require'd into THIS process
|
|
84
|
+
│ └─ Gem.pre_install / post_install / done_installing hooks register
|
|
85
|
+
│
|
|
86
|
+
└─ 3. Install hookah-gem
|
|
87
|
+
└─ run_pre_install_hooks ← hooks from hookah-core are already active!
|
|
88
|
+
└─ run_post_install_hooks ← same
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
To use this pattern, add the plugin-carrying gem as a runtime dependency:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# hookah-gem.gemspec
|
|
95
|
+
spec.add_dependency "hookah-core" # hookah-core ships lib/rubygems_plugin.rb
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Critical caveat
|
|
99
|
+
|
|
100
|
+
The hot-load only fires when `Gem::Specification.find_all_by_name(spec.name).size == 1`,
|
|
101
|
+
i.e. **this is the very first version of the dependency on the system**. If the
|
|
102
|
+
user already has any version of `hookah-core` installed the plugin stub is
|
|
103
|
+
regenerated but the file is **not** `require`'d into the live process. The
|
|
104
|
+
next time the `gem` command runs (a fresh process) `Gem.load_plugins` will pick
|
|
105
|
+
it up from the stubs directory.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Available Hooks (registered inside `lib/rubygems_plugin.rb`)
|
|
110
|
+
|
|
111
|
+
| Hook | Fires | Abort? |
|
|
112
|
+
|------|-------|--------|
|
|
113
|
+
| `Gem.pre_install { \|installer\| }` | Before files are extracted | `return false` raises `Gem::InstallError` |
|
|
114
|
+
| `Gem.post_build { \|installer\| }` | After native exts compile | `return false` removes gem dir + raises |
|
|
115
|
+
| `Gem.post_install { \|installer\| }` | After gem fully installed | No |
|
|
116
|
+
| `Gem.done_installing { \|dep_installer, specs\| }` | After entire batch done | No |
|
|
117
|
+
|
|
118
|
+
`installer` is a `Gem::Installer`; `installer.spec` is the `Gem::Specification`
|
|
119
|
+
being installed. `dep_installer` is the `Gem::DependencyInstaller` that drove
|
|
120
|
+
the whole `gem install` run.
|
|
121
|
+
|
|
122
|
+
See `lib/rubygems_plugin.rb` in this repo for an annotated skeleton.
|
|
123
|
+
|
|
124
|
+
## Installation
|
|
125
|
+
|
|
126
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
127
|
+
|
|
128
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Usage
|
|
141
|
+
|
|
142
|
+
TODO: Write usage instructions here
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
147
|
+
|
|
148
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
149
|
+
|
|
150
|
+
## Contributing
|
|
151
|
+
|
|
152
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hookah-gem. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/hookah-gem/blob/main/CODE_OF_CONDUCT.md).
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
157
|
+
|
|
158
|
+
## Code of Conduct
|
|
159
|
+
|
|
160
|
+
Everyone interacting in the Hookah::Gem project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/hookah-gem/blob/main/CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# Hot-Load Analysis: Can `Gem::Installer#load_plugin` Solve the Chicken-and-Egg Problem?
|
|
2
|
+
|
|
3
|
+
**TL;DR — The hot-load alone cannot solve the cold-start resolution problem
|
|
4
|
+
(resolution runs before installation), but it enables a metadata-based
|
|
5
|
+
two-phase approach via `MetadataDepsHook`. More importantly, when the plugin is
|
|
6
|
+
pre-installed (the common case), all `gem install` paths work today — including
|
|
7
|
+
direct `gem install @kaspth/oaken`.**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The Chicken-and-Egg Problem (recap)
|
|
12
|
+
|
|
13
|
+
When a user runs `gem install my-gem` and `my-gem`'s gemspec contains:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
spec.add_dependency "namespaced-gem"
|
|
17
|
+
spec.add_dependency "https://beta.gem.coop/@myspace/foo", "~> 1.0"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
…and `namespaced-gem` is **not yet installed**, the patches from
|
|
21
|
+
`rubygems_plugin.rb` are not loaded. RubyGems sees the URI string as a literal
|
|
22
|
+
gem name, queries rubygems.org for it, finds nothing, and aborts.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Exact `gem install` Execution Sequence
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
gem install my-gem
|
|
30
|
+
│
|
|
31
|
+
├─ 1. BOOT — Gem.load_plugins / Gem.load_env_plugins
|
|
32
|
+
│ └─ Only loads rubygems_plugin.rb from ALREADY-INSTALLED gems
|
|
33
|
+
│ └─ namespaced-gem NOT installed → NO patches loaded
|
|
34
|
+
│
|
|
35
|
+
├─ 2. RESOLVE — DependencyInstaller#resolve_dependencies
|
|
36
|
+
│ ├─ Fetches my-gem's spec from rubygems.org
|
|
37
|
+
│ ├─ Sees deps: ["namespaced-gem", "https://beta.gem.coop/@myspace/foo"]
|
|
38
|
+
│ ├─ Tries to look up "https://beta.gem.coop/@myspace/foo" on rubygems.org
|
|
39
|
+
│ ├─ No match found
|
|
40
|
+
│ └─ ❌ ABORTS — resolution fails BEFORE any gem is installed
|
|
41
|
+
│
|
|
42
|
+
└─ 3. INSTALL (never reached)
|
|
43
|
+
├─ Would install namespaced-gem first (leaf dep, via TSort)
|
|
44
|
+
├─ Gem::Installer#load_plugin would fire
|
|
45
|
+
├─ rubygems_plugin.rb would be require'd into running process
|
|
46
|
+
├─ All patches would activate
|
|
47
|
+
└─ my-gem install would proceed with patches active
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**The hot-load fires at step 3, but the failure is at step 2.**
|
|
51
|
+
|
|
52
|
+
The resolution phase (`RequestSet#resolve`) runs in its entirety before
|
|
53
|
+
`RequestSet#install` begins. The `InstallerSet#find_all` method encounters the
|
|
54
|
+
URI dependency string, has no patch to intercept it, queries the default source
|
|
55
|
+
(rubygems.org), finds no gem by that name, and the resolver aborts with
|
|
56
|
+
`Gem::UnsatisfiableDependencyError`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Impact on Each Use Case
|
|
61
|
+
|
|
62
|
+
### Use Case 1: Gem Authors (`gem install my-gem`, first time)
|
|
63
|
+
|
|
64
|
+
**Hot-load: too late for cold-start.** When the plugin is NOT pre-installed,
|
|
65
|
+
resolution fails before install begins.
|
|
66
|
+
|
|
67
|
+
However, when the plugin IS pre-installed (Use Cases 2–4), `gem install my-gem`
|
|
68
|
+
with URI deps in the gemspec **works today** — `GemResolverPatch` intercepts
|
|
69
|
+
the resolver, `ApiSpecPatch` synthesizes specs from Compact Index data, and
|
|
70
|
+
`DownloadPatch` handles namespace download errors.
|
|
71
|
+
|
|
72
|
+
**For the Bundler path** (`bundle install` with `gemspec`), the hot-load is
|
|
73
|
+
**irrelevant** — the existing `BundlerIntegration` TracePoint approach already
|
|
74
|
+
handles this. Bundler loads `Bundler::Dsl`, the TracePoint fires, patches
|
|
75
|
+
activate, and URI deps are remapped before Bundler's own resolution begins.
|
|
76
|
+
This path works today.
|
|
77
|
+
|
|
78
|
+
### Use Case 2: Application Developers (pre-installed)
|
|
79
|
+
|
|
80
|
+
**Hot-load: irrelevant.** The plugin is already installed (via
|
|
81
|
+
`gem install namespaced-gem`), so `rubygems_plugin.rb` loads at boot via
|
|
82
|
+
`Gem.load_plugins`. The patches are active before any gemspec is evaluated.
|
|
83
|
+
|
|
84
|
+
### Use Case 3: Global Installation (pre-installed)
|
|
85
|
+
|
|
86
|
+
**Same as Use Case 2.** Plugin already in gem path. Hot-load not involved.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## What the Hot-Load CAN Do
|
|
91
|
+
|
|
92
|
+
While the hot-load can't fix the resolution-phase failure, it **does** enable:
|
|
93
|
+
|
|
94
|
+
1. **`post_install` / `done_installing` hooks** — After `namespaced-gem` is
|
|
95
|
+
installed as a leaf dependency (in a separate install invocation, or as part
|
|
96
|
+
of a successful resolution that didn't include URI deps), hooks registered
|
|
97
|
+
by `rubygems_plugin.rb` fire for all subsequent gems in the same batch.
|
|
98
|
+
|
|
99
|
+
2. **Mid-batch patch activation** — If `namespaced-gem` is installed as part
|
|
100
|
+
of a batch (e.g. `gem install namespaced-gem other-gem`), the hot-load
|
|
101
|
+
activates all patches before `other-gem`'s install phase. However, the
|
|
102
|
+
**resolution** for the entire batch still happened before any installs, so
|
|
103
|
+
this only helps if `other-gem` doesn't have URI deps that need resolving
|
|
104
|
+
(or if they were resolved via other means).
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Viable Approaches Leveraging the Hot-Load Discovery
|
|
109
|
+
|
|
110
|
+
### Approach A: Metadata-Based Two-Phase Resolution (recommended)
|
|
111
|
+
|
|
112
|
+
Instead of putting URI deps directly in `add_dependency` (which must survive
|
|
113
|
+
resolution on rubygems.org), encode them in `spec.metadata`:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# Published gemspec on rubygems.org
|
|
117
|
+
Gem::Specification.new do |spec|
|
|
118
|
+
spec.name = "my-gem"
|
|
119
|
+
spec.version = "1.0.0"
|
|
120
|
+
|
|
121
|
+
# namespaced-gem is a normal runtime dep — resolves fine on rubygems.org
|
|
122
|
+
spec.add_dependency "namespaced-gem"
|
|
123
|
+
|
|
124
|
+
# URI deps stored in metadata — invisible to RubyGems' resolver
|
|
125
|
+
spec.metadata["namespaced_dependencies"] = [
|
|
126
|
+
"https://beta.gem.coop/@myspace/foo ~> 1.0",
|
|
127
|
+
"@myorg/bar >= 2.0"
|
|
128
|
+
].join("\n")
|
|
129
|
+
|
|
130
|
+
# Normal deps work as usual
|
|
131
|
+
spec.add_dependency "rack", "~> 3.0"
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The flow becomes:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
gem install my-gem
|
|
139
|
+
│
|
|
140
|
+
├─ 1. BOOT — no namespaced-gem installed → no patches
|
|
141
|
+
│
|
|
142
|
+
├─ 2. RESOLVE — sees deps: ["namespaced-gem", "rack"]
|
|
143
|
+
│ └─ ✅ All plain names — resolves fine on rubygems.org
|
|
144
|
+
│
|
|
145
|
+
├─ 3. INSTALL (topological order)
|
|
146
|
+
│ ├─ Install namespaced-gem (leaf dep)
|
|
147
|
+
│ │ └─ load_plugin fires → rubygems_plugin.rb loaded
|
|
148
|
+
│ │ └─ All patches activate
|
|
149
|
+
│ │ └─ done_installing hook registers
|
|
150
|
+
│ ├─ Install rack
|
|
151
|
+
│ └─ Install my-gem
|
|
152
|
+
│ └─ post_install hook fires
|
|
153
|
+
│ └─ Reads spec.metadata["namespaced_dependencies"]
|
|
154
|
+
│ └─ Parses URI deps
|
|
155
|
+
│ └─ Triggers second resolution pass for URI deps
|
|
156
|
+
│ (patches are now active!)
|
|
157
|
+
│ └─ Installs namespace-sourced gems
|
|
158
|
+
│
|
|
159
|
+
└─ 4. DONE — all gems installed ✅
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**This is the only approach that enables single-command `gem install my-gem`
|
|
163
|
+
without pre-installation of the plugin.**
|
|
164
|
+
|
|
165
|
+
#### Implementation sketch
|
|
166
|
+
|
|
167
|
+
A new `MetadataDepsHook` module, loaded by `rubygems_plugin.rb`, would:
|
|
168
|
+
|
|
169
|
+
1. Register a `Gem.done_installing` hook (or `Gem.post_install` per-gem).
|
|
170
|
+
2. After each gem installs, check `spec.metadata["namespaced_dependencies"]`.
|
|
171
|
+
3. If present, parse the URI dep strings and trigger a
|
|
172
|
+
`Gem::DependencyInstaller` for each, with all patches now active.
|
|
173
|
+
|
|
174
|
+
#### Trade-offs
|
|
175
|
+
|
|
176
|
+
- **Pro:** Single-command install works. No pre-installation required.
|
|
177
|
+
- **Pro:** Compatible with the hot-load mechanism — patches activate before
|
|
178
|
+
the hook fires.
|
|
179
|
+
- **Con:** URI deps are not visible in the standard `add_dependency` list.
|
|
180
|
+
Tools that inspect gemspec dependencies won't see them.
|
|
181
|
+
- **Con:** Requires gem authors to use `metadata` instead of (or in addition
|
|
182
|
+
to) `add_dependency` for URI deps — a less intuitive API.
|
|
183
|
+
- **Con:** The second resolution pass is a separate install; version conflicts
|
|
184
|
+
between the first and second pass must be handled.
|
|
185
|
+
|
|
186
|
+
### Approach B: Dual Encoding (best of both worlds)
|
|
187
|
+
|
|
188
|
+
Gem authors use **both** `add_dependency` (for Bundler, which handles URI deps
|
|
189
|
+
via `BundlerIntegration`) **and** `metadata` (for `gem install`, via the
|
|
190
|
+
hot-load hook):
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
Gem::Specification.new do |spec|
|
|
194
|
+
spec.add_dependency "namespaced-gem"
|
|
195
|
+
|
|
196
|
+
# For Bundler (handled by BundlerIntegration patch):
|
|
197
|
+
spec.add_dependency "https://beta.gem.coop/@myspace/foo", "~> 1.0"
|
|
198
|
+
|
|
199
|
+
# For gem install (handled by done_installing hook after hot-load):
|
|
200
|
+
spec.metadata["namespaced_dependencies"] = \
|
|
201
|
+
"https://beta.gem.coop/@myspace/foo ~> 1.0"
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
When `gem install my-gem` runs:
|
|
206
|
+
- Resolution ignores the URI dep (rubygems.org returns no match, but the gem
|
|
207
|
+
itself still resolves because the URI dep is not "required" for resolution to
|
|
208
|
+
complete — **this needs verification**; if the resolver hard-fails on
|
|
209
|
+
unresolvable deps, this won't work).
|
|
210
|
+
|
|
211
|
+
**⚠️ Problem:** RubyGems' resolver will try to resolve ALL `add_dependency`
|
|
212
|
+
entries. An unresolvable URI dep causes `Gem::UnsatisfiableDependencyError`
|
|
213
|
+
and aborts the entire resolution. Dual encoding only works if we can teach
|
|
214
|
+
the resolver to **skip** URI deps during resolution and defer them.
|
|
215
|
+
|
|
216
|
+
This could be done with a minimal boot-time shim (see Approach C).
|
|
217
|
+
|
|
218
|
+
### Approach C: Minimal Boot-Time Shim (no full plugin needed)
|
|
219
|
+
|
|
220
|
+
Ship a **second, tiny gem** (`namespaced-gem-shim`) that contains only a
|
|
221
|
+
`rubygems_plugin.rb` with a single patch: teach `InstallerSet#find_all` to
|
|
222
|
+
**silently skip** URI-named deps during resolution (returning an empty array
|
|
223
|
+
instead of failing). The full `namespaced-gem` plugin then handles actual
|
|
224
|
+
installation via the `done_installing` hot-load hook.
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# namespaced-gem-shim/lib/rubygems_plugin.rb
|
|
228
|
+
# This gem is tiny and can be pre-installed globally, or it can be the
|
|
229
|
+
# gem that gets hot-loaded.
|
|
230
|
+
|
|
231
|
+
# Teach the resolver to not crash on URI dep names.
|
|
232
|
+
module NamespacedGemShim
|
|
233
|
+
module InstallerSetSkipUri
|
|
234
|
+
def find_all(req)
|
|
235
|
+
return [] if req.name.match?(%r{\Ahttps?://|^@[^/]+/|^pkg:gem/})
|
|
236
|
+
super
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
Gem::Resolver::InstallerSet.prepend(NamespacedGemShim::InstallerSetSkipUri)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
With this shim installed:
|
|
245
|
+
1. Resolution encounters the URI dep, `find_all` returns `[]`, resolver
|
|
246
|
+
treats it as an optional/unsatisfiable dep (depending on `type`).
|
|
247
|
+
2. All other deps resolve normally.
|
|
248
|
+
3. `namespaced-gem` installs as a leaf dep, hot-loads full patches.
|
|
249
|
+
4. `done_installing` hook processes the URI deps that were skipped.
|
|
250
|
+
|
|
251
|
+
**⚠️ Problem:** Runtime deps are not optional — the resolver will still fail
|
|
252
|
+
if it can't satisfy a required dep. The shim would need to also patch the
|
|
253
|
+
resolver's conflict-handling to treat URI deps as "deferred" rather than
|
|
254
|
+
"missing."
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Verdict
|
|
259
|
+
|
|
260
|
+
| Approach | Single-command install? | Gem author API | Complexity |
|
|
261
|
+
|----------|----------------------|----------------|------------|
|
|
262
|
+
| **A: Metadata only** | ✅ Yes | `metadata` field (non-standard) | Medium |
|
|
263
|
+
| **B: Dual encoding** | ❌ Resolver aborts on URI dep | Both `add_dependency` + `metadata` | High |
|
|
264
|
+
| **C: Shim gem** | ⚠️ Depends on resolver skip | Standard `add_dependency` | Very High |
|
|
265
|
+
| **Pre-install** (status quo) | Requires 2 commands | Standard `add_dependency` | None |
|
|
266
|
+
|
|
267
|
+
### Recommendation
|
|
268
|
+
|
|
269
|
+
**For `gem install` (Use Cases 2–4, plugin pre-installed):** This **already
|
|
270
|
+
works**. `GemResolverPatch` intercepts URI deps during resolution,
|
|
271
|
+
`ApiSpecPatch` synthesizes specs from Compact Index data (bypassing the missing
|
|
272
|
+
Marshal endpoint), and the namespace server serves `/gems/` for downloads.
|
|
273
|
+
|
|
274
|
+
**For `gem install my-gem` (Use Case 1, cold-start):** **Approach A** has been
|
|
275
|
+
implemented as `MetadataDepsHook` (metadata-based deps + `done_installing`
|
|
276
|
+
hook). This enables single-command `gem install my-gem` via the hot-load
|
|
277
|
+
mechanism when gem authors encode URI deps in
|
|
278
|
+
`spec.metadata["namespaced_dependencies"]`.
|
|
279
|
+
|
|
280
|
+
**For Bundler (all use cases):** The current architecture is already correct.
|
|
281
|
+
The `BundlerIntegration` TracePoint-based deferred patching handles URI deps
|
|
282
|
+
in `add_dependency` seamlessly. **No changes needed.**
|
|
283
|
+
|
|
284
|
+
**For `gem install` of direct URI names** (`gem install @kaspth/oaken`):
|
|
285
|
+
This **works today** (plugin must be pre-installed). The namespace server
|
|
286
|
+
serves both the Compact Index and `/gems/` endpoints.
|
|
287
|
+
|
|
288
|
+
### Implementation Status (completed)
|
|
289
|
+
|
|
290
|
+
1. ✅ `lib/namespaced/gem/metadata_deps_hook.rb` — `Gem.done_installing` hook
|
|
291
|
+
that reads `namespaced_dependencies` from installed specs' metadata and
|
|
292
|
+
triggers a second install pass.
|
|
293
|
+
2. ✅ Wired into `rubygems_plugin.rb` alongside existing patches (step 6).
|
|
294
|
+
3. ✅ `Namespaced::Gem.add_namespaced_dependency(spec, uri, version)` helper
|
|
295
|
+
that writes both `add_dependency` and `metadata` automatically.
|
|
296
|
+
4. ✅ `add_dependency` URI support works for both the Bundler and
|
|
297
|
+
`gem install` paths (when plugin is pre-installed).
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## The Hot-Load Mechanism IS Valuable
|
|
302
|
+
|
|
303
|
+
The discovery in HOT_HOOK.md is genuinely important. It confirms that:
|
|
304
|
+
|
|
305
|
+
1. `rubygems_plugin.rb` files hot-load during `gem install` via
|
|
306
|
+
`Gem::Installer#load_plugin` — this is a documented, reliable RubyGems
|
|
307
|
+
feature.
|
|
308
|
+
2. TSort-based topological install order guarantees the plugin gem installs
|
|
309
|
+
before dependents.
|
|
310
|
+
3. Hooks registered by the hot-loaded plugin (`pre_install`, `post_install`,
|
|
311
|
+
`done_installing`) are active for all subsequent gems in the batch.
|
|
312
|
+
4. The hot-load fires only on first install (`find_all_by_name.size == 1`) —
|
|
313
|
+
upgrades don't re-trigger it (the next boot picks up the new version).
|
|
314
|
+
|
|
315
|
+
The key insight is that **hooks fire post-install, not post-resolve**. The
|
|
316
|
+
hot-load enables a **second-phase install** pattern where URI deps are deferred
|
|
317
|
+
past the initial resolution and handled by hooks after the plugin is live.
|
|
318
|
+
This pattern is implemented in `MetadataDepsHook` and enables the cold-start
|
|
319
|
+
`gem install my-gem` path (when the gem author uses
|
|
320
|
+
`spec.metadata["namespaced_dependencies"]`).
|
|
321
|
+
|
|
322
|
+
For the pre-installed plugin case (Use Cases 2–4), the hot-load is not needed —
|
|
323
|
+
the plugin loads at boot and all patches are active for the entire `gem install`
|
|
324
|
+
pipeline, including resolution. **Both `gem install @kaspth/oaken` and
|
|
325
|
+
`gem install my-gem` (with URI deps in the gemspec) work today.**
|