appbundler 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,70 +1,70 @@
1
- # Appbundler
2
-
3
- Appbundler reads a Gemfile.lock and generates code with
4
- `gem "some-dep", "= VERSION"` statements to lock the app's dependencies
5
- to the versions selected by bundler. This code is used in binstubs for
6
- the application so that running (e.g.) `chef-client` on the command line
7
- activates the locked dependencies for `chef` before running the command.
8
-
9
- This provides the following benefits:
10
- * The application loads faster because rubygems is not resolving
11
- dependency constraints at runtime.
12
- * The application runs with the same dependencies that it would if
13
- bundler was used, so we can test applications (that will be installed
14
- in an omnibus package) using the default bundler workflow.
15
- * There's no need to `bundle exec` or patch the bundler runtime into the
16
- app.
17
- * The app can load gems not included in the Gemfile/gemspec. Our use
18
- case for this is to load plugins (e.g., for knife and test kitchen).
19
- * A user can use rvm and still use the application (see below).
20
- * The application is protected from installation of incompatible
21
- dependencies.
22
-
23
- # Usage
24
-
25
- Install via rubygems: `gem install appbundler` or clone this project and
26
- bundle install:
27
-
28
- ```
29
- git clone https://github.com/opscode/appbundler.git
30
- cd appbundler
31
- bundle install
32
- ```
33
-
34
- Clone whatever project you want to appbundle somewhere else, and bundle
35
- install it:
36
-
37
- ```
38
- mkdir ~/oc
39
- cd ~/oc
40
- git clone https://github.com/opscode/chef.git
41
- cd chef
42
- bundle install
43
- ```
44
-
45
- Create a bin directory where your bundled binstubs will live:
46
-
47
- ```
48
- mkdir ~/appbundle-bin
49
- # Add to your PATH if you like
50
- ```
51
-
52
- Now you can app bundle your project (chef in our example):
53
-
54
- ```
55
- bin/appbundler ~/oc/chef ~/appbundler-bin
56
- ```
57
-
58
- Now you can run all of the app's executables with locked down deps:
59
-
60
- ```
61
- ~/appbunlder-bin/chef-client -v
62
- ```
63
-
64
-
65
- # RVM
66
-
67
- The generated binstubs explicitly disable rvm, so the above won't work
68
- if you're using rvm. This is intentional, because our use case is for
69
- omnibus applications where rvm's environment variables can break the
70
- embedded application by making ruby look for gems in rvm's gem repo.
1
+ # Appbundler
2
+
3
+ Appbundler reads a Gemfile.lock and generates code with
4
+ `gem "some-dep", "= VERSION"` statements to lock the app's dependencies
5
+ to the versions selected by bundler. This code is used in binstubs for
6
+ the application so that running (e.g.) `chef-client` on the command line
7
+ activates the locked dependencies for `chef` before running the command.
8
+
9
+ This provides the following benefits:
10
+ * The application loads faster because rubygems is not resolving
11
+ dependency constraints at runtime.
12
+ * The application runs with the same dependencies that it would if
13
+ bundler was used, so we can test applications (that will be installed
14
+ in an omnibus package) using the default bundler workflow.
15
+ * There's no need to `bundle exec` or patch the bundler runtime into the
16
+ app.
17
+ * The app can load gems not included in the Gemfile/gemspec. Our use
18
+ case for this is to load plugins (e.g., for knife and test kitchen).
19
+ * A user can use rvm and still use the application (see below).
20
+ * The application is protected from installation of incompatible
21
+ dependencies.
22
+
23
+ # Usage
24
+
25
+ Install via rubygems: `gem install appbundler` or clone this project and
26
+ bundle install:
27
+
28
+ ```
29
+ git clone https://github.com/opscode/appbundler.git
30
+ cd appbundler
31
+ bundle install
32
+ ```
33
+
34
+ Clone whatever project you want to appbundle somewhere else, and bundle
35
+ install it:
36
+
37
+ ```
38
+ mkdir ~/oc
39
+ cd ~/oc
40
+ git clone https://github.com/opscode/chef.git
41
+ cd chef
42
+ bundle install
43
+ ```
44
+
45
+ Create a bin directory where your bundled binstubs will live:
46
+
47
+ ```
48
+ mkdir ~/appbundle-bin
49
+ # Add to your PATH if you like
50
+ ```
51
+
52
+ Now you can app bundle your project (chef in our example):
53
+
54
+ ```
55
+ bin/appbundler ~/oc/chef ~/appbundler-bin
56
+ ```
57
+
58
+ Now you can run all of the app's executables with locked down deps:
59
+
60
+ ```
61
+ ~/appbunlder-bin/chef-client -v
62
+ ```
63
+
64
+
65
+ # RVM
66
+
67
+ The generated binstubs explicitly disable rvm, so the above won't work
68
+ if you're using rvm. This is intentional, because our use case is for
69
+ omnibus applications where rvm's environment variables can break the
70
+ embedded application by making ruby look for gems in rvm's gem repo.
data/Rakefile CHANGED
@@ -1 +1 @@
1
- require "bundler/gem_tasks"
1
+ require "bundler/gem_tasks"
data/appbundler.gemspec CHANGED
@@ -1,27 +1,27 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'appbundler/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "appbundler"
8
- spec.version = Appbundler::VERSION
9
- spec.authors = ["danielsdeleo"]
10
- spec.email = ["dan@opscode.com"]
11
- spec.description = %q{Extracts a dependency solution from bundler's Gemfile.lock to speed gem activation}
12
- spec.summary = spec.description
13
- spec.homepage = ""
14
- spec.license = "Apache2"
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_development_dependency "rake"
22
- spec.add_development_dependency "rspec", "~> 3.0"
23
- spec.add_development_dependency "pry"
24
- spec.add_development_dependency "mixlib-shellout", "~> 1.0"
25
-
26
- spec.add_dependency "mixlib-cli", "~> 1.4"
27
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'appbundler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "appbundler"
8
+ spec.version = Appbundler::VERSION
9
+ spec.authors = ["danielsdeleo"]
10
+ spec.email = ["dan@opscode.com"]
11
+ spec.description = %q{Extracts a dependency solution from bundler's Gemfile.lock to speed gem activation}
12
+ spec.summary = spec.description
13
+ spec.homepage = ""
14
+ spec.license = "Apache2"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "rspec", "~> 3.0"
23
+ spec.add_development_dependency "pry"
24
+ spec.add_development_dependency "mixlib-shellout", "~> 1.0"
25
+
26
+ spec.add_dependency "mixlib-cli", "~> 1.4"
27
+ end
data/bin/appbundler CHANGED
@@ -1,13 +1,13 @@
1
- #!/usr/bin/env ruby
2
-
3
- Kernel.trap(:INT) { exit 1 }
4
-
5
- begin
6
- require 'appbundler/cli'
7
- rescue LoadError
8
- $:.unshift File.expand_path("../../lib", __FILE__)
9
- require 'appbundler/cli'
10
- end
11
-
12
- Appbundler::CLI.run(ARGV)
13
-
1
+ #!/usr/bin/env ruby
2
+
3
+ Kernel.trap(:INT) { exit 1 }
4
+
5
+ begin
6
+ require 'appbundler/cli'
7
+ rescue LoadError
8
+ $:.unshift File.expand_path("../../lib", __FILE__)
9
+ require 'appbundler/cli'
10
+ end
11
+
12
+ Appbundler::CLI.run(ARGV)
13
+
data/lib/appbundler.rb CHANGED
@@ -1,4 +1,4 @@
1
-
2
- module Appbundler
3
- end
4
-
1
+
2
+ module Appbundler
3
+ end
4
+
@@ -1,192 +1,194 @@
1
- require 'bundler'
2
- require 'fileutils'
3
- require 'pp'
4
-
5
- module Appbundler
6
- class App
7
-
8
- BINSTUB_FILE_VERSION=1
9
-
10
- attr_reader :app_root
11
- attr_reader :target_bin_dir
12
-
13
- def self.demo
14
- demo = new("/Users/ddeleo/oc/chef")
15
-
16
- knife = demo.executables.grep(/knife/).first
17
- puts demo.binstub(knife)
18
- end
19
-
20
- def initialize(app_root, target_bin_dir)
21
- @app_root = app_root
22
- @target_bin_dir = target_bin_dir
23
- end
24
-
25
- # Copy over any .bundler and Gemfile.lock files to the target gem
26
- # directory. This will let us run tests from under that directory.
27
- def copy_bundler_env
28
- gem_path = app_gemspec.gem_dir
29
- FileUtils.install(gemfile_lock, gem_path, :mode => 0644)
30
- if File.exist?(dot_bundle_dir) && File.directory?(dot_bundle_dir)
31
- FileUtils.cp_r(dot_bundle_dir, gem_path)
32
- FileUtils.chmod_R("ugo+rX", File.join(gem_path, ".bundle"))
33
- end
34
- end
35
-
36
- def write_executable_stubs
37
- executables_to_create = executables.map do |real_executable_path|
38
- basename = File.basename(real_executable_path)
39
- stub_path = File.join(target_bin_dir, basename)
40
- [real_executable_path, stub_path]
41
- end
42
-
43
- executables_to_create.each do |real_executable_path, stub_path|
44
- File.open(stub_path, "wb", 0755) do |f|
45
- f.write(binstub(real_executable_path))
46
- end
47
- if RUBY_PLATFORM =~ /mswin|mingw|windows/
48
- batch_wrapper_path = "#{stub_path}.bat"
49
- File.open(batch_wrapper_path, "wb", 0755) do |f|
50
- f.write(batchfile_stub)
51
- end
52
- end
53
- end
54
-
55
- executables_to_create
56
- end
57
-
58
- def name
59
- File.basename(app_root)
60
- end
61
-
62
- def dot_bundle_dir
63
- File.join(app_root, ".bundle")
64
- end
65
-
66
- def gemfile_lock
67
- File.join(app_root, "Gemfile.lock")
68
- end
69
-
70
- def ruby
71
- Gem.ruby
72
- end
73
-
74
- def batchfile_stub
75
- ruby_relpath_windows = ruby_relative_path.gsub('/', '\\')
76
- <<-E
77
- @ECHO OFF
78
- "%~dp0\\#{ruby_relpath_windows}" "%~dpn0" %*
79
- E
80
- end
81
-
82
- # Relative path from #target_bin_dir to #ruby. This is used to
83
- # generate batch files for windows in a way that the package can be
84
- # installed in a custom location. On Unix we don't support custom
85
- # install locations so this isn't needed.
86
- def ruby_relative_path
87
- ruby_pathname = Pathname.new(ruby)
88
- bindir_pathname = Pathname.new(target_bin_dir)
89
- ruby_pathname.relative_path_from(bindir_pathname).to_s
90
- end
91
-
92
- def shebang
93
- "#!#{ruby}\n"
94
- end
95
-
96
- # A specially formatted comment that documents the format version of the
97
- # binstub files we generate.
98
- #
99
- # This comment should be unusual enough that we can reliably (enough)
100
- # detect whether a binstub was created by Appbundler and parse it to learn
101
- # what version of the format it uses. If we ever need to support reading or
102
- # mutating existing binstubs, we'll know what file version we're starting
103
- # with.
104
- def file_format_comment
105
- "#--APP_BUNDLER_BINSTUB_FORMAT_VERSION=#{BINSTUB_FILE_VERSION}--\n"
106
- end
107
-
108
- # Ruby code (as a string) that clears GEM_HOME and GEM_PATH environment
109
- # variables. In an omnibus context, this is important so users can use
110
- # things like rvm without accidentally pointing the app at rvm's
111
- # ruby and gems.
112
- #
113
- # Environment sanitization can be skipped by setting the
114
- # APPBUNDLER_ALLOW_RVM environment variable to "true". This feature
115
- # exists to make tests run correctly on travis.ci (which uses rvm).
116
- def env_sanitizer
117
- %Q{ENV["GEM_HOME"] = ENV["GEM_PATH"] = nil unless ENV["APPBUNDLER_ALLOW_RVM"] == "true"}
118
- end
119
-
120
- def runtime_activate
121
- @runtime_activate ||= begin
122
- statements = runtime_dep_specs.map {|s| %Q|gem "#{s.name}", "= #{s.version}"|}
123
- activate_code = ""
124
- activate_code << env_sanitizer << "\n"
125
- activate_code << statements.join("\n") << "\n"
126
- activate_code
127
- end
128
- end
129
-
130
- def binstub(bin_file)
131
- shebang + file_format_comment + runtime_activate + load_statement_for(bin_file)
132
- end
133
-
134
- def load_statement_for(bin_file)
135
- name, version = app_spec.name, app_spec.version
136
- bin_basename = File.basename(bin_file)
137
- <<-E
138
- gem "#{name}", "= #{version}"
139
-
140
- spec = Gem::Specification.find_by_name("#{name}", "= #{version}")
141
- bin_file = spec.bin_file("#{bin_basename}")
142
-
143
- Kernel.load(bin_file)
144
- E
145
- end
146
-
147
- def executables
148
- spec = app_gemspec
149
- spec.executables.map {|e| spec.bin_file(e)}
150
- end
151
-
152
- def runtime_dep_specs
153
- add_dependencies_from(app_spec)
154
- end
155
-
156
- def app_dependency_names
157
- @app_dependency_names ||= app_spec.dependencies.map(&:name)
158
- end
159
-
160
- def app_gemspec
161
- Gem::Specification.find_by_name(app_spec.name, app_spec.version)
162
- end
163
-
164
- def app_spec
165
- spec_for(name)
166
- end
167
-
168
- def gemfile_lock_specs
169
- parsed_gemfile_lock.specs
170
- end
171
-
172
- def parsed_gemfile_lock
173
- @parsed_gemfile_lock ||= Bundler::LockfileParser.new(IO.read(gemfile_lock))
174
- end
175
-
176
- private
177
-
178
- def add_dependencies_from(spec, collected_deps=[])
179
- spec.dependencies.each do |dep|
180
- next if collected_deps.any? {|s| s.name == dep.name }
181
- next_spec = spec_for(dep.name)
182
- collected_deps << next_spec
183
- add_dependencies_from(next_spec, collected_deps)
184
- end
185
- collected_deps
186
- end
187
-
188
- def spec_for(dep_name)
189
- gemfile_lock_specs.find {|s| s.name == dep_name } or raise "No spec #{dep_name}"
190
- end
191
- end
192
- end
1
+ require 'bundler'
2
+ require 'fileutils'
3
+ require 'pp'
4
+
5
+ module Appbundler
6
+ class App
7
+
8
+ BINSTUB_FILE_VERSION=1
9
+
10
+ attr_reader :app_root
11
+ attr_reader :target_bin_dir
12
+
13
+ def self.demo
14
+ demo = new("/Users/ddeleo/oc/chef")
15
+
16
+ knife = demo.executables.grep(/knife/).first
17
+ puts demo.binstub(knife)
18
+ end
19
+
20
+ def initialize(app_root, target_bin_dir)
21
+ @app_root = app_root
22
+ @target_bin_dir = target_bin_dir
23
+ end
24
+
25
+ # Copy over any .bundler and Gemfile.lock files to the target gem
26
+ # directory. This will let us run tests from under that directory.
27
+ def copy_bundler_env
28
+ gem_path = app_gemspec.gem_dir
29
+ FileUtils.install(gemfile_lock, gem_path, :mode => 0644)
30
+ if File.exist?(dot_bundle_dir) && File.directory?(dot_bundle_dir)
31
+ FileUtils.cp_r(dot_bundle_dir, gem_path)
32
+ FileUtils.chmod_R("ugo+rX", File.join(gem_path, ".bundle"))
33
+ end
34
+ end
35
+
36
+ def write_executable_stubs
37
+ executables_to_create = executables.map do |real_executable_path|
38
+ basename = File.basename(real_executable_path)
39
+ stub_path = File.join(target_bin_dir, basename)
40
+ [real_executable_path, stub_path]
41
+ end
42
+
43
+ executables_to_create.each do |real_executable_path, stub_path|
44
+ File.open(stub_path, "wb", 0755) do |f|
45
+ f.write(binstub(real_executable_path))
46
+ end
47
+ if RUBY_PLATFORM =~ /mswin|mingw|windows/
48
+ batch_wrapper_path = "#{stub_path}.bat"
49
+ File.open(batch_wrapper_path, "wb", 0755) do |f|
50
+ f.write(batchfile_stub)
51
+ end
52
+ end
53
+ end
54
+
55
+ executables_to_create
56
+ end
57
+
58
+ def name
59
+ File.basename(app_root)
60
+ end
61
+
62
+ def dot_bundle_dir
63
+ File.join(app_root, ".bundle")
64
+ end
65
+
66
+ def gemfile_lock
67
+ File.join(app_root, "Gemfile.lock")
68
+ end
69
+
70
+ def ruby
71
+ Gem.ruby
72
+ end
73
+
74
+ def batchfile_stub
75
+ ruby_relpath_windows = ruby_relative_path.gsub('/', '\\')
76
+ <<-E
77
+ @ECHO OFF
78
+ "%~dp0\\#{ruby_relpath_windows}" "%~dpn0" %*
79
+ E
80
+ end
81
+
82
+ # Relative path from #target_bin_dir to #ruby. This is used to
83
+ # generate batch files for windows in a way that the package can be
84
+ # installed in a custom location. On Unix we don't support custom
85
+ # install locations so this isn't needed.
86
+ def ruby_relative_path
87
+ ruby_pathname = Pathname.new(ruby)
88
+ bindir_pathname = Pathname.new(target_bin_dir)
89
+ ruby_pathname.relative_path_from(bindir_pathname).to_s
90
+ end
91
+
92
+ def shebang
93
+ "#!#{ruby}\n"
94
+ end
95
+
96
+ # A specially formatted comment that documents the format version of the
97
+ # binstub files we generate.
98
+ #
99
+ # This comment should be unusual enough that we can reliably (enough)
100
+ # detect whether a binstub was created by Appbundler and parse it to learn
101
+ # what version of the format it uses. If we ever need to support reading or
102
+ # mutating existing binstubs, we'll know what file version we're starting
103
+ # with.
104
+ def file_format_comment
105
+ "#--APP_BUNDLER_BINSTUB_FORMAT_VERSION=#{BINSTUB_FILE_VERSION}--\n"
106
+ end
107
+
108
+ # Ruby code (as a string) that clears GEM_HOME and GEM_PATH environment
109
+ # variables. In an omnibus context, this is important so users can use
110
+ # things like rvm without accidentally pointing the app at rvm's
111
+ # ruby and gems.
112
+ #
113
+ # Environment sanitization can be skipped by setting the
114
+ # APPBUNDLER_ALLOW_RVM environment variable to "true". This feature
115
+ # exists to make tests run correctly on travis.ci (which uses rvm).
116
+ def env_sanitizer
117
+ %Q{ENV["GEM_HOME"] = ENV["GEM_PATH"] = nil unless ENV["APPBUNDLER_ALLOW_RVM"] == "true"}
118
+ end
119
+
120
+ def runtime_activate
121
+ @runtime_activate ||= begin
122
+ statements = runtime_dep_specs.map {|s| %Q|gem "#{s.name}", "= #{s.version}"|}
123
+ activate_code = ""
124
+ activate_code << env_sanitizer << "\n"
125
+ activate_code << statements.join("\n") << "\n"
126
+ activate_code
127
+ end
128
+ end
129
+
130
+ def binstub(bin_file)
131
+ shebang + file_format_comment + runtime_activate + load_statement_for(bin_file)
132
+ end
133
+
134
+ def load_statement_for(bin_file)
135
+ name, version = app_spec.name, app_spec.version
136
+ bin_basename = File.basename(bin_file)
137
+ <<-E
138
+ gem "#{name}", "= #{version}"
139
+
140
+ spec = Gem::Specification.find_by_name("#{name}", "= #{version}")
141
+ bin_file = spec.bin_file("#{bin_basename}")
142
+
143
+ Kernel.load(bin_file)
144
+ E
145
+ end
146
+
147
+ def executables
148
+ spec = app_gemspec
149
+ spec.executables.map {|e| spec.bin_file(e)}
150
+ end
151
+
152
+ def runtime_dep_specs
153
+ add_dependencies_from(app_spec)
154
+ end
155
+
156
+ def app_dependency_names
157
+ @app_dependency_names ||= app_spec.dependencies.map(&:name)
158
+ end
159
+
160
+ def app_gemspec
161
+ Gem::Specification.find_by_name(app_spec.name, app_spec.version)
162
+ end
163
+
164
+ def app_spec
165
+ spec_for(name)
166
+ end
167
+
168
+ def gemfile_lock_specs
169
+ parsed_gemfile_lock.specs
170
+ end
171
+
172
+ def parsed_gemfile_lock
173
+ @parsed_gemfile_lock ||= Bundler::LockfileParser.new(IO.read(gemfile_lock))
174
+ end
175
+
176
+ private
177
+
178
+ def add_dependencies_from(spec, collected_deps=[])
179
+ spec.dependencies.each do |dep|
180
+ next if collected_deps.any? {|s| s.name == dep.name }
181
+ # a bundler dep will not get pinned in Gemfile.lock
182
+ next if dep.name == "bundler"
183
+ next_spec = spec_for(dep.name)
184
+ collected_deps << next_spec
185
+ add_dependencies_from(next_spec, collected_deps)
186
+ end
187
+ collected_deps
188
+ end
189
+
190
+ def spec_for(dep_name)
191
+ gemfile_lock_specs.find {|s| s.name == dep_name } or raise "No spec #{dep_name}"
192
+ end
193
+ end
194
+ end