toys-core 0.10.0 → 0.10.5

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.
@@ -16,48 +16,39 @@ module Toys
16
16
  # This class is not loaded by default. Before using it directly, you should
17
17
  # `require "toys/utils/exec"`
18
18
  #
19
- # ## Configuration options
19
+ # ## Features
20
20
  #
21
- # A variety of options can be used to control subprocesses. These include:
21
+ # ### Controlling processes
22
22
  #
23
- # * `:name` (Object) An optional object that can be used to identify this
24
- # subprocess. It is available in the controller and result objects.
25
- # * `:env` (Hash) Environment variables to pass to the subprocess
26
- # * `:logger` (Logger) Logger to use for logging the actual command. If
27
- # not present, the command is not logged.
28
- # * `:log_level` (Integer,false) Level for logging the actual command.
29
- # Defaults to Logger::INFO if not present. You may also pass `false` to
30
- # disable logging of the command.
31
- # * `:log_cmd` (String) The string logged for the actual command.
32
- # Defaults to the `inspect` representation of the command.
33
- # * `:background` (Boolean) Runs the process in the background, returning
34
- # a controller object instead of a result object.
35
- # * `:result_callback` (Proc) Called and passed the result object when a
36
- # subprocess exits.
37
- # * `:in` Connects the input stream of the subprocess. See the section on
38
- # stream handling.
39
- # * `:out` Connects the standard output stream of the subprocess. See the
40
- # section on stream handling.
41
- # * `:err` Connects the standard error stream of the subprocess. See the
42
- # section on stream handling.
23
+ # A process can be started in the *foreground* or the *background*. If you
24
+ # start a foreground process, it will "take over" your standard input and
25
+ # output streams by default, and it will keep control until it completes.
26
+ # If you start a background process, its streams will be redirected to null
27
+ # by default, and control will be returned to you immediately.
28
+ #
29
+ # When a process is running, you can control it using a
30
+ # {Toys::Utils::Exec::Controller} object. Use a controller to interact with
31
+ # the process's input and output streams, send it signals, or wait for it
32
+ # to complete.
43
33
  #
44
- # In addition, the following options recognized by `Process#spawn` are
45
- # supported.
34
+ # When running a process in the foreground, the controller will be yielded
35
+ # to an optional block. For example, the following code starts a process in
36
+ # the foreground and passes its output stream to a controller.
46
37
  #
47
- # * `:chdir`
48
- # * `:close_others`
49
- # * `:new_pgroup`
50
- # * `:pgroup`
51
- # * `:umask`
52
- # * `:unsetenv_others`
38
+ # exec_service.exec(["git", "init"], out: :controller) do |controller|
39
+ # loop do
40
+ # line = controller.out.gets
41
+ # break if line.nil?
42
+ # puts "Got line: #{line}"
43
+ # end
44
+ # end
53
45
  #
54
- # Any other options are ignored.
46
+ # When running a process in the background, the controller is returned from
47
+ # the method that starts the process:
55
48
  #
56
- # Configuration options may be provided to any method that starts a
57
- # subprocess. You may also modify default values by calling
58
- # {Toys::Utils::Exec#configure_defaults}.
49
+ # controller = exec_service.exec(["git", "init"], background: true)
59
50
  #
60
- # ## Stream handling
51
+ # ### Stream handling
61
52
  #
62
53
  # By default, subprocess streams are connected to the corresponding streams
63
54
  # in the parent process. You can change this behavior, redirecting streams
@@ -74,16 +65,16 @@ module Toys
74
65
  # Following is a full list of the stream handling options, along with how
75
66
  # to specify them using the `:in`, `:out`, and `:err` options.
76
67
  #
77
- # * **Close the stream:** You may close the stream by passing `:close` as
78
- # the option value. This is the same as passing `:close` to
79
- # `Process#spawn`.
68
+ # * **Inherit parent stream:** You may inherit the corresponding stream
69
+ # in the parent process by passing `:inherit` as the option value. This
70
+ # is the default if the subprocess is *not* run in the background.
80
71
  # * **Redirect to null:** You may redirect to a null stream by passing
81
72
  # `:null` as the option value. This connects to a stream that is not
82
73
  # closed but contains no data, i.e. `/dev/null` on unix systems. This
