prebundler 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +12 -0
- data/README.md +93 -0
- data/Rakefile +18 -0
- data/bin/prebundle +87 -0
- data/lib/prebundler/cli/base.rb +24 -0
- data/lib/prebundler/cli/install.rb +105 -0
- data/lib/prebundler/cli/list.rb +31 -0
- data/lib/prebundler/cli.rb +7 -0
- data/lib/prebundler/configurator.rb +11 -0
- data/lib/prebundler/file_backend.rb +28 -0
- data/lib/prebundler/gem_ref.rb +111 -0
- data/lib/prebundler/gemfile.rb +36 -0
- data/lib/prebundler/gemfile_interpreter.rb +59 -0
- data/lib/prebundler/git_gem_ref.rb +106 -0
- data/lib/prebundler/path_gem_ref.rb +26 -0
- data/lib/prebundler/s3_backend.rb +70 -0
- data/lib/prebundler/version.rb +3 -0
- data/lib/prebundler/write_pipe.rb +26 -0
- data/lib/prebundler.rb +26 -0
- data/prebundler.gemspec +28 -0
- metadata +135 -0
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
data/Gemfile
ADDED
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,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,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
|
data/prebundler.gemspec
ADDED
@@ -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: []
|