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
data/ISSUE.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# gem.coop namespace servers missing legacy Marshal API endpoints
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
`beta.gem.coop` namespace servers implement only the **Compact Index** API
|
|
6
|
+
(used by Bundler) and the `/gems/` download endpoint, but not the **legacy
|
|
7
|
+
Marshal API** (`quick/Marshal.4.8/`). The
|
|
8
|
+
[namespaced-gem](https://gitlab.com/galtzo-floss/namespaced-gem) plugin works
|
|
9
|
+
around this by synthesizing `Gem::Specification` objects from Compact Index data
|
|
10
|
+
(via `ApiSpecPatch`), so **both `bundle install` and `gem install` now work**.
|
|
11
|
+
|
|
12
|
+
A secondary issue: the production `gem.coop` server returns **HTTP 200 with
|
|
13
|
+
body `"404"`** for namespace endpoints, instead of a proper HTTP 404 status
|
|
14
|
+
code.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Context
|
|
19
|
+
|
|
20
|
+
The `namespaced-gem` RubyGems plugin enables gemspec dependencies to be declared
|
|
21
|
+
as full URIs (e.g. `spec.add_dependency "https://beta.gem.coop/@kaspth/oaken"`).
|
|
22
|
+
It patches both Bundler and RubyGems' native resolver to parse these URIs,
|
|
23
|
+
derive the namespace source URL, and resolve against it.
|
|
24
|
+
|
|
25
|
+
Each namespace (e.g. `https://beta.gem.coop/@kaspth/`) is treated as its own
|
|
26
|
+
**discrete gem server** — completely independent of the root server or any
|
|
27
|
+
other namespace.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What works: Compact Index (Bundler)
|
|
32
|
+
|
|
33
|
+
The Compact Index endpoints are served correctly under namespace paths.
|
|
34
|
+
Bundler uses **only** these two endpoints, so `bundle install` / `bundle lock`
|
|
35
|
+
works today.
|
|
36
|
+
|
|
37
|
+
| Endpoint | URL | Status |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `versions` | `https://beta.gem.coop/@kaspth/versions` | ✅ 200 — returns gem listing |
|
|
40
|
+
| `info/{gem}` | `https://beta.gem.coop/@kaspth/info/oaken` | ✅ 200 — returns per-version dependency data |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## What's missing: Legacy Marshal API (`gem install`)
|
|
45
|
+
|
|
46
|
+
When `gem install` resolves a dependency, it uses RubyGems' native resolver
|
|
47
|
+
(`Gem::DependencyInstaller` → `Gem::Resolver::InstallerSet`). The flow is:
|
|
48
|
+
|
|
49
|
+
1. **Discovery** — `Gem::Source#dependency_resolver_set` probes
|
|
50
|
+
`{source}/versions`. Since that returns 200, it creates an `APISet` backed
|
|
51
|
+
by `{source}/info/`. This step **succeeds** — the gem and its versions are
|
|
52
|
+
found via the Compact Index.
|
|
53
|
+
|
|
54
|
+
2. **Spec fetch** — `InstallerSet#add_always_install` calls
|
|
55
|
+
`APISpecification#spec`, which calls `Gem::Source#fetch_spec`. This method
|
|
56
|
+
constructs the URL:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
{source}/quick/Marshal.4.8/{gem}-{version}.gemspec.rz
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
and expects a **deflated (`Zlib`) Marshal-serialized `Gem::Specification`**
|
|
63
|
+
in response. This endpoint **returns 404**.
|
|
64
|
+
|
|
65
|
+
3. **Gem download** — After resolution, RubyGems downloads the `.gem` file
|
|
66
|
+
from:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
{source}/gems/{gem}-{version}.gem
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This endpoint also **returns 404**.
|
|
73
|
+
|
|
74
|
+
4. **Crash** — The 404 HTML body is passed to `Zlib::Inflate.inflate`, which
|
|
75
|
+
raises `Zlib::DataError: incorrect header check`.
|
|
76
|
+
|
|
77
|
+
### Endpoints that return 404 (all under the namespace path)
|
|
78
|
+
|
|
79
|
+
| Endpoint | URL | Expected response |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `quick/Marshal.4.8/{gem}-{ver}.gemspec.rz` | `https://beta.gem.coop/@kaspth/quick/Marshal.4.8/oaken-2.5.1.gemspec.rz` | Deflated Marshal-serialized `Gem::Specification` |
|
|
82
|
+
| `gems/{gem}-{ver}.gem` | `https://beta.gem.coop/@kaspth/gems/oaken-2.5.1.gem` | The `.gem` file |
|
|
83
|
+
| `specs.4.8.gz` | `https://beta.gem.coop/@kaspth/specs.4.8.gz` | Gzipped Marshal array of all `[name, version, platform]` tuples |
|
|
84
|
+
| `latest_specs.4.8.gz` | `https://beta.gem.coop/@kaspth/latest_specs.4.8.gz` | Gzipped Marshal array of latest `[name, version, platform]` tuples |
|
|
85
|
+
|
|
86
|
+
**All of these must be served under the namespace path** (e.g.
|
|
87
|
+
`https://beta.gem.coop/@kaspth/quick/…`, not `https://beta.gem.coop/quick/…`),
|
|
88
|
+
because each namespace is its own discrete gem server.
|
|
89
|
+
|
|
90
|
+
The minimum required for `gem install` to work are:
|
|
91
|
+
|
|
92
|
+
1. **`quick/Marshal.4.8/{gem}-{ver}.gemspec.rz`** — the gemspec, serialized
|
|
93
|
+
with `Marshal.dump` then compressed with `Zlib::Deflate.deflate`.
|
|
94
|
+
2. **`gems/{gem}-{ver}.gem`** — the `.gem` package file for download.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Reproduction
|
|
99
|
+
|
|
100
|
+
### Direct install
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Requires Ruby >= 3.2, RubyGems >= 4.0.5
|
|
104
|
+
gem install namespaced-gem # install the plugin first
|
|
105
|
+
|
|
106
|
+
gem install @kaspth/oaken # shorthand (defaults to https://beta.gem.coop)
|
|
107
|
+
# => ERROR: Zlib::DataError — incorrect header check
|
|
108
|
+
|
|
109
|
+
gem install https://beta.gem.coop/@kaspth/oaken # full URI
|
|
110
|
+
# => ERROR: Zlib::DataError — incorrect header check
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Transitive dependency (Use Case 1)
|
|
114
|
+
|
|
115
|
+
Even when the plugin is already installed, `gem install` of a gem whose gemspec
|
|
116
|
+
contains URI dependencies also fails:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
gem install namespaced-gem # plugin loaded on next boot
|
|
120
|
+
|
|
121
|
+
gem install my-gem # my-gem.gemspec has:
|
|
122
|
+
# spec.add_dependency "https://beta.gem.coop/@kaspth/oaken", "~> 1.0"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The resolution phase succeeds — the `InstallerSetPatch#find_all` intercept
|
|
126
|
+
correctly remaps the URI dep and queries the compact index. But the
|
|
127
|
+
**installation phase** fails because RubyGems downloads `.gem` files from
|
|
128
|
+
`{source}/gems/{name}-{version}.gem`, which returns 404.
|
|
129
|
+
|
|
130
|
+
Additionally, there is a **chicken-and-egg problem** for first-time installs:
|
|
131
|
+
if `namespaced-gem` is listed as a dependency of `my-gem` (rather than being
|
|
132
|
+
pre-installed), the plugin is not loaded when `gem install my-gem` starts —
|
|
133
|
+
because RubyGems loads `rubygems_plugin.rb` files only from _already installed_
|
|
134
|
+
gems at boot. In this case, RubyGems encounters the URI dependency string
|
|
135
|
+
without the patches in place and tries to look it up as a literal gem name on
|
|
136
|
+
rubygems.org, failing immediately.
|
|
137
|
+
|
|
138
|
+
### Full stack trace (direct install)
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
ERROR: While executing gem ... (Zlib::DataError)
|
|
142
|
+
incorrect header check
|
|
143
|
+
.../rubygems/util.rb:47:in 'Zlib::Inflate.inflate'
|
|
144
|
+
.../rubygems/util.rb:47:in 'Gem::Util.inflate'
|
|
145
|
+
.../rubygems/source.rb:132:in 'Gem::Source#fetch_spec'
|
|
146
|
+
.../rubygems/resolver/api_specification.rb:93:in 'Gem::Resolver::APISpecification#spec'
|
|
147
|
+
.../rubygems/resolver/installer_set.rb:99:in 'Gem::Resolver::InstallerSet#add_always_install'
|
|
148
|
+
.../rubygems/dependency_installer.rb:243:in 'Gem::DependencyInstaller#resolve_dependencies'
|
|
149
|
+
.../rubygems/commands/install_command.rb:198:in 'Gem::Commands::InstallCommand#install_gem'
|
|
150
|
+
...
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Impact: which code paths work today
|
|
156
|
+
|
|
157
|
+
| Scenario | API used | Works? |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `bundle lock` / `bundle install` with URI deps in gemspec | Compact Index only | ✅ |
|
|
160
|
+
| `bundler/inline` with a namespace source block | Compact Index only | ✅ |
|
|
161
|
+
| `gem install @kaspth/oaken` (direct, plugin pre-installed) | Compact Index + gem download | ✅ |
|
|
162
|
+
| `gem install my-gem` (transitive URI deps, plugin pre-installed) | Compact Index + gem download | ✅ |
|
|
163
|
+
| `gem install my-gem` (transitive URI deps, plugin NOT pre-installed) | N/A — plugin not loaded | ❌ |
|
|
164
|
+
|
|
165
|
+
All code paths now work when the `namespaced-gem` plugin is installed, thanks
|
|
166
|
+
to `ApiSpecPatch` (which synthesizes specs from Compact Index data, bypassing
|
|
167
|
+
the missing `quick/Marshal.4.8/` endpoint) and the namespace server's `/gems/`
|
|
168
|
+
download endpoint. The only remaining failure case is the cold-start scenario
|
|
169
|
+
where the plugin is not yet installed — RubyGems loads plugins only from
|
|
170
|
+
already-installed gems at boot.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Separate issue: `gem.coop` (production) returns HTTP 200 with body `"404"`
|
|
175
|
+
|
|
176
|
+
The production server at `gem.coop` (not `beta.gem.coop`) returns **HTTP 200**
|
|
177
|
+
with a plain-text body of `"404"` for namespace endpoints:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
GET https://gem.coop/@kaspth/versions → 200, body: "404"
|
|
181
|
+
GET https://gem.coop/@kaspth/info/oaken → 200, body: "404"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This is problematic because RubyGems interprets an HTTP 200 response as a
|
|
185
|
+
successful Compact Index reply. It creates an `APISet` and attempts to parse
|
|
186
|
+
the string `"404"` as version data, leading to confusing downstream failures.
|
|
187
|
+
|
|
188
|
+
These should return a proper **HTTP 404** status code so that RubyGems raises
|
|
189
|
+
`Gem::RemoteFetcher::FetchError` and falls back gracefully.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Environment
|
|
194
|
+
|
|
195
|
+
- Ruby 4.0.1
|
|
196
|
+
- RubyGems 4.0.5+
|
|
197
|
+
- `namespaced-gem` (development, HEAD)
|
|
198
|
+
- Tested: 2026-03-07
|
data/README.md
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# 🔌 namespaced-gem
|
|
2
|
+
|
|
3
|
+
A RubyGems plugin that enables gemspec dependencies to be declared as
|
|
4
|
+
full URIs, pointing to **namespaced gem sources** such as
|
|
5
|
+
[gem.coop namespaces](https://gem.coop/updates/5/).
|
|
6
|
+
|
|
7
|
+
This implements the ideas discussed in
|
|
8
|
+
[gem-coop/gem.coop#12](https://github.com/gem-coop/gem.coop/issues/12).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## The Problem
|
|
13
|
+
|
|
14
|
+
gem.coop's public beta introduced _namespaces_ — isolated gem registries per
|
|
15
|
+
user or organization:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
https://beta.gem.coop/@myspace # namespace index
|
|
19
|
+
https://beta.gem.coop/@myspace/my-gem # canonical gem URI
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Today, gemspecs declare dependencies as plain names:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
spec.add_dependency "rack", "~> 3.0"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
There is no standard way to express _which gem server_ (and _which namespace_)
|
|
29
|
+
a dependency comes from inside the gemspec itself. The user must manually add
|
|
30
|
+
a `source` block to their Gemfile — which defeats the purpose of publishing a
|
|
31
|
+
self-describing gemspec.
|
|
32
|
+
|
|
33
|
+
The question this prototype asks: **can a gemspec declare its own source for a
|
|
34
|
+
dependency, using the dependency name string alone?**
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
spec.add_dependency "https://beta.gem.coop/@myspace/my-gem", "~> 1.0"
|
|
38
|
+
|
|
39
|
+
# or using a Package URL (purl-spec):
|
|
40
|
+
spec.add_dependency "pkg:gem/@myspace/my-gem?repository_url=https://beta.gem.coop", "~> 1.0"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Key Finding: RubyGems 4.0.5+ Happens to Allow URI Names
|
|
46
|
+
|
|
47
|
+
RubyGems 4.0.5 removed the old `Gem::Dependency::VALID_NAME_PATTERN`
|
|
48
|
+
restriction entirely — any String is now accepted as a dependency name:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
dep = Gem::Dependency.new("https://beta.gem.coop/@ns/foo", "~> 1.0")
|
|
52
|
+
dep.name # => "https://beta.gem.coop/@ns/foo"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
However, **RubyGems has no idea what to do with a URI-named dependency.** It
|
|
56
|
+
will happily store the string, but `gem install` will try to look up that
|
|
57
|
+
literal name on rubygems.org — and fail. Neither RubyGems nor Bundler knows
|
|
58
|
+
how to extract the real gem name, derive the namespace source URL, or resolve
|
|
59
|
+
transitive dependencies that use URI names.
|
|
60
|
+
|
|
61
|
+
**This gem bridges that gap.** It teaches both RubyGems' resolver (`gem
|
|
62
|
+
install`) and Bundler's resolver (`bundle install`) how to parse URI dependency
|
|
63
|
+
names, route them to the correct namespace source, and remap transitive deps
|
|
64
|
+
on the fly.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## How It Works
|
|
69
|
+
|
|
70
|
+
This gem ships a `rubygems_plugin.rb` that is automatically loaded by RubyGems
|
|
71
|
+
at boot — before any gemspec is parsed.
|
|
72
|
+
|
|
73
|
+
### 1. `Gem::Dependency` patch (`DependencyPatch`)
|
|
74
|
+
|
|
75
|
+
Prepends helper methods `#uri_gem?` and `#uri_dependency` onto
|
|
76
|
+
`Gem::Dependency`.
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
dep = Gem::Dependency.new("https://beta.gem.coop/@myspace/my-gem", "~> 1.0")
|
|
80
|
+
dep.uri_gem? # => true
|
|
81
|
+
dep.uri_dependency # => #<Namespaced::Gem::UriDependency gem_name="my-gem" source_url="https://beta.gem.coop/@myspace">
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. URI parser (`UriDependency`)
|
|
85
|
+
|
|
86
|
+
Parses a URI dependency name into its components:
|
|
87
|
+
|
|
88
|
+
| Part | Example |
|
|
89
|
+
|---------------|------------------------------------|
|
|
90
|
+
| `server_base` | `https://beta.gem.coop` |
|
|
91
|
+
| `namespace` | `@myspace` |
|
|
92
|
+
| `gem_name` | `my-gem` |
|
|
93
|
+
| `source_url` | `https://beta.gem.coop/@myspace` |
|
|
94
|
+
|
|
95
|
+
Supports three forms:
|
|
96
|
+
- **Full URI**: `https://beta.gem.coop/@myspace/my-gem`
|
|
97
|
+
- **Shorthand**: `@myspace/my-gem` (defaults to `https://beta.gem.coop`)
|
|
98
|
+
- **Package URL** ([purl-spec](https://github.com/package-url/purl-spec)):
|
|
99
|
+
- `pkg:gem/@myspace/my-gem` (namespace in path, default server)
|
|
100
|
+
- `pkg:gem/@myspace/my-gem?repository_url=https://beta.gem.coop` (explicit server)
|
|
101
|
+
- `pkg:gem/my-gem?repository_url=https://beta.gem.coop/@myspace` (namespace in qualifier)
|
|
102
|
+
|
|
103
|
+
All three forms resolve to the same internal representation and are
|
|
104
|
+
interchangeable anywhere a dependency name is accepted.
|
|
105
|
+
|
|
106
|
+
### 3. Bundler DSL integration (`BundlerIntegration`)
|
|
107
|
+
|
|
108
|
+
Patches `Bundler::Dsl#gemspec` so that after standard gemspec processing, any
|
|
109
|
+
URI-named runtime dependencies automatically inject a `source` block:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# What the user writes in their Gemfile:
|
|
113
|
+
gemspec
|
|
114
|
+
|
|
115
|
+
# What this patch injects automatically for URI deps:
|
|
116
|
+
# source "https://beta.gem.coop/@myspace" do
|
|
117
|
+
# gem "my-gem", "~> 1.0"
|
|
118
|
+
# end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This means the Gemfile needs **no manual source declarations** for URI deps
|
|
122
|
+
found in the gemspec.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
There are four ways to use `namespaced-gem` today, depending on your situation.
|
|
129
|
+
|
|
130
|
+
### Use Case 1: Gem authors (primary)
|
|
131
|
+
|
|
132
|
+
Add `namespaced-gem` as a runtime dependency of your gem. It is published on
|
|
133
|
+
rubygems.org and acts as a bridge to gem.coop namespaces.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
Gem::Specification.new do |spec|
|
|
137
|
+
spec.name = "my-gem"
|
|
138
|
+
spec.version = "1.0.0"
|
|
139
|
+
|
|
140
|
+
# This gem must be a runtime dependency so that its rubygems_plugin.rb
|
|
141
|
+
# is installed and loaded by RubyGems at boot — before any gemspec
|
|
142
|
+
# containing URI dependencies is evaluated.
|
|
143
|
+
spec.add_dependency "namespaced-gem"
|
|
144
|
+
|
|
145
|
+
# Traditional dependency from RubyGems.org:
|
|
146
|
+
spec.add_dependency "rack", "~> 3.0"
|
|
147
|
+
|
|
148
|
+
# Namespaced dependency from gem.coop (full URI):
|
|
149
|
+
spec.add_dependency "https://beta.gem.coop/@myspace/special-gem", "~> 0.5"
|
|
150
|
+
|
|
151
|
+
# Shorthand (defaults to beta.gem.coop):
|
|
152
|
+
spec.add_dependency "@myorg/internal-tool", ">= 2.0"
|
|
153
|
+
|
|
154
|
+
# Package URL (purl-spec):
|
|
155
|
+
spec.add_dependency "pkg:gem/@myorg/another-tool", ">= 1.0"
|
|
156
|
+
spec.add_dependency "pkg:gem/@myorg/extra?repository_url=https://beta.gem.coop", "~> 3.0"
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
When a downstream user uses **Bundler** (the expected path), their Gemfile can
|
|
161
|
+
remain:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
source "https://rubygems.org"
|
|
165
|
+
gemspec
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The Bundler integration automatically injects the correct `source` blocks for
|
|
169
|
+
any URI dependencies found in the gemspec. Bundler uses only the Compact Index
|
|
170
|
+
API, which gem.coop namespace servers already support.
|
|
171
|
+
|
|
172
|
+
Both `bundle install` and `gem install my-gem` work — see
|
|
173
|
+
[Use Case 4](#use-case-4-direct-gem-install-with-a-namespace) for the
|
|
174
|
+
`gem install` path.
|
|
175
|
+
|
|
176
|
+
### Use Case 2: Application developers
|
|
177
|
+
|
|
178
|
+
If you are not publishing a gem but want to use URI-style dependencies in an
|
|
179
|
+
application, install `namespaced-gem` directly:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
gem install namespaced-gem
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Because `rubygems_plugin.rb` files are only loaded from **installed** gems, the
|
|
186
|
+
gem must be present in the gem path _before_ RubyGems evaluates any gemspec
|
|
187
|
+
that contains URI dependencies. In practice this means:
|
|
188
|
+
|
|
189
|
+
1. Install the gem first: `gem install namespaced-gem`
|
|
190
|
+
2. Then declare URI dependencies in your gemspec or Gemfile as usual.
|
|
191
|
+
|
|
192
|
+
> **Note:** Simply listing `gem "namespaced-gem"` in a Gemfile is _not
|
|
193
|
+
> sufficient_ on its own — Bundler evaluates the Gemfile (and its `gemspec`
|
|
194
|
+
> directive) before it installs gems, so the plugin would not yet be loaded.
|
|
195
|
+
> The gem must already be installed via `gem install` (or as a transitive
|
|
196
|
+
> dependency of another installed gem, as in Use Case 1).
|
|
197
|
+
|
|
198
|
+
### Use Case 3: Global installation (enable namespace support Ruby-wide)
|
|
199
|
+
|
|
200
|
+
Install `namespaced-gem` once into your Ruby environment and every subsequent
|
|
201
|
+
`gem install` and `bundle install` in that Ruby will be able to resolve
|
|
202
|
+
URI-named dependencies — no per-project configuration needed.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
gem install namespaced-gem
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
That's it. The `rubygems_plugin.rb` is now in the gem path and will be loaded
|
|
209
|
+
by RubyGems on every boot. From this point forward:
|
|
210
|
+
|
|
211
|
+
- `gem install some-gem` will automatically resolve any URI-named transitive
|
|
212
|
+
dependencies found in `some-gem`'s gemspec.
|
|
213
|
+
- `bundle install` in any project will automatically inject the correct
|
|
214
|
+
`source` blocks for URI dependencies found in gemspecs.
|
|
215
|
+
|
|
216
|
+
This is useful for CI images, Docker containers, or development machines where
|
|
217
|
+
you want namespace support available globally without requiring each gem or
|
|
218
|
+
project to explicitly depend on `namespaced-gem`.
|
|
219
|
+
|
|
220
|
+
```dockerfile
|
|
221
|
+
# Example: Dockerfile
|
|
222
|
+
RUN gem install namespaced-gem
|
|
223
|
+
# All subsequent gem/bundle commands in this image now support URI deps.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Example: CI setup step
|
|
228
|
+
gem install namespaced-gem
|
|
229
|
+
bundle install # URI deps in any gemspec are resolved automatically
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Use Case 4: Direct `gem install` with a namespace
|
|
233
|
+
|
|
234
|
+
Once `namespaced-gem` is installed, you can install namespaced gems directly:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
gem install namespaced-gem # one-time setup (if not already installed)
|
|
238
|
+
|
|
239
|
+
gem install @kaspth/oaken # shorthand (defaults to beta.gem.coop)
|
|
240
|
+
gem install https://beta.gem.coop/@kaspth/oaken # full URI
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
This works because the plugin patches multiple layers of RubyGems' native
|
|
244
|
+
`gem install` pipeline:
|
|
245
|
+
|
|
246
|
+
1. **`GemResolverPatch`** intercepts the resolver and routes URI-named
|
|
247
|
+
dependencies to the correct namespace source via the Compact Index
|
|
248
|
+
(`versions` / `info/` endpoints).
|
|
249
|
+
2. **`ApiSpecPatch`** synthesizes a `Gem::Specification` from the Compact Index
|
|
250
|
+
data already fetched — bypassing the legacy `quick/Marshal.4.8/` endpoint
|
|
251
|
+
that namespace servers don't serve.
|
|
252
|
+
3. **`DownloadPatch`** provides clear, actionable error messages if the
|
|
253
|
+
namespace server fails to serve the `.gem` file.
|
|
254
|
+
|
|
255
|
+
All three forms of URI dependency names are supported:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
gem install @kaspth/oaken
|
|
259
|
+
gem install https://beta.gem.coop/@kaspth/oaken
|
|
260
|
+
gem install "pkg:gem/@kaspth/oaken"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Architecture
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
lib/
|
|
269
|
+
rubygems_plugin.rb # Loaded by RubyGems at boot (or hot-loaded during install)
|
|
270
|
+
namespaced/
|
|
271
|
+
gem.rb # Main module
|
|
272
|
+
gem/
|
|
273
|
+
version.rb
|
|
274
|
+
uri_dependency.rb # URI parser (value object)
|
|
275
|
+
namespace_source_registry.rb # Thread-safe registry of namespace source URLs
|
|
276
|
+
dependency_patch.rb # Gem::Dependency patch (helper methods)
|
|
277
|
+
api_spec_patch.rb # Gem::Resolver::APISpecification — Compact Index spec synthesis
|
|
278
|
+
download_patch.rb # Gem::Source#download — namespace download error handling
|
|
279
|
+
bundler_integration.rb # Bundler::Dsl#gemspec patch
|
|
280
|
+
bundler_resolver_patch.rb # Bundler::Definition / Resolver transitive dep handling
|
|
281
|
+
gem_resolver_patch.rb # Gem::RequestSet / InstallerSet for `gem install`
|
|
282
|
+
metadata_deps_hook.rb # Gem.done_installing hook for hot-load deferred deps
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Known Limitations
|
|
288
|
+
|
|
289
|
+
1. **Plugin must be installed before first use (application developers only).**
|
|
290
|
+
This gem works as a RubyGems plugin (`rubygems_plugin.rb`), which means it
|
|
291
|
+
must be _installed_ in the gem path so that RubyGems loads the plugin at
|
|
292
|
+
boot before any gemspec containing URI dependencies is evaluated. For gem
|
|
293
|
+
authors (Use Case 1), this happens automatically — when a user installs your
|
|
294
|
+
gem, `namespaced-gem` is installed as a transitive dependency and available
|
|
295
|
+
on the next boot. For global installations (Use Case 3), the plugin is
|
|
296
|
+
already in the gem path by definition. For application developers
|
|
297
|
+
(Use Case 2), the gem must be installed explicitly with
|
|
298
|
+
`gem install namespaced-gem` before running `bundle install`, because
|
|
299
|
+
Bundler evaluates the Gemfile before it installs gems. In Ruby 4.0+,
|
|
300
|
+
RubyGems auto-loads `bundler/setup` when it detects a Gemfile in the working
|
|
301
|
+
directory, and this happens _before_ `RUBYOPT` `-r` flags are processed —
|
|
302
|
+
so the plugin must already be in the gem path.
|
|
303
|
+
|
|
304
|
+
2. **Gemspec linting:** Tools that validate gemspecs (e.g. `gem build`, `rake
|
|
305
|
+
release`) work fine because `SpecificationPolicy#validate_name` only
|
|
306
|
+
validates the gem's *own* name — it does not check dependency names.
|
|
307
|
+
|
|
308
|
+
3. **`gem.coop` (production) returns HTTP 200 with body `"404"` for namespace
|
|
309
|
+
endpoints.** Namespace resolution only works against `beta.gem.coop`
|
|
310
|
+
currently. The production `gem.coop` server returns HTTP 200 with a
|
|
311
|
+
plain-text body of `"404"` for namespace paths, which RubyGems
|
|
312
|
+
misinterprets as valid Compact Index data. The shorthand default server is
|
|
313
|
+
`beta.gem.coop` for this reason. See [ISSUE.md](ISSUE.md) for details.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Version Constraints
|
|
318
|
+
|
|
319
|
+
Version requirements work exactly as they always have — they are the second
|
|
320
|
+
argument to `add_dependency`, completely separate from the name:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
spec.add_dependency "https://beta.gem.coop/@myspace/my-gem", "~> 1.0"
|
|
324
|
+
# ^^^^^^^^^^ URI name ^^^^^^^^^^^^^^^^^^ ^^^^^^^^
|
|
325
|
+
# version
|
|
326
|
+
|
|
327
|
+
spec.add_dependency "pkg:gem/@myspace/my-gem", "~> 1.0"
|
|
328
|
+
# ^^^^^^^ purl name ^^^^^^ ^^^^^^^^
|
|
329
|
+
# version
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
All standard operators (`~>`, `>=`, `=`, etc.) are supported unchanged.
|
|
333
|
+
|
|
334
|
+
> **Note:** The purl spec allows a `@version` component in the name itself
|
|
335
|
+
> (e.g. `pkg:gem/@ns/foo@2.0`). If present it is **ignored** — version
|
|
336
|
+
> constraints always come from the second argument to `add_dependency`.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Development
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
bundle install
|
|
344
|
+
bundle exec rspec # unit + offline integration tests
|
|
345
|
+
bundle exec rspec --tag network # network integration tests (hits beta.gem.coop)
|
|
346
|
+
bundle exec rake # tests + rubocop
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The network integration tests resolve a real gem (`@kaspth/oaken`) from
|
|
350
|
+
`beta.gem.coop` and verify `bundle lock` produces a correct `Gemfile.lock`.
|
|
351
|
+
They are excluded from the default `rspec` run and must be opted into with
|
|
352
|
+
`--tag network`.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Contributing
|
|
357
|
+
|
|
358
|
+
Bug reports and pull requests are welcome on GitLab at
|
|
359
|
+
<https://gitlab.com/galtzo-floss/namespaced-gem>.
|
|
360
|
+
|
|
361
|
+
This project is intended to be a safe, welcoming space for collaboration, and
|
|
362
|
+
contributors are expected to adhere to the
|
|
363
|
+
[code of conduct](https://gitlab.com/galtzo-floss/namespaced-gem/-/blob/main/CODE_OF_CONDUCT.md).
|
|
364
|
+
|
|
365
|
+
## Code of Conduct
|
|
366
|
+
|
|
367
|
+
Everyone interacting in the namespaced-gem project's codebases, issue trackers,
|
|
368
|
+
chat rooms and mailing lists is expected to follow the
|
|
369
|
+
[code of conduct](https://gitlab.com/galtzo-floss/namespaced-gem/-/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/WORKAROUND.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
**STATUS: RESOLVED — Approach B was implemented.** `ApiSpecPatch` synthesizes specs from Compact Index data (bypassing the Marshal endpoint), `DownloadPatch` handles namespace download errors, and `beta.gem.coop` serves `/gems/` under namespace paths. Both `gem install @kaspth/oaken` and `bundle install` with URI deps now work.
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Plan: Two Approaches for gem install @kaspth/oaken with Compact Index-Only Servers
|
|
6
|
+
The namespaced-gem plugin already handles Bundler workflows, but gem install fails because RubyGems requires two legacy Marshal API endpoints (quick/Marshal.4.8/… and gems/…) that gem.coop namespace servers don't serve. Two approaches can bridge this gap: (A) delegate to Bundler's infrastructure, or (B) patch RubyGems' native classes to use the Compact Index.
|
|
7
|
+
|
|
8
|
+
CRITICAL INVARIANT — Namespaces Are Not Optional
|
|
9
|
+
A namespaced gem (e.g. @kaspth/oaken) and a non-namespaced gem with the same base name (e.g. oaken on rubygems.org) are COMPLETELY DIFFERENT, UNRELATED gems. The namespace is an integral part of the gem's identity. It MUST NEVER be stripped, ignored, or used to fall back to a non-namespaced gem. Each namespace server (e.g. https://beta.gem.coop/@kaspth/) is a discrete, independent gem source — exactly like a private gem server that happens to have a gem with a common name. Every code path in this plugin — resolution, spec fetching, downloading — must preserve the namespace and source URL end-to-end. If an operation fails against the namespace source, it must ERROR, not silently retry against the root server or rubygems.org.
|
|
10
|
+
|
|
11
|
+
Approach A: Delegate to Bundler from within the plugin
|
|
12
|
+
Concept: When gem install @kaspth/oaken is detected, intercept early in the gem install pipeline and hand off resolution + installation to Bundler, which already works with the Compact Index-only server. The Bundler-based installer MUST use the full namespace source URL (e.g. https://beta.gem.coop/@kaspth/) as the sole source — never the root server or rubygems.org.
|
|
13
|
+
Steps
|
|
14
|
+
Intercept in Gem::Commands::InstallCommand#install_gem — prepend a method in a new install_command_patch.rb that detects URI-named gem arguments (via UriDependency.uri?). For URI deps, bypass DependencyInstaller entirely and call a Bundler-based installer instead.
|
|
15
|
+
Create a Namespaced::Gem::BundlerGemInstaller helper class — this class constructs an in-memory Bundler definition (or a temp Gemfile string) with the right source block + gem declaration, invokes Bundler::Installer.install (or uses Bundler::Inline internals via Bundler::CLI::Install or gemfile() from bundler/inline), and installs the gem into the same Gem.dir RubyGems would use. The source block MUST be scoped to the namespace URL only — e.g. source("https://beta.gem.coop/@kaspth/") { gem "oaken" }.
|
|
16
|
+
Configure Bundler to install into the system gem path — set Bundler.settings[:path] to Gem.dir and Bundler.settings[:system] to true (or use BUNDLE_PATH/BUNDLE_SYSTEM_GEMS) so gems land where gem install users expect them — not in a vendor/bundle directory.
|
|
17
|
+
Map Bundler output back to RubyGems' InstallCommand expectations — after Bundler installs, report the installed spec(s) back to the install command's output format (gem name, version, summary line), so the user sees the same CLI output they'd expect from gem install.
|
|
18
|
+
Wire the patch in rubygems_plugin.rb — load and apply InstallCommandPatch alongside the existing patches.
|
|
19
|
+
Risks / Trade-offs
|
|
20
|
+
Heavy dependency on Bundler internals — Bundler's Definition, Installer, and settings APIs are not designed for one-shot single-gem installs; edge cases around lockfile generation, Bundler.root, and Bundler.reset! state will need careful handling.
|
|
21
|
+
bundler/inline is the easiest entry point but it calls Bundler.reset! and exit on failure, so using it in-process requires careful error handling or subprocess isolation.
|
|
22
|
+
Gem path mismatch — Bundler defaults to project-local paths; mapping back to Gem.dir needs explicit config. Version conflicts with already-installed gems must be considered.
|
|
23
|
+
Transitive deps — Bundler would resolve the full transitive tree automatically; this is a pro, but may surprise users who expect gem install to only install the named gem + immediate runtime deps.
|
|
24
|
+
|
|
25
|
+
Approach B: Patch RubyGems' native classes to use the Compact Index
|
|
26
|
+
Concept: Instead of delegating to Bundler, patch the two failing points in RubyGems' own pipeline — fetch_spec (which needs Marshal data) and gem download (which needs the correct URL) — to use the already-working Compact Index data and downloads served by the namespace server.
|
|
27
|
+
Steps
|
|
28
|
+
Patch Gem::Resolver::APISpecification#spec — in a new api_spec_patch.rb, prepend #spec so that for namespace sources (detected via UriDependency), instead of calling source.fetch_spec(tuple) (which hits the missing Marshal endpoint), construct a Gem::Specification from the Compact Index info/ data. The info/{gem} endpoint returns dependency names, version constraints, and platform data — enough to build a minimal Gem::Specification with name, version, platform, and dependencies populated. This is sufficient for the resolver.
|
|
29
|
+
Parse the Compact Index info/ response — create a Namespaced::Gem::CompactIndexClient utility that fetches {source_url}/info/{gem_name}, parses the Compact Index format (line-per-version: version deps_list|checksum,ruby:required_ruby,rubygems:required_rubygems), and returns structured data. Bundler already has Bundler::Fetcher::CompactIndex — consider extracting its parser or using the compact_index gem directly.
|
|
30
|
+
Patch the gem download URL — prepend Gem::RemoteFetcher#download (or Gem::Source#download) in a new download_patch.rb so that for namespace sources, the .gem download is attempted from {namespace_source}/gems/{gem}-{ver}.gem. The download MUST use the namespace source URL — never the root server URL. If the namespace server returns 404 for /gems/, that is a server bug to be reported (per ISSUE.md), NOT a reason to silently fall back to the root server. Stripping the namespace and downloading from the root would risk fetching a COMPLETELY DIFFERENT gem that happens to share the same base name.
|
|
31
|
+
Track namespace source metadata through the resolver — the existing InstallerSetPatch#find_all creates a Gem::Source for the namespace URL. The APISpecification objects returned carry a reference to that source. Ensure the download patch can identify these specs (e.g., by checking spec.source.uri against known namespace URLs stored in a registry) to know when to route the download through the namespace source.
|
|
32
|
+
Wire both patches in rubygems_plugin.rb — load ApiSpecPatch and DownloadPatch alongside existing patches, applied at boot.
|
|
33
|
+
Risks / Trade-offs
|
|
34
|
+
Compact Index info/ data is incomplete vs. a full Gem::Specification — fields like summary, description, authors, executables, extensions, metadata, post_install_message are NOT in the Compact Index. The synthesized spec is sufficient for resolution but not for installation (e.g., Gem::Installer needs extensions to compile C extensions). However, the actual .gem file contains a full embedded gemspec, so once downloaded the real spec takes over.
|
|
35
|
+
Namespace server MUST serve /gems/ under the namespace path — the namespace server is a discrete gem server and must serve all endpoints (including /gems/) under its namespace path. If it does not, that is a server-side bug. The plugin should surface a clear error (e.g., "Namespace server at {url} returned 404 for {gem}.gem — the server must serve /gems/ under the namespace path") rather than silently falling back to the root server.
|
|
36
|
+
Tighter coupling to RubyGems internals — Gem::Resolver::APISpecification#spec and Gem::Source#fetch_spec are internal APIs that may change across RubyGems versions. required_rubygems_version already gates to >= 4.0.5.
|
|
37
|
+
Simpler, lighter-weight — no Bundler dependency at runtime for gem install, no temp files, no state management.
|
|
38
|
+
|
|
39
|
+
Further Considerations
|
|
40
|
+
Hybrid approach? Approach B for spec synthesis is simpler for the happy path, while Approach A could serve as an alternative implementation strategy if the Compact Index data proves insufficient (e.g., gems with native extensions where the synthesized spec is too minimal). Consider implementing B first, with A as an alternative. In either case, a namespaced gem is ALWAYS resolved and downloaded from its namespace source — never from the root server or any other source.
|
|
41
|
+
Server-side fix is complementary — the ISSUE.md already requests gem.coop serve the Marshal endpoints AND the /gems/ endpoint under the namespace path. If/when the server adds the Marshal endpoints, the native RubyGems code path would work without any plugin patches for spec fetching. The patches should detect when the native path succeeds (i.e., the Marshal endpoint is available under the namespace) and defer to it. This is NOT a "fallback" to a different source — it is the same namespace source supporting more endpoint types. The namespace is always preserved.
|
|
42
|
+
NO FALLBACK TO ROOT OR NON-NAMESPACED SOURCES — To reiterate: if spec fetching or gem download fails against the namespace source, the correct behavior is to raise an error, not to retry against the root server or rubygems.org. The namespace identifies a completely different gem. Stripping it would be like resolving "acme-corp-utils" and silently downloading "utils" instead — a security and correctness violation.
|
|
43
|
+
The compact_index gem (used internally by Bundler) provides a battle-tested parser for the info/ format — should we add it as a runtime dependency, vendor a minimal parser, or reuse Bundler's CompactIndex::Client directly?
|