rubycut-babushka 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. data/Gemfile +8 -0
  2. data/Gemfile.lock +31 -0
  3. data/README.markdown +246 -0
  4. data/Rakefile +26 -0
  5. data/bin/babushka +11 -0
  6. data/deps/babushka.rb +101 -0
  7. data/deps/dev.rb +12 -0
  8. data/deps/fhs.rb +31 -0
  9. data/deps/git.rb +29 -0
  10. data/deps/homebrew.rb +30 -0
  11. data/deps/os_x.rb +33 -0
  12. data/deps/packages.rb +22 -0
  13. data/deps/pkg_managers.rb +110 -0
  14. data/deps/ruby.rb +23 -0
  15. data/deps/rubygems.rb +24 -0
  16. data/deps/system.rb +10 -0
  17. data/deps/templates/app.rb +68 -0
  18. data/deps/templates/external.rb +12 -0
  19. data/deps/templates/installer.rb +31 -0
  20. data/deps/templates/managed.rb +105 -0
  21. data/deps/templates/ppa.rb +24 -0
  22. data/deps/templates/src.rb +42 -0
  23. data/deps/templates/tmbundle.rb +15 -0
  24. data/lib/babushka.rb +28 -0
  25. data/lib/babushka/accepts_block_for.rb +72 -0
  26. data/lib/babushka/accepts_list_for.rb +49 -0
  27. data/lib/babushka/accepts_value_for.rb +24 -0
  28. data/lib/babushka/base.rb +78 -0
  29. data/lib/babushka/bug_reporter.rb +55 -0
  30. data/lib/babushka/cmdline.rb +133 -0
  31. data/lib/babushka/cmdline/handler.rb +41 -0
  32. data/lib/babushka/cmdline/helpers.rb +127 -0
  33. data/lib/babushka/cmdline/parser.rb +69 -0
  34. data/lib/babushka/colorizer.rb +59 -0
  35. data/lib/babushka/core_patches/array.rb +171 -0
  36. data/lib/babushka/core_patches/blank.rb +22 -0
  37. data/lib/babushka/core_patches/bytes.rb +52 -0
  38. data/lib/babushka/core_patches/hash.rb +107 -0
  39. data/lib/babushka/core_patches/hashish.rb +14 -0
  40. data/lib/babushka/core_patches/integer.rb +25 -0
  41. data/lib/babushka/core_patches/io.rb +8 -0
  42. data/lib/babushka/core_patches/numeric.rb +16 -0
  43. data/lib/babushka/core_patches/object.rb +27 -0
  44. data/lib/babushka/core_patches/string.rb +116 -0
  45. data/lib/babushka/core_patches/symbol.rb +12 -0
  46. data/lib/babushka/core_patches/try.rb +15 -0
  47. data/lib/babushka/core_patches/uri.rb +24 -0
  48. data/lib/babushka/dep.rb +470 -0
  49. data/lib/babushka/dep_context.rb +18 -0
  50. data/lib/babushka/dep_definer.rb +115 -0
  51. data/lib/babushka/dep_pool.rb +49 -0
  52. data/lib/babushka/dep_runner.rb +85 -0
  53. data/lib/babushka/dsl.rb +26 -0
  54. data/lib/babushka/git_repo.rb +185 -0
  55. data/lib/babushka/helpers/git_helpers.rb +32 -0
  56. data/lib/babushka/helpers/log_helpers.rb +176 -0
  57. data/lib/babushka/helpers/path_helpers.rb +34 -0
  58. data/lib/babushka/helpers/run_helpers.rb +145 -0
  59. data/lib/babushka/helpers/shell_helpers.rb +229 -0
  60. data/lib/babushka/helpers/suggest_helpers.rb +16 -0
  61. data/lib/babushka/helpers/uri_helpers.rb +36 -0
  62. data/lib/babushka/ip.rb +160 -0
  63. data/lib/babushka/lambda_chooser.rb +40 -0
  64. data/lib/babushka/levenshtein.rb +125 -0
  65. data/lib/babushka/meta_dep.rb +65 -0
  66. data/lib/babushka/meta_dep_context.rb +15 -0
  67. data/lib/babushka/parameter.rb +143 -0
  68. data/lib/babushka/pkg_helper.rb +81 -0
  69. data/lib/babushka/pkg_helpers/apt_helper.rb +61 -0
  70. data/lib/babushka/pkg_helpers/base_helper.rb +19 -0
  71. data/lib/babushka/pkg_helpers/binpkgsrc_helper.rb +48 -0
  72. data/lib/babushka/pkg_helpers/binports_helper.rb +34 -0
  73. data/lib/babushka/pkg_helpers/brew_helper.rb +110 -0
  74. data/lib/babushka/pkg_helpers/gem_helper.rb +120 -0
  75. data/lib/babushka/pkg_helpers/macports_helper.rb +22 -0
  76. data/lib/babushka/pkg_helpers/npm_helper.rb +45 -0
  77. data/lib/babushka/pkg_helpers/pacman_helper.rb +27 -0
  78. data/lib/babushka/pkg_helpers/pip_helper.rb +45 -0
  79. data/lib/babushka/pkg_helpers/src_helper.rb +16 -0
  80. data/lib/babushka/pkg_helpers/yum_helper.rb +25 -0
  81. data/lib/babushka/popen.rb +40 -0
  82. data/lib/babushka/prompt.rb +176 -0
  83. data/lib/babushka/renderable.rb +67 -0
  84. data/lib/babushka/resource.rb +215 -0
  85. data/lib/babushka/run_reporter.rb +60 -0
  86. data/lib/babushka/shell.rb +108 -0
  87. data/lib/babushka/source.rb +216 -0
  88. data/lib/babushka/source_pool.rb +146 -0
  89. data/lib/babushka/system_definitions.rb +97 -0
  90. data/lib/babushka/system_profile.rb +210 -0
  91. data/lib/babushka/task.rb +142 -0
  92. data/lib/babushka/vars.rb +108 -0
  93. data/lib/babushka/version_of.rb +65 -0
  94. data/lib/babushka/version_str.rb +57 -0
  95. data/lib/babushka/xml_string.rb +28 -0
  96. data/lib/components.rb +82 -0
  97. data/lib/fancypath/fancypath.rb +200 -0
  98. data/lib/inkan/inkan.rb +76 -0
  99. data/spec/acceptance/acceptance.rb +43 -0
  100. data/spec/acceptance_helper.rb +113 -0
  101. data/spec/archives/Blah.app.zip +0 -0
  102. data/spec/archives/archive.tar +0 -0
  103. data/spec/archives/archive.tar.bz2 +0 -0
  104. data/spec/archives/archive.tar.gz +0 -0
  105. data/spec/archives/archive.tbz2 +0 -0
  106. data/spec/archives/archive.tgz +0 -0
  107. data/spec/archives/archive.zip +0 -0
  108. data/spec/archives/content.txt +5 -0
  109. data/spec/archives/invalid_archive +5 -0
  110. data/spec/archives/nested archive/content.txt +5 -0
  111. data/spec/archives/nested_archive.tar +0 -0
  112. data/spec/archives/really_a_gzip.zip +0 -0
  113. data/spec/archives/test-0.3.1.tgz +0 -0
  114. data/spec/archives/tgz_archive +0 -0
  115. data/spec/archives/zip_without_extension +0 -0
  116. data/spec/babushka/accepts_for_spec.rb +174 -0
  117. data/spec/babushka/accepts_for_support.rb +72 -0
  118. data/spec/babushka/cmdline/console_spec.rb +11 -0
  119. data/spec/babushka/cmdline/help_spec.rb +61 -0
  120. data/spec/babushka/cmdline/version_spec.rb +10 -0
  121. data/spec/babushka/core_patches_spec.rb +171 -0
  122. data/spec/babushka/dep_context_spec.rb +58 -0
  123. data/spec/babushka/dep_definer_spec.rb +152 -0
  124. data/spec/babushka/dep_definer_support.rb +36 -0
  125. data/spec/babushka/dep_spec.rb +567 -0
  126. data/spec/babushka/dep_support.rb +29 -0
  127. data/spec/babushka/deps_spec.rb +113 -0
  128. data/spec/babushka/gem_helper_spec.rb +90 -0
  129. data/spec/babushka/git_repo_spec.rb +396 -0
  130. data/spec/babushka/ip_spec.rb +131 -0
  131. data/spec/babushka/lambda_chooser_spec.rb +115 -0
  132. data/spec/babushka/meta_dep_definer_spec.rb +127 -0
  133. data/spec/babushka/meta_dep_wrapper_spec.rb +32 -0
  134. data/spec/babushka/parameter_spec.rb +135 -0
  135. data/spec/babushka/path_helpers_spec.rb +102 -0
  136. data/spec/babushka/prompt_spec.rb +188 -0
  137. data/spec/babushka/renderable_spec.rb +100 -0
  138. data/spec/babushka/resource_spec.rb +141 -0
  139. data/spec/babushka/run_helpers_spec.rb +26 -0
  140. data/spec/babushka/shell_helpers_spec.rb +244 -0
  141. data/spec/babushka/shell_spec.rb +19 -0
  142. data/spec/babushka/source_pool_spec.rb +320 -0
  143. data/spec/babushka/source_pool_support.rb +31 -0
  144. data/spec/babushka/source_spec.rb +382 -0
  145. data/spec/babushka/source_support.rb +17 -0
  146. data/spec/babushka/system_profile_spec.rb +61 -0
  147. data/spec/babushka/task_spec.rb +141 -0
  148. data/spec/babushka/uri_spec.rb +13 -0
  149. data/spec/babushka/vars_spec.rb +59 -0
  150. data/spec/babushka/version_of_spec.rb +110 -0
  151. data/spec/babushka/version_str_spec.rb +130 -0
  152. data/spec/babushka/version_str_support.rb +37 -0
  153. data/spec/babushka/xml_string_spec.rb +98 -0
  154. data/spec/deps/bad/broken.rb +7 -0
  155. data/spec/deps/bad/working.rb +3 -0
  156. data/spec/deps/good/meta.rb +14 -0
  157. data/spec/deps/good/test.rb +11 -0
  158. data/spec/deps/outer/deps.rb +19 -0
  159. data/spec/deps/outer/more deps.rb +11 -0
  160. data/spec/deps/params/params.rb +10 -0
  161. data/spec/fancypath/fancypath_spec.rb +272 -0
  162. data/spec/fancypath_support.rb +10 -0
  163. data/spec/inkan/inkan_spec.rb +217 -0
  164. data/spec/renderable/different_example.conf.erb +4 -0
  165. data/spec/renderable/example.conf.erb +3 -0
  166. data/spec/renderable/example.sh +6 -0
  167. data/spec/renderable/with_binding.conf.erb +4 -0
  168. data/spec/renderable/xml_example.conf.erb +8 -0
  169. data/spec/repos/remote.git.tgz +0 -0
  170. data/spec/spec_helper.rb +87 -0
  171. metadata +238 -0
