gem-mirror 0.0.1
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/.gitignore +5 -0
- data/.yardopts +11 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/MANIFEST +33 -0
- data/README.md +80 -0
- data/Rakefile +12 -0
- data/bin/gem-mirror +5 -0
- data/doc/.gitkeep +0 -0
- data/doc/Contributing.md +125 -0
- data/doc/DCO.md +25 -0
- data/doc/css/.gitkeep +0 -0
- data/doc/css/common.css +68 -0
- data/gem-mirror.gemspec +26 -0
- data/lib/gem-mirror.rb +31 -0
- data/lib/gem-mirror/cli.rb +65 -0
- data/lib/gem-mirror/cli/checksum.rb +41 -0
- data/lib/gem-mirror/cli/index.rb +31 -0
- data/lib/gem-mirror/cli/init.rb +21 -0
- data/lib/gem-mirror/cli/update.rb +23 -0
- data/lib/gem-mirror/configuration.rb +132 -0
- data/lib/gem-mirror/gem.rb +53 -0
- data/lib/gem-mirror/gems_fetcher.rb +194 -0
- data/lib/gem-mirror/mirror_directory.rb +59 -0
- data/lib/gem-mirror/mirror_file.rb +63 -0
- data/lib/gem-mirror/source.rb +106 -0
- data/lib/gem-mirror/version.rb +3 -0
- data/lib/gem-mirror/versions_fetcher.rb +30 -0
- data/lib/gem-mirror/versions_file.rb +64 -0
- data/task/manifest.rake +8 -0
- data/template/config.rb +25 -0
- data/template/public/checksums/.gitkeep +0 -0
- data/template/public/gems/.gitkeep +0 -0
- metadata +174 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
module GemMirror
|
2
|
+
module CLI
|
3
|
+
##
|
4
|
+
# Hash containing the default Slop options.
|
5
|
+
#
|
6
|
+
# @return [Hash]
|
7
|
+
#
|
8
|
+
SLOP_OPTIONS = {
|
9
|
+
:strict => true,
|
10
|
+
:banner => 'Usage: gem-mirror [COMMAND] [OPTIONS]'
|
11
|
+
}
|
12
|
+
|
13
|
+
##
|
14
|
+
# @return [Slop]
|
15
|
+
#
|
16
|
+
def self.options
|
17
|
+
return @options ||= default_options
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Loads the specified configuration file or displays an error if it doesn't
|
22
|
+
# exist.
|
23
|
+
#
|
24
|
+
# @param [String] config_file
|
25
|
+
# @return [GemMirror::Configuration]
|
26
|
+
#
|
27
|
+
def self.load_configuration(config_file)
|
28
|
+
config_file ||= Configuration.default_configuration_file
|
29
|
+
config_file = File.expand_path(config_file, Dir.pwd)
|
30
|
+
|
31
|
+
unless File.file?(config_file)
|
32
|
+
abort "The configuration file #{config_file} does not exist"
|
33
|
+
end
|
34
|
+
|
35
|
+
require(config_file)
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# @return [Slop]
|
40
|
+
#
|
41
|
+
def self.default_options
|
42
|
+
return Slop.new(SLOP_OPTIONS.dup) do
|
43
|
+
separator "\nOptions:\n"
|
44
|
+
|
45
|
+
on :h, :help, 'Shows this help message' do
|
46
|
+
puts self
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
on :v, :version, 'Shows the current version' do
|
51
|
+
puts CLI.version_information
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Returns a String containing some platform/version related information.
|
58
|
+
#
|
59
|
+
# @return [String]
|
60
|
+
#
|
61
|
+
def self.version_information
|
62
|
+
return "gem-mirror v#{VERSION} on #{RUBY_DESCRIPTION}"
|
63
|
+
end
|
64
|
+
end # CLI
|
65
|
+
end # GemMirror
|
@@ -0,0 +1,41 @@
|
|
1
|
+
GemMirror::CLI.options.command 'checksum' do
|
2
|
+
banner 'Usage: gem-mirror checksum [OPTIONS]'
|
3
|
+
description 'Generates SHA512 checksums of all gems'
|
4
|
+
separator "\nOptions:\n"
|
5
|
+
|
6
|
+
on :h, :help, 'Shows this help message' do
|
7
|
+
puts self
|
8
|
+
exit
|
9
|
+
end
|
10
|
+
|
11
|
+
on :c=, :config=, 'Path to the configuration file'
|
12
|
+
|
13
|
+
run do |opts, args|
|
14
|
+
GemMirror::CLI.load_configuration(opts[:c])
|
15
|
+
|
16
|
+
config = GemMirror.configuration
|
17
|
+
|
18
|
+
unless File.directory?(config.checksums)
|
19
|
+
config.logger("The directory #{config.checksums} does not exist")
|
20
|
+
abort
|
21
|
+
end
|
22
|
+
|
23
|
+
unless File.directory?(config.destination)
|
24
|
+
config.logger("The directory #{config.destination} does not exist")
|
25
|
+
abort
|
26
|
+
end
|
27
|
+
|
28
|
+
Dir[File.join(config.gems_directory, '*.gem')].each do |gem|
|
29
|
+
basename = File.basename(gem)
|
30
|
+
name = basename + '.sha512'
|
31
|
+
|
32
|
+
config.logger.info("Creating checksum for #{basename}")
|
33
|
+
|
34
|
+
hash = Digest::SHA512.hexdigest(File.read(gem))
|
35
|
+
handle = File.open(File.join(config.checksums, name), 'w')
|
36
|
+
|
37
|
+
handle.write(hash)
|
38
|
+
handle.close
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
GemMirror::CLI.options.command 'index' do
|
2
|
+
banner 'Usage: gem-mirror index [OPTIONS]'
|
3
|
+
description 'Indexes a list of Gems'
|
4
|
+
separator "\nOptions:\n"
|
5
|
+
|
6
|
+
on :h, :help, 'Shows this help message' do
|
7
|
+
puts self
|
8
|
+
exit
|
9
|
+
end
|
10
|
+
|
11
|
+
on :c=, :config=, 'Path to the configuration file'
|
12
|
+
on :l, :legacy, 'Build legacy indexes'
|
13
|
+
|
14
|
+
run do |opts, args|
|
15
|
+
GemMirror::CLI.load_configuration(opts[:c])
|
16
|
+
|
17
|
+
config = GemMirror.configuration
|
18
|
+
|
19
|
+
unless File.directory?(config.destination)
|
20
|
+
config.logger.error("The directory #{config.destination} does not exist")
|
21
|
+
abort
|
22
|
+
end
|
23
|
+
|
24
|
+
indexer = Gem::Indexer.new(config.destination, :build_legacy => opts[:l])
|
25
|
+
indexer.ui = Gem::SilentUI.new
|
26
|
+
|
27
|
+
config.logger.info("Generating indexes")
|
28
|
+
|
29
|
+
indexer.generate_index
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
GemMirror::CLI.options.command 'init' do
|
2
|
+
banner 'Usage: gem-mirror init [DIRECTORY] [OPTIONS]'
|
3
|
+
description 'Sets up a new mirror'
|
4
|
+
separator "\nOptions:\n"
|
5
|
+
|
6
|
+
on :h, :help, 'Shows this help message' do
|
7
|
+
puts self
|
8
|
+
exit
|
9
|
+
end
|
10
|
+
|
11
|
+
run do |opts, args|
|
12
|
+
directory = File.expand_path(args[0] || Dir.pwd)
|
13
|
+
template = GemMirror::Configuration.template_directory
|
14
|
+
|
15
|
+
Dir.mkdir(directory) unless File.directory?(directory)
|
16
|
+
|
17
|
+
FileUtils.cp_r(File.join(template, '.'), directory)
|
18
|
+
|
19
|
+
puts "Initialized empty mirror in #{directory}"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
GemMirror::CLI.options.command 'update' do
|
2
|
+
banner 'Usage: gem-mirror update [OPTIONS]'
|
3
|
+
description 'Updates the list of Gems'
|
4
|
+
separator "\nOptions:\n"
|
5
|
+
|
6
|
+
on :h, :help, 'Shows this help message' do
|
7
|
+
puts self
|
8
|
+
exit
|
9
|
+
end
|
10
|
+
|
11
|
+
on :c=, :config=, 'Path to the configuration file'
|
12
|
+
|
13
|
+
run do |opts, args|
|
14
|
+
GemMirror::CLI.load_configuration(opts[:c])
|
15
|
+
|
16
|
+
GemMirror.configuration.sources.each do |source|
|
17
|
+
versions = GemMirror::VersionsFetcher.new(source).fetch
|
18
|
+
gems = GemMirror::GemsFetcher.new(source, versions)
|
19
|
+
|
20
|
+
gems.fetch
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module GemMirror
|
2
|
+
##
|
3
|
+
# @return [GemMirror::Configuration]
|
4
|
+
#
|
5
|
+
def self.configuration
|
6
|
+
return @configuration ||= Configuration.new
|
7
|
+
end
|
8
|
+
|
9
|
+
##
|
10
|
+
# Configuration class used for storing data about a mirror such as the
|
11
|
+
# destination directory, sources, ignored Gems, etc.
|
12
|
+
#
|
13
|
+
class Configuration < Confstruct::Configuration
|
14
|
+
##
|
15
|
+
# @return [Logger]
|
16
|
+
#
|
17
|
+
def logger
|
18
|
+
return @logger ||= Logger.new(STDOUT)
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# @return [String]
|
23
|
+
#
|
24
|
+
def self.template_directory
|
25
|
+
return File.expand_path('../../../template', __FILE__)
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# @return [String]
|
30
|
+
#
|
31
|
+
def self.default_configuration_file
|
32
|
+
return File.expand_path('config.rb', Dir.pwd)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Returns the name of the directory that contains the quick
|
37
|
+
# specification files.
|
38
|
+
#
|
39
|
+
# @return [String]
|
40
|
+
#
|
41
|
+
def self.marshal_identifier
|
42
|
+
return "Marshal.#{marshal_version}"
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns the name of the file that contains an index of all the versions.
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
#
|
50
|
+
def self.versions_file
|
51
|
+
return "specs.#{marshal_version}.gz"
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Returns a String containing the Marshal version.
|
56
|
+
#
|
57
|
+
# @return [String]
|
58
|
+
#
|
59
|
+
def self.marshal_version
|
60
|
+
return "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# @return [GemMirror::MirrorDirectory]
|
65
|
+
#
|
66
|
+
def mirror_directory
|
67
|
+
return @mirror_directory ||= MirrorDirectory.new(gems_directory)
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# @return [String]
|
72
|
+
#
|
73
|
+
def gems_directory
|
74
|
+
return File.join(destination, 'gems')
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Returns a Hash containing various Gems to ignore and their versions.
|
79
|
+
#
|
80
|
+
# @return [Hash]
|
81
|
+
#
|
82
|
+
def ignored_gems
|
83
|
+
return @ignored_gems ||= Hash.new { |hash, key| hash[key] = [] }
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Adds a Gem to the list of Gems to ignore.
|
88
|
+
#
|
89
|
+
# @param [String] name
|
90
|
+
# @param [String] version
|
91
|
+
#
|
92
|
+
def ignore_gem(name, version)
|
93
|
+
ignored_gems[name] ||= []
|
94
|
+
ignored_gems[name] << version
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Checks if a Gem should be ignored.
|
99
|
+
#
|
100
|
+
# @param [String] name
|
101
|
+
# @param [String] version
|
102
|
+
# @return [TrueClass|FalseClass]
|
103
|
+
#
|
104
|
+
def ignore_gem?(name, version)
|
105
|
+
return ignored_gems[name].include?(version)
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Returns a list of sources to mirror.
|
110
|
+
#
|
111
|
+
# @return [Array]
|
112
|
+
#
|
113
|
+
def sources
|
114
|
+
return @sources ||= []
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# Adds a new source to mirror.
|
119
|
+
#
|
120
|
+
# @param [String] name
|
121
|
+
# @param [String] url
|
122
|
+
# @param [Proc] block
|
123
|
+
# @yieldparam [GemMirror::Source] source
|
124
|
+
#
|
125
|
+
def source(name, url, &block)
|
126
|
+
source = Source.new(name, url)
|
127
|
+
source.instance_eval(&block)
|
128
|
+
|
129
|
+
sources << source
|
130
|
+
end
|
131
|
+
end # Configuration
|
132
|
+
end # GemMirror
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module GemMirror
|
2
|
+
##
|
3
|
+
# The Gem class contains data about a Gem such as the name, requirement as
|
4
|
+
# well as providing some methods to more easily extract the specific version
|
5
|
+
# number.
|
6
|
+
#
|
7
|
+
# @!attribute [r] name
|
8
|
+
# @return [String]
|
9
|
+
# @!attribute [r] requirement
|
10
|
+
# @return [Gem::Requirement]
|
11
|
+
#
|
12
|
+
class Gem
|
13
|
+
attr_reader :name, :requirement
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns a `Gem::Version` instance based on the specified requirement.
|
17
|
+
#
|
18
|
+
# @param [Gem::Requirement] requirement
|
19
|
+
# @return [Gem::Version]
|
20
|
+
#
|
21
|
+
def self.version_for(requirement)
|
22
|
+
return ::Gem::Version.new(requirement.requirements.sort.last.last.version)
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# @param [String] name
|
27
|
+
# @param [Gem::Requirement] requirement
|
28
|
+
#
|
29
|
+
def initialize(name, requirement = nil)
|
30
|
+
@name = name
|
31
|
+
@requirement = requirement || ::Gem::Requirement.default
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# @return [Gem::Version]
|
36
|
+
#
|
37
|
+
def version
|
38
|
+
return @version ||= self.class.version_for(requirement)
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Returns the filename of the Gemfile.
|
43
|
+
#
|
44
|
+
# @param [String] gem_version
|
45
|
+
# @return [String]
|
46
|
+
#
|
47
|
+
def filename(gem_version = nil)
|
48
|
+
gem_version ||= version.to_s
|
49
|
+
|
50
|
+
return "#{name}-#{gem_version}.gem"
|
51
|
+
end
|
52
|
+
end # Gem
|
53
|
+
end # GemMirror
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module GemMirror
|
2
|
+
##
|
3
|
+
# The GemsFetcher class is responsible for downloading Gems from an external
|
4
|
+
# source as well as downloading all the associated dependencies.
|
5
|
+
#
|
6
|
+
# @!attribute [r] source
|
7
|
+
# @return [Source]
|
8
|
+
# @!attribute [r] versions_file
|
9
|
+
# @return [GemMirror::VersionsFile]
|
10
|
+
#
|
11
|
+
class GemsFetcher
|
12
|
+
attr_reader :source, :versions_file
|
13
|
+
|
14
|
+
##
|
15
|
+
# @param [Source] source
|
16
|
+
# @param [GemMirror::VersionsFile] versions_file
|
17
|
+
#
|
18
|
+
def initialize(source, versions_file)
|
19
|
+
@source = source
|
20
|
+
@versions_file = versions_file
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Fetches the Gems and all associated dependencies.
|
25
|
+
#
|
26
|
+
def fetch
|
27
|
+
source.gems.each do |gem|
|
28
|
+
versions_file.versions_for(gem.name).each do |version|
|
29
|
+
filename = gem.filename(version)
|
30
|
+
satisfied = gem.requirement.satisfied_by?(version)
|
31
|
+
name = gem.name
|
32
|
+
|
33
|
+
if gem_exists?(filename) or ignore_gem?(name, version) or !satisfied
|
34
|
+
logger.debug("Skipping #{filename}")
|
35
|
+
next
|
36
|
+
end
|
37
|
+
|
38
|
+
# Prevent circular dependencies from messing things up.
|
39
|
+
configuration.ignore_gem(gem.name, version)
|
40
|
+
|
41
|
+
spec = fetch_specification(gem, version)
|
42
|
+
|
43
|
+
next unless spec
|
44
|
+
|
45
|
+
spec = load_specification(spec)
|
46
|
+
deps = dependencies_for(spec)
|
47
|
+
|
48
|
+
unless deps.empty?
|
49
|
+
logger.info("Fetching dependencies for #{filename}")
|
50
|
+
|
51
|
+
fetch_dependencies(deps)
|
52
|
+
end
|
53
|
+
|
54
|
+
logger.info("Fetching #{filename}")
|
55
|
+
|
56
|
+
gemfile = fetch_gem(gem, version)
|
57
|
+
|
58
|
+
if gemfile
|
59
|
+
configuration.mirror_directory.add_file(filename, gemfile)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Tries to download the specification for a Gem and version. This method
|
67
|
+
# returns the raw inflated data instead of an instance of
|
68
|
+
# `Gem::Specification`.
|
69
|
+
#
|
70
|
+
# @param [GemMirror::Gem] gem
|
71
|
+
# @param [Gem::Version] version
|
72
|
+
# @return [String]
|
73
|
+
#
|
74
|
+
def fetch_specification(gem, version)
|
75
|
+
specification = nil
|
76
|
+
filename = gem.filename(version)
|
77
|
+
|
78
|
+
begin
|
79
|
+
specification = source.fetch_specification(gem.name, version)
|
80
|
+
rescue => e
|
81
|
+
logger.error("Failed to retrieve #{filename}: #{e.message}")
|
82
|
+
logger.debug("Adding #{filename} to the list of ignored Gems")
|
83
|
+
|
84
|
+
configuration.ignore_gem(gem.name, version)
|
85
|
+
end
|
86
|
+
|
87
|
+
return specification
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Tries to download the Gemfile for the specified Gem and version.
|
92
|
+
#
|
93
|
+
# @param [GemMirror::Gem] gem
|
94
|
+
# @param [Gem::Version] version
|
95
|
+
# @return [String]
|
96
|
+
#
|
97
|
+
def fetch_gem(gem, version)
|
98
|
+
gemfile = nil
|
99
|
+
filename = gem.filename(version)
|
100
|
+
|
101
|
+
begin
|
102
|
+
gemfile = source.fetch_gem(gem.name, version)
|
103
|
+
rescue => e
|
104
|
+
logger.error("Failed to retrieve #{filename}: #{e.message}")
|
105
|
+
logger.debug("Adding #{filename} to the list of ignored Gems")
|
106
|
+
|
107
|
+
configuration.ignore_gem(gem.name, version)
|
108
|
+
end
|
109
|
+
|
110
|
+
return gemfile
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Reads the inflated data of a Gemspec and returns the loaded specification
|
115
|
+
# instance.
|
116
|
+
#
|
117
|
+
# @param [String] raw_spec
|
118
|
+
# @return [Gem::Specification]
|
119
|
+
#
|
120
|
+
def load_specification(raw_spec)
|
121
|
+
stream = Zlib::Inflate.new
|
122
|
+
content = stream.inflate(raw_spec)
|
123
|
+
|
124
|
+
stream.finish
|
125
|
+
stream.close
|
126
|
+
|
127
|
+
return Marshal.load(content)
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Fetches the Gem files for the specified dependencies.
|
132
|
+
#
|
133
|
+
# @param [Array] deps
|
134
|
+
#
|
135
|
+
def fetch_dependencies(deps)
|
136
|
+
self.class.new(source.updated(deps), versions_file).fetch
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Returns an Array containing all the dependencies of a given Gem
|
141
|
+
# specification.
|
142
|
+
#
|
143
|
+
# @param [Gem::Specification] spec
|
144
|
+
# @return [Array]
|
145
|
+
#
|
146
|
+
def dependencies_for(spec)
|
147
|
+
dependencies = []
|
148
|
+
possible_dependencies = configuration.development ? spec.dependencies \
|
149
|
+
: spec.runtime_dependencies
|
150
|
+
|
151
|
+
possible_dependencies.each do |dependency|
|
152
|
+
gem = Gem.new(dependency.name, dependency.requirement)
|
153
|
+
|
154
|
+
unless ignore_gem?(gem.name, gem.version)
|
155
|
+
dependencies << gem
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
return dependencies
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# @see GemMirror::Configuration#logger
|
164
|
+
# @return [Logger]
|
165
|
+
#
|
166
|
+
def logger
|
167
|
+
return configuration.logger
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# @see GemMirror.configuration
|
172
|
+
#
|
173
|
+
def configuration
|
174
|
+
return GemMirror.configuration
|
175
|
+
end
|
176
|
+
|
177
|
+
##
|
178
|
+
# Checks if a given Gem has already been downloaded.
|
179
|
+
#
|
180
|
+
# @param [String] filename
|
181
|
+
# @return [TrueClass|FalseClass]
|
182
|
+
#
|
183
|
+
def gem_exists?(filename)
|
184
|
+
return configuration.mirror_directory.file_exists?(filename)
|
185
|
+
end
|
186
|
+
|
187
|
+
##
|
188
|
+
# @see GemMirror::Configuration#ignore_gem?
|
189
|
+
#
|
190
|
+
def ignore_gem?(*args)
|
191
|
+
return configuration.ignore_gem?(*args)
|
192
|
+
end
|
193
|
+
end # GemsFetcher
|
194
|
+
end # GemMirror
|