bowline-bundler 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.
Files changed (43) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +291 -0
  3. data/Rakefile +45 -0
  4. data/VERSION +1 -0
  5. data/bin/bowline-bundle +59 -0
  6. data/lib/bowline/bundler/bundle.rb +323 -0
  7. data/lib/bowline/bundler/cli.rb +87 -0
  8. data/lib/bowline/bundler/dependency.rb +62 -0
  9. data/lib/bowline/bundler/dsl.rb +182 -0
  10. data/lib/bowline/bundler/environment.rb +87 -0
  11. data/lib/bowline/bundler/finder.rb +51 -0
  12. data/lib/bowline/bundler/gem_bundle.rb +11 -0
  13. data/lib/bowline/bundler/gem_ext.rb +34 -0
  14. data/lib/bowline/bundler/remote_specification.rb +53 -0
  15. data/lib/bowline/bundler/resolver.rb +250 -0
  16. data/lib/bowline/bundler/runtime.rb +2 -0
  17. data/lib/bowline/bundler/source.rb +361 -0
  18. data/lib/bowline/bundler/templates/app_script.erb +3 -0
  19. data/lib/bowline/bundler/templates/environment.erb +156 -0
  20. data/lib/bowline/bundler/templates/environment_picker.erb +4 -0
  21. data/lib/bowline/bundler.rb +35 -0
  22. data/spec/bundler/cli_spec.rb +558 -0
  23. data/spec/bundler/directory_spec.rb +255 -0
  24. data/spec/bundler/dsl_spec.rb +126 -0
  25. data/spec/bundler/fetcher_spec.rb +138 -0
  26. data/spec/bundler/git_spec.rb +266 -0
  27. data/spec/bundler/installer_spec.rb +155 -0
  28. data/spec/bundler/manifest_file_spec.rb +105 -0
  29. data/spec/bundler/manifest_spec.rb +257 -0
  30. data/spec/bundler/runtime_spec.rb +141 -0
  31. data/spec/bundler/system_gems_spec.rb +42 -0
  32. data/spec/quality_spec.rb +57 -0
  33. data/spec/resolver/engine_spec.rb +112 -0
  34. data/spec/resolver/error_spec.rb +50 -0
  35. data/spec/resolver/fake_source_index_spec.rb +43 -0
  36. data/spec/resolver/prerelease_spec.rb +113 -0
  37. data/spec/spec_helper.rb +46 -0
  38. data/spec/support/builders.rb +257 -0
  39. data/spec/support/core_ext.rb +18 -0
  40. data/spec/support/helpers.rb +126 -0
  41. data/spec/support/matchers.rb +201 -0
  42. data/spec/support/path_utils.rb +63 -0
  43. metadata +126 -0
@@ -0,0 +1,323 @@
1
+ module Bundler
2
+ class InvalidRepository < StandardError ; end
3
+
4
+ class Bundle
5
+ attr_reader :gemfile, :environment
6
+
7
+ def self.load(gemfile = nil)
8
+ gemfile = Pathname.new(gemfile || default_gemfile).expand_path
9
+
10
+ unless gemfile.file?
11
+ raise ManifestFileNotFound, "Manifest file not found: #{gemfile.to_s.inspect}"
12
+ end
13
+
14
+ new(gemfile)
15
+ end
16
+
17
+ def self.default_gemfile
18
+ current = Pathname.new(Dir.pwd)
19
+
20
+ until current.root?
21
+ filename = current.join("Gemfile")
22
+ return filename if filename.exist?
23
+ current = current.parent
24
+ end
25
+
26
+ raise DefaultManifestNotFound
27
+ end
28
+
29
+ # TODO: passing in the filename is not good
30
+ def initialize(gemfile)
31
+ @gemfile = gemfile
32
+ @environment = Environment.new(self)
33
+ Dsl.evaluate(gemfile, self, @environment)
34
+
35
+ # path = env.gem_path
36
+
37
+ FileUtils.mkdir_p(gem_path)
38
+
39
+ @cache_path = gem_path.join('cache')
40
+ @cache = GemDirectorySource.new(self, :location => @cache_path)
41
+
42
+ @specs_path = gem_path.join('specifications')
43
+ @gems_path = gem_path.join('gems')
44
+ end
45
+
46
+ def root
47
+ gemfile.parent
48
+ end
49
+
50
+ def path
51
+ @path ||= root.join("vendor/gems")
52
+ end
53
+
54
+ def path=(path)
55
+ @path = (path.relative? ? root.join(path) : path).expand_path
56
+ end
57
+
58
+ def gem_path
59
+ path.join("#{Gem.ruby_engine}/#{Gem::ConfigMap[:ruby_version]}")
60
+ end
61
+
62
+ def bindir
63
+ @bindir ||= root.join("bin")
64
+ end
65
+
66
+ def bindir=(path)
67
+ @bindir = (path.relative? ? root.join(path) : path).expand_path
68
+ end
69
+
70
+ def install(options = {})
71
+ dependencies = @environment.dependencies
72
+ sources = @environment.sources
73
+
74
+ # ========== from env
75
+ if only_envs = options[:only]
76
+ dependencies.reject! { |d| !only_envs.any? {|env| d.in?(env) } }
77
+ end
78
+ # ==========
79
+
80
+ # TODO: clean this up
81
+ sources.each do |s|
82
+ s.local = options[:cached]
83
+ end
84
+
85
+ # Check to see whether the existing cache meets all the requirements
86
+ begin
87
+ valid = nil
88
+ # valid = Resolver.resolve(dependencies, [source_index], source_requirements)
89
+ rescue Bundler::GemNotFound
90
+ end
91
+
92
+ sources = only_local(sources) if options[:cached]
93
+
94
+ # Check the remote sources if the existing cache does not meet the requirements
95
+ # or the user passed --update
96
+ if options[:update] || !valid
97
+ Bundler.logger.info "Calculating dependencies..."
98
+ bundle = Resolver.resolve(dependencies, [@cache] + sources)
99
+ download(bundle, options)
100
+ do_install(bundle, options)
101
+ valid = bundle
102
+ end
103
+
104
+ generate_bins(valid, options)
105
+ cleanup(valid, options)
106
+ configure(valid, options)
107
+
108
+ Bundler.logger.info "Done."
109
+ end
110
+
111
+ def cache(*gemfiles)
112
+ FileUtils.mkdir_p(@cache_path)
113
+ gemfiles.each do |gemfile|
114
+ Bundler.logger.info "Caching: #{File.basename(gemfile)}"
115
+ FileUtils.cp(gemfile, @cache_path)
116
+ end
117
+ end
118
+
119
+ def list_outdated(options={})
120
+ outdated_gems = source_index.outdated.sort
121
+
122
+ if outdated_gems.empty?
123
+ Bundler.logger.info "All gems are up to date."
124
+ else
125
+ Bundler.logger.info "Outdated gems:"
126
+ outdated_gems.each do |name|
127
+ Bundler.logger.info " * #{name}"
128
+ end
129
+ end
130
+ end
131
+
132
+ def prune(options = {})
133
+ dependencies, sources = @environment.gem_dependencies, @environment.sources
134
+
135
+ sources.each do |s|
136
+ s.local = true
137
+ end
138
+
139
+ sources = only_local(sources)
140
+ bundle = Resolver.resolve(dependencies, [@cache] + sources)
141
+ @cache.gems.each do |name, specs|
142
+ specs.each do |spec|
143
+ unless bundle.any? { |s| s.name == spec.name && s.version == spec.version }
144
+ Bundler.logger.info "Pruning #{spec.name} (#{spec.version}) from the cache"
145
+ FileUtils.rm @cache_path.join("#{spec.full_name}.gem")
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def list(options = {})
152
+ Bundler.logger.info "Currently bundled gems:"
153
+ gems.each do |spec|
154
+ Bundler.logger.info " * #{spec.name} (#{spec.version})"
155
+ end
156
+ end
157
+
158
+ def gems
159
+ source_index.gems.values
160
+ end
161
+
162
+ def source_index
163
+ index = Gem::SourceIndex.from_gems_in(@specs_path)
164
+ index.each { |n, spec| spec.loaded_from = @specs_path.join("#{spec.full_name}.gemspec") }
165
+ index
166
+ end
167
+
168
+ def download_path_for(type)
169
+ @repos[type].download_path_for
170
+ end
171
+
172
+ def setup_environment
173
+ unless @environment.system_gems
174
+ ENV["GEM_HOME"] = gem_path
175
+ ENV["GEM_PATH"] = gem_path
176
+ end
177
+ ENV["PATH"] = "#{bindir}:#{ENV["PATH"]}"
178
+ ENV["RUBYOPT"] = "-r#{gem_path}/environment #{ENV["RUBYOPT"]}"
179
+ end
180
+
181
+ private
182
+
183
+ def only_local(sources)
184
+ sources.select { |s| s.can_be_local? }
185
+ end
186
+
187
+ def download(bundle, options)
188
+ bundle.sort_by {|s| s.full_name.downcase }.each do |spec|
189
+ next if spec.no_bundle?
190
+ spec.source.download(spec)
191
+ end
192
+ end
193
+
194
+ def do_install(bundle, options)
195
+ bundle.each do |spec|
196
+ next if spec.no_bundle?
197
+ spec.loaded_from = @specs_path.join("#{spec.full_name}.gemspec")
198
+ # Do nothing if the gem is already expanded
199
+ next if @gems_path.join(spec.full_name).directory?
200
+
201
+ case spec.source
202
+ when GemSource, GemDirectorySource, SystemGemSource
203
+ expand_gemfile(spec, options)
204
+ else
205
+ expand_vendored_gem(spec, options)
206
+ end
207
+ end
208
+ end
209
+
210
+ def generate_bins(bundle, options)
211
+ bundle.each do |spec|
212
+ next if spec.no_bundle?
213
+ # HAX -- Generate the bin
214
+ bin_dir = bindir
215
+ path = gem_path
216
+ gems_path = @gems_path
217
+ installer = Gem::Installer.allocate
218
+ installer.instance_eval do
219
+ @spec = spec
220
+ @bin_dir = bin_dir
221
+ @gem_dir = gems_path.join(spec.full_name)
222
+ @gem_home = path
223
+ @wrappers = true
224
+ @format_executable = false
225
+ @env_shebang = false
226
+ end
227
+ installer.generate_bin
228
+ end
229
+ end
230
+
231
+ def expand_gemfile(spec, options)
232
+ Bundler.logger.info "Installing #{spec.name} (#{spec.version})"
233
+
234
+ gemfile = @cache_path.join("#{spec.full_name}.gem").to_s
235
+
236
+ if build_args = options[:build_options] && options[:build_options][spec.name]
237
+ Gem::Command.build_args = build_args.map {|k,v| "--with-#{k}=#{v}"}
238
+ end
239
+
240
+ installer = Gem::Installer.new(gemfile, options.merge(
241
+ :install_dir => gem_path,
242
+ :ignore_dependencies => true,
243
+ :env_shebang => true,
244
+ :wrappers => true,
245
+ :bin_dir => bindir
246
+ ))
247
+ installer.install
248
+ rescue Gem::InstallError
249
+ cleanup_spec(spec)
250
+ raise
251
+ ensure
252
+ Gem::Command.build_args = []
253
+ end
254
+
255
+ def expand_vendored_gem(spec, options)
256
+ add_spec(spec)
257
+ FileUtils.mkdir_p(@gems_path)
258
+ File.symlink(spec.location, @gems_path.join(spec.full_name))
259
+ end
260
+
261
+ def add_spec(spec)
262
+ destination = @specs_path
263
+ destination.mkdir unless destination.exist?
264
+
265
+ File.open(destination.join("#{spec.full_name}.gemspec"), 'w') do |f|
266
+ f.puts spec.to_ruby
267
+ end
268
+ end
269
+
270
+ def cleanup(valid, options)
271
+ to_delete = gems
272
+ to_delete.delete_if do |spec|
273
+ valid.any? { |other| spec.name == other.name && spec.version == other.version }
274
+ end
275
+
276
+ valid_executables = valid.map { |s| s.executables }.flatten.compact
277
+
278
+ to_delete.each do |spec|
279
+ Bundler.logger.info "Deleting gem: #{spec.name} (#{spec.version})"
280
+ cleanup_spec(spec)
281
+ # Cleanup the bin directory
282
+ spec.executables.each do |bin|
283
+ next if valid_executables.include?(bin)
284
+ Bundler.logger.info "Deleting bin file: #{bin}"
285
+ FileUtils.rm_rf(bindir.join(bin))
286
+ end
287
+ end
288
+ end
289
+
290
+ def cleanup_spec(spec)
291
+ FileUtils.rm_rf(@specs_path.join("#{spec.full_name}.gemspec"))
292
+ FileUtils.rm_rf(@gems_path.join(spec.full_name))
293
+ end
294
+
295
+ def expand(options)
296
+ each_repo do |repo|
297
+ repo.expand(options)
298
+ end
299
+ end
300
+
301
+ def configure(specs, options)
302
+ FileUtils.mkdir_p(gem_path)
303
+
304
+ File.open(gem_path.join("environment.rb"), "w") do |file|
305
+ file.puts @environment.environment_rb(specs, options)
306
+ end
307
+
308
+ generate_environment_picker
309
+ end
310
+
311
+ def generate_environment_picker
312
+ FileUtils.cp("#{File.dirname(__FILE__)}/templates/environment_picker.erb", path.join("environment.rb"))
313
+ end
314
+
315
+ def require_code(file, dep)
316
+ constraint = case
317
+ when dep.only then %{ if #{dep.only.inspect}.include?(env)}
318
+ when dep.except then %{ unless #{dep.except.inspect}.include?(env)}
319
+ end
320
+ "require #{file.inspect}#{constraint}"
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,87 @@
1
+ require "optparse"
2
+
3
+ module Bundler
4
+ class CLI
5
+ def self.run(command, options = {})
6
+ new(options).run(command)
7
+ rescue DefaultManifestNotFound => e
8
+ Bundler.logger.error "Could not find a Gemfile to use"
9
+ exit 3
10
+ rescue InvalidEnvironmentName => e
11
+ Bundler.logger.error "Gemfile error: #{e.message}"
12
+ exit 4
13
+ rescue InvalidRepository => e
14
+ Bundler.logger.error e.message
15
+ exit 5
16
+ rescue VersionConflict => e
17
+ Bundler.logger.error e.message
18
+ exit 6
19
+ rescue GemNotFound => e
20
+ Bundler.logger.error e.message
21
+ exit 7
22
+ rescue InvalidCacheArgument => e
23
+ Bundler.logger.error e.message
24
+ exit 8
25
+ rescue SourceNotCached => e
26
+ Bundler.logger.error e.message
27
+ exit 9
28
+ rescue ManifestFileNotFound => e
29
+ Bundler.logger.error e.message
30
+ exit 10
31
+ end
32
+
33
+ def initialize(options)
34
+ @options = options
35
+ @bundle = Bundle.load(@options[:manifest])
36
+ end
37
+
38
+ def bundle
39
+ @bundle.install(@options)
40
+ end
41
+
42
+ def cache
43
+ gemfile = @options[:cache]
44
+
45
+ if File.extname(gemfile) == ".gem"
46
+ if !File.exist?(gemfile)
47
+ raise InvalidCacheArgument, "'#{gemfile}' does not exist."
48
+ end
49
+ @bundle.cache(gemfile)
50
+ elsif File.directory?(gemfile) || gemfile.include?('/')
51
+ if !File.directory?(gemfile)
52
+ raise InvalidCacheArgument, "'#{gemfile}' does not exist."
53
+ end
54
+ gemfiles = Dir["#{gemfile}/*.gem"]
55
+ if gemfiles.empty?
56
+ raise InvalidCacheArgument, "'#{gemfile}' contains no gemfiles"
57
+ end
58
+ @bundle.cache(*gemfiles)
59
+ else
60
+ raise InvalidCacheArgument, "w0t? '#{gemfile}' means nothing to me."
61
+ end
62
+ end
63
+
64
+ def prune
65
+ @bundle.prune(@options)
66
+ end
67
+
68
+ def list
69
+ @bundle.list(@options)
70
+ end
71
+
72
+ def list_outdated
73
+ @bundle.list_outdated(@options)
74
+ end
75
+
76
+ def exec
77
+ @bundle.setup_environment
78
+ # w0t?
79
+ super(*$command)
80
+ end
81
+
82
+ def run(command)
83
+ send(command)
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,62 @@
1
+ module Bundler
2
+ class InvalidEnvironmentName < StandardError; end
3
+
4
+ class Dependency < Gem::Dependency
5
+ attr_reader :name, :version, :require_as, :only, :except
6
+ attr_accessor :source
7
+
8
+ def initialize(name, options = {}, &block)
9
+ options.each do |k, v|
10
+ options[k.to_s] = v
11
+ end
12
+
13
+ super(name, options["version"] || ">= 0")
14
+
15
+ @require_as = options["require_as"]
16
+ @only = options["only"]
17
+ @except = options["except"]
18
+ @source = options["source"]
19
+ @block = block
20
+
21
+ if (@only && @only.include?("rubygems")) || (@except && @except.include?("rubygems"))
22
+ raise InvalidEnvironmentName, "'rubygems' is not a valid environment name"
23
+ end
24
+ end
25
+
26
+ def in?(environment)
27
+ environment = environment.to_s
28
+
29
+ return false unless !@only || @only.include?(environment)
30
+ return false if @except && @except.include?(environment)
31
+ true
32
+ end
33
+
34
+ def require_env(environment)
35
+ return unless in?(environment)
36
+
37
+ if @require_as
38
+ Array(@require_as).each { |file| require file }
39
+ else
40
+ begin
41
+ require name
42
+ rescue LoadError
43
+ # Do nothing
44
+ end
45
+ end
46
+
47
+ @block.call if @block
48
+ end
49
+
50
+ def no_bundle?
51
+ source == SystemGemSource.instance
52
+ end
53
+
54
+ def ==(o)
55
+ [name, version, require_as, only, except] ==
56
+ [o.name, o.version, o.require_as, o.only, o.except]
57
+ end
58
+
59
+ alias version version_requirements
60
+
61
+ end
62
+ end
@@ -0,0 +1,182 @@
1
+ module Bundler
2
+ class ManifestFileNotFound < StandardError; end
3
+ class InvalidKey < StandardError; end
4
+ class DefaultManifestNotFound < StandardError; end
5
+
6
+ class Dsl
7
+ def self.evaluate(file, bundle, environment)
8
+ builder = new(bundle, environment)
9
+ builder.instance_eval(File.read(file.to_s), file.to_s, 1)
10
+ environment
11
+ end
12
+
13
+ def initialize(bundle, environment)
14
+ @bundle = bundle
15
+ @environment = environment
16
+ @directory_sources = []
17
+ @git_sources = {}
18
+ @only, @except, @directory, @git = nil, nil, nil, nil
19
+ end
20
+
21
+ def bundle_path(path)
22
+ @bundle.path = Pathname.new(path)
23
+ end
24
+
25
+ def bin_path(path)
26
+ @bundle.bindir = Pathname.new(path)
27
+ end
28
+
29
+ def disable_rubygems
30
+ @environment.rubygems = false
31
+ end
32
+
33
+ def disable_system_gems
34
+ @environment.system_gems = false
35
+ end
36
+
37
+ def source(source)
38
+ source = GemSource.new(@bundle, :uri => source)
39
+ unless @environment.sources.include?(source)
40
+ @environment.add_source(source)
41
+ end
42
+ end
43
+
44
+ def only(*env)
45
+ old, @only = @only, _combine_only(env)
46
+ yield
47
+ @only = old
48
+ end
49
+
50
+ def except(*env)
51
+ old, @except = @except, _combine_except(env)
52
+ yield
53
+ @except = old
54
+ end
55
+
56
+ def directory(path, options = {})
57
+ raise DirectorySourceError, "cannot nest calls to directory or git" if @directory || @git
58
+ @directory = DirectorySource.new(@bundle, options.merge(:location => path))
59
+ @directory_sources << @directory
60
+ @environment.add_priority_source(@directory)
61
+ retval = yield if block_given?
62
+ @directory = nil
63
+ retval
64
+ end
65
+
66
+ def git(uri, options = {})
67
+ raise DirectorySourceError, "cannot nest calls to directory or git" if @directory || @git
68
+ @git = GitSource.new(@bundle, options.merge(:uri => uri))
69
+ @git_sources[uri] = @git
70
+ @environment.add_priority_source(@git)
71
+ retval = yield if block_given?
72
+ @git = nil
73
+ retval
74
+ end
75
+
76
+ def clear_sources
77
+ @environment.clear_sources
78
+ end
79
+
80
+ def gem(name, *args)
81
+ options = args.last.is_a?(Hash) ? args.pop : {}
82
+ version = args.last
83
+
84
+ keys = :vendored_at, :path, :only, :except, :git, :path, :bundle, :require_as, :tag, :branch, :ref
85
+ unless (invalid = options.keys - keys).empty?
86
+ raise InvalidKey, "Only #{keys.join(", ")} are valid options to #gem. You used #{invalid.join(", ")}"
87
+ end
88
+
89
+ if path = options.delete(:vendored_at)
90
+ options[:path] = path
91
+ warn "The :vendored_at option is deprecated. Use :path instead.\nFrom #{caller[0]}"
92
+ end
93
+
94
+ options[:only] = _combine_only(options[:only] || options["only"])
95
+ options[:except] = _combine_except(options[:except] || options["except"])
96
+
97
+ dep = Dependency.new(name, options.merge(:version => version))
98
+
99
+ if options.key?(:bundle) && !options[:bundle]
100
+ dep.source = SystemGemSource.new(@bundle)
101
+ elsif @git || options[:git]
102
+ dep.source = _handle_git_option(name, version, options)
103
+ elsif @directory || options[:path]
104
+ dep.source = _handle_vendored_option(name, version, options)
105
+ end
106
+
107
+ @environment.dependencies << dep
108
+ end
109
+
110
+ private
111
+
112
+ def _version?(version)
113
+ version && Gem::Version.new(version) rescue false
114
+ end
115
+
116
+ def _handle_vendored_option(name, version, options)
117
+ dir, path = _find_directory_source(options[:path])
118
+
119
+ if dir
120
+ dir.required_specs << name
121
+ dir.add_spec(path, name, version) if _version?(version)
122
+ dir
123
+ else
124
+ directory options[:path] do
125
+ _handle_vendored_option(name, version, {})
126
+ end
127
+ end
128
+ end
129
+
130
+ def _find_directory_source(path)
131
+ if @directory
132
+ return @directory, Pathname.new(path || '')
133
+ end
134
+
135
+ path = @bundle.gemfile.dirname.join(path)
136
+
137
+ @directory_sources.each do |s|
138
+ if s.location.expand_path.to_s < path.expand_path.to_s
139
+ return s, path.relative_path_from(s.location)
140
+ end
141
+ end
142
+
143
+ nil
144
+ end
145
+
146
+ def _handle_git_option(name, version, options)
147
+ git = options[:git].to_s
148
+ ref = options[:ref] || options[:tag]
149
+ branch = options[:branch]
150
+
151
+ if source = @git || @git_sources[git]
152
+ if ref && source.ref != ref
153
+ raise GitSourceError, "'#{git}' already specified with ref: #{source.ref}"
154
+ elsif branch && source.branch != branch
155
+ raise GitSourceError, "'#{git}' already specified with branch: #{source.branch}"
156
+ end
157
+
158
+ source.required_specs << name
159
+ source.add_spec(Pathname.new(options[:path] || '.'), name, version) if _version?(version)
160
+ source
161
+ else
162
+ git(git, :ref => ref, :branch => branch) do
163
+ _handle_git_option(name, version, options)
164
+ end
165
+ end
166
+ end
167
+
168
+ def _combine_only(only)
169
+ return @only unless only
170
+ only = Array(only).compact.uniq.map { |o| o.to_s }
171
+ only &= @only if @only
172
+ only
173
+ end
174
+
175
+ def _combine_except(except)
176
+ return @except unless except
177
+ except = Array(except).compact.uniq.map { |o| o.to_s }
178
+ except |= @except if @except
179
+ except
180
+ end
181
+ end
182
+ end