83
74
  # is the default if the subprocess is run in the background.
84
- # * **Inherit parent stream:** You may inherit the corresponding stream
85
- # in the parent process by passing `:inherit` as the option value. This
86
- # is the default if the subprocess is *not* run in the background.
75
+ # * **Close the stream:** You may close the stream by passing `:close` as
76
+ # the option value. This is the same as passing `:close` to
77
+ # `Process#spawn`.
87
78
  # * **Redirect to a file:** You may redirect to a file. This reads from
88
79
  # an existing file when connected to `:in`, and creates or appends to a
89
80
  # file when connected to `:out` or `:err`. To specify a file, use the
@@ -113,6 +104,104 @@ module Toys
113
104
  # yields the {Toys::Utils::Exec::Controller}, giving you access to
114
105
  # streams.
115
106
  #
107
+ # ### Result handling
108
+ #
109
+ # A subprocess result is represented by a {Toys::Utils::Exec::Result}
110
+ # object, which includes the exit code, the content of any captured output
111
+ # streams, and any exeption raised when attempting to run the process.
112
+ # When you run a process in the foreground, the method will return a result
113
+ # object. When you run a process in the background, you can obtain the
114
+ # result from the controller once the process completes.
115
+ #
116
+ # The following example demonstrates running a process in the foreground
117
+ # and getting the exit code:
118
+ #
119
+ # result = exec_service.exec(["git", "init"])
120
+ # puts "exit code: #{result.exit_code}"
121
+ #
122
+ # The following example demonstrates starting a process in the background,
123
+ # waiting for it to complete, and getting its exit code:
124
+ #
125
+ # controller = exec_service.exec(["git", "init"], background: true)
126
+ # result = controller.result(timeout: 1.0)
127
+ # if result
128
+ # puts "exit code: #{result.exit_code}"
129
+ # else
130
+ # puts "timed out"
131
+ # end
132
+ #
133
+ # You can also provide a callback that is executed once a process
134
+ # completes. For example:
135
+ #
136
+ # my_callback = proc do |result|
137
+ # puts "exit code: #{result.exit_code}"
138
+ # end
139
+ # exec_service.exec(["git", "init"], result_callback: my_callback)
140
+ #
141
+ # ## Configuration options
142
+ #
143
+ # A variety of options can be used to control subprocesses. These can be
144
+ # provided to any method that starts a subprocess. Youc an also set
145
+ # defaults by calling {Toys::Utils::Exec#configure_defaults}.
146
+ #
147
+ # Options that affect the behavior of subprocesses:
148
+ #
149
+ # * `:env` (Hash) Environment variables to pass to the subprocess.
150
+ # Keys represent variable names and should be strings. Values should be
151
+ # either strings or `nil`, which unsets the variable.
152
+ #
153
+ # * `:background` (Boolean) Runs the process in the background if `true`.
154
+ #
155
+ # * `:result_callback` (Proc) Called and passed the result object when
156
+ # the subprocess exits.
157
+ #
158
+ # Options for connecting input and output streams. See the section above on
159
+ # stream handling for info on the values that can be passed.
160
+ #
161
+ # * `:in` Connects the input stream of the subprocess. See the section on
162
+ # stream handling.
163
+ #
164
+ # * `:out` Connects the standard output stream of the subprocess. See the
165
+ # section on stream handling.
166
+ #
167
+ # * `:err` Connects the standard error stream of the subprocess. See the
168
+ # section on stream handling.
169
+ #
170
+ # Options related to logging and reporting:
171
+ #
172
+ # * `:logger` (Logger) Logger to use for logging the actual command. If
173
+ # not present, the command is not logged.
174
+ #
175
+ # * `:log_level` (Integer,false) Level for logging the actual command.
176
+ # Defaults to Logger::INFO if not present. You may also pass `false` to
177
+ # disable logging of the command.
178
+ #
179
+ # * `:log_cmd` (String) The string logged for the actual command.
180
+ # Defaults to the `inspect` representation of the command.
181
+ #
182
+ # * `:name` (Object) An optional object that can be used to identify this
183
+ # subprocess. It is available in the controller and result objects.
184
+ #
185
+ # In addition, the following options recognized by
186
+ # [`Process#spawn`](https://ruby-doc.org/core/Process.html#method-c-spawn)
187
+ # are supported.
188
+ #
189
+ # * `:chdir` (String) Set the working directory for the command.
190
+ #
191
+ # * `:close_others` (Boolean) Whether to close non-redirected
192
+ # non-standard file descriptors.
193
+ #
194
+ # * `:new_pgroup` (Boolean) Create new process group (Windows only).
195
+ #
196
+ # * `:pgroup` (Integer,true,nil) The process group setting.
197
+ #
198
+ # * `:umask` (Integer) Umask setting for the new process.
199
+ #
200
+ # * `:unsetenv_others` (Boolean) Clear environment variables except those
201
+ # explicitly set.
202
+ #
203
+ # Any other option key will result in an `ArgumentError`.
204
+ #
116
205
  class Exec
