gem 0.0.1.alpha

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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Samuel Cochran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ # Gem
2
+
3
+ Just enough not-Rubygems to index a collection of gems for download... and maybe more.
4
+
5
+ There are some severe caveats. Some are by design, others will be addressed later.
6
+
7
+ Created by [Samuel Cochran](http://sj26.com) for [Railscamp X](http://railscamps.com).
8
+
9
+ ## Thoughts
10
+
11
+ Rubygems is a bit... horrible. It was a good fit for the problem it solved for a long time, but we've outgrown it. Here are a list of my thoughts on what's wrong and how we could fix it. Many of these ideas are or will permeate this `gem` gem as a (mostly backwards-compatible) drop-in replacement for rubygems.
12
+
13
+ Local index should be more granular and allow stream processing. Same with specs lists. Marshalling and constantly re-downloading a whole index, or even just a list of specs, is ludicrous at current sizes.
14
+
15
+ Break index up into more files for efficient lookup?
16
+
17
+ Learn from PIP, have quick lookups for common cases.
18
+
19
+ JSON is a better choice than YAML—it's just plain faster and becoming ubiquitous. Built-in (usually reasonably fast) support in 1.9, including streaming, which is cross-platform friendly (much moreso than YAML).
20
+
21
+ Auto-negotiated transport compression should counteract any size difference and work for partial updates.
22
+
23
+ Compression is purely a transport/storage concern.
24
+
25
+ Index journal for efficient partial updates via HTTP.
26
+
27
+ Use plain old HTTP (but the full functionality of 1.1) and static files to allow serving at edges by cloudfront, easy proxying/caching, etc.
28
+
29
+ Same directory structure and index format locally and remotely, backwards compatible:
30
+
31
+ Given the following definitions:
32
+
33
+ * `<basename>`: `<name>-<version>[-<platform>]`
34
+ * `<name>`: gem name, i.e. "rails"
35
+ * `<version>`: gem version, i.e. "3.2.2"
36
+ * `<platform>`: gem platform, omittted if platform is "ruby", the default
37
+
38
+ Proposed directory structure:
39
+
40
+ * `cache/` -> `gems/` — a symlink for old gemball location, backwards compat only.
41
+ * `index/` — index of gemspecs.
42
+ * All gemspec indexes are stored as simple tuples, `[[<name>, <version>, <platform>[, <yanked?>]]+]`
43
+ * Journalled (append-only).
44
+ * Yanked gems add another entry with <yanked?> set to true.
45
+ * `specs.json[.gz]` — index of all (non-prerelease) gemspecs
46
+ * `latest_specs.json[.gz]` — index of latest (non-prerelease) gemspec
47
+ * `prerelease_specs.json[.gz] — index of all (non-prerelease) gemspecs ([[name, version, platform]*]), journalled.
48
+ * `<name>/` — gem name specific indexes
49
+ * `specs.json[.gz]` — index of gemspecs for a gem name (for complex requirement resolution)
50
+ * `latest_specs.json[.gz]` — latest gemspec for each platform for a gem name (`gem install <name>`)
51
+ * `prerelease_specs.json[.gz]` — latest prerelease gemspec for each platform for a gem name (`gem install —prerelease <name>`)
52
+ * eventually, we might need to replicate by version segment for targeted requirements if it would provide efficiency gains — gems with many semantic versions, etc:
53
+ `version-<version-prefix>/{specs,latest_specs,prerelease_specs}.json.gz`
54
+ * could also introduce something for platform, ala:
55
+ `version-<version-prefix>/]platform-<platform>/`
56
+ * `sources/` — cached source indexes
57
+ * `<source-sha>/` — SHA of source URL (i.e. `http://rubygems.org`)
58
+ * `index/` — a cache of the top-level index/ for a particular source
59
+ * `gems/` — installed gems, backwards compatible.
60
+ * `<basename>.gem` — the gem ball
61
+ * `<basename>/` — the unpacked gem tree
62
+ * `specifications/` — gem specifications of installed gems, backwards compatible.
63
+ * `<basename>.gemspec` — ruby format, without file/test lists
64
+
65
+ Use net/http/persistent per-source for gem operations like rubygems-mirror, respecting HTTP content-type, transport-encoding (compression), range and freshness controls, and negating overhead of many small files.
66
+
67
+ Multiplex mirroring and potentially other operations over a pool of threads like rubygems-mirror.
68
+
69
+ Break old marshalling support into modules, only include them when backwards-compatible behaviour required (i.e. during upgrade, when compat is requested/configured).
70
+
71
+ Add more checks and guards to make sure the index/gemspecs/gems can't get into an invalid state.
72
+
73
+ ## License
74
+
75
+ MIT (see LICENSE). Some parts adapted from Rubygems, which is under the Ruby or MIT license.
data/bin/gem ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby -I ../lib
2
+
3
+ require 'gem'
4
+
5
+ raise ArgumentError, "Bad command" unless Gem.respond_to? ARGV.first
6
+
7
+ Gem.public_send ARGV.shift, *ARGV
@@ -0,0 +1,249 @@
1
+ require 'fileutils'
2
+ require 'net/http/persistent'
3
+ require 'rbconfig'
4
+ require 'time'
5
+ require 'yaml'
6
+ require 'zlib'
7
+
8
+ module Gem
9
+ VERSION = '1.8.11'.freeze
10
+
11
+ # XXX: Find methods not implemented yet
12
+ def self.method_missing symbol, *args
13
+ # XXX: WHUT, why doesn't "[athing]"[1, -1] not give "athing"? Grr.
14
+ puts "TODO: #{name}.#{symbol}(#{args.inspect.tap { |s| s.slice!(0, 1); s.slice!(-1) } })"
15
+ end
16
+
17
+ def self.[] name, version=nil, platform=nil
18
+ name.gsub! /\.gem\Z/, ""
19
+ if version.nil?
20
+ versions = Dir[File.join(path, "cache", "#{name}-*.gem")].map do |filename|
21
+ File.basename(filename)
22
+ end.map do |basename|
23
+ basename.slice(name.length + 1, basename.length - name.length - 1 - 4)
24
+ end.map do |version|
25
+ Version.new version
26
+ end.reject(&:prerelease?).sort
27
+ version = versions.last.to_s unless versions.empty?
28
+ end
29
+
30
+ specification = Specification.new name, version, platform
31
+ filename = File.join path, "cache", "#{specification.basename}.gem"
32
+
33
+ Specification.from_gem filename
34
+ end
35
+
36
+ def self.all
37
+ # If we have an index, run the block if given
38
+ @index.gems.each(&proc) if block_given? and @index
39
+
40
+ # Otherwise build the index and run the block for each built spec
41
+ @index ||= SourceIndex.new.tap do |index|
42
+ progress = nil
43
+ Dir.foreach("gems").select do |path|
44
+ path =~ /\.gem\Z/
45
+ end.tap do |names|
46
+ progress = ProgressBar.new("Loading index", names.length)
47
+ end.each do |name|
48
+ begin
49
+ if specification = self[name].for_cache!
50
+ index.gems << specification
51
+ yield specification if block_given?
52
+ end
53
+ rescue StandardError
54
+ puts "Failed to load gem #{name.inspect}: #{$!}", $!.inspect, $!.backtrace
55
+ end
56
+ progress.inc
57
+ end
58
+ progress.finish
59
+ puts "#{index.gems.length} gems loaded into index"
60
+ index.gems.sort!
61
+ end
62
+ end
63
+
64
+ def self.quick_index specification
65
+ File.write("quick/#{specification.basename}.gemspec.rz", Zlib.deflate(YAML.dump(specification)))
66
+ File.write("quick/Marshal.#{marshal_version}/#{specification.basename}.gemspec.rz", Zlib.deflate(Marshal.dump(specification)))
67
+ end
68
+
69
+ def self.index
70
+ FileUtils.mkdir_p "quick/Marshal.#{marshal_version}"
71
+
72
+ all(&method(:quick_index))
73
+
74
+ print "Marshal index... "
75
+ File.write("Marshal.#{marshal_version}.Z", Zlib.deflate(Marshal.dump(all.gems.map { |spec| [spec.basename, spec] })))
76
+ puts "done."
77
+
78
+ # deprecated: Marshal.dump(all, File.open("Marshal.#{marshal_version}", "w"))
79
+
80
+ # deprecated:
81
+ #puts "Quick index"
82
+ #File.open('quick/index', 'w') do |quick_index|
83
+ # all.gems.each do |specification|
84
+ # quick_index.write("#{specification.name.to_s}-#{specification.version.version}\n")
85
+ # end
86
+ #end
87
+
88
+ # deprecated:
89
+ #puts "Master index"
90
+ #YAML.dump(all, File.open("yaml", "w"))
91
+ #File.write("yaml.Z", Zlib.deflate(File.read("yaml")))
92
+
93
+ # un-gzipped indexes are deprecated, so generate gzipped directly:
94
+
95
+ print "Writing specs... "
96
+ Marshal.dump(all.gems.reject(&:prerelease?).map do |specification|
97
+ platform = specification.platform
98
+ platform = "ruby" if platform.nil? or platform.empty?
99
+ [specification.name, specification.version, platform]
100
+ end, Zlib::GzipWriter.new(File.open("specs.#{marshal_version}.gz", "w")))
101
+ puts "done."
102
+
103
+ print "Writing lastest_specs... "
104
+ Marshal.dump(all.gems.group_by(&:name).map do |name, specifications|
105
+ specification = specifications.reject(&:prerelease?).last
106
+ platform = specification.platform
107
+ platform = "ruby" if platform.nil? or platform.empty?
108
+ [specification.name, specification.version, platform]
109
+ end, Zlib::GzipWriter.new(File.open("latest_specs.#{marshal_version}.gz", "w")))
110
+ puts "done."
111
+
112
+ print "Writing prerelease_specs... "
113
+ Marshal.dump(all.gems.select(&:prerelease?).map do |specification|
114
+ platform = specification.platform
115
+ platform = "ruby" if platform.nil? or platform.empty?
116
+ [specification.name, specification.version, platform]
117
+ end, Zlib::GzipWriter.new(File.open("prerelease_specs.#{marshal_version}.gz", "w")))
118
+ puts "done."
119
+
120
+ # TODO: index.rss
121
+ end
122
+
123
+ def self.mirror source=source
124
+ http = Net::HTTP::Persistent.new "gem-mirror"
125
+ print "#{File.exist? "specs.#{marshal_version}.gz" and "Updating" or "Fetching"} specifications... "
126
+ ["specs", "latest_specs", "prerelease_specs"].each.in_threads do |specs_name|
127
+ path = "#{specs_name}.#{marshal_version}.gz"
128
+ uri = URI "#{source}/#{specs_name}.#{marshal_version}.gz"
129
+ headers = {}
130
+ headers['If-Modified-Since'] = File.mtime(path).rfc2822 if File.exist? path
131
+ catch :done do
132
+ loop do
133
+ request = Net::HTTP::Get.new uri.path, headers
134
+ http.request(uri, request) do |response|
135
+ puts response.inspect
136
+ if response.code == "304"
137
+ # Nothing to do, we already have latest version
138
+ throw :done
139
+ elsif response.code[0] == "3" and response["Location"]
140
+ # Redirect
141
+ url = URI.join uri.to_s, response["Location"]
142
+ elsif response.code == "206"
143
+ File.open(path, 'a') do |file|
144
+ response.read_body do |chunk|
145
+ file.write chunk
146
+ end
147
+ end
148
+ elsif response.code == "200"
149
+ File.open(path, 'w') do |file|
150
+ response.read_body do |chunk|
151
+ file.write chunk
152
+ end
153
+ end
154
+ last_modified = Time.parse response['Last-Modified']
155
+ File.utime last_modified, last_modified, path
156
+ throw :done
157
+ else
158
+ raise StandardError, "Unknown response: #{response.inspect}"
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ puts "done."
165
+ FileUtils.mkdir_p "gems"
166
+ progress = nil
167
+ ["latest_specs", "specs", "prerelease_specs"].each do |specs_name|
168
+ Marshal.load(IO.popen("gunzip -c #{specs_name}.#{marshal_version}.gz", "r", err: nil)).tap do |tuples|
169
+ progress = ProgressBar.new("Mirroring #{specs_name.gsub('_', ' ')}", tuples.length)
170
+ end.each.in_thread_pool(of: 8) do |tuple|
171
+ name, version, platform = tuple
172
+ begin
173
+ specification = Specification.new name: name, version: version, platform: platform
174
+ path = "gems/#{specification.basename}.gem"
175
+ unless File.exist? path and Specification.try_from_gem(path)
176
+ uri = URI "#{source}/#{path}"
177
+ headers = {}
178
+ headers["Range"] = "bytes=#{File.size(path)}-" if File.exist? path
179
+ catch :done do
180
+ loop do
181
+ request = Net::HTTP::Get.new uri.path, headers
182
+ http.request uri, request do |response|
183
+ puts response.inspect
184
+ if response.code == "304"
185
+ # Nothing to do, we already have latest version
186
+ throw :done
187
+ elsif response.code[0] == "3" and response["Location"]
188
+ # Redirect
189
+ url = URI.join uri.to_s, response["Location"]
190
+ elsif response.code == "200" or response.code == "206"
191
+ # TODO: Check range properly
192
+ File.open(path, response.code == '206' ? 'a' : 'w') do |file|
193
+ response.read_body do |chunk|
194
+ file.write chunk
195
+ end
196
+ end
197
+ last_modified = Time.parse response['Last-Modified']
198
+ File.utime last_modified, last_modified, path
199
+ throw :done
200
+ else
201
+ raise StandardError, "Unknown response: #{response.inspect}"
202
+ end
203
+ end
204
+ end
205
+ end
206
+ progress.puts specification.basename
207
+ end
208
+ rescue StandardError
209
+ progress.puts "Failed to mirror gem #{name.inspect}: #{$!}", $!.inspect, $!.backtrace
210
+ end
211
+ progress.inc
212
+ end
213
+ progress.finish
214
+ end
215
+ puts "#{`/bin/ls -1f | wc -l`.to_i - 2} gems mirrored."
216
+ index
217
+ end
218
+
219
+ protected
220
+
221
+ def self.shellescape arg
222
+ if not arg.is_a? String
223
+ arg.to_s
224
+ else
225
+ arg.dup
226
+ end.tap do |arg|
227
+ # Process as a single byte sequence because not all shell
228
+ # implementations are multibyte aware.
229
+ arg.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
230
+
231
+ # A LF cannot be escaped with a backslash because a backslash + LF
232
+ # combo is regarded as line continuation and simply ignored.
233
+ arg.gsub!(/\n/, "'\n'")
234
+ end
235
+ end
236
+ end
237
+
238
+ require 'gem/tar'
239
+ require 'gem/thread_poolable'
240
+
241
+ require 'gem/configuration'
242
+ require 'gem/version'
243
+ require 'gem/requirement'
244
+ require 'gem/dependency'
245
+ require 'gem/platform'
246
+ require 'gem/specification'
247
+ require 'gem/progressbar'
248
+
249
+ require 'gem/require'
@@ -0,0 +1,43 @@
1
+ module Gem::Configuration
2
+ def sources
3
+ @sources ||= %w(http://rubygems.org)
4
+ end
5
+
6
+ def sources= value
7
+ @sources = value
8
+ end
9
+
10
+ def source
11
+ sources.first
12
+ end
13
+
14
+ def source= value
15
+ self.sources = [value]
16
+ end
17
+
18
+ def ruby_engine
19
+ if defined? RUBY_ENGINE then
20
+ RUBY_ENGINE
21
+ else
22
+ 'ruby'
23
+ end
24
+ end
25
+
26
+ def path
27
+ @path ||= begin
28
+ File.join *if defined? RUBY_FRAMEWORK_VERSION
29
+ [File.dirname(RbConfig::CONFIG["sitedir"]), 'Gems', RbConfig::CONFIG["ruby_version"]]
30
+ elsif RbConfig::CONFIG["rubylibprefix"] then
31
+ [RbConfig::CONFIG["rubylibprefix"], 'gems', RbConfig::CONFIG["ruby_version"]]
32
+ else
33
+ [RbConfig::CONFIG["libdir"], ruby_engine, 'gems', RbConfig::CONFIG["ruby_version"]]
34
+ end
35
+ end
36
+ end
37
+
38
+ def marshal_version
39
+ "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
40
+ end
41
+ end
42
+
43
+ Gem.extend Gem::Configuration
@@ -0,0 +1,2 @@
1
+ class Gem::Dependency
2
+ end
@@ -0,0 +1,76 @@
1
+ class Gem::Platform
2
+ RUBY = 'ruby'
3
+
4
+ attr_accessor :cpu, :os, :version
5
+
6
+ def initialize arch
7
+ case arch
8
+ when Array then
9
+ @cpu, @os, @version = arch
10
+ when String then
11
+ arch = arch.split '-'
12
+
13
+ if arch.length > 2 and arch.last !~ /\d/ then # reassemble x86-linux-gnu
14
+ extra = arch.pop
15
+ arch.last << "-#{extra}"
16
+ end
17
+
18
+ cpu = arch.shift
19
+
20
+ @cpu = case cpu
21
+ when /i\d86/ then 'x86'
22
+ else cpu
23
+ end
24
+
25
+ if arch.length == 2 and arch.last =~ /^\d+(\.\d+)?$/ then # for command-line
26
+ @os, @version = arch
27
+ return
28
+ end
29
+
30
+ os, = arch
31
+ @cpu, os = nil, cpu if os.nil? # legacy jruby
32
+
33
+ @os, @version = case os
34
+ when /aix(\d+)/ then ['aix', $1 ]
35
+ when /cygwin/ then ['cygwin', nil]
36
+ when /darwin(\d+)?/ then ['darwin', $1 ]
37
+ when /freebsd(\d+)/ then ['freebsd', $1 ]
38
+ when /hpux(\d+)/ then ['hpux', $1 ]
39
+ when /^java$/, /^jruby$/ then ['java', nil]
40
+ when /^java([\d.]*)/ then ['java', $1 ]
41
+ when /^dotnet$/ then ['dotnet', nil]
42
+ when /^dotnet([\d.]*)/ then ['dotnet', $1 ]
43
+ when /linux/ then ['linux', $1 ]
44
+ when /mingw32/ then ['mingw32', nil]
45
+ when /(mswin\d+)(\_(\d+))?/ then
46
+ os, version = $1, $3
47
+ @cpu = 'x86' if @cpu.nil? and os =~ /32$/
48
+ [os, version]
49
+ when /netbsdelf/ then ['netbsdelf', nil]
50
+ when /openbsd(\d+\.\d+)/ then ['openbsd', $1 ]
51
+ when /solaris(\d+\.\d+)/ then ['solaris', $1 ]
52
+ # test
53
+ when /^(\w+_platform)(\d+)/ then [$1, $2 ]
54
+ else ['unknown', nil]
55
+ end
56
+ when Gem::Platform then
57
+ @cpu = arch.cpu
58
+ @os = arch.os
59
+ @version = arch.version
60
+ else
61
+ raise ArgumentError, "invalid argument #{arch.inspect}"
62
+ end
63
+ end
64
+
65
+ def to_a
66
+ [@cpu, @os, @version]
67
+ end
68
+
69
+ def to_s
70
+ to_a.compact.join '-'
71
+ end
72
+
73
+ def empty?
74
+ to_s.empty?
75
+ end
76
+ end