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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "uri_dependency"
|
|
4
|
+
|
|
5
|
+
module Namespaced
|
|
6
|
+
module Gem
|
|
7
|
+
# Implements a `Gem.done_installing` hook that processes deferred
|
|
8
|
+
# namespace dependencies stored in a gem's metadata.
|
|
9
|
+
#
|
|
10
|
+
# ## Why This Exists
|
|
11
|
+
#
|
|
12
|
+
# When `gem install my-gem` runs and `namespaced-gem` is listed as a
|
|
13
|
+
# runtime dependency of `my-gem`, the following sequence occurs:
|
|
14
|
+
#
|
|
15
|
+
# 1. RubyGems resolves ALL dependencies before installing ANY.
|
|
16
|
+
# 2. URI-style deps (e.g. "https://beta.gem.coop/@ns/foo") can't be
|
|
17
|
+
# resolved on rubygems.org — they'd cause resolution to fail.
|
|
18
|
+
# 3. After resolution, gems install in topological order (leaves first).
|
|
19
|
+
# 4. `namespaced-gem` installs first → `load_plugin` fires →
|
|
20
|
+
# `rubygems_plugin.rb` is hot-loaded → this hook registers.
|
|
21
|
+
# 5. After ALL gems in the batch finish installing, `done_installing`
|
|
22
|
+
# fires and this hook processes the deferred namespace deps.
|
|
23
|
+
#
|
|
24
|
+
# ## Gemspec Convention
|
|
25
|
+
#
|
|
26
|
+
# Gem authors encode namespace dependencies in metadata:
|
|
27
|
+
#
|
|
28
|
+
# spec.metadata["namespaced_dependencies"] = <<~DEPS
|
|
29
|
+
# https://beta.gem.coop/@myspace/foo ~> 1.0
|
|
30
|
+
# @myorg/bar >= 2.0
|
|
31
|
+
# pkg:gem/@myorg/baz ~> 3.0
|
|
32
|
+
# DEPS
|
|
33
|
+
#
|
|
34
|
+
# Each line is: `<uri-name> <version-constraint>...`
|
|
35
|
+
#
|
|
36
|
+
# For Bundler workflows, the same deps should ALSO be declared via
|
|
37
|
+
# `add_dependency` (Bundler handles URI deps natively via
|
|
38
|
+
# BundlerIntegration). The metadata field is only needed for the
|
|
39
|
+
# `gem install` code path.
|
|
40
|
+
#
|
|
41
|
+
# ## Helper for Gem Authors
|
|
42
|
+
#
|
|
43
|
+
# Use `Namespaced::Gem.add_namespaced_dependency` in your gemspec to
|
|
44
|
+
# automatically write both `add_dependency` and the metadata field:
|
|
45
|
+
#
|
|
46
|
+
# Namespaced::Gem.add_namespaced_dependency(spec, "https://beta.gem.coop/@ns/foo", "~> 1.0")
|
|
47
|
+
#
|
|
48
|
+
module MetadataDepsHook
|
|
49
|
+
METADATA_KEY = "namespaced_dependencies"
|
|
50
|
+
|
|
51
|
+
# Register the done_installing hook. Called from rubygems_plugin.rb.
|
|
52
|
+
def self.register!
|
|
53
|
+
return if @registered
|
|
54
|
+
|
|
55
|
+
::Gem.done_installing do |_dep_installer, specs|
|
|
56
|
+
MetadataDepsHook.process_batch(specs)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@registered = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Process a batch of just-installed gem specs, looking for deferred
|
|
63
|
+
# namespace dependencies in their metadata.
|
|
64
|
+
def self.process_batch(specs)
|
|
65
|
+
specs.each do |spec|
|
|
66
|
+
process_spec(spec)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Process a single spec's metadata for namespace dependencies.
|
|
71
|
+
def self.process_spec(spec)
|
|
72
|
+
raw = spec.metadata[METADATA_KEY]
|
|
73
|
+
return unless raw.is_a?(String) && !raw.strip.empty?
|
|
74
|
+
|
|
75
|
+
deps = parse_metadata_deps(raw)
|
|
76
|
+
return if deps.empty?
|
|
77
|
+
|
|
78
|
+
install_namespace_deps(deps, spec)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Parse the metadata string into an array of [uri_name, version_constraints].
|
|
82
|
+
#
|
|
83
|
+
# Format: one dep per line, `<uri-name> <version constraints...>`
|
|
84
|
+
# https://beta.gem.coop/@myspace/foo ~> 1.0
|
|
85
|
+
# @myorg/bar >= 2.0, < 3.0
|
|
86
|
+
# pkg:gem/@myorg/baz ~> 3.0
|
|
87
|
+
def self.parse_metadata_deps(raw)
|
|
88
|
+
raw.strip.lines.filter_map do |line|
|
|
89
|
+
line = line.strip
|
|
90
|
+
next if line.empty? || line.start_with?("#")
|
|
91
|
+
|
|
92
|
+
# Split into URI name and version constraints.
|
|
93
|
+
# The URI name is the first whitespace-delimited token.
|
|
94
|
+
parts = line.split(/\s+/, 2)
|
|
95
|
+
uri_name = parts[0]
|
|
96
|
+
version_str = parts[1]
|
|
97
|
+
|
|
98
|
+
next unless UriDependency.uri?(uri_name)
|
|
99
|
+
|
|
100
|
+
version_reqs = if version_str && !version_str.strip.empty?
|
|
101
|
+
# Split on comma for multiple constraints: "~> 1.0, < 2.0"
|
|
102
|
+
version_str.split(",").map(&:strip).reject(&:empty?)
|
|
103
|
+
else
|
|
104
|
+
[">= 0"]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
[uri_name, version_reqs]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Install namespace dependencies using the now-active patches.
|
|
112
|
+
#
|
|
113
|
+
# At this point, rubygems_plugin.rb has been hot-loaded, so:
|
|
114
|
+
# - DependencyPatch is active (uri_gem?, uri_dependency)
|
|
115
|
+
# - GemResolverPatch is active (RequestSet/InstallerSet handle URI deps)
|
|
116
|
+
# - ApiSpecPatch is active (synthesizes specs from Compact Index)
|
|
117
|
+
# - DownloadPatch is active (clear namespace download errors)
|
|
118
|
+
def self.install_namespace_deps(deps, parent_spec)
|
|
119
|
+
deps.each do |uri_name, version_reqs|
|
|
120
|
+
uri_dep = UriDependency.parse(uri_name)
|
|
121
|
+
|
|
122
|
+
warn "[namespaced-gem] Installing deferred namespace dependency: " \
|
|
123
|
+
"#{uri_dep.gem_name} (#{version_reqs.join(", ")}) " \
|
|
124
|
+
"from #{uri_dep.source_url} " \
|
|
125
|
+
"(required by #{parent_spec.name})"
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
installer = ::Gem::DependencyInstaller.new(
|
|
129
|
+
domain: :both,
|
|
130
|
+
force: false,
|
|
131
|
+
# Let the user's existing gem path config apply
|
|
132
|
+
)
|
|
133
|
+
# The GemResolverPatch will intercept this URI dep during
|
|
134
|
+
# resolution and route it to the correct namespace source.
|
|
135
|
+
installer.install(uri_name, ::Gem::Requirement.new(*version_reqs))
|
|
136
|
+
rescue ::Gem::UnsatisfiableDependencyError, Namespaced::Gem::Error => e
|
|
137
|
+
warn "[namespaced-gem] WARNING: Failed to install namespace dependency " \
|
|
138
|
+
"#{uri_dep.gem_name} from #{uri_dep.source_url}: #{e.message}"
|
|
139
|
+
warn "[namespaced-gem] This may be a server-side issue. See ISSUE.md."
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Convenience method for gem authors to declare a namespaced dependency
|
|
146
|
+
# in both `add_dependency` (for Bundler) and `metadata` (for gem install).
|
|
147
|
+
#
|
|
148
|
+
# Usage in gemspec:
|
|
149
|
+
# require "namespaced/gem"
|
|
150
|
+
# Namespaced::Gem.add_namespaced_dependency(spec, "https://beta.gem.coop/@ns/foo", "~> 1.0")
|
|
151
|
+
#
|
|
152
|
+
def self.add_namespaced_dependency(spec, uri_name, *version_constraints)
|
|
153
|
+
# Standard add_dependency — works with Bundler via BundlerIntegration
|
|
154
|
+
spec.add_dependency(uri_name, *version_constraints)
|
|
155
|
+
|
|
156
|
+
# Also store in metadata for the gem install hot-load path
|
|
157
|
+
version_str = version_constraints.join(", ")
|
|
158
|
+
entry = "#{uri_name} #{version_str}"
|
|
159
|
+
|
|
160
|
+
existing = spec.metadata[MetadataDepsHook::METADATA_KEY]
|
|
161
|
+
spec.metadata[MetadataDepsHook::METADATA_KEY] = if existing && !existing.strip.empty?
|
|
162
|
+
"#{existing.strip}\n#{entry}"
|
|
163
|
+
else
|
|
164
|
+
entry
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Namespaced
|
|
4
|
+
module Gem
|
|
5
|
+
# Thread-safe registry of namespace source URLs.
|
|
6
|
+
#
|
|
7
|
+
# When the GemResolverPatch or other code creates a Gem::Source for a
|
|
8
|
+
# namespace URL (e.g. "https://beta.gem.coop/@kaspth"), it registers
|
|
9
|
+
# that URL here. Later, patches like ApiSpecPatch and DownloadPatch
|
|
10
|
+
# can check whether a given Gem::Source is a namespace source — without
|
|
11
|
+
# relying on heuristics like scanning the URL for "@" segments.
|
|
12
|
+
#
|
|
13
|
+
# This registry is the single source of truth for "is this source a
|
|
14
|
+
# namespace source that our plugin manages?"
|
|
15
|
+
module NamespaceSourceRegistry
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@sources = {} # normalized_url_string => true
|
|
18
|
+
|
|
19
|
+
# Register a namespace source URL. Safe to call multiple times with
|
|
20
|
+
# the same URL (idempotent).
|
|
21
|
+
def self.register(source_url)
|
|
22
|
+
normalized = normalize(source_url)
|
|
23
|
+
@mutex.synchronize { @sources[normalized] = true }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns true if +source+ is a Gem::Source whose URI was previously
|
|
27
|
+
# registered as a namespace source.
|
|
28
|
+
def self.namespace_source?(source)
|
|
29
|
+
return false unless source.is_a?(::Gem::Source)
|
|
30
|
+
|
|
31
|
+
uri = source.respond_to?(:uri) && source.uri
|
|
32
|
+
return false unless uri
|
|
33
|
+
|
|
34
|
+
namespace_source_url?(uri)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns true if the given URL string (or URI object) was registered.
|
|
38
|
+
def self.namespace_source_url?(url)
|
|
39
|
+
normalized = normalize(url)
|
|
40
|
+
@mutex.synchronize { @sources.key?(normalized) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clear all registered sources. Primarily for testing.
|
|
44
|
+
def self.clear!
|
|
45
|
+
@mutex.synchronize { @sources.clear }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns a frozen copy of all registered source URLs (for debugging).
|
|
49
|
+
def self.registered_urls
|
|
50
|
+
@mutex.synchronize { @sources.keys.freeze }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def normalize(url)
|
|
57
|
+
url.to_s.chomp("/")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Namespaced
|
|
6
|
+
module Gem
|
|
7
|
+
# Represents a gem dependency specified as a URI, enabling namespaced gem
|
|
8
|
+
# sources (e.g. gem.coop namespaces) to be declared directly in gemspecs.
|
|
9
|
+
#
|
|
10
|
+
# Supported formats:
|
|
11
|
+
# Full URI: "https://beta.gem.coop/@myspace/my-gem"
|
|
12
|
+
# Shorthand: "@myspace/my-gem" (defaults to https://beta.gem.coop)
|
|
13
|
+
# Package URL (purl-spec):
|
|
14
|
+
# "pkg:gem/@myspace/my-gem"
|
|
15
|
+
# "pkg:gem/@myspace/my-gem?repository_url=https://beta.gem.coop"
|
|
16
|
+
# "pkg:gem/my-gem?repository_url=https://beta.gem.coop/@myspace"
|
|
17
|
+
#
|
|
18
|
+
# Usage in a gemspec:
|
|
19
|
+
# spec.add_dependency "https://beta.gem.coop/@myspace/my-gem", "~> 1.0"
|
|
20
|
+
# spec.add_dependency "pkg:gem/@myspace/my-gem", "~> 1.0"
|
|
21
|
+
class UriDependency
|
|
22
|
+
DEFAULT_SERVER = "https://beta.gem.coop"
|
|
23
|
+
|
|
24
|
+
# Pattern matching full HTTPS/HTTP URIs to a namespaced gem.
|
|
25
|
+
# e.g. https://beta.gem.coop/@namespace/gem-name
|
|
26
|
+
FULL_URI_PATTERN = %r{
|
|
27
|
+
\A
|
|
28
|
+
(https?://[^/]+) # server base, e.g. https://beta.gem.coop
|
|
29
|
+
/
|
|
30
|
+
(@[^/]+) # namespace, e.g. @myspace
|
|
31
|
+
/
|
|
32
|
+
([a-zA-Z0-9._\-]+) # gem name (dots allowed per RubyGems convention)
|
|
33
|
+
\z
|
|
34
|
+
}x
|
|
35
|
+
|
|
36
|
+
# Shorthand: @namespace/gem-name (no server; defaults to beta.gem.coop)
|
|
37
|
+
SHORTHAND_PATTERN = %r{
|
|
38
|
+
\A
|
|
39
|
+
(@[^/]+) # namespace, e.g. @myspace
|
|
40
|
+
/
|
|
41
|
+
([a-zA-Z0-9._\-]+) # gem name (dots allowed per RubyGems convention)
|
|
42
|
+
\z
|
|
43
|
+
}x
|
|
44
|
+
|
|
45
|
+
# Package URL (purl-spec): pkg:gem/[@namespace/]gem-name[@version][?qualifiers]
|
|
46
|
+
# See https://github.com/package-url/purl-spec
|
|
47
|
+
#
|
|
48
|
+
# Supported forms:
|
|
49
|
+
# pkg:gem/@myspace/my-gem — namespace, default server
|
|
50
|
+
# pkg:gem/@myspace/my-gem?repository_url=https://beta.gem.coop — namespace + explicit server
|
|
51
|
+
# pkg:gem/my-gem?repository_url=https://beta.gem.coop/@myspace — namespace embedded in qualifier
|
|
52
|
+
#
|
|
53
|
+
# The @version component (if present) is captured but ignored — version
|
|
54
|
+
# constraints come from the second argument to add_dependency.
|
|
55
|
+
PURL_PATTERN = %r{
|
|
56
|
+
\A
|
|
57
|
+
pkg:gem/
|
|
58
|
+
(?:(@[^/]+)/)? # optional namespace, e.g. @myspace
|
|
59
|
+
([a-zA-Z0-9._\-]+) # gem name
|
|
60
|
+
(?:@[^?]+)? # optional @version (ignored)
|
|
61
|
+
(?:\?(.+))? # optional ?qualifiers
|
|
62
|
+
\z
|
|
63
|
+
}x
|
|
64
|
+
|
|
65
|
+
attr_reader :original, :server_base, :namespace, :gem_name
|
|
66
|
+
|
|
67
|
+
# Returns true if +name+ looks like a URI dependency (full URI, shorthand,
|
|
68
|
+
# or purl).
|
|
69
|
+
def self.uri?(name)
|
|
70
|
+
return false unless name.is_a?(String)
|
|
71
|
+
|
|
72
|
+
FULL_URI_PATTERN.match?(name) || SHORTHAND_PATTERN.match?(name) || PURL_PATTERN.match?(name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Parses +name+ into a UriDependency. Raises ArgumentError if not a URI dep.
|
|
76
|
+
def self.parse(name)
|
|
77
|
+
new(name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def initialize(name)
|
|
81
|
+
@original = -String(name)
|
|
82
|
+
|
|
83
|
+
if (m = FULL_URI_PATTERN.match(@original))
|
|
84
|
+
@server_base = -m[1]
|
|
85
|
+
@namespace = -m[2]
|
|
86
|
+
@gem_name = -m[3]
|
|
87
|
+
elsif (m = SHORTHAND_PATTERN.match(@original))
|
|
88
|
+
@server_base = DEFAULT_SERVER
|
|
89
|
+
@namespace = -m[1]
|
|
90
|
+
@gem_name = -m[2]
|
|
91
|
+
elsif (m = PURL_PATTERN.match(@original))
|
|
92
|
+
parse_purl(m)
|
|
93
|
+
else
|
|
94
|
+
raise ArgumentError, "Not a valid URI dependency: #{name.inspect}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
freeze
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The Bundler source URL for this dependency's namespace.
|
|
101
|
+
# This is what you'd put in a Gemfile `source` block.
|
|
102
|
+
def source_url
|
|
103
|
+
"#{server_base}/#{namespace}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Value equality — two UriDependency objects are equal when they refer
|
|
107
|
+
# to the same gem in the same namespace on the same server.
|
|
108
|
+
def ==(other)
|
|
109
|
+
other.is_a?(self.class) &&
|
|
110
|
+
server_base == other.server_base &&
|
|
111
|
+
namespace == other.namespace &&
|
|
112
|
+
gem_name == other.gem_name
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
alias eql? ==
|
|
116
|
+
|
|
117
|
+
def hash
|
|
118
|
+
[self.class, server_base, namespace, gem_name].hash
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_s
|
|
122
|
+
"#{source_url}/#{gem_name}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def inspect
|
|
126
|
+
"#<#{self.class} gem_name=#{gem_name.inspect} source_url=#{source_url.inspect}>"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Parse a purl match into server_base, namespace, and gem_name.
|
|
132
|
+
#
|
|
133
|
+
# Three forms:
|
|
134
|
+
# 1. pkg:gem/@ns/name → namespace from purl, default server
|
|
135
|
+
# 2. pkg:gem/@ns/name?repository_url=https://server
|
|
136
|
+
# → namespace from purl, server from qualifier
|
|
137
|
+
# 3. pkg:gem/name?repository_url=https://server/@ns
|
|
138
|
+
# → both from qualifier
|
|
139
|
+
def parse_purl(match)
|
|
140
|
+
purl_namespace = match[1] # may be nil
|
|
141
|
+
@gem_name = -match[2]
|
|
142
|
+
qualifiers = parse_qualifiers(match[3])
|
|
143
|
+
repo_url = qualifiers["repository_url"]
|
|
144
|
+
|
|
145
|
+
if purl_namespace
|
|
146
|
+
# Forms 1 & 2: namespace is in the purl path
|
|
147
|
+
@namespace = -purl_namespace
|
|
148
|
+
@server_base = repo_url ? -repo_url.chomp("/") : DEFAULT_SERVER
|
|
149
|
+
elsif repo_url
|
|
150
|
+
# Form 3: namespace is embedded in repository_url
|
|
151
|
+
# e.g. https://beta.gem.coop/@myspace
|
|
152
|
+
repo_uri = URI.parse(repo_url)
|
|
153
|
+
path_segments = repo_uri.path.split("/").reject(&:empty?)
|
|
154
|
+
ns_segment = path_segments.find { |s| s.start_with?("@") }
|
|
155
|
+
|
|
156
|
+
unless ns_segment
|
|
157
|
+
raise ArgumentError,
|
|
158
|
+
"purl qualifier repository_url must include a @namespace or the purl path must: #{@original.inspect}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
@namespace = -ns_segment
|
|
162
|
+
@server_base = -"#{repo_uri.scheme}://#{repo_uri.host}#{repo_uri.port == repo_uri.default_port ? "" : ":#{repo_uri.port}"}"
|
|
163
|
+
else
|
|
164
|
+
raise ArgumentError,
|
|
165
|
+
"purl for a namespaced gem must include a @namespace in the path or a repository_url qualifier: #{@original.inspect}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Parse a URL query string into a Hash. Returns an empty Hash for nil.
|
|
170
|
+
def parse_qualifiers(raw)
|
|
171
|
+
return {} unless raw
|
|
172
|
+
|
|
173
|
+
URI.decode_www_form(raw).to_h
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "gem/version"
|
|
4
|
+
require_relative "gem/uri_dependency"
|
|
5
|
+
require_relative "gem/namespace_source_registry"
|
|
6
|
+
require_relative "gem/dependency_patch"
|
|
7
|
+
require_relative "gem/api_spec_patch"
|
|
8
|
+
require_relative "gem/download_patch"
|
|
9
|
+
require_relative "gem/bundler_integration"
|
|
10
|
+
require_relative "gem/bundler_resolver_patch"
|
|
11
|
+
require_relative "gem/gem_resolver_patch"
|
|
12
|
+
require_relative "gem/metadata_deps_hook"
|
|
13
|
+
|
|
14
|
+
module Namespaced
|
|
15
|
+
# Namespaced::Gem is a RubyGems plugin/shim that enables gemspec dependencies
|
|
16
|
+
# to be declared as full URIs, pointing to namespaced gem sources such as
|
|
17
|
+
# gem.coop namespaces (e.g. https://beta.gem.coop/@myspace/my-gem).
|
|
18
|
+
#
|
|
19
|
+
# When installed, this gem's rubygems_plugin.rb is loaded at RubyGems boot,
|
|
20
|
+
# patching Gem::Dependency to accept URI names, Bundler::Dsl to automatically
|
|
21
|
+
# inject source blocks, and both Bundler's and RubyGems' resolvers to remap
|
|
22
|
+
# URI-named transitive deps to their real names and namespace sources.
|
|
23
|
+
module Gem
|
|
24
|
+
class Error < StandardError; end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RubyGems plugin for namespaced-gem.
|
|
4
|
+
#
|
|
5
|
+
# This file is automatically loaded by RubyGems at boot time (before any
|
|
6
|
+
# gemspecs are evaluated) because it matches the `rubygems*_plugin.rb` naming
|
|
7
|
+
# convention in the gem's require path.
|
|
8
|
+
#
|
|
9
|
+
# What it does:
|
|
10
|
+
# 1. Patches Gem::Dependency to accept URI-style gem names such as
|
|
11
|
+
# "https://beta.gem.coop/@namespace/gem-name".
|
|
12
|
+
# 2. Patches Gem::RequestSet and Gem::Resolver::InstallerSet so that
|
|
13
|
+
# `gem install` can resolve URI-named deps against the correct namespace
|
|
14
|
+
# source, both as direct deps and as transitive deps.
|
|
15
|
+
# 3. Patches Gem::Resolver::APISpecification#spec to synthesize a
|
|
16
|
+
# Gem::Specification from Compact Index data for namespace sources —
|
|
17
|
+
# bypassing the missing Marshal endpoint.
|
|
18
|
+
# 4. Patches Gem::Source#download to provide clear error messages when
|
|
19
|
+
# namespace servers fail to serve .gem files.
|
|
20
|
+
# 5. Registers deferred patches for Bundler::Dsl, Bundler::Definition, and
|
|
21
|
+
# Bundler::Resolver so that `bundle install` / `bundle add` automatically
|
|
22
|
+
# resolve URI deps (both direct and transitive) against namespace sources.
|
|
23
|
+
# 6. Registers a Gem.done_installing hook that processes deferred namespace
|
|
24
|
+
# dependencies stored in spec.metadata["namespaced_dependencies"]. This
|
|
25
|
+
# enables the "hot-load" path: when namespaced-gem is installed as a leaf
|
|
26
|
+
# dependency, Gem::Installer#load_plugin hot-loads this file, and the
|
|
27
|
+
# done_installing hook triggers a second install pass for URI deps.
|
|
28
|
+
#
|
|
29
|
+
# See lib/namespaced/gem/dependency_patch.rb — Gem::Dependency patch
|
|
30
|
+
# lib/namespaced/gem/gem_resolver_patch.rb — Gem::RequestSet / InstallerSet
|
|
31
|
+
# lib/namespaced/gem/api_spec_patch.rb — Gem::Resolver::APISpecification
|
|
32
|
+
# lib/namespaced/gem/download_patch.rb — Gem::Source#download
|
|
33
|
+
# lib/namespaced/gem/bundler_integration.rb — Bundler::Dsl patch
|
|
34
|
+
# lib/namespaced/gem/bundler_resolver_patch.rb — Bundler::Definition / Resolver
|
|
35
|
+
|
|
36
|
+
require_relative "namespaced/gem/dependency_patch"
|
|
37
|
+
require_relative "namespaced/gem/gem_resolver_patch"
|
|
38
|
+
require_relative "namespaced/gem/api_spec_patch"
|
|
39
|
+
require_relative "namespaced/gem/download_patch"
|
|
40
|
+
require_relative "namespaced/gem/bundler_integration"
|
|
41
|
+
require_relative "namespaced/gem/bundler_resolver_patch"
|
|
42
|
+
require_relative "namespaced/gem/metadata_deps_hook"
|
|
43
|
+
|
|
44
|
+
# 1. Patch Gem::Dependency immediately — must run before any gemspec is parsed.
|
|
45
|
+
Namespaced::Gem::DependencyPatch.apply!
|
|
46
|
+
|
|
47
|
+
# 2. Patch Gem::RequestSet / InstallerSet for `gem install` — always available.
|
|
48
|
+
Namespaced::Gem::GemResolverPatch.apply!
|
|
49
|
+
|
|
50
|
+
# 3. Patch Gem::Resolver::APISpecification#spec to synthesize specs for
|
|
51
|
+
# namespace sources (bypasses the missing Marshal endpoint).
|
|
52
|
+
Namespaced::Gem::ApiSpecPatch.apply!
|
|
53
|
+
|
|
54
|
+
# 4. Patch Gem::Source#download for clear namespace download error messages.
|
|
55
|
+
Namespaced::Gem::DownloadPatch.apply!
|
|
56
|
+
|
|
57
|
+
# 5. Patch Bundler if loaded, or defer until it is.
|
|
58
|
+
Namespaced::Gem::BundlerIntegration.apply_when_ready!
|
|
59
|
+
Namespaced::Gem::BundlerResolverPatch.apply_when_ready!
|
|
60
|
+
|
|
61
|
+
# 6. Register the done_installing hook for deferred namespace dependencies.
|
|
62
|
+
#
|
|
63
|
+
# This enables the "hot-load" path for `gem install my-gem` where my-gem
|
|
64
|
+
# depends on namespaced-gem and stores URI deps in metadata:
|
|
65
|
+
#
|
|
66
|
+
# spec.metadata["namespaced_dependencies"] = "https://beta.gem.coop/@ns/foo ~> 1.0"
|
|
67
|
+
#
|
|
68
|
+
# When namespaced-gem is installed as a leaf dependency (topological order),
|
|
69
|
+
# Gem::Installer#load_plugin hot-loads this file into the running process.
|
|
70
|
+
# The done_installing hook then fires after ALL gems in the batch finish
|
|
71
|
+
# installing, reads the metadata, and triggers a second resolution pass
|
|
72
|
+
# for the deferred namespace deps — with all patches now active.
|
|
73
|
+
Namespaced::Gem::MetadataDepsHook.register!
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Namespaced
|
|
2
|
+
module Gem
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class UriDependency
|
|
9
|
+
DEFAULT_SERVER: String
|
|
10
|
+
FULL_URI_PATTERN: Regexp
|
|
11
|
+
SHORTHAND_PATTERN: Regexp
|
|
12
|
+
PURL_PATTERN: Regexp
|
|
13
|
+
|
|
14
|
+
attr_reader original: String
|
|
15
|
+
attr_reader server_base: String
|
|
16
|
+
attr_reader namespace: String
|
|
17
|
+
attr_reader gem_name: String
|
|
18
|
+
|
|
19
|
+
def self.uri?: (untyped name) -> bool
|
|
20
|
+
def self.parse: (String name) -> UriDependency
|
|
21
|
+
|
|
22
|
+
def initialize: (String name) -> void
|
|
23
|
+
|
|
24
|
+
def source_url: () -> String
|
|
25
|
+
def ==: (untyped other) -> bool
|
|
26
|
+
alias eql? ==
|
|
27
|
+
def hash: () -> Integer
|
|
28
|
+
def to_s: () -> String
|
|
29
|
+
def inspect: () -> String
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_purl: (MatchData match) -> void
|
|
34
|
+
def parse_qualifiers: (String? raw) -> Hash[String, String]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module DependencyPatch
|
|
38
|
+
def self.apply!: () -> void
|
|
39
|
+
|
|
40
|
+
module InstanceMethods
|
|
41
|
+
def uri_gem?: () -> bool
|
|
42
|
+
def uri_dependency: () -> UriDependency?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module BundlerIntegration
|
|
47
|
+
def self.apply!: () -> void
|
|
48
|
+
def self.apply_when_ready!: () -> void
|
|
49
|
+
|
|
50
|
+
module DslPatch
|
|
51
|
+
def gemspec: (?Hash[Symbol, untyped]? opts) -> void
|
|
52
|
+
|
|
53
|
+
private def inject_uri_sources_for: (Gem::Specification spec) -> void
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module BundlerResolverPatch
|
|
58
|
+
def self.apply!: () -> void
|
|
59
|
+
def self.apply_when_ready!: () -> void
|
|
60
|
+
|
|
61
|
+
module DefinitionPatch
|
|
62
|
+
def initialize: (untyped lockfile, Array[untyped] dependencies, untyped sources, *untyped) -> void
|
|
63
|
+
|
|
64
|
+
private def remap_uri_dependencies: (Array[untyped] dependencies) -> [Array[untyped], Array[String]]
|
|
65
|
+
private def dep_options_for: (untyped dep, String source_url) -> Hash[String, untyped]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module ResolverPatch
|
|
69
|
+
def to_dependency_hash: (Array[untyped] dependencies, untyped packages) -> untyped
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
module GemResolverPatch
|
|
74
|
+
def self.apply!: () -> void
|
|
75
|
+
|
|
76
|
+
module RequestSetPatch
|
|
77
|
+
def resolve: (?untyped set) -> untyped
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
module InstallerSetPatch
|
|
81
|
+
def find_all: (untyped req) -> Array[untyped]
|
|
82
|
+
|
|
83
|
+
private def _namespaced_resolver_set: (String source_url) -> untyped
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: namespaced-gem
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Peter H. Boling
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: "\U0001F50C A RubyGems plugin that allows gemspec dependencies to be
|
|
13
|
+
declared as full\nURIs pointing to namespaced gem sources such as gem.coop namespaces\n(e.g.
|
|
14
|
+
`https://beta.gem.coop/@myspace/my-gem`).\n\nWhen installed, this gem patches both
|
|
15
|
+
RubyGems' native resolver (`gem\ninstall`) and Bundler's resolver (`bundle install`)
|
|
16
|
+
to parse URI dependency\nnames, route them to the correct namespace source, and
|
|
17
|
+
remap transitive deps\non the fly — so `gem install @kaspth/oaken` and `bundle install`
|
|
18
|
+
with URI\ndeps in gemspecs both Just Work™.\n\nSee https://github.com/gem-coop/gem.coop/issues/12
|
|
19
|
+
for the original discussion.\n"
|
|
20
|
+
email:
|
|
21
|
+
- peter.boling@gmail.com
|
|
22
|
+
executables: []
|
|
23
|
+
extensions: []
|
|
24
|
+
extra_rdoc_files: []
|
|
25
|
+
files:
|
|
26
|
+
- ".idea/.gitignore"
|
|
27
|
+
- ".idea/git_toolbox_prj.xml"
|
|
28
|
+
- ".idea/modules.xml"
|
|
29
|
+
- CHANGELOG.md
|
|
30
|
+
- CODE_OF_CONDUCT.md
|
|
31
|
+
- HOT_HOOK.md
|
|
32
|
+
- HOT_LOAD_ANALYSIS.md
|
|
33
|
+
- ISSUE.md
|
|
34
|
+
- README.md
|
|
35
|
+
- Rakefile
|
|
36
|
+
- WORKAROUND.md
|
|
37
|
+
- lib/example_hooks.rb
|
|
38
|
+
- lib/namespaced/gem.rb
|
|
39
|
+
- lib/namespaced/gem/api_spec_patch.rb
|
|
40
|
+
- lib/namespaced/gem/bundler_integration.rb
|
|
41
|
+
- lib/namespaced/gem/bundler_resolver_patch.rb
|
|
42
|
+
- lib/namespaced/gem/dependency_patch.rb
|
|
43
|
+
- lib/namespaced/gem/download_patch.rb
|
|
44
|
+
- lib/namespaced/gem/gem_resolver_patch.rb
|
|
45
|
+
- lib/namespaced/gem/metadata_deps_hook.rb
|
|
46
|
+
- lib/namespaced/gem/namespace_source_registry.rb
|
|
47
|
+
- lib/namespaced/gem/uri_dependency.rb
|
|
48
|
+
- lib/namespaced/gem/version.rb
|
|
49
|
+
- lib/rubygems_plugin.rb
|
|
50
|
+
- sig/namespaced/gem.rbs
|
|
51
|
+
homepage: https://gitlab.com/galtzo-floss/namespaced-gem
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
allowed_push_host: https://rubygems.org
|
|
56
|
+
homepage_uri: https://gitlab.com/galtzo-floss/namespaced-gem
|
|
57
|
+
source_code_uri: https://gitlab.com/galtzo-floss/namespaced-gem
|
|
58
|
+
changelog_uri: https://gitlab.com/galtzo-floss/namespaced-gem/-/blob/main/CHANGELOG.md
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 3.2.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 4.0.5
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 4.0.7
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: "\U0001F50C RubyGems plugin enabling URI-style gemspec dependencies for namespaced
|
|
77
|
+
gem sources (e.g. gem.coop namespaces)."
|
|
78
|
+
test_files: []
|