kitchen-omnibus-chef 0.0.1

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.
@@ -0,0 +1,352 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2015, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # https://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "json" unless defined?(JSON)
19
+
20
+ module Kitchen
21
+ module Provisioner
22
+ module Chef
23
+ # Internal object to manage common sandbox preparation for
24
+ # Chef-related provisioners.
25
+ #
26
+ # @author Fletcher Nichol <fnichol@nichol.ca>
27
+ # @api private
28
+ class CommonSandbox
29
+ include Logging
30
+
31
+ # Constructs a new object, taking config, a sandbox path, and an
32
+ # instance.
33
+ #
34
+ # @param config [Hash] configuration hash
35
+ # @param sandbox_path [String] path to local sandbox directory
36
+ # @param instance [Instance] an instance
37
+ def initialize(config, sandbox_path, instance)
38
+ @config = config
39
+ @sandbox_path = sandbox_path
40
+ @instance = instance
41
+ end
42
+
43
+ # Populate the sandbox.
44
+ def populate
45
+ prepare_json
46
+ prepare_cache
47
+ prepare_cookbooks
48
+ prepare(:data)
49
+ prepare(:data_bags)
50
+ prepare(:environments)
51
+ prepare(:nodes)
52
+ prepare(:roles)
53
+ prepare(:clients)
54
+ prepare(
55
+ :secret,
56
+ type: :file,
57
+ dest_name: "encrypted_data_bag_secret",
58
+ key_name: :encrypted_data_bag_secret_key_path
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ # @return [Hash] configuration hash
65
+ # @api private
66
+ attr_reader :config
67
+
68
+ # @return [Instance] an instance
69
+ # @api private
70
+ attr_reader :instance
71
+
72
+ # @return [String] path to local sandbox directory
73
+ # @api private
74
+ attr_reader :sandbox_path
75
+
76
+ # @return [String] name of the policy_group, nil results in "local"
77
+ # @api private
78
+ attr_reader :policy_group
79
+
80
+ # Generates a list of all files in the cookbooks directory in the
81
+ # sandbox path.
82
+ #
83
+ # @return [Array<String>] an array of absolute paths to files
84
+ # @api private
85
+ def all_files_in_cookbooks
86
+ Util.list_directory(tmpbooks_dir, include_dot: true, recurse: true)
87
+ .select { |fn| File.file?(fn) }
88
+ end
89
+
90
+ # @return [String] an absolute path to a Policyfile, relative to the
91
+ # kitchen root
92
+ # @api private
93
+ def policyfile
94
+ basename = config[:policyfile_path] || config[:policyfile] || "Policyfile.rb"
95
+ File.expand_path(basename, config[:kitchen_root])
96
+ end
97
+
98
+ # @return [String] an absolute path to a Berksfile, relative to the
99
+ # kitchen root
100
+ # @api private
101
+ def berksfile
102
+ basename = config[:berksfile_path] || "Berksfile"
103
+ File.expand_path(basename, config[:kitchen_root])
104
+ end
105
+
106
+ # @return [String] an absolute path to a cookbooks/ directory, relative
107
+ # to the kitchen root
108
+ # @api private
109
+ def cookbooks_dir
110
+ File.join(config[:kitchen_root], "cookbooks")
111
+ end
112
+
113
+ # Copies a cookbooks/ directory into the sandbox path.
114
+ #
115
+ # @api private
116
+ def cp_cookbooks
117
+ info("Preparing cookbooks from project directory")
118
+ debug("Using cookbooks from #{cookbooks_dir}")
119
+
120
+ FileUtils.mkdir_p(tmpbooks_dir)
121
+ FileUtils.cp_r(File.join(cookbooks_dir, "."), tmpbooks_dir)
122
+
123
+ cp_site_cookbooks if File.directory?(site_cookbooks_dir)
124
+ cp_this_cookbook if File.exist?(metadata_rb)
125
+ end
126
+
127
+ # Copies a site-cookbooks/ directory into the sandbox path.
128
+ #
129
+ # @api private
130
+ def cp_site_cookbooks
131
+ info("Preparing site-cookbooks from project directory")
132
+ debug("Using cookbooks from #{site_cookbooks_dir}")
133
+
134
+ FileUtils.mkdir_p(tmpsitebooks_dir)
135
+ FileUtils.cp_r(File.join(site_cookbooks_dir, "."), tmpsitebooks_dir)
136
+ end
137
+
138
+ # Copies the current project, assumed to be a Chef cookbook into the
139
+ # sandbox path.
140
+ #
141
+ # @api private
142
+ def cp_this_cookbook
143
+ info("Preparing current project directory as a cookbook")
144
+ debug("Using metadata.rb from #{metadata_rb}")
145
+
146
+ cb_name = MetadataChopper.extract(metadata_rb).first || raise(UserError,
147
+ "The metadata.rb does not define the 'name' key." \
148
+ " Please add: `name '<cookbook_name>'` to metadata.rb and retry")
149
+
150
+ cb_path = File.join(tmpbooks_dir, cb_name)
151
+
152
+ glob = Util.list_directory(config[:kitchen_root])
153
+
154
+ FileUtils.mkdir_p(cb_path)
155
+ FileUtils.cp_r(glob, cb_path)
156
+ end
157
+
158
+ # Removes all non-cookbook files in the sandbox path.
159
+ #
160
+ # @api private
161
+ def filter_only_cookbook_files
162
+ info("Removing non-cookbook files before transfer")
163
+ FileUtils.rm(all_files_in_cookbooks - only_cookbook_files)
164
+ Util.list_directory(tmpbooks_dir, recurse: true)
165
+ .reverse_each { |fn| FileUtils.rmdir(fn) if File.directory?(fn) && Dir.empty?(fn) }
166
+ end
167
+
168
+ # @return [Logger] the instance's logger or Test Kitchen's common
169
+ # logger otherwise
170
+ # @api private
171
+ def logger
172
+ instance ? instance.logger : Kitchen.logger
173
+ end
174
+
175
+ # Creates a minimal, no-op cookbook in the sandbox path.
176
+ #
177
+ # @api private
178
+ def make_fake_cookbook
179
+ info("Policyfile, Berksfile, cookbooks/, or metadata.rb not found " \
180
+ "so Chef Infra Client will run, but do nothing. Is this intended?")
181
+ name = File.basename(config[:kitchen_root])
182
+ fake_cb = File.join(tmpbooks_dir, name)
183
+ FileUtils.mkdir_p(fake_cb)
184
+ File.open(File.join(fake_cb, "metadata.rb"), "wb") do |file|
185
+ file.write(%{name "#{name}"\n})
186
+ end
187
+ end
188
+
189
+ # @return [String] an absolute path to a metadata.rb, relative to the
190
+ # kitchen root
191
+ # @api private
192
+ def metadata_rb
193
+ File.join(config[:kitchen_root], "metadata.rb")
194
+ end
195
+
196
+ # Generates a list of all typical cookbook files needed in a Chef run,
197
+ # located in the cookbooks directory in the sandbox path.
198
+ #
199
+ # @return [Array<String>] an array of absolute paths to files
200
+ # @api private
201
+ def only_cookbook_files
202
+ glob = File.join("*", "{#{config[:cookbook_files_glob]}}")
203
+ Util.safe_glob(tmpbooks_dir, glob, File::FNM_DOTMATCH)
204
+ .select { |fn| File.file?(fn) && ! %w{. ..}.include?(fn) }
205
+ end
206
+
207
+ # Prepares a generic Chef component source directory or file for
208
+ # inclusion in the sandbox path. These components might includes nodes,
209
+ # roles, etc.
210
+ #
211
+ # @param component [Symbol,String] a component name such as `:node`
212
+ # @param opts [Hash] optional configuration
213
+ # @option opts [Symbol] :type whether the component is a directory or
214
+ # file (default: `:directory`)
215
+ # @option opts [Symbol] :key_name the key name in the config hash from
216
+ # which to pull the source path (default: `"#{component}_path"`)
217
+ # @option opts [String] :dest_name the destination file or directory
218
+ # basename in the sandbox path (default: `component.to_s`)
219
+ # @api private
220
+ def prepare(component, opts = {})
221
+ opts = { type: :directory }.merge(opts)
222
+ key_name = opts.fetch(:key_name, "#{component}_path")
223
+ src = config[key_name.to_sym]
224
+ return if src.nil?
225
+
226
+ info("Preparing #{component}")
227
+ debug("Using #{component} from #{src}")
228
+
229
+ dest = File.join(sandbox_path, opts.fetch(:dest_name, component.to_s))
230
+
231
+ case opts[:type]
232
+ when :directory
233
+ FileUtils.mkdir_p(dest)
234
+ Array(src).each { |dir| FileUtils.cp_r(Util.list_directory(dir), dest) }
235
+ when :file
236
+ FileUtils.mkdir_p(File.dirname(dest))
237
+ Array(src).each { |file| FileUtils.cp_r(file, dest) }
238
+ end
239
+ end
240
+
241
+ # Prepares a cache directory for inclusion in the sandbox path.
242
+ #
243
+ # @api private
244
+ def prepare_cache
245
+ FileUtils.mkdir_p(File.join(sandbox_path, "cache"))
246
+ end
247
+
248
+ # Prepares Chef cookbooks for inclusion in the sandbox path.
249
+ #
250
+ # @api private
251
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
252
+ def prepare_cookbooks
253
+ if File.exist?(policyfile)
254
+ resolve_with_policyfile
255
+ elsif File.exist?(berksfile)
256
+ resolve_with_berkshelf
257
+ elsif File.directory?(cookbooks_dir)
258
+ cp_cookbooks
259
+ elsif File.exist?(metadata_rb)
260
+ cp_this_cookbook
261
+ else
262
+ make_fake_cookbook
263
+ end
264
+
265
+ filter_only_cookbook_files
266
+ end
267
+
268
+ # Prepares a Chef JSON file, sometimes called a dna.json or
269
+ # first-boot.json, for inclusion in the sandbox path.
270
+ #
271
+ # @api private
272
+ def prepare_json
273
+ dna = if File.exist?(policyfile)
274
+ update_dna_for_policyfile
275
+ else
276
+ config[:attributes].merge(run_list: config[:run_list])
277
+ end
278
+
279
+ info("Preparing dna.json")
280
+ debug("Creating dna.json from #{dna.inspect}")
281
+
282
+ File.open(File.join(sandbox_path, "dna.json"), "wb") do |file|
283
+ file.write(dna.to_json)
284
+ end
285
+ end
286
+
287
+ def update_dna_for_policyfile
288
+ policy = Chef::Policyfile.new(
289
+ policyfile, sandbox_path,
290
+ logger:,
291
+ always_update: config[:always_update_cookbooks],
292
+ policy_group:,
293
+ license: config[:chef_license]
294
+ )
295
+ Kitchen.mutex.synchronize do
296
+ policy.compile
297
+ end
298
+ policy_name = JSON.parse(File.read(policy.lockfile))["name"]
299
+ policy_group = config[:policy_group] || "local"
300
+ config[:attributes].merge(policy_name:, policy_group:)
301
+ end
302
+
303
+ # Performs a Policyfile cookbook resolution inside a common mutex.
304
+ #
305
+ # @api private
306
+ def resolve_with_policyfile
307
+ Kitchen.mutex.synchronize do
308
+ Chef::Policyfile.new(
309
+ policyfile, sandbox_path,
310
+ logger:,
311
+ always_update: config[:always_update_cookbooks],
312
+ policy_group: config[:policy_group],
313
+ license: config[:chef_license]
314
+ ).resolve
315
+ end
316
+ end
317
+
318
+ # Performs a Berkshelf cookbook resolution inside a common mutex.
319
+ #
320
+ # @api private
321
+ def resolve_with_berkshelf
322
+ Kitchen.mutex.synchronize do
323
+ Chef::Berkshelf.new(berksfile, tmpbooks_dir,
324
+ logger:,
325
+ always_update: config[:always_update_cookbooks]).resolve
326
+ end
327
+ end
328
+
329
+ # @return [String] an absolute path to a site-cookbooks/ directory,
330
+ # relative to the kitchen root
331
+ # @api private
332
+ def site_cookbooks_dir
333
+ File.join(config[:kitchen_root], "site-cookbooks")
334
+ end
335
+
336
+ # @return [String] an absolute path to a cookbooks/ directory in the
337
+ # sandbox path
338
+ # @api private
339
+ def tmpbooks_dir
340
+ File.join(sandbox_path, "cookbooks")
341
+ end
342
+
343
+ # @return [String] an absolute path to a site cookbooks directory in the
344
+ # sandbox path
345
+ # @api private
346
+ def tmpsitebooks_dir
347
+ File.join(sandbox_path, "cookbooks")
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,173 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2013, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # https://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "shellwords" unless defined?(Shellwords)
19
+ require "rbconfig" unless defined?(RbConfig)
20
+
21
+ require "kitchen/errors"
22
+ require "kitchen/logging"
23
+ require "kitchen/shell_out"
24
+ require "kitchen/which"
25
+
26
+ module Kitchen
27
+ module Provisioner
28
+ module Chef
29
+ # Chef cookbook resolver that uses Policyfiles to calculate dependencies.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class Policyfile
33
+ include Logging
34
+ include ShellOut
35
+ include Which
36
+
37
+ # Creates a new cookbook resolver.
38
+ #
39
+ # @param policyfile [String] path to a Policyfile
40
+ # @param path [String] path in which to vendor the resulting
41
+ # cookbooks
42
+ # @param logger [Kitchen::Logger] a logger to use for output, defaults
43
+ # to `Kitchen.logger`
44
+ def initialize(policyfile, path, license: nil, logger: Kitchen.logger, always_update: false, policy_group: nil)
45
+ @policyfile = policyfile
46
+ @path = path
47
+ @logger = logger
48
+ @always_update = always_update
49
+ @policy_group = policy_group
50
+ @license = license
51
+ end
52
+
53
+ # Loads the library code required to use the resolver.
54
+ #
55
+ # @param logger [Kitchen::Logger] a logger to use for output, defaults
56
+ # to `Kitchen.logger`
57
+ def self.load!(logger: Kitchen.logger)
58
+ # intentionally left blank
59
+ end
60
+
61
+ # Performs the cookbook resolution and vendors the resulting cookbooks
62
+ # in the desired path.
63
+ def resolve
64
+ if policy_group
65
+ info("Exporting cookbook dependencies from Policyfile #{path} with policy_group #{policy_group} using `#{cli_path} export`...")
66
+ run_command("#{cli_path} export #{escape_path(policyfile)} #{escape_path(path)} --policy_group #{policy_group} --force #{chef_license(license)}")
67
+ else
68
+ info("Exporting cookbook dependencies from Policyfile #{path} using `#{cli_path} export`...")
69
+ run_command("#{cli_path} export #{escape_path(policyfile)} #{escape_path(path)} --force #{chef_license(license)}")
70
+ end
71
+ end
72
+
73
+ # Runs `chef install` to determine the correct cookbook set and
74
+ # generate the policyfile lock.
75
+ def compile
76
+ if File.exist?(lockfile)
77
+ info("Installing cookbooks for Policyfile #{policyfile} using `#{cli_path} install`")
78
+ else
79
+ info("Policy lock file doesn't exist, running `#{cli_path} install` for Policyfile #{policyfile}...")
80
+ end
81
+ run_command("#{cli_path} install #{escape_path(policyfile)} #{chef_license(license)}")
82
+
83
+ if always_update
84
+ info("Updating policy lock using `#{cli_path} update`")
85
+ run_command("#{cli_path} update #{escape_path(policyfile)} #{chef_license(license)}")
86
+ end
87
+ end
88
+
89
+ # Return the path to the lockfile corresponding to this policyfile.
90
+ #
91
+ # @return [String]
92
+ def lockfile
93
+ policyfile.gsub(/\.rb\Z/, ".lock.json")
94
+ end
95
+
96
+ private
97
+
98
+ # @return [String] path to a Policyfile
99
+ # @api private
100
+ attr_reader :policyfile
101
+
102
+ # @return [String] path in which to vendor the resulting cookbooks
103
+ # @api private
104
+ attr_reader :path
105
+
106
+ # @return [Kitchen::Logger] a logger to use for output
107
+ # @api private
108
+ attr_reader :logger
109
+
110
+ # @return [Boolean] If true, always update cookbooks in the policy.
111
+ # @api private
112
+ attr_reader :always_update
113
+
114
+ # @return [String] name of the policy_group, nil results in "local"
115
+ # @api private
116
+ attr_reader :policy_group
117
+
118
+ # @return [String] name of the chef_license
119
+ # @api private
120
+ attr_reader :license
121
+
122
+ # Escape spaces in a path in way that works with both Sh (Unix) and
123
+ # Windows.
124
+ #
125
+ # @param path [String] Path to escape
126
+ # @return [String]
127
+ # @api private
128
+ def escape_path(path)
129
+ if /mswin|mingw/.match?(RbConfig::CONFIG["host_os"])
130
+ # I know what you're thinking: "just use Shellwords.escape". That
131
+ # method produces incorrect results on Windows with certain input
132
+ # which would be a metacharacter in Sh but is not for one or more of
133
+ # Windows command line parsing libraries. This covers the 99% case of
134
+ # spaces in the path without breaking other stuff.
135
+ if /[ \t\n\v"]/.match?(path)
136
+ "\"#{path.gsub(/[ \t\n\v\"\\]/) { |m| "\\" + m[0] }}\""
137
+ else
138
+ path
139
+ end
140
+ else
141
+ Shellwords.escape(path)
142
+ end
143
+ end
144
+
145
+ # Find the `chef` or `chef-cli` commands in the path or raise `chef` is present in
146
+ # ChefDK / Workstation releases, but is no longer shipped in any gems now that we
147
+ # use a Go based wrapper for the `chef` command in Workstation. The Ruby CLI has been
148
+ # renamed `chef-cli` under the hood and is shipped in the `chef-cli` gem.
149
+ #
150
+ # @api private
151
+ # @returns [String]
152
+ def cli_path
153
+ @cli_path ||= which("chef-cli") || which("chef") || no_cli_found_error
154
+ end
155
+
156
+ # @api private
157
+ def no_cli_found_error
158
+ @logger.fatal("The `chef` or `chef-cli` executables cannot be found in your " \
159
+ "PATH. Ensure you have installed Chef Workstation " \
160
+ "from https://www.chef.io/downloads/ and that your PATH " \
161
+ "setting includes the path to the `chef` or `chef-cli` commands.")
162
+ raise UserError, "Could not find the chef or chef-cli executables in your PATH."
163
+ end
164
+
165
+ # Return `"--chef-license #{license}"` when `license` is not nil or empty and the empty string otherwise.
166
+ def chef_license(license)
167
+ (license.nil? || license.empty?) ? "" : "--chef-license #{license}"
168
+ end
169
+
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,121 @@
1
+ #
2
+ # Author:: SAWANOBORI Yukihiko <sawanoboriyu@higanworks.com>)
3
+ #
4
+ # Copyright (C) 2015, HiganWorks LLC
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # https://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ # Usage:
19
+ #
20
+ # puts your recipes to` apply/` directory.
21
+ #
22
+ # An example of .kitchen.yml.
23
+ #
24
+ # ---
25
+ # driver:
26
+ # name: vagrant
27
+ #
28
+ # provisioner:
29
+ # name: chef_apply
30
+ #
31
+ # platforms:
32
+ # - name: ubuntu-24.04
33
+ # - name: almalinux-10
34
+ #
35
+ # suites:
36
+ # - name: default
37
+ # run_list:
38
+ # - recipe1
39
+ # - recipe2
40
+ #
41
+ #
42
+ # The chef-apply runs twice below.
43
+ #
44
+ # chef-apply apply/recipe1.rb
45
+ # chef-apply apply/recipe2.rb
46
+
47
+ require_relative "chef_base"
48
+
49
+ module Kitchen
50
+ module Provisioner
51
+ # Chef Apply provisioner.
52
+ #
53
+ # @author SAWANOBORI Yukihiko <sawanoboriyu@higanworks.com>)
54
+ class ChefApply < ChefBase
55
+ kitchen_provisioner_api_version 2
56
+
57
+ plugin_version Kitchen::VERSION
58
+
59
+ default_config :chef_apply_path do |provisioner|
60
+ provisioner
61
+ .remote_path_join(%W{#{provisioner[:chef_omnibus_root]} bin chef-apply})
62
+ .tap { |path| path.concat(".bat") if provisioner.windows_os? }
63
+ end
64
+
65
+ default_config :apply_path do |provisioner|
66
+ provisioner.calculate_path("apply")
67
+ end
68
+ expand_path_for :apply_path
69
+
70
+ # (see ChefBase#create_sandbox)
71
+ def create_sandbox
72
+ @sandbox_path = Dir.mktmpdir("#{instance.name}-sandbox-")
73
+ File.chmod(0755, sandbox_path)
74
+ info("Preparing files for transfer")
75
+ debug("Creating local sandbox in #{sandbox_path}")
76
+
77
+ prepare_json
78
+ prepare(:apply)
79
+ end
80
+
81
+ # (see ChefBase#init_command)
82
+ def init_command
83
+ dirs = %w{
84
+ apply
85
+ }.sort.map { |dir| remote_path_join(config[:root_path], dir) }
86
+
87
+ vars = if powershell_shell?
88
+ init_command_vars_for_powershell(dirs)
89
+ else
90
+ init_command_vars_for_bourne(dirs)
91
+ end
92
+
93
+ prefix_command(shell_code_from_file(vars, "chef_base_init_command"))
94
+ end
95
+
96
+ # (see ChefSolo#run_command)
97
+ def run_command
98
+ level = config[:log_level]
99
+ lines = []
100
+ config[:run_list].map do |recipe|
101
+ cmd = sudo(config[:chef_apply_path]).dup
102
+ .tap { |str| str.insert(0, "& ") if powershell_shell? }
103
+ args = [
104
+ "apply/#{recipe}.rb",
105
+ "--log_level #{level}",
106
+ "--no-color",
107
+ ]
108
+ args << "--logfile #{config[:log_file]}" if config[:log_file]
109
+ args << "--chef-license #{config[:chef_license]}" if config[:chef_license]
110
+
111
+ lines << wrap_shell_code(
112
+ [cmd, *args].join(" ")
113
+ .tap { |str| str.insert(0, reload_ps1_path) if windows_os? }
114
+ )
115
+ end
116
+
117
+ prefix_command(lines.join("\n"))
118
+ end
119
+ end
120
+ end
121
+ end