117
206
  ##
118
207
  # Create an exec service.
@@ -120,14 +209,16 @@ module Toys
120
209
  # @param block [Proc] A block that is called if a key is not found. It is
121
210
  # passed the unknown key, and expected to return a default value
122
211
  # (which can be nil).
123
- # @param opts [keywords] Initial default options.
212
+ # @param opts [keywords] Initial default options. See {Toys::Utils::Exec}
213
+ # for a description of the options.
124
214
  #
125
215
  def initialize(**opts, &block)
126
216
  @default_opts = Opts.new(&block).add(opts)
127
217
  end
128
218
 
129
219
  ##
130
- # Set default options
220
+ # Set default options. See {Toys::Utils::Exec} for a description of the
221
+ # options.
131
222
  #
132
223
  # @param opts [keywords] New default options to set
133
224
  # @return [self]
@@ -146,7 +237,7 @@ module Toys
146
237
  #
147
238
  # @param cmd [String,Array<String>] The command to execute.
148
239
  # @param opts [keywords] The command options. See the section on
149
- # configuration options in the {Toys::Utils::Exec} module docs.
240
+ # configuration options in the {Toys::Utils::Exec} class docs.
150
241
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
151
242
  # for the subprocess streams.
152
243
  #
@@ -179,7 +270,7 @@ module Toys
179
270
  #
180
271
  # @param args [String,Array<String>] The arguments to ruby.
181
272
  # @param opts [keywords] The command options. See the section on
182
- # configuration options in the {Toys::Utils::Exec} module docs.
273
+ # configuration options in the {Toys::Utils::Exec} class docs.
183
274
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
184
275
  # for the subprocess streams.
185
276
  #
@@ -204,7 +295,7 @@ module Toys
204
295
  #
205
296
  # @param func [Proc] The proc to call.
206
297
  # @param opts [keywords] The command options. See the section on
207
- # configuration options in the {Toys::Utils::Exec} module docs.
298
+ # configuration options in the {Toys::Utils::Exec} class docs.
208
299
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
209
300
  # for the subprocess streams.
210
301
  #
@@ -231,7 +322,7 @@ module Toys
231
322
  #
232
323
  # @param cmd [String,Array<String>] The command to execute.
233
324
  # @param opts [keywords] The command options. See the section on
234
- # configuration options in the {Toys::Utils::Exec} module docs.
325
+ # configuration options in the {Toys::Utils::Exec} class docs.
235
326
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
236
327
  # for the subprocess streams.
237
328
  #
@@ -253,7 +344,7 @@ module Toys
253
344
  #
254
345
  # @param args [String,Array<String>] The arguments to ruby.
255
346
  # @param opts [keywords] The command options. See the section on
256
- # configuration options in the {Toys::Utils::Exec} module docs.
347
+ # configuration options in the {Toys::Utils::Exec} class docs.
257
348
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
258
349
  # for the subprocess streams.
259
350
  #
@@ -275,7 +366,7 @@ module Toys
275
366
  #
276
367
  # @param func [Proc] The proc to call.
277
368
  # @param opts [keywords] The command options. See the section on
278
- # configuration options in the {Toys::Utils::Exec} module docs.
369
+ # configuration options in the {Toys::Utils::Exec} class docs.
279
370
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
280
371
  # for the subprocess streams.
281
372
  #
@@ -295,7 +386,7 @@ module Toys
295
386
  #
296
387
  # @param cmd [String] The shell command to execute.
