gem 0.0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
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