appbundler 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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