297
388
  # @param opts [keywords] The command options. See the section on
298
- # configuration options in the {Toys::Utils::Exec} module docs.
389
+ # configuration options in the {Toys::Utils::Exec} class docs.
299
390
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
300
391
  # for the subprocess streams.
301
392
  #
@@ -64,6 +64,13 @@ module Toys
64
64
  class AlreadyBundledError < BundlerFailedError
65
65
  end
66
66
 
67
+ ##
68
+ # The bundle contained a toys or toys-core dependency that is
69
+ # incompatible with the currently running version.
70
+ #
71
+ class IncompatibleToysError < BundlerFailedError
72
+ end
73
+
67
74
  ##
68
75
  # Activate the given gem. If it is not present, attempt to install it (or
69
76
  # inform the user to update the bundle).
@@ -81,16 +88,22 @@ module Toys
81
88
  #
82
89
  # @param on_missing [:confirm,:error,:install] What to do if a needed gem
83
90
  # is not installed. Possible values:
91
+ #
84
92
  # * `:confirm` - prompt the user on whether to install
85
93
  # * `:error` - raise an exception
86
94
  # * `:install` - just install the gem
95
+ #
87
96
  # The default is `:confirm`.
97
+ #
88
98
  # @param on_conflict [:error,:warn,:ignore] What to do if bundler has
89
99
  # already been run with a different Gemfile. Possible values:
100
+ #
90
101
  # * `:error` - raise an exception
91
102
  # * `:ignore` - just silently proceed without bundling again
92
103
  # * `:warn` - print a warning and proceed without bundling again
104
+ #
93
105
  # The default is `:error`.
106
+ #
94
107
  # @param terminal [Toys::Utils::Terminal] Terminal to use (optional)
95
108
  # @param input [IO] Input IO (optional, defaults to STDIN)
96
109
  # @param output [IO] Output IO (optional, defaults to STDOUT)
@@ -146,8 +159,9 @@ module Toys
146
159
  search_dirs: nil)
147
160
  Gems.synchronize do
148
161
  gemfile_path = find_gemfile(Array(search_dirs))
149
- activate("bundler", "~> 2.1")
150
162
  if configure_gemfile(gemfile_path)
163
+ activate("bundler", "~> 2.1")
164
+ require "bundler"
151
165
  setup_bundle(gemfile_path, groups || [])
152
166
  end
153
167
  end
@@ -184,14 +198,14 @@ module Toys
184
198
  error.message.include?("Could not find")
185
199
  end
186
200
  if !is_missing_spec || @on_missing == :error
187
- report_error(name, requirements, error)
201
+ report_activation_error(name, requirements, error)
188
202
  return
189
203
  end
190
204
  confirm_and_install_gem(name, requirements)
191
205
  begin
192
206
  gem(name, *requirements)
193
207
  rescue ::Gem::LoadError => e
194
- report_error(name, requirements, e)
208
+ report_activation_error(name, requirements, e)
195
209
  end
196
210
  end
197
211
 
@@ -215,7 +229,7 @@ module Toys
215
229
  ::Gem::Specification.reset
216
230
  end
217
231
 
218
- def report_error(name, requirements, err)
232
+ def report_activation_error(name, requirements, err)
219
233
  if ::ENV["BUNDLE_GEMFILE"]
220
234
  raise GemfileUpdateNeededError.new(gem_requirements_text(name, requirements),
221
235
  ::ENV["BUNDLE_GEMFILE"])
@@ -233,12 +247,14 @@ module Toys
233
247
 
234
248
  def configure_gemfile(gemfile_path)
235
249
  old_path = ::ENV["BUNDLE_GEMFILE"]
236
- if old_path && gemfile_path != old_path
237
- case @on_conflict
238
- when :warn
239
- terminal.puts("Warning: could not set up bundler because it is already set up.", :red)
240
- when :error
241
- raise AlreadyBundledError, "Could not set up bundler because it is already set up"
250
+ if old_path
251
+ if gemfile_path != old_path
252
+ case @on_conflict
253
+ when :warn
254
+ terminal.puts("Warning: could not set up bundler because it is already set up.", :red)
255
+ when :error
256
+ raise AlreadyBundledError, "Could not set up bundler because it is already set up"
257
+ end
242
258
  end
