bcpm 0.11
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/CHANGELOG +133 -0
- data/Manifest +23 -0
- data/README +292 -0
- data/Rakefile +23 -0
- data/bcpm.gemspec +35 -0
- data/bin/bcpm +5 -0
- data/lib/bcpm.rb +23 -0
- data/lib/bcpm/cleanup.rb +24 -0
- data/lib/bcpm/cli.rb +262 -0
- data/lib/bcpm/config.rb +100 -0
- data/lib/bcpm/dist.rb +133 -0
- data/lib/bcpm/duel.rb +153 -0
- data/lib/bcpm/git.rb +93 -0
- data/lib/bcpm/match.rb +275 -0
- data/lib/bcpm/player.rb +410 -0
- data/lib/bcpm/regen.rb +102 -0
- data/lib/bcpm/socket.rb +8 -0
- data/lib/bcpm/tests/assertion_error.rb +13 -0
- data/lib/bcpm/tests/assertions.rb +102 -0
- data/lib/bcpm/tests/case_base.rb +166 -0
- data/lib/bcpm/tests/environment.rb +256 -0
- data/lib/bcpm/tests/suite.rb +114 -0
- data/lib/bcpm/tests/test_match.rb +166 -0
- data/lib/bcpm/update.rb +38 -0
- metadata +129 -0
data/lib/bcpm/player.rb
ADDED
@@ -0,0 +1,410 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
# :nodoc: namespace
|
4
|
+
module Bcpm
|
5
|
+
|
6
|
+
# Manages player packages.
|
7
|
+
module Player
|
8
|
+
# Clones a player code from a repository, and sets it up for development on the local machine.
|
9
|
+
#
|
10
|
+
# Returns the player's name, or nil if something went wrong.
|
11
|
+
def self.install(repo_uri, repo_branch)
|
12
|
+
name = player_name repo_uri
|
13
|
+
local_path = File.join local_root, name
|
14
|
+
if File.exist?(local_path)
|
15
|
+
puts "Player already installed at #{local_path}!"
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
return nil unless Bcpm::Git.clone_repo(repo_uri, repo_branch, local_path)
|
19
|
+
|
20
|
+
unless source_path = package_path(local_path)
|
21
|
+
puts "Repository #{repo_uri} doesn't seem to contain a player!"
|
22
|
+
FileUtils.rm_rf local_path
|
23
|
+
return nil
|
24
|
+
end
|
25
|
+
|
26
|
+
Bcpm::Dist.add_player source_path
|
27
|
+
configure local_path
|
28
|
+
name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Downloads player code from a repository for one-time use, without linking it to the repository.
|
32
|
+
#
|
33
|
+
# Returns the path to the player on the local system, or nil if something went wrong.
|
34
|
+
def self.checkpoint(repo_uri, repo_branch, local_name)
|
35
|
+
old_name = player_name repo_uri
|
36
|
+
local_path = File.join local_root, local_name
|
37
|
+
if File.exist?(local_path)
|
38
|
+
puts "Player already installed at #{local_path}!"
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
if old_name == repo_uri
|
42
|
+
return nil unless Bcpm::Git.checkpoint_local_repo(File.join(local_root, old_name), local_path)
|
43
|
+
else
|
44
|
+
return nil unless Bcpm::Git.checkpoint_repo(repo_uri, repo_branch, local_path)
|
45
|
+
end
|
46
|
+
if local_name == old_name
|
47
|
+
source_path = package_path(local_path)
|
48
|
+
else
|
49
|
+
source_path = rename(local_path, old_name)
|
50
|
+
end
|
51
|
+
unless source_path
|
52
|
+
puts "Repository #{repo_uri} doesn't seem to contain a player!"
|
53
|
+
FileUtils.rm_rf local_path
|
54
|
+
return nil
|
55
|
+
end
|
56
|
+
|
57
|
+
Bcpm::Dist.add_player source_path
|
58
|
+
configure local_path
|
59
|
+
local_path
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a player from the built-in template.
|
63
|
+
#
|
64
|
+
# Returns the path to the player on the local system, or nil if something went wrong.
|
65
|
+
def self.create(local_name)
|
66
|
+
old_name = 'template'
|
67
|
+
local_path = File.join local_root, local_name
|
68
|
+
if File.exist?(local_path)
|
69
|
+
puts "Player already installed at #{local_path}!"
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
write_template local_path
|
73
|
+
unless source_path = rename(local_path, old_name)
|
74
|
+
puts "Repository #{repo_uri} doesn't seem to contain a player!"
|
75
|
+
FileUtils.rm_rf local_path
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
|
79
|
+
Bcpm::Dist.add_player source_path
|
80
|
+
configure local_path
|
81
|
+
local_path
|
82
|
+
end
|
83
|
+
|
84
|
+
# Undoes the effects of an install or checkpoint call.
|
85
|
+
#
|
86
|
+
# Returns true for success, or false if something went wrong.
|
87
|
+
def self.uninstall(local_name)
|
88
|
+
local_path = File.join local_root, local_name
|
89
|
+
source_path = package_path local_path
|
90
|
+
Bcpm::Dist.remove_player source_path if source_path
|
91
|
+
return false unless File.exist? local_path
|
92
|
+
FileUtils.rm_rf local_path
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Re-configures a player's source code project.
|
97
|
+
def self.reconfigure(local_name)
|
98
|
+
local_path = File.join local_root, local_name
|
99
|
+
unless source_path = package_path(local_path)
|
100
|
+
puts "Directory #{local_path} doesn't seem to contain a player!"
|
101
|
+
FileUtils.rm_rf local_path
|
102
|
+
return nil
|
103
|
+
end
|
104
|
+
Bcpm::Dist.add_player source_path
|
105
|
+
configure local_path
|
106
|
+
end
|
107
|
+
|
108
|
+
# Runs the player's test suite.
|
109
|
+
def self.run_suite(local_name)
|
110
|
+
local_path = File.join local_root, local_name
|
111
|
+
unless has_suite? local_path
|
112
|
+
puts "No test suite found at #{local_path}!"
|
113
|
+
return false
|
114
|
+
end
|
115
|
+
new_suite(local_path).run false
|
116
|
+
end
|
117
|
+
|
118
|
+
# Runs a case in the test suite against a player codebase.
|
119
|
+
def self.run_case(case_name, live, local_name, branch = 'master')
|
120
|
+
local_path = File.join local_root, local_name
|
121
|
+
unless has_suite? local_path
|
122
|
+
puts "No test suite found at #{local_path}!"
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
|
126
|
+
case_file = case_name + '.rb'
|
127
|
+
files = suite_files(local_path).select { |file| File.basename(file) == case_file }
|
128
|
+
unless files.length == 1
|
129
|
+
puts "Ambiguous case name! It matched #{files.count} cases.\n#{files.join("\n")}\n"
|
130
|
+
return false
|
131
|
+
end
|
132
|
+
suite = Bcpm::Tests::Suite.new(local_path)
|
133
|
+
suite.add_cases files
|
134
|
+
suite.run live
|
135
|
+
end
|
136
|
+
|
137
|
+
# Creates a Suite instance for running all the tests.
|
138
|
+
def self.new_suite(local_path)
|
139
|
+
suite = Bcpm::Tests::Suite.new local_path
|
140
|
+
suite.add_cases suite_files(local_path)
|
141
|
+
suite
|
142
|
+
end
|
143
|
+
|
144
|
+
# All the test cases in the suite of the given player.
|
145
|
+
def self.suite_files(local_path)
|
146
|
+
Dir.glob File.join(local_path, 'suite', '**', '*.rb')
|
147
|
+
end
|
148
|
+
|
149
|
+
# True if a battlecode distribution is installed on the local machine.
|
150
|
+
def self.has_suite?(local_path)
|
151
|
+
File.exist? File.join(local_path, 'suite')
|
152
|
+
end
|
153
|
+
|
154
|
+
# Configures a player's source code project.
|
155
|
+
def self.configure(local_path)
|
156
|
+
File.open File.join(local_path, '.project'), 'wb' do |f|
|
157
|
+
f.write eclipse_project(local_path)
|
158
|
+
end
|
159
|
+
|
160
|
+
File.open File.join(local_path, '.classpath'), 'wb' do |f|
|
161
|
+
f.write eclipse_classpath(local_path)
|
162
|
+
end
|
163
|
+
|
164
|
+
File.open File.join(local_path, 'build.xml'), 'wb' do |f|
|
165
|
+
f.write ant_config('bc.conf')
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# The directory containing all players code.
|
170
|
+
def self.local_root
|
171
|
+
Bcpm::Config[:player_root] ||= default_local_root
|
172
|
+
end
|
173
|
+
|
174
|
+
# The directory containing all players code.
|
175
|
+
def self.default_local_root
|
176
|
+
path = Dir.pwd
|
177
|
+
unless File.exist?(File.join(path, '.metadata'))
|
178
|
+
puts "Please chdir into your Eclipse workspace!"
|
179
|
+
exit 1
|
180
|
+
end
|
181
|
+
path
|
182
|
+
end
|
183
|
+
|
184
|
+
# All installed players.
|
185
|
+
#
|
186
|
+
# This might contain false positives.
|
187
|
+
def self.list
|
188
|
+
Dir.glob(File.join(local_root, '*', '.project')).map do |project_file|
|
189
|
+
File.basename File.dirname(project_file)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# All installed players who are properly hooked into the distribution.
|
194
|
+
#
|
195
|
+
# These players can be used into tests and simulations.
|
196
|
+
def self.list_active
|
197
|
+
list.select { |player_name| wired? player_name }
|
198
|
+
end
|
199
|
+
|
200
|
+
# True if the given player name is installed and wired.
|
201
|
+
def self.wired?(player_name)
|
202
|
+
local_path = File.join local_root, player_name
|
203
|
+
source_path = package_path local_path, nil, true
|
204
|
+
(source_path && Bcpm::Dist.contains_player?(source_path)) ? true : false
|
205
|
+
end
|
206
|
+
|
207
|
+
# Cleans up all the installed players.
|
208
|
+
def self.uninstall_all
|
209
|
+
list_good.each { |player_name| uninstall player_name }
|
210
|
+
end
|
211
|
+
|
212
|
+
# Extracts the player name out of the git repository URI for the player's code.
|
213
|
+
def self.player_name(repo_uri)
|
214
|
+
name = File.basename(repo_uri)
|
215
|
+
name = name[0...-4] if name[-4, 4] == '.git'
|
216
|
+
name
|
217
|
+
end
|
218
|
+
|
219
|
+
# Renames a player to match its path on the local system.
|
220
|
+
#
|
221
|
+
# Returns the path to the player's source package.
|
222
|
+
def self.rename(local_path, old_name)
|
223
|
+
new_name = File.basename local_path
|
224
|
+
return nil unless old_source_dir = package_path(local_path, old_name)
|
225
|
+
new_source_dir = File.join File.dirname(old_source_dir), new_name
|
226
|
+
FileUtils.mv old_source_dir, new_source_dir
|
227
|
+
|
228
|
+
Dir.glob(File.join(new_source_dir, '**', '*.java')).each do |file|
|
229
|
+
contents = File.read file
|
230
|
+
contents.gsub! /(^|[^A-Za-z0-9_.])#{old_name}([^A-Za-z0-9_]|$)/, "\\1#{new_name}\\2"
|
231
|
+
File.open(file, 'wb') { |f| f.write contents }
|
232
|
+
end
|
233
|
+
new_source_dir
|
234
|
+
end
|
235
|
+
|
236
|
+
# Extracts the path to a player's source package given their repository.
|
237
|
+
#
|
238
|
+
# Args:
|
239
|
+
# local_path:: path to the player's git repository on the local machine
|
240
|
+
# name_override:: (optional) supplies the player name; if not set, the name is extracted from
|
241
|
+
# the path, by convention
|
242
|
+
# silent:: if set, won't output errors that help the user debug the problem
|
243
|
+
def self.package_path(local_path, name_override = nil, silent = false)
|
244
|
+
# All the packages should be in the 'src' directory.
|
245
|
+
source_dir = File.join local_path, 'src'
|
246
|
+
unless File.exist? source_dir
|
247
|
+
puts "Missing src directory" unless silent
|
248
|
+
return nil
|
249
|
+
end
|
250
|
+
|
251
|
+
# Ignore maintainance files/folder such as .gitignore / .svn.
|
252
|
+
package_dirs = Dir.glob(File.join(source_dir, '*')).
|
253
|
+
reject { |path| File.basename(path)[0, 1] == '.' }
|
254
|
+
unless package_dirs.length == 1
|
255
|
+
puts "src directory doesn't contain exactly one package directory!" unless silent
|
256
|
+
return nil
|
257
|
+
end
|
258
|
+
|
259
|
+
path = package_dirs.first
|
260
|
+
unless (name_override || File.basename(local_path)) == File.basename(path)
|
261
|
+
puts "The package in the src directory doesn't match the player name!" unless silent
|
262
|
+
return nil
|
263
|
+
end
|
264
|
+
path
|
265
|
+
end
|
266
|
+
|
267
|
+
# The contents of an Ant configuration file (build.xml) pointing to a simulator config file.
|
268
|
+
def self.ant_config(simulator_config)
|
269
|
+
contents = File.read Bcpm::Dist.ant_file
|
270
|
+
# Point to the distribution instead of current root.
|
271
|
+
contents.gsub! 'basedir="."', 'basedir="' + Bcpm::Dist.dist_path + '"'
|
272
|
+
contents.gsub! '<property name="path.base" location="."',
|
273
|
+
'<property name="path.base" location="' + Bcpm::Dist.dist_path + '"'
|
274
|
+
# Set VM memory.
|
275
|
+
contents.gsub! /\<jvmarg\s+value\=\"\-Xmx.*\"/,
|
276
|
+
"<jvmarg value=\"-Xmx#{Bcpm::Match::vm_ram}m\""
|
277
|
+
# Replace hardcoded bc.conf reference.
|
278
|
+
contents.gsub! 'bc.conf', simulator_config
|
279
|
+
contents
|
280
|
+
end
|
281
|
+
|
282
|
+
# The contents of an Eclipse .classpath for a player project.
|
283
|
+
def self.eclipse_classpath(local_path)
|
284
|
+
jar_path = File.join Bcpm::Dist.dist_path, 'lib', 'battlecode-server.jar'
|
285
|
+
|
286
|
+
<<END_CONFIG
|
287
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
288
|
+
<classpath>
|
289
|
+
<classpathentry kind="src" path="src"/>
|
290
|
+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
291
|
+
<classpathentry kind="lib" path="#{jar_path}"/>
|
292
|
+
<classpathentry kind="output" path="bin"/>
|
293
|
+
</classpath>
|
294
|
+
END_CONFIG
|
295
|
+
end
|
296
|
+
|
297
|
+
# The contents of an Eclipse .project file for a player project.
|
298
|
+
def self.eclipse_project(local_path)
|
299
|
+
project_name = File.basename local_path
|
300
|
+
|
301
|
+
<<END_CONFIG
|
302
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
303
|
+
<projectDescription>
|
304
|
+
<name>#{project_name}</name>
|
305
|
+
<comment></comment>
|
306
|
+
<projects>
|
307
|
+
</projects>
|
308
|
+
<buildSpec>
|
309
|
+
<buildCommand>
|
310
|
+
<name>org.eclipse.jdt.core.javabuilder</name>
|
311
|
+
<arguments>
|
312
|
+
</arguments>
|
313
|
+
</buildCommand>
|
314
|
+
</buildSpec>
|
315
|
+
<natures>
|
316
|
+
<nature>org.eclipse.jdt.core.javanature</nature>
|
317
|
+
</natures>
|
318
|
+
</projectDescription>
|
319
|
+
END_CONFIG
|
320
|
+
end
|
321
|
+
|
322
|
+
# Writes the built-in player template.
|
323
|
+
def self.write_template(local_path)
|
324
|
+
src_path = File.join local_path, 'src', 'template'
|
325
|
+
FileUtils.mkdir_p src_path
|
326
|
+
File.open File.join(src_path, 'RobotPlayer.java'), 'wb' do |f|
|
327
|
+
f.write <<END_SOURCE
|
328
|
+
package template;
|
329
|
+
|
330
|
+
import battlecode.common.RobotController;
|
331
|
+
|
332
|
+
public class RobotPlayer implements Runnable {
|
333
|
+
public static RobotController rc;
|
334
|
+
|
335
|
+
public RobotPlayer(RobotController controller) {
|
336
|
+
rc = controller;
|
337
|
+
}
|
338
|
+
|
339
|
+
public void run() {
|
340
|
+
while (true) {
|
341
|
+
rc.yield();
|
342
|
+
}
|
343
|
+
}
|
344
|
+
}
|
345
|
+
END_SOURCE
|
346
|
+
end
|
347
|
+
|
348
|
+
src_path = File.join local_path, 'src', 'template', 'test', 'players'
|
349
|
+
FileUtils.mkdir_p src_path
|
350
|
+
File.open File.join(src_path, 'RobotPlayer.java'), 'wb' do |f|
|
351
|
+
f.write <<END_SOURCE
|
352
|
+
package template.test.players;
|
353
|
+
|
354
|
+
import battlecode.common.RobotController;
|
355
|
+
|
356
|
+
public class RobotPlayer implements Runnable {
|
357
|
+
public static RobotController rc;
|
358
|
+
|
359
|
+
public RobotPlayer(RobotController controller) {
|
360
|
+
rc = controller;
|
361
|
+
}
|
362
|
+
|
363
|
+
public void run() {
|
364
|
+
while (true) {
|
365
|
+
rc.yield();
|
366
|
+
}
|
367
|
+
}
|
368
|
+
}
|
369
|
+
END_SOURCE
|
370
|
+
end
|
371
|
+
|
372
|
+
suite_path = File.join local_path, 'suite'
|
373
|
+
FileUtils.mkdir_p suite_path
|
374
|
+
File.open File.join(suite_path, 'win_vs_yield.rb'), 'wb' do |f|
|
375
|
+
f.write <<END_SOURCE
|
376
|
+
vs 'yield'
|
377
|
+
suite_map '#{Bcpm::Dist.maps.first}'
|
378
|
+
replace_class 'RobotPlayer', 'test.players.RobotPlayer'
|
379
|
+
|
380
|
+
match do
|
381
|
+
it 'must win in any way' do
|
382
|
+
should_win
|
383
|
+
end
|
384
|
+
end
|
385
|
+
END_SOURCE
|
386
|
+
end
|
387
|
+
|
388
|
+
maps_path = File.join suite_path, 'maps'
|
389
|
+
FileUtils.mkdir_p maps_path
|
390
|
+
Bcpm::Dist.copy_map Bcpm::Dist.maps.first, maps_path
|
391
|
+
|
392
|
+
File.open File.join(local_path, '.gitignore'), 'wb' do |f|
|
393
|
+
f.write <<END_SOURCE
|
394
|
+
# Auto-generated by bcpm.
|
395
|
+
build.xml
|
396
|
+
.classpath
|
397
|
+
.project
|
398
|
+
|
399
|
+
# Build output.
|
400
|
+
bin
|
401
|
+
|
402
|
+
# Temporary files.
|
403
|
+
~*
|
404
|
+
.DS_Store
|
405
|
+
END_SOURCE
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end # module Bcpm::Player
|
409
|
+
|
410
|
+
end # namespace Bcpm
|
data/lib/bcpm/regen.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Bcpm
|
3
|
+
|
4
|
+
# Source code auto-generation capabilities.
|
5
|
+
module Regen
|
6
|
+
# Re-generates automatically generated source code files.
|
7
|
+
def self.run(file_names)
|
8
|
+
source_lines = {}
|
9
|
+
vars = {}
|
10
|
+
|
11
|
+
# Read in source blocks.
|
12
|
+
file_names.each do |file_name|
|
13
|
+
lines = File.open(file_name, 'rb') { |f| f.read.split "\n" }
|
14
|
+
|
15
|
+
current_block = nil
|
16
|
+
lines.each do |line|
|
17
|
+
if current_block
|
18
|
+
if /^\s+\/\/\$\s+\-gen\:source(\s.*)?$/ =~ line
|
19
|
+
current_block = nil
|
20
|
+
else
|
21
|
+
source_lines[current_block] << line
|
22
|
+
end
|
23
|
+
else
|
24
|
+
block_match = /^\s+\/\/\$\s+\+gen\:source\s+(\S+)\s+(.*)$/.match line
|
25
|
+
if block_match
|
26
|
+
current_block = block_match[1]
|
27
|
+
if source_lines[current_block]
|
28
|
+
print "Duplicate source block #{current_block}\n"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
source_lines[current_block] ||= []
|
32
|
+
vars[current_block] = block_match[2].scan /\S+/
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
if current_block
|
37
|
+
print "Un-closed source block #{current_block}\n"
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Replace target blocks.
|
43
|
+
file_names.each do |file_name|
|
44
|
+
lines = File.open(file_name, 'rb') { |f| f.read.split "\n" }
|
45
|
+
output_lines = []
|
46
|
+
|
47
|
+
current_block = nil
|
48
|
+
disabled = false
|
49
|
+
lines.each do |line|
|
50
|
+
if current_block
|
51
|
+
if /^\s+\/\/\$\s+\-gen\:target(\s.*)?$/ =~ line
|
52
|
+
output_lines << line
|
53
|
+
current_block = nil
|
54
|
+
end
|
55
|
+
else
|
56
|
+
block_match = /^\s+\/\/\$\s+\+gen\:target\s+(\S+)\s+(.*)$/.match line
|
57
|
+
output_lines << line
|
58
|
+
|
59
|
+
if block_match
|
60
|
+
current_block = block_match[1]
|
61
|
+
source_vars = vars[current_block]
|
62
|
+
if !source_vars
|
63
|
+
print "Missing source block #{current_block}\n"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
target_vars = block_match[2].scan /\S+/
|
67
|
+
if target_vars.length != source_vars.length
|
68
|
+
print "Source/target variable mismatch.\n"
|
69
|
+
print "Source: #{vars.join(' ')}\nTarget: #{vars.join(' ')}\n"
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
|
73
|
+
source_target = Hash[source_vars.zip(target_vars)]
|
74
|
+
regexp = Regexp.new source_vars.map { |var| "(#{var})" }.join('|')
|
75
|
+
new_lines = source_lines[current_block].map &:dup
|
76
|
+
disabled = false
|
77
|
+
new_lines.each do |line|
|
78
|
+
if disabled
|
79
|
+
disabled = false if /^\s+\/\/\$\s+\-gen\:off(\s.*)?$/ =~ line
|
80
|
+
else
|
81
|
+
if /^\s+\/\/\$\s+\+gen\:off(\s.*)?$/ =~ line
|
82
|
+
disabled = true
|
83
|
+
else
|
84
|
+
line.gsub!(regexp) { |match| source_target[match] }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
output_lines.concat new_lines
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
if current_block
|
93
|
+
print "Un-closed target block #{current_block}\n"
|
94
|
+
exit
|
95
|
+
end
|
96
|
+
|
97
|
+
File.open(file_name, 'wb') { |f| f.write output_lines.join("\n") }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end # module Bcpm::Regen
|
101
|
+
|
102
|
+
end # namespace Bcpm
|