@@ -0,0 +1,67 @@
1
+ module Babushka
2
+ class Renderable
3
+ include ShellHelpers
4
+ include RunHelpers
5
+
6
+ SEAL_REGEXP = /# Generated by babushka-[\d\.]+ at [a-zA-Z\d\-\:\s+]+, from [0-9a-f]{40}+\. [0-9a-f]{40}/
7
+
8
+ attr_reader :path
9
+ def initialize path
10
+ @path = path
11
+ end
12
+
13
+ def render source, opts = {}
14
+ shell("cat > '#{path}'",
15
+ :input => inkan_output_for(source, opts),
16
+ :sudo => opts[:sudo]
17
+ ).tap {|result|
18
+ if result
19
+ sudo "chmod #{opts[:perms]} '#{path}'" if opts[:perms]
20
+ end
21
+ }
22
+ end
23
+
24
+ def exists?
25
+ path.p.exists?
26
+ end
27
+
28
+ def clean?
29
+ Inkan.legitimate? path
30
+ end
31
+
32
+ def from? source
33
+ exists? && source_sha == sha_of(source)
34
+ end
35
+
36
+ private
37
+
38
+ def inkan_output_for source, opts = {}
39
+ Inkan.render {|inkan|
40
+ inkan.credit = "Generated #{_by_babushka}, from #{sha_of(source)}"
41
+ inkan.comment = opts[:comment] if opts[:comment]
42
+ inkan.comment_suffix = opts[:comment_suffix] if opts[:comment_suffix]
43
+ inkan.print render_erb(source, opts[:context])
44
+ }
45
+ end
46
+
47
+ def render_erb source, custom_context
48
+ require 'erb'
49
+ (custom_context || self).instance_eval {
50
+ ERB.new(source.p.read).result(binding)
51
+ }
52
+ end
53
+
54
+ def sha_of source
55
+ require 'digest/sha1'
56
+ raise "Source doesn't exist: #{source.p}" unless source.p.exists?
57
+ Digest::SHA1.hexdigest(source.p.read)
58
+ end
59
+
60
+ def source_sha
61
+ File.open(path.p) {|f|
62
+ first_line = f.gets
63
+ first_line[/\A#!/] ? f.gets : first_line
64
+ }.scan(/, from ([0-9a-f]{40})\./).flatten.first
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,215 @@
1
+ module Babushka
2
+ class ResourceError < StandardError
3
+ end
4
+ class Resource
5
+ include LogHelpers
6
+ extend LogHelpers
7
+ include ShellHelpers
8
+ extend ShellHelpers
9
+ include PathHelpers
10
+ extend PathHelpers
11
+
12
+ def self.get url, &block
13
+ filename = URI.unescape(url.to_s).p.basename
14
+ if filename.to_s.blank?
15
+ log_error "Not a valid URL to download: #{url}"
16
+ else
17
+ download_path = in_download_dir {|path|
18
+ downloaded_file = download(url, filename)
19
+ path / downloaded_file if downloaded_file
20
+ }
21
+ block.call download_path unless download_path.nil?
22
+ end
23
+ end
24
+
25
+ def self.extract url, &block
26
+ get url do |download_path|
27
+ in_build_dir {
28
+ Resource.for(download_path).extract(&block)
29
+ }
30
+ end
31
+ end
32
+
33
+ def self.download url, filename = url.to_s.p.basename
34
+ if filename.p.exists? && !filename.p.empty?
35
+ log_ok "Already downloaded #{filename}."
36
+ filename
37
+ elsif (result = shell(%Q{curl -I -X GET "#{url}"})).nil?
38
+ log_error "Couldn't download #{url}: `curl` exited with non-zero status."
39
+ else
40
+ response_code = result.val_for(/HTTP\/1\.\d/) # not present for ftp://, etc.
41
+ if response_code && response_code[/^[23]/].nil?
42
+ log_error "Couldn't download #{url}: #{response_code}."
43
+ elsif !(location = result.val_for('Location')).nil?
44
+ log "Following redirect from #{url}"
45
+ download URI.escape(location), location.p.basename
46
+ else
47
+ success = log_block "Downloading #{url}" do
48
+ shell %Q{curl -# -o "#{filename}.tmp" "#{url}" && mv -f "#{filename}.tmp" "#{filename}"}, :progress => /[\d\.]+%/
49
+ end
50
+ filename if success
51
+ end
52
+ end
53
+ end
54
+
55
+ def self.detect_type_by_extension path
56
+ TYPES.keys.detect {|key|
57
+ TYPES[key][:exts].any? {|extension|
58
+ path.has_extension? extension
59
+ }
60
+ }
61
+ end
62
+
63
+ def self.detect_type_by_contents path
64
+ TYPES.keys.detect {|key|
65
+ shell("file '#{path}'")[TYPES[key][:file_match]]
66
+ }
67
+ end
68
+
69
+ def self.type path
70
+ detect_type_by_extension(path) || detect_type_by_contents(path)
71
+ end
72
+
73
+ TYPES = {
74
+ :deb => {:file_match => 'Debian binary package', :exts => %w[deb]},
75
+ :pkg => {:file_match => 'xar archive', :exts => %w[pkg]},
76
+ :tar => {:file_match => 'tar archive', :exts => %w[tar]},
77
+ :gzip => {:file_match => 'gzip compressed data', :exts => %w[tgz tar.gz]},
78
+ :bzip2 => {:file_match => 'bzip2 compressed data', :exts => %w[tbz2 tar.bz2]},
79
+ :zip => {:file_match => 'Zip archive data', :exts => %w[zip]},
80
+ :dmg => {:file_match => 'VAX COFF executable not stripped', :exts => %w[dmg]}
81
+ }
82
+
83
+ attr_reader :path, :name
84
+
85
+ def initialize path, opts = {}
86
+ @path = path.p
87
+ @name = TYPES[type][:exts].inject(filename) {|fn,t| fn.gsub(/\.#{t}$/, '') }
88
+ end
89
+
90
+ def filename
91
+ path.basename.to_s
92
+ end
93
+
94
+ def type
95
+ self.class.type path
96
+ end
97
+
98
+ def supported?
99
+ !type.nil?
100
+ end
101
+
102
+ def extract &block
103
+ cd(archive_prefix, :create => true) { process_extract(&block) }
104
+ end
105
+
106
+ def process_extract &block
107
+ shell("mkdir -p '#{name}'") and
108
+ cd(name) {
109
+ unless log_shell("Extracting #{filename}", extract_command)
110
+ log_error "Couldn't extract #{path} - probably a bad download."
111
+ else
112
+ cd(content_subdir) {
113
+ block.nil? or block.call(self)
114
+ }
115
+ end
116
+ }
117
+ end
118
+
119
+ def content_subdir
120
+ identity_dirs.reject {|dir|
121
+ %w[app pkg bundle tmbundle prefPane].map {|i|
122
+ /\.#{i}$/
123
+ }.any? {|dont_descend|
124
+ dir[dont_descend]
125
+ }
126
+ }.first
127
+ end
128
+
129
+ def identity_dirs
130
+ everything = Dir.glob('*')
131
+ if everything.length == 1 && File.directory?(everything.first)
132
+ everything
133
+ else
134
+ Dir.glob('*/').map {|dir| dir.chomp('/') }.select {|dir|
135
+ dir.downcase.gsub(/[ \-_\.]/, '') == name.downcase.gsub(/[ \-_\.]/, '')
136
+ }
137
+ end
138
+ end
139
+
140
+ def archive_prefix
141
+ BuildPrefix
142
+ end
143
+ end
144
+
145
+ class FileResource < Resource
146
+ def extract &block
147
+ in_download_dir {
148
+ block.call(self)
149
+ }
150
+ end
151
+ end
152
+
153
+ class TarResource < Resource
154
+ def extract_command
155
+ "tar -#{extract_option(type)}xf '#{path}'"
156
+ end
157
+ def extract_option type
158
+ {
159
+ :tar => '',
160
+ :gzip => 'z',
161
+ :bzip2 => 'j'
162
+ }[type]
163
+ end
164
+ end
165
+
166
+ class ZipResource < Resource
167
+ def extract_command
168
+ "unzip -o '#{path}'"
169
+ end
170
+ end
171
+
172
+ class DmgResource < Resource
173
+ def extract &block
174
+ in_download_dir {
175
+ output = log_shell "Attaching #{filename}", "hdiutil attach '#{filename.p.basename}'"
176
+ if output.nil?
177
+ log_error "Couldn't mount #{filename.p}."
178
+ elsif (path = mountpoint_for(output)).nil?
179
+ raise "Couldn't find where `hdiutil` mounted #{filename.p}."
180
+ else
181
+ cd(path) {
182
+ block.call(self)
183
+ }.tap {
184
+ log_shell "Detaching #{filename}", "hdiutil detach '#{path}'"
185
+ }
186
+ end
187
+ }
188
+ end
189
+
190
+ def mountpoint_for output
191
+ output.scan(/\s+(\/Volumes\/[^\n]+)/).flatten.first
192
+ end
193
+ end
194
+
195
+ class Resource
196
+ CLASSES = {
197
+ :deb => FileResource,
198
+ :pkg => FileResource,
199
+ :tar => TarResource,
200
+ :gzip => TarResource,
201
+ :bzip2 => TarResource,
202
+ :zip => ZipResource,
203
+ :dmg => DmgResource
204
+ }
205
+
206
+ def self.for path, opts = {}
207
+ path = path.p
208
+ filename = path.basename.to_s
209
+ raise ResourceError, "The archive #{filename} does not exist." unless path.exists?
210
+ klass = CLASSES[type(path)]
211
+ raise ResourceError, "Don't know how to extract #{filename}." if klass.nil?
212
+ klass.new(path, opts)
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,60 @@
1
+ module Babushka
2
+ class RunReporter
3
+ class << self
4
+ include LogHelpers
5
+
6
+ def queue dep, result, reportable
7
+ if dep.dep_source.type != :public
8
+ debug "Not reporting #{dep.contextual_name}, since it's not in a public source."
9
+ else
10
+ queue_report dep, (reportable ? 'error' : (result ? 'ok' : 'fail'))
11
+ end
12
+ end
13
+
14
+ def post_reports
15
+ require 'net/http'
16
+
17
+ while Base.task.running? && (report = most_recent_report)
18
+ post_report report
19
+ end
20
+ end
21
+
22
+
23
+ private
24
+
25
+ def post_report report
26
+ submit_report_to_webservice(report.p.read).tap {|result|
27
+ report.p.rm if result
28
+ }
29
+ end
30
+
31
+ require 'net/http'
32
+ def submit_report_to_webservice data
33
+ Net::HTTP.start('babushka.me') {|http|
34
+ http.open_timeout = http.read_timeout = 5
35
+ http.post '/runs.json', data
36
+ }.is_a?(Net::HTTPSuccess)
37
+ rescue Errno::ECONNREFUSED, SocketError
38
+ log_error "Couldn't connect to the babushka webservice." unless Base.task.running?
39
+ rescue Timeout::Error, Errno::ETIMEDOUT
40
+ debug "Timeout while submitting run report."
41
+ end
42
+
43
+ def most_recent_report
44
+ ReportPrefix.p.glob('*').sort.last
45
+ end
46
+
47
+ def queue_report dep, result
48
+ ReportPrefix.p.mkdir
49
+ (ReportPrefix / Time.now.to_f).open('w') {|f|
50
+ f << run_report_for(dep, result).to_http_params
51
+ }
52
+ end
53
+
54
+ def run_report_for dep, result
55
+ Base.task.task_info(dep, result)
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,108 @@
1
+ module Babushka
2
+ class Shell
3
+ include LogHelpers
4
+
5
+ class ShellCommandFailed < StandardError
6
+ attr_reader :cmd, :stdout, :stderr
7
+ def initialize cmd, stdout, stderr
8
+ @cmd, @stdout, @stderr = cmd, stdout, stderr
9
+ message = if stderr.empty?
10
+ "Shell command failed: '#{cmd.join(' ')}'"
11
+ else
12
+ "Shell command failed: '#{cmd.join(' ')}':\n#{stderr}"
13
+ end
14
+ super message
15
+ end
16
+ end
17
+
18
+ attr_reader :cmd, :opts, :env, :result, :stdout, :stderr
19
+
20
+ def initialize *cmd
21
+ @opts = cmd.extract_options!
22
+ raise "You can't use :spinner and :progress together in Babushka::Shell." if opts[:spinner] && opts[:progress]
23
+ @env = cmd.first.is_a?(Hash) ? cmd.shift : {}
24
+ @cmd = cmd
25
+ @progress = nil
26
+ end
27
+
28
+ def ok?; result == 0 end
29
+
30
+ def run &block
31
+ @stdout, @stderr = '', ''
32
+ @result = invoke
33
+ print "#{" " * (@progress.length + 1)}#{"\b" * (@progress.length + 1)}" unless @progress.nil?
34
+
35
+ if block_given?
36
+ yield(self)
37
+ elsif ok?
38
+ stdout.chomp
39
+ else
40
+ raise ShellCommandFailed.new(cmd, stdout, stderr)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def invoke
47
+ debug "$ #{@cmd.join(' ')}".colorize('grey')
48
+ Babushka::Open3.popen3 @cmd, popen_opts do |stdin,stdout,stderr,thread|
49
+ unless @opts[:input].nil?
50
+ stdin << @opts[:input]
51
+ stdin.close
52
+ end
53
+
54
+ spinner_offset = -1
55
+ should_spin = @opts[:spinner] && !Base.task.opt(:debug)
56
+
57
+ # For very short-running commands, check for output in a tight loop.
58
+ # The sleep below would at least halve the speed of quick #shell calls.
59
+ # This means really quick calls (e.g. `whoami`, `pwd`, etc) aren't
60
+ # delayed, but the CPU is only pegged for a fraction of a second on
61
+ # slower calls (e.g. `gem env`, `make`, etc).
62
+ 1_000.times { break if stdout.ready_for_read? || stderr.ready_for_read? }
63
+
64
+ loop {
65
+ read_from stdout, @stdout do
66
+ print " #{%w[| / - \\][spinner_offset = ((spinner_offset + 1) % 4)]}\b\b" if should_spin
67
+ end
68
+ read_from stderr, @stderr, :stderr
69
+
70
+ if stdout.closed? && stderr.closed?
71
+ break
72
+ else
73
+ # We sleep here because otherwise babushka itself would peg the CPU
74
+ # while waiting for output from long-running shell commands.
75
+ sleep 0.05
76
+ end
77
+ }
78
+ end
79
+ end
80
+
81
+ def read_from io, buf, log_as = nil
82
+ while !io.closed? && io.ready_for_read?
83
+ output = nil
84
+ # Only try reading up to a backspace if we're looking for progress output.
85
+ output = io.gets("\r") if @opts[:progress]
86
+ output = io.gets if output.nil?
87
+
88
+ if output.nil?
89
+ io.close
90
+ else
91
+ debug output.chomp, :log => @opts[:log], :as => log_as
92
+ buf << output
93
+ if @opts[:progress] && (@progress = output[@opts[:progress]])
94
+ print " #{@progress}#{"\b" * (@progress.length + 1)}"
95
+ end
96
+ yield if block_given?
97
+ end
98
+ end
99
+ end
100
+
101
+ def popen_opts
102
+ {}.tap {|opts|
103
+ opts[:chdir] = @opts[:cd].p.to_s if @opts[:cd]
104
+ opts[:env] = @env if @env
105
+ }
106
+ end
107
+ end
108
+ end