243
259
  return false
244
260
  end
@@ -247,13 +263,60 @@ module Toys
247
263
  end
248
264
 
249
265
  def setup_bundle(gemfile_path, groups)
250
- require "bundler"
251
266
  begin
252
- ::Bundler.setup(*groups)
253
- rescue ::Bundler::GemNotFound
267
+ modify_bundle_definition(gemfile_path)
268
+ ::Bundler.ui.silence { ::Bundler.setup(*groups) }
269
+ rescue ::Bundler::GemNotFound, ::Bundler::VersionConflict
270
+ restore_toys_libs
254
271
  install_bundle(gemfile_path)
255
272
  ::Bundler.reset!
256
- ::Bundler.setup(*groups)
273
+ modify_bundle_definition(gemfile_path)
274
+ ::Bundler.ui.silence { ::Bundler.setup(*groups) }
275
+ end
276
+ restore_toys_libs
277
+ end
278
+
279
+ def modify_bundle_definition(gemfile_path)
280
+ builder = ::Bundler::Dsl.new
281
+ builder.eval_gemfile(gemfile_path)
282
+ toys_gems = ["toys-core"]
283
+ remove_gem_from_definition(builder, "toys-core")
284
+ removed_toys = remove_gem_from_definition(builder, "toys")
285
+ add_gem_to_definition(builder, "toys-core")
286
+ if removed_toys || ::Toys.const_defined?(:VERSION)
287
+ add_gem_to_definition(builder, "toys")
288
+ toys_gems << "toys"
289
+ end
290
+ definition = builder.to_definition(gemfile_path + ".lock", { gems: toys_gems })
291
+ ::Bundler.instance_variable_set(:@definition, definition)
292
+ end
293
+
294
+ def remove_gem_from_definition(builder, name)
295
+ existing_dep = builder.dependencies.find { |dep| dep.name == name }
296
+ return false unless existing_dep
297
+ unless existing_dep.requirement.satisfied_by?(::Gem::Version.new(::Toys::Core::VERSION))
298
+ raise IncompatibleToysError,
299
+ "The bundle lists #{name} #{existing_dep.requirement} as a dependency, which is" \
300
+ " incompatible with the current version #{::Toys::Core::VERSION}."
301
+ end
302
+ builder.dependencies.delete(existing_dep)
303
+ true
304
+ end
305
+
306
+ def add_gem_to_definition(builder, name)
307
+ if ::ENV["TOYS_DEV"] == "true"
308
+ path = ::File.join(::File.dirname(::File.dirname(::Toys::CORE_LIB_PATH)), name)
309
+ end
310
+ command = "gem #{name.inspect}, #{::Toys::Core::VERSION.inspect}, path: #{path.inspect}\n"
311
+ builder.eval_gemfile("current #{name}", command)
312
+ end
313
+
314
+ def restore_toys_libs
315
+ $LOAD_PATH.delete(::Toys::CORE_LIB_PATH)
316
+ $LOAD_PATH.unshift(::Toys::CORE_LIB_PATH)
317
+ if ::Toys.const_defined?(:LIB_PATH)
318
+ $LOAD_PATH.delete(::Toys::LIB_PATH)
319
+ $LOAD_PATH.unshift(::Toys::LIB_PATH)
257
320
  end
258
321
  end
259
322
 
@@ -264,7 +327,8 @@ module Toys
264
327
  when :error
265
328
  false
266
329
  else
267
- terminal.confirm("Your bundle is not complete. Install? ", default: @default_confirm)
330
+ terminal.confirm("Your bundle requires additional gems. Install? ",
331
+ default: @default_confirm)
268
332
  end
269
333
  end
270
334
 
@@ -276,7 +340,12 @@ module Toys
276
340
  " `cd #{gemfile_dir} && bundle install`"
277
341
  end
278
342
  require "bundler/cli"
279
- ::Bundler::CLI.start(["install"])
343
+ begin
344
+ ::Bundler::CLI.start(["install"])
345
+ rescue ::Bundler::GemNotFound, ::Bundler::InstallError
346
+ terminal.puts("Failed to install. Trying update...")
347
+ ::Bundler::CLI.start(["update"])
348
+ end
280
349
  end
281
350
  end
282
351
  end