prebundler 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2cb3b9c01592cb314beb744cfe49b05daf72f4e7
4
+ data.tar.gz: acb37cc9d4fc50a278758ff20cb17746f17c864d
5
+ SHA512:
6
+ metadata.gz: 5aed2eee4563955bf5bc7f568a1b162b117c24d4ef06e62c38f3167901d42a0d2d5e92762866330535c914b737eb7a51aa499f924c516e9d72c2432c50b5e506
7
+ data.tar.gz: 9d0a8bd2b1953742cff48b7155f2d5901b80e595fbf33a98ffae8ca52b55e5191c538d6a0d97b6d60cd2ae4418a36c13eed9c6a1028df63ffcb0719787fd4885
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ 0.0.2
2
+ ===
3
+ - Better CLI interface.
4
+
5
+ 0.0.1
6
+ ===
7
+ - Birthday!
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry-byebug'
7
+ gem 'rake'
8
+ end
9
+
10
+ group :test do
11
+ gem 'rspec', '~> 3.0'
12
+ end
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ ## prebundler
2
+ Speed up gem installation by prebuilding gems and storing them in S3.
3
+
4
+ ## Installation
5
+
6
+ `gem install prebundler`
7
+
8
+ ### Why?
9
+
10
+ If you've ever worked on a large application that has hundreds of dependencies, you have probably felt the pain of running `bundle install` for the first time - it takes forever! This is especially true in the Docker world, where you're basically running `bundle install` for the first time every time. All your dependencies have to be reinstalled even if just one of them changes. This is because Docker images are built as a series of layers. If a layer changes, for example because it contains an altered Gemfile.lock, the entire layer (and all the layers that come after) have to be rebuilt.
11
+
12
+ Unfortunately bundler doesn't really have a great way of mitigating the problem of having to constantly reinstall a huge number of gems. Sure, you can pass the `--jobs` flag to `bundle install` which will fetch gems in parallel, but because of Ruby's global interpreter lock only the I/O parts of gem installation can be truly parallelized. The slowest gems to install are the ones that need to build native extensions (i.e. extensions written in C) as part of the installation process, which is entirely CPU-bound and can't be parallelized.
13
+
14
+ So what are your options?
15
+
16
+ 1. **Depend on fewer gems**. Totally valid but often impractical, especially since you may depend on a lot of gems _transiently_, meaning they're pulled in by the dependencies of your dependencies.
17
+
18
+ 2. **Vendor everything**. In other words, store copies of all your gems in the `vendor` directory. That way, you don't have to even run `bundle install` during the Docker build. It'll work great as long as you use the same operating system and architecture in development, production, staging, etc or don't depend on gems with native extensions. By and large, native extensions must be built specifically for the architecture they will run on, meaning the same native extension that runs on Mac OS will not run on Ubuntu Linux.
19
+
20
+ 3. **Bear the pain**. Sure, you could use long `bundle install` times as an [excuse to have fun](https://xkcd.com/303/), but if bundling is, for example, blocking you from releasing a new feature or hotfix then you might be hoping we can do better. Hint: we can.
21
+
22
+ ### Enter Prebundler
23
+
24
+ Prebundler speeds up gem dependency installation by caching the results of installing each gem, including the results from building native extensions. The first time you run prebundler, it:
25
+
26
+ 1. Installs each gem in Gemfile.lock one at a time
27
+ 2. Puts the resulting files into a tarball (eg. nokogiri.tar)
28
+ 3. Uploads the tarballs to S3 or some other storage backend
29
+
30
+ The second time you run prebundler, it:
31
+
32
+ 1. Sucks down gems en masse from S3 (i.e. lots of parallel I/O)
33
+ 2. Untars the gems straight onto the filesystem
34
+ 3. Runs `bundle check` and falls back to `bundle install` if anything's missing
35
+
36
+ ### Ok, but does it work?
37
+
38
+ For the main repository behind lumosity.com which contains 445 gem dependencies, prebundler reduced the amount of time spent bundling from 6 minutes 7 seconds to 43 seconds on Travis CI. That's an 88% speed increase. On my laptop with 16 cores and an enterprise-grade internet connection, prebundler finished installing the same set of 445 gems in a little over 15 seconds.
39
+
40
+ Oh I see. You were asking if it installs the gems correctly. The answer is yes. Running `bundle check` found no missing dependencies, and the app can successfully boot and serve traffic.
41
+
42
+ Ah sorry. You were asking if it's production ready. I'm not ready to make that claim. Prebundler needs to be tested by more people with more diverse use cases before I might consider it production ready. You can help me with that.
43
+
44
+ ### You've convinced me. How do I give Prebundler a try?
45
+
46
+ So glad you asked. First, create a file called `.prebundle_config` in your repo, usually in the same directory as your Gemfile.lock:
47
+
48
+ ```ruby
49
+ Prebundler.configure do |config|
50
+ config.storage_backend = Prebundler::S3Backend.new(
51
+ access_key_id: ENV['S3_ACCESS_KEY'],
52
+ secret_access_key: ENV['S3_SECRET_KEY'],
53
+ bucket: 'my-sweet-bucket',
54
+ region: 'us-east-1' # or whatever
55
+ )
56
+ end
57
+ ```
58
+
59
+ Next, modify your Dockerfile to use prebundler instead of bundler (although bundler should be installed too):
60
+
61
+ ```dockerfile
62
+ ARG S3_ACCESS_KEY=required # placeholder
63
+ ARG S3_SECRET_KEY=required # placeholder
64
+ ENV S3_ACCESS_KEY ${S3_ACCESS_KEY} # copy build arg into ENV
65
+ ENV S3_SECRET_KEY ${S3_SECRET_KEY} # copy build arg into ENV
66
+
67
+ RUN gem install bundler
68
+ RUN gem install prebundler && prebundle install
69
+ ```
70
+
71
+ Now when you build your Docker image, you'll need to pass two additional build arguments:
72
+
73
+ ```sh
74
+ docker build \
75
+ --build-arg S3_ACCESS_KEY=... \
76
+ --build-arg S3_SECRET_KEY=... \
77
+ -t registry.tld/organization/repo:tag \
78
+ ./
79
+ ```
80
+
81
+ Then sit back and watch the magic, keeping in mind of course that the first time you do a Docker build prebundler will be extremely slow. Run it again and watch it fly.
82
+
83
+ ### Disclaimer
84
+
85
+ Remember, everybody's applications are a little different. What works for one doesn't necessarily work for the other. If you don't depend on that many gems with native extensions, then prebundler might not help you very much. I guess what I'm trying to say is, your mileage may vary.
86
+
87
+ ## License
88
+
89
+ Licensed under the MIT license. See LICENSE for details.
90
+
91
+ ## Authors
92
+
93
+ * Cameron C. Dutro: http://github.com/camertron
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
4
+
5
+ require 'bundler'
6
+ require 'rspec/core/rake_task'
7
+ require 'rubygems/package_task'
8
+
9
+ require 'prebundler'
10
+
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ task default: :spec
14
+
15
+ desc 'Run specs'
16
+ RSpec::Core::RakeTask.new do |t|
17
+ t.pattern = './spec/**/*_spec.rb'
18
+ end
data/bin/prebundle ADDED
@@ -0,0 +1,87 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'gli'
4
+ require 'etc'
5
+ require 'bundler'
6
+ require 'prebundler'
7
+ require 'prebundler/version'
8
+
9
+ # @TODO: remove
10
+ require 'pry-byebug'
11
+
12
+ $out = Prebundler::WritePipe.new
13
+
14
+ include GLI::App
15
+
16
+ program_desc 'Gem dependency prebuilder'
17
+
18
+ version Prebundler::VERSION
19
+
20
+ subcommand_option_handling :normal
21
+ arguments :strict
22
+
23
+ desc "Don't log to stdout"
24
+ switch [:s, :silent]
25
+
26
+ desc 'Path to config file.'
27
+ default_value './.prebundle_config'
28
+ flag [:c, :config]
29
+
30
+ desc 'Install gems from the Gemfile.lock.'
31
+ command :install do |c|
32
+ c.desc 'Maximum number of parallel gem installs.'
33
+ c.default_value Etc.nprocessors
34
+ c.flag [:j, :jobs], type: Integer
35
+
36
+ c.desc 'Path to the gemfile to install gems from.'
37
+ c.default_value ENV.fetch('BUNDLE_GEMFILE', './Gemfile')
38
+ c.flag [:g, :gemfile]
39
+
40
+ c.desc 'Path to the bundle installation directory.'
41
+ c.default_value ENV.fetch('BUNDLE_PATH', Bundler.bundle_path.to_s)
42
+ c.flag [:b, :'bundle-path']
43
+
44
+ c.action do |global_options, options, args|
45
+ raise 'Must specify a non-zero number of jobs' if options[:jobs] < 1
46
+ Prebundler::Cli::Install.run($out, global_options, options, args)
47
+ end
48
+ end
49
+
50
+ desc 'List each gem and associated source.'
51
+ command :list do |c|
52
+ c.desc 'Path to the gemfile.'
53
+ c.default_value ENV.fetch('BUNDLE_GEMFILE', './Gemfile')
54
+ c.flag [:g, :gemfile]
55
+
56
+ c.desc 'Filter by source. Will perform partial matching.'
57
+ c.flag [:s, :source], multiple: true
58
+
59
+ c.action do |global_options, options, args|
60
+ Prebundler::Cli::List.run($out, global_options, options, args)
61
+ end
62
+ end
63
+
64
+ pre do |global, command, options, args|
65
+ # Pre logic here
66
+ # Return true to proceed; false to abort and not call the
67
+ # chosen command
68
+ # Use skips_pre before a command to skip this block
69
+ # on that command only
70
+ $out.silence! if global[:silent]
71
+ load global[:config]
72
+ true
73
+ end
74
+
75
+ post do |global, command, options, args|
76
+ # Post logic here
77
+ # Use skips_post before a command to skip this
78
+ # block on that command only
79
+ end
80
+
81
+ on_error do |exception|
82
+ # Error logic here
83
+ # return false to skip default error handling
84
+ true
85
+ end
86
+
87
+ exit run(ARGV)
@@ -0,0 +1,24 @@
1
+ module Prebundler
2
+ module Cli
3
+ class Base
4
+ class << self
5
+ def run(out, global_options, options, args)
6
+ new(out, global_options, options, args).run
7
+ end
8
+ end
9
+
10
+ attr_reader :out, :global_options, :options, :args
11
+
12
+ def initialize(out, global_options, options, args)
13
+ @out = out
14
+ @global_options = global_options
15
+ @options = options
16
+ @args = args
17
+ end
18
+
19
+ def run
20
+ raise NotImplementedError, "please define '#{__method__}' in derived classes"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,105 @@
1
+ require 'securerandom'
2
+ require 'tmpdir'
3
+ require 'parallel'
4
+ require 'bundler'
5
+
6
+ module Prebundler
7
+ module Cli
8
+ class Install < Base
9
+ def run
10
+ prepare
11
+ install
12
+ check
13
+ end
14
+
15
+ private
16
+
17
+ def prepare
18
+ gem_list.each do |_, gem_ref|
19
+ next if backend_file_list.include?(gem_ref.tar_file)
20
+ install_gem(gem_ref) if gem_ref.installable?
21
+ end
22
+ end
23
+
24
+ def install
25
+ if options[:jobs] <= 1
26
+ install_in_serial
27
+ else
28
+ install_in_parallel
29
+ end
30
+ end
31
+
32
+ def install_in_serial
33
+ gem_list.each do |_, gem_ref|
34
+ install_gem_ref(gem_ref)
35
+ end
36
+ end
37
+
38
+ def install_in_parallel
39
+ Parallel.each(gem_list, in_processes: options[:jobs]) do |_, gem_ref|
40
+ install_gem_ref(gem_ref)
41
+ end
42
+ end
43
+
44
+ def install_gem_ref(gem_ref)
45
+ return unless gem_ref.installable?
46
+
47
+ unless File.exist?(gem_ref.install_dir)
48
+ install_gem(gem_ref)
49
+ end
50
+
51
+ out.puts "Installed #{gem_ref.id}"
52
+ end
53
+
54
+ def install_gem(gem_ref)
55
+ dest_file = File.join(Dir.tmpdir, "#{SecureRandom.hex}.tar")
56
+
57
+ if backend_file_list.include?(gem_ref.tar_file)
58
+ out.puts "Installing #{gem_ref.id} from backend"
59
+ config.storage_backend.retrieve_file(gem_ref.tar_file, dest_file)
60
+ gem_ref.install_from_tar(dest_file)
61
+ FileUtils.rm(dest_file)
62
+ else
63
+ out.puts "Installing #{gem_ref.id} from source"
64
+ gem_ref.install
65
+ store_gem(gem_ref, dest_file) if gem_ref.storable?
66
+ end
67
+ end
68
+
69
+ def store_gem(gem_ref, dest_file)
70
+ out.puts "Storing #{gem_ref.id}"
71
+ gem_ref.add_to_tar(dest_file)
72
+ config.storage_backend.store_file(dest_file, gem_ref.tar_file)
73
+ end
74
+
75
+ def check
76
+ system 'bundle check'
77
+
78
+ if $?.exitstatus != 0
79
+ out.puts 'Bundle not satisfied, falling back to `bundle install`'
80
+ system 'bundle install'
81
+ end
82
+ end
83
+
84
+ def gem_list
85
+ @gem_list ||= Prebundler::GemfileInterpreter.interpret(gemfile_path, bundle_path)
86
+ end
87
+
88
+ def backend_file_list
89
+ @backend_file_list ||= config.storage_backend.list_files
90
+ end
91
+
92
+ def gemfile_path
93
+ options.fetch(:gemfile)
94
+ end
95
+
96
+ def bundle_path
97
+ options.fetch(:'bundle-path')
98
+ end
99
+
100
+ def config
101
+ Prebundler.config
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,31 @@
1
+ module Prebundler
2
+ module Cli
3
+ class List < Base
4
+ def run
5
+ gem_list.each do |_, gem_ref|
6
+ next unless show_gem?(gem_ref)
7
+ out.puts "#{gem_ref.id} from #{gem_ref.source}"
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def show_gem?(gem_ref)
14
+ return true if options[:source].empty?
15
+ options[:source].any? { |source| gem_ref.source.include?(source) }
16
+ end
17
+
18
+ def gem_list
19
+ @gem_list ||= Prebundler::GemfileInterpreter.interpret(gemfile_path, bundle_path)
20
+ end
21
+
22
+ def gemfile_path
23
+ options.fetch(:gemfile)
24
+ end
25
+
26
+ def bundle_path
27
+ nil # not necessary for resolution
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module Prebundler
2
+ module Cli
3
+ autoload :Base, 'prebundler/cli/base'
4
+ autoload :Install, 'prebundler/cli/install'
5
+ autoload :List, 'prebundler/cli/list'
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require 'shellwords'
2
+
3
+ module Prebundler
4
+ class Configurator
5
+ attr_accessor :storage_backend, :gem_sources
6
+
7
+ def initialize
8
+ @gem_sources = []
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ require 'fileutils'
2
+
3
+ module Prebundler
4
+ class FileBackend
5
+ attr_reader :local_path, :docker_mount_point
6
+
7
+ def initialize(options = {})
8
+ @local_path = options.fetch(:local_path)
9
+ @docker_mount_point = options.fetch(:docker_mount_point)
10
+ end
11
+
12
+ def store_file(source_file, dest_file)
13
+ FileUtils.cp(source_file, File.join(docker_mount_point, dest_file))
14
+ end
15
+
16
+ def retrieve_file(source_file, dest_file)
17
+ FileUtils.cp(File.join(local_path, source_file), dest_file)
18
+ end
19
+
20
+ def list_files
21
+ Dir.chdir(docker_mount_point) { Dir.glob('**/*.*') }
22
+ end
23
+
24
+ def docker_flags
25
+ ['-v', "#{File.expand_path(local_path)}:#{docker_mount_point}"]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,111 @@
1
+ require 'fileutils'
2
+
3
+ module Prebundler
4
+ class GemRef
5
+ REF_TYPES = [PathGemRef, GitGemRef]
6
+ DEFAULT_SOURCE = 'https://rubygems.org'
7
+
8
+ class << self
9
+ def create(name, bundle_path, options = {})
10
+ ref_type = REF_TYPES.find { |rt| rt.accepts?(options) } || self
11
+ ref_type.new(name, bundle_path, options)
12
+ end
13
+ end
14
+
15
+ attr_reader :name, :bundle_path
16
+ attr_accessor :spec, :dependencies
17
+
18
+ def initialize(name, bundle_path, options = {})
19
+ @name = name
20
+ @bundle_path = bundle_path
21
+ @groups = options[:groups]
22
+ @source = options[:source]
23
+ @dependencies = options[:dependencies]
24
+ end
25
+
26
+ def dependencies
27
+ @dependencies ||= []
28
+ end
29
+
30
+ def groups
31
+ @groups ||= []
32
+ end
33
+
34
+ def source
35
+ @source ||= DEFAULT_SOURCE
36
+ end
37
+
38
+ def id
39
+ "#{name}-#{version}"
40
+ end
41
+
42
+ def version
43
+ spec.version.to_s
44
+ end
45
+
46
+ def install
47
+ system "gem install -N --ignore-dependencies --source #{source} #{name} -v #{version}"
48
+ $?.exitstatus == 0
49
+ end
50
+
51
+ def install_from_tar(tar_file)
52
+ system "tar -C #{bundle_path} -xf #{tar_file}"
53
+ $?.exitstatus == 0
54
+ end
55
+
56
+ def add_to_tar(tar_file)
57
+ tar_flags = File.exist?(tar_file) ? '-rf' : '-cf'
58
+
59
+ system "tar -C #{bundle_path} #{tar_flags} #{tar_file} #{relative_gem_dir}"
60
+ system "tar -C #{bundle_path} -rf #{tar_file} #{relative_gemspec_dir}"
61
+
62
+ if File.directory?(extension_dir)
63
+ system "tar -C #{bundle_path} -rf #{tar_file} #{relative_extension_dir}"
64
+ end
65
+ end
66
+
67
+ def installable?
68
+ true
69
+ end
70
+
71
+ def storable?
72
+ true
73
+ end
74
+
75
+ def install_path
76
+ File.join(bundle_path, 'gems')
77
+ end
78
+
79
+ def spec_path
80
+ File.join(bundle_path, 'specifications')
81
+ end
82
+
83
+ def install_dir
84
+ File.join(install_path, id)
85
+ end
86
+
87
+ def extension_dir
88
+ File.join(bundle_path, relative_extension_dir)
89
+ end
90
+
91
+ def relative_extension_dir
92
+ File.join('extensions', Bundler.local_platform.to_s, Gem.extension_api_version.to_s, id)
93
+ end
94
+
95
+ def relative_gem_dir
96
+ File.join('gems', id)
97
+ end
98
+
99
+ def relative_gemspec_dir
100
+ File.join('specifications', gemspec_file)
101
+ end
102
+
103
+ def tar_file
104
+ File.join(Bundler.local_platform.to_s, Gem.extension_api_version.to_s, "#{id}.tar")
105
+ end
106
+
107
+ def gemspec_file
108
+ "#{id}.gemspec"
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,36 @@
1
+ require 'tsort'
2
+
3
+ module Prebundler
4
+ class Gemfile
5
+ include TSort
6
+ include Enumerable
7
+
8
+ attr_reader :gems
9
+
10
+ def initialize(gems)
11
+ @gems = gems
12
+ end
13
+
14
+ def each
15
+ return to_enum(__method__) unless block_given?
16
+
17
+ tsort_each do |name|
18
+ yield name, gems[name]
19
+ end
20
+ end
21
+
22
+ alias_method :each_pair, :each
23
+
24
+ private
25
+
26
+ def tsort_each_node(&block)
27
+ gems.keys.each(&block)
28
+ end
29
+
30
+ def tsort_each_child(name, &block)
31
+ gems[name].dependencies.each do |dep|
32
+ yield dep if gems.include?(dep)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ module Prebundler
2
+ class GemfileInterpreter
3
+ def self.interpret(path, bundle_path)
4
+ Gemfile.new(new(path, bundle_path).gems)
5
+ end
6
+
7
+ attr_reader :gems, :bundle_path
8
+
9
+ def initialize(path, bundle_path)
10
+ @gems = {}
11
+ @current_groups = []
12
+ @bundle_path = bundle_path
13
+ instance_eval(File.read(path))
14
+
15
+ lockfile = Bundler::LockfileParser.new(File.read("#{path}.lock"))
16
+
17
+ lockfile.specs.each do |spec|
18
+ gems[spec.name] ||= GemRef.create(spec.name, bundle_path)
19
+ gems[spec.name].spec = spec
20
+ gems[spec.name].dependencies = spec.dependencies.map(&:name)
21
+ end
22
+ end
23
+
24
+ def current_context
25
+ {
26
+ path: @current_path,
27
+ groups: @current_groups,
28
+ source: @current_source
29
+ }
30
+ end
31
+
32
+ def gem(name, *args)
33
+ options = args.find { |a| a.is_a?(Hash) } || {}
34
+ gems[name] = GemRef.create(name, bundle_path, current_context.merge(options))
35
+ end
36
+
37
+ def path(dir)
38
+ @current_path = dir
39
+ yield if block_given?
40
+ @current_path = nil
41
+ end
42
+
43
+ def source(url)
44
+ @current_source = url
45
+ yield if block_given?
46
+ @current_source = nil
47
+ end
48
+
49
+ def group(*groups)
50
+ @current_groups = groups
51
+ yield if block_given?
52
+ @current_groups = []
53
+ end
54
+
55
+ def gemspec
56
+ # do nothing
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,106 @@
1
+ require 'fileutils'
2
+ require 'uri'
3
+ require 'digest/sha1'
4
+
5
+ module Prebundler
6
+ class GitGemRef < GemRef
7
+ class << self
8
+ def accepts?(options)
9
+ options.include?(:git) || options.include?(:github)
10
+ end
11
+ end
12
+
13
+ attr_reader :strategy
14
+
15
+ def initialize(name, bundle_path, options = {})
16
+ super
17
+ @strategy = options.include?(:git) ? :git : :github
18
+ @uri = options[@strategy]
19
+ end
20
+
21
+ def install
22
+ return if File.exist?(cache_dir) || File.exist?(install_dir)
23
+ system "git clone #{uri} \"#{cache_dir}\" --bare --no-hardlinks --quiet"
24
+ return $? if $?.exitstatus != 0
25
+ system "git clone --no-checkout --quiet \"#{cache_dir}\" \"#{install_dir}\""
26
+ return $? if $?.exitstatus != 0
27
+ Dir.chdir(install_dir) { system "git reset --hard --quiet #{revision}" }
28
+ serialize_gemspecs
29
+ copy_gemspecs
30
+ $?
31
+ end
32
+
33
+ def version
34
+ revision[0...12]
35
+ end
36
+
37
+ def installable?
38
+ true
39
+ end
40
+
41
+ def storable?
42
+ false
43
+ end
44
+
45
+ def install_path
46
+ File.join(bundle_path, 'bundler', 'gems')
47
+ end
48
+
49
+ def cache_path
50
+ File.join(bundle_path, 'cache', 'bundler', 'git')
51
+ end
52
+
53
+ def cache_dir
54
+ File.join(cache_path, "#{name}-#{uri_hash}")
55
+ end
56
+
57
+ def uri
58
+ if strategy == :github
59
+ "git://github.com/#{@uri}.git"
60
+ else
61
+ @uri
62
+ end
63
+ end
64
+
65
+ alias_method :source, :uri
66
+
67
+ def revision
68
+ spec.source.revision
69
+ end
70
+
71
+ private
72
+
73
+ def copy_gemspecs
74
+ FileUtils.cp(Dir[File.join(install_dir, '*.gemspec')], spec_path)
75
+ end
76
+
77
+ # adapted from
78
+ # https://github.com/bundler/bundler/blob/fea23637886c1b1bde471c98344b8844f82e60ce/lib/bundler/source/git.rb#L237
79
+ def serialize_gemspecs
80
+ Dir[File.join(install_dir, '*.gemspec')].each do |path|
81
+ # Evaluate gemspecs and cache the result. Gemspecs
82
+ # in git might require git or other dependencies.
83
+ # The gemspecs we cache should already be evaluated.
84
+ spec = Bundler.load_gemspec(path)
85
+ next unless spec
86
+ Bundler.rubygems.set_installed_by_version(spec)
87
+ # Bundler.rubygems.validate(spec)
88
+ File.open(path, 'wb') { |file| file.write(spec.to_ruby) }
89
+ end
90
+ end
91
+
92
+ # copied from
93
+ # https://github.com/bundler/bundler/blob/fea23637886c1b1bde471c98344b8844f82e60ce/lib/bundler/source/git.rb#L281
94
+ def uri_hash
95
+ if uri =~ %r{^\w+://(\w+@)?}
96
+ # Downcase the domain component of the URI
97
+ # and strip off a trailing slash, if one is present
98
+ input = URI.parse(uri).normalize.to_s.sub(%r{/$}, "")
99
+ else
100
+ # If there is no URI scheme, assume it is an ssh/git URI
101
+ input = uri
102
+ end
103
+ Digest::SHA1.hexdigest(input)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,26 @@
1
+ module Prebundler
2
+ class PathGemRef < GemRef
3
+ class << self
4
+ def accepts?(options)
5
+ !!options[:path]
6
+ end
7
+ end
8
+
9
+ attr_reader :path
10
+
11
+ def initialize(name, bundle_path, options = {})
12
+ super
13
+ @path = options[:path]
14
+ end
15
+
16
+ def installable?
17
+ false
18
+ end
19
+
20
+ def storable?
21
+ false
22
+ end
23
+
24
+ alias_method :source, :path
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ require 'aws-sdk'
2
+
3
+ module Prebundler
4
+ class S3Backend
5
+ attr_reader :access_key_id, :secret_access_key, :bucket, :region
6
+
7
+ def initialize(options = {})
8
+ @access_key_id = options.fetch(:access_key_id)
9
+ @secret_access_key = options.fetch(:secret_access_key)
10
+ @bucket = options.fetch(:bucket)
11
+ @region = options.fetch(:region)
12
+ end
13
+
14
+ def store_file(source_file, dest_file)
15
+ File.open(source_file) do |io|
16
+ client.put_object(bucket: bucket, key: dest_file, body: io)
17
+ end
18
+ end
19
+
20
+ def retrieve_file(source_file, dest_file)
21
+ client.get_object(
22
+ bucket: bucket,
23
+ key: source_file,
24
+ response_target: dest_file
25
+ )
26
+ end
27
+
28
+ def list_files
29
+ truncated = true
30
+ continuation_token = nil
31
+ files = []
32
+ base_options = {
33
+ bucket: bucket,
34
+ prefix: "#{Bundler.local_platform.to_s}/#{Gem.extension_api_version.to_s}"
35
+ }
36
+
37
+ while truncated
38
+ options = if continuation_token
39
+ base_options.merge(continuation_token: continuation_token)
40
+ else
41
+ base_options
42
+ end
43
+
44
+ response = client.list_objects_v2(options)
45
+ truncated = response.is_truncated
46
+ continuation_token = response.continuation_token
47
+
48
+ response.contents.each do |file|
49
+ files << file.key
50
+ end
51
+ end
52
+
53
+ files
54
+ end
55
+
56
+ def docker_flags
57
+ []
58
+ end
59
+
60
+ private
61
+
62
+ def client
63
+ @client ||= Aws::S3::Client.new(region: region, credentials: credentials)
64
+ end
65
+
66
+ def credentials
67
+ @credentials ||= Aws::Credentials.new(access_key_id, secret_access_key)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,3 @@
1
+ module Prebundler
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,26 @@
1
+ module Prebundler
2
+ class WritePipe
3
+ def initialize
4
+ @silent = false
5
+ end
6
+
7
+ def write(text)
8
+ return if silent?
9
+ STDOUT.write(text)
10
+ end
11
+
12
+ def puts(text)
13
+ return if silent?
14
+ STDOUT.write("#{text}\n")
15
+ end
16
+
17
+ def silence!
18
+ @silent = true
19
+ self
20
+ end
21
+
22
+ def silent?
23
+ !!@silent
24
+ end
25
+ end
26
+ end
data/lib/prebundler.rb ADDED
@@ -0,0 +1,26 @@
1
+ module Prebundler
2
+ autoload :Cli, 'prebundler/cli'
3
+ autoload :Configurator, 'prebundler/configurator'
4
+ autoload :FileBackend, 'prebundler/file_backend'
5
+ autoload :PathGemRef, 'prebundler/path_gem_ref'
6
+ autoload :Gemfile, 'prebundler/gemfile'
7
+ autoload :GemfileInterpreter, 'prebundler/gemfile_interpreter'
8
+ autoload :GemRef, 'prebundler/gem_ref'
9
+ autoload :GitGemRef, 'prebundler/git_gem_ref'
10
+ autoload :S3Backend, 'prebundler/s3_backend'
11
+ autoload :WritePipe, 'prebundler/write_pipe'
12
+
13
+ class << self
14
+ attr_reader :config
15
+
16
+ def configure
17
+ return if configured?
18
+ @config = Configurator.new
19
+ yield @config
20
+ end
21
+
22
+ def configured?
23
+ !!@config
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
+ require 'prebundler/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'prebundler'
6
+ s.version = ::Prebundler::VERSION
7
+ s.authors = ['Cameron Dutro']
8
+ s.email = ['camertron@gmail.com']
9
+ s.homepage = 'http://github.com/camertron'
10
+
11
+ s.description = s.summary = 'Gem dependency prebuilder'
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+ s.has_rdoc = true
15
+
16
+ s.add_dependency 'bundler'
17
+ s.add_dependency 'parallel', '~> 1.0'
18
+ s.add_dependency 'gli', '~> 2.0'
19
+
20
+ # @TODO: remove these, maybe move s3 support into separate gem?
21
+ s.add_dependency 'aws-sdk', '~> 2.0'
22
+ s.add_dependency 'pry-byebug'
23
+
24
+ s.executables << 'prebundle'
25
+
26
+ s.require_path = 'lib'
27
+ s.files = Dir['{lib,spec}/**/*', 'Gemfile', 'CHANGELOG.md', 'README.md', 'Rakefile', 'prebundler.gemspec']
28
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prebundler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-11-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parallel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: gli
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: aws-sdk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Gem dependency prebuilder
84
+ email:
85
+ - camertron@gmail.com
86
+ executables:
87
+ - prebundle
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - CHANGELOG.md
92
+ - Gemfile
93
+ - README.md
94
+ - Rakefile
95
+ - bin/prebundle
96
+ - lib/prebundler.rb
97
+ - lib/prebundler/cli.rb
98
+ - lib/prebundler/cli/base.rb
99
+ - lib/prebundler/cli/install.rb
100
+ - lib/prebundler/cli/list.rb
101
+ - lib/prebundler/configurator.rb
102
+ - lib/prebundler/file_backend.rb
103
+ - lib/prebundler/gem_ref.rb
104
+ - lib/prebundler/gemfile.rb
105
+ - lib/prebundler/gemfile_interpreter.rb
106
+ - lib/prebundler/git_gem_ref.rb
107
+ - lib/prebundler/path_gem_ref.rb
108
+ - lib/prebundler/s3_backend.rb
109
+ - lib/prebundler/version.rb
110
+ - lib/prebundler/write_pipe.rb
111
+ - prebundler.gemspec
112
+ homepage: http://github.com/camertron
113
+ licenses: []
114
+ metadata: {}
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 2.6.13
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: Gem dependency prebuilder
135
+ test_files: []