NCPrePatcher 0.2.0-x64-mingw-ucrt

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/lib/ncpp.rb ADDED
@@ -0,0 +1,478 @@
1
+ require_relative 'nitro/nitro.rb'
2
+ require_relative 'unarm/unarm.rb'
3
+ require_relative 'ncpp/interpreter.rb'
4
+
5
+ require 'json'
6
+ require 'optparse'
7
+ require 'fileutils'
8
+ require 'pathname'
9
+
10
+ module NCPP
11
+
12
+ # TODO: MUST move away from globals
13
+ $clean_rom = nil
14
+ $target_rom = nil
15
+
16
+ alias $rom $clean_rom # In most cases the clean rom will be desired
17
+
18
+ $config = nil
19
+
20
+ NCP_CONFIG_FILE_PATH = 'ncpatcher.json'
21
+ CONFIG_FILE_PATH = 'ncpp_config.json'
22
+ NCPP_DEFS_FILENAME = 'ncpp_defs'
23
+ NCPP_GLB_DEFS_FILENAME = 'ncpp_global'
24
+
25
+ CONFIG_TEMPLATE = {
26
+ clean_rom: '', target_rom: '',
27
+ sources: [], source_file_types: %w[cpp hpp inl c h s],
28
+ symbols9: '', symbols7: '',
29
+ gen_path: 'ncpp-gen',
30
+ command_prefix: 'ncpp_'
31
+ }
32
+
33
+ REQUIRED_CONFIG_FIELDS = [:clean_rom, :sources].freeze
34
+
35
+
36
+ def self.glean_from_arm_config(cfg, arm_cfg, cpu: 9)
37
+ cfg[('symbols'+cpu.to_s).to_sym] = arm_cfg['symbols']
38
+ arm_cfg['regions'].each do |region|
39
+ if region['sources'][0].is_a? Array
40
+ cfg[:sources].concat(region['sources'].map {|src,glb| "#{src}#{glb ? '/*' : ''}" })
41
+ else
42
+ cfg[:sources].concat(region['sources'])
43
+ end
44
+ end
45
+ cfg
46
+ end
47
+
48
+ def self.update_ncp_configs(ncp_cfg, arm9_cfg, arm7_cfg = nil, revert: false)
49
+ if revert
50
+ ncp_cfg['pre-build']&.delete('ncpp')
51
+ ncp_cfg['pre-build']&.delete('ncpp.bat')
52
+ else
53
+ ncp_cfg['pre-build'] |= [(/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil ? 'ncpp.bat' : 'ncpp']
54
+ end
55
+
56
+ gen_path = ($config || CONFIG_TEMPLATE)[:gen_path]
57
+ prefix = "#{gen_path}/"
58
+
59
+ [arm9_cfg, arm7_cfg].compact.each do |cfg|
60
+
61
+ [['source', File.join(gen_path, 'source')], ['source7', File.join(gen_path, 'source7')]].each do |name, path|
62
+ entry = cfg['includes'].find { it[0] == (revert ? path : name) }
63
+ entry[0] = (revert ? name : path) if entry
64
+ end
65
+
66
+ cfg['regions'].each do |region|
67
+ region['sources'].map! do |src|
68
+ if src.is_a?(Array)
69
+ src[0] =
70
+ if revert
71
+ src[0].sub(/^#{Regexp.escape(prefix)}/, '')
72
+ else
73
+ File.join(gen_path, src[0].to_s)
74
+ end
75
+ src
76
+ else
77
+ if revert
78
+ src.sub(/^#{Regexp.escape(prefix)}/, '')
79
+ else
80
+ File.join(gen_path, src.to_s)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ File.write(NCP_CONFIG_FILE_PATH, JSON.pretty_generate(ncp_cfg))
88
+ File.write(ncp_cfg['arm9']['target'], JSON.pretty_generate(arm9_cfg))
89
+ File.write(ncp_cfg['arm7']['target'], JSON.pretty_generate(arm7_cfg)) if arm7_cfg
90
+ end
91
+
92
+ def self.get_missing_config_reqs(cfg)
93
+ missing_fields = []
94
+
95
+ cfg.each do |field, value|
96
+ next unless REQUIRED_CONFIG_FIELDS.include? field.to_sym
97
+ missing_fields << field if value.empty? || value.nil?
98
+ end
99
+
100
+ missing_fields
101
+ end
102
+
103
+ def self.list_missing_config_fields(missing, cfg_path)
104
+ plural = missing.length > 1
105
+ puts "Please fill #{plural ? 'out' : 'in'} the following field#{plural ? 's' : ''} in #{cfg_path.bold_red}:"
106
+ missing.each {|field| puts " * ".purple + field.to_s}
107
+ exit
108
+ end
109
+
110
+ def self.init(cfg_path = CONFIG_FILE_PATH, verbose: true)
111
+
112
+ if !File.exist?(cfg_path)
113
+ ncp_cfg = JSON.load_file(NCP_CONFIG_FILE_PATH)
114
+ arm9_cfg = arm7_cfg = nil
115
+
116
+ arm9_cfg = JSON.load_file(ncp_cfg['arm9']['target'])
117
+ cfg = glean_from_arm_config(CONFIG_TEMPLATE, arm9_cfg, cpu: 9)
118
+
119
+ if !ncp_cfg['arm7'].empty?
120
+ arm7_cfg = JSON.load_file(ncp_cfg['arm7']['target'])
121
+ cfg = glean_from_arm_config(cfg, arm7_cfg, cpu: 7)
122
+ end
123
+
124
+ update_ncp_configs(ncp_cfg,arm9_cfg,arm7_cfg)
125
+
126
+ File.write(cfg_path, JSON.pretty_generate(cfg))
127
+ puts "Created #{cfg_path} in current directory.".cyan if verbose
128
+
129
+ missing = get_missing_config_reqs(cfg)
130
+ list_missing_config_fields(missing, cfg_path) unless missing.empty?
131
+
132
+ else
133
+ cfg = JSON.load_file(cfg_path)
134
+ missing = get_missing_config_reqs(cfg)
135
+ list_missing_config_fields(missing, cfg_path) unless missing.empty?
136
+ end
137
+
138
+ $config = cfg
139
+ $clean_rom = Nitro::Rom.new(cfg['clean_rom'].gsub(/\$\{env:([^}]+)\}/) { ENV[$1] })
140
+ $target_rom = Nitro::Rom.new(cfg['target_rom'].gsub(/\$\{env:([^}]+)\}/) { ENV[$1]}) unless cfg['target_rom'].empty?
141
+
142
+ Unarm.load_symbols9(cfg['symbols9'].gsub(/\$\{env:([^}]+)\}/) { ENV[$1] }) unless cfg['symbols9'].empty?
143
+ Unarm.load_symbols7(cfg['symbols7'].gsub(/\$\{env:([^}]+)\}/) { ENV[$1] }) unless cfg['symbols7'].empty?
144
+ end
145
+
146
+ def self.uninstall(cfg_path = CONFIG_FILE_PATH)
147
+ ncp_cfg = JSON.load_file(NCP_CONFIG_FILE_PATH)
148
+
149
+ arm9_cfg = ncp_cfg['arm9'].empty? ? nil : JSON.load_file(ncp_cfg['arm9']['target'])
150
+ arm7_cfg = ncp_cfg['arm7'].empty? ? nil : JSON.load_file(ncp_cfg['arm7']['target'])
151
+
152
+ update_ncp_configs(ncp_cfg, arm9_cfg, arm7_cfg, revert: true)
153
+
154
+ return if !File.exist?(cfg_path)
155
+
156
+ cfg = JSON.load_file(cfg_path)
157
+ FileUtils.rm_rf(cfg['gen_path']) unless cfg['gen_path'].empty?
158
+ File.delete(cfg_path)
159
+ end
160
+
161
+ def self.show_rom_info(rom)
162
+ puts "Game title: #{rom.header.game_title}\n" \
163
+ "Game code: #{rom.header.game_code}\n" \
164
+ "Maker code: #{rom.header.maker_code}\n" \
165
+ "Size: #{(rom.size / 1024.0 / 1024.0).round(2)} MB\n" \
166
+ "Overlay count: #{rom.overlay_count}"
167
+ puts "Arm9 symbol count: #{Unarm.symbols9.count}" unless Unarm.symbols9.nil?
168
+ puts "Arm7 symbol count: #{Unarm.symbols7.count}" unless Unarm.symbols7.nil?
169
+ puts
170
+ end
171
+
172
+ # update timestamp cache entry and returns whether it has been modified
173
+ def self.update_ts_cache_entry(ts_cache, entry_file)
174
+ last_modified = File.mtime(entry_file).to_s
175
+ modified = !ts_cache[entry_file].eql?(last_modified)
176
+ ts_cache[entry_file] = last_modified
177
+ modified
178
+ end
179
+
180
+ # evaluates the given rb file as a module and returns: { commands: COMMANDS, variables: VARIABLES }
181
+ def self.eval_rb_defs(file_path)
182
+ mod = Module.new
183
+ mod.module_eval(File.read(file_path), file_path)
184
+ {
185
+ commands: mod.const_defined?(:COMMANDS) ? mod.const_get(:COMMANDS) : {},
186
+ variables: mod.const_defined?(:VARIABLES) ? mod.const_get(:VARIABLES) : {}
187
+ }
188
+ end
189
+
190
+ # evaluates the given ncpp file and returns: { commands: COMMANDS, variables: VARIABLES }
191
+ def self.eval_ncpp_defs(file_path, extra_cmds, extra_vars, safe)
192
+ interpreter = NCPPFileInterpreter.new($config['command_prefix'], extra_cmds, extra_vars, safe: safe)
193
+ interpreter.run(file_path)
194
+ { commands: interpreter.get_new_commands, variables: interpreter.get_new_variables }
195
+ end
196
+
197
+ def self.run(args)
198
+ ncpp_filename = nil
199
+ config_filename = nil
200
+ interactive = false
201
+ ncpp_script = false
202
+ quiet = false
203
+ debug = false
204
+ show_rom_info = false
205
+ safe_mode = false
206
+ puritan_mode = false
207
+ no_cache = false
208
+ no_cache_pass = false
209
+ clear_gen = false
210
+
211
+ OptionParser.new do |opts|
212
+ opts.on('--run FILE', 'Specify an NCPP script file to run') do |f|
213
+ ncpp_filename = f
214
+ ncpp_script = true
215
+ end
216
+
217
+ opts.on('--config FILE', 'Specify a config file (defaults to ncpp_config.json)') do |f|
218
+ config_filename = f
219
+ end
220
+
221
+ opts.on('--interactive', '--repl', 'Run the Read-Eval-Print Loop interpreter') do
222
+ interactive = true
223
+ end
224
+
225
+ opts.on('-q', '--quiet', '--sybau', 'Don\'t print parsing info') do
226
+ quiet = true
227
+ end
228
+
229
+ opts.on('-d', '--debug', 'Enable debug info printing') do
230
+ debug = true
231
+ end
232
+
233
+ opts.on('--safe', 'Run interpreter in safe mode to disable the execution of inline Ruby code') do
234
+ safe_mode = true
235
+ end
236
+
237
+ opts.on('--puritanism', 'Run interpreter in puritan mode to disable the execution of impure expressions') do
238
+ puritan_mode = true
239
+ end
240
+
241
+ opts.on('--no-cache', 'Disable interpreter runtime command caching') do
242
+ no_cache = true
243
+ no_cache_pass = true
244
+ end
245
+
246
+ opts.on('--no-cache-pass', 'Disable the passing of command cache between preprocessor interpreter instances') do
247
+ no_cache_pass = true
248
+ end
249
+
250
+ opts.on('--clear-gen', 'Force all preprocessed files in gen folder to be regenerated') do
251
+ clear_gen = true
252
+ end
253
+
254
+ opts.on('--show-rom-info', 'Show ROM info on startup') do
255
+ show_rom_info = true
256
+ end
257
+
258
+ opts.on('--remove', 'Removes NCPrePatcher from your project') do
259
+ uninstall
260
+ exit
261
+ end
262
+
263
+ opts.on('-v', '--version', 'Show NCPrePatcher version') do
264
+ puts VERSION
265
+ exit
266
+ end
267
+
268
+ opts.on('-h', '--help', 'Show this help message') do
269
+ puts opts
270
+ exit
271
+ end
272
+ end.parse!(args)
273
+
274
+ ncp_project = File.exist? NCP_CONFIG_FILE_PATH
275
+
276
+ if !ncp_project && !interactive && !ncpp_script
277
+ puts "In preprocessor mode, NCPrePatcher must be run in a directory with an #{NCP_CONFIG_FILE_PATH.bold_red} "\
278
+ "file."
279
+ exit(1)
280
+ end
281
+
282
+ if config_filename
283
+ init(config_filename)
284
+ elsif ncp_project
285
+ init
286
+ end
287
+
288
+ show_rom_info($rom) if show_rom_info && !$rom.nil?
289
+
290
+ if ncpp_script
291
+ interpreter = NCPPFileInterpreter.new($config.nil? ? COMMAND_PREFIX : $config['command_prefix'], safe: safe_mode,
292
+ no_cache: no_cache)
293
+ exit_code = interpreter.run(ncpp_filename, debug: debug)
294
+ exit(exit_code)
295
+ end
296
+
297
+ if interactive
298
+ REPL.new(safe: safe_mode, puritan: puritan_mode, no_cache: no_cache).run(debug: debug)
299
+ exit
300
+ end
301
+
302
+ ncp_cfg = JSON.load_file(NCP_CONFIG_FILE_PATH)
303
+ root_dir = Pathname.new(File.dirname(NCP_CONFIG_FILE_PATH))
304
+ code_root_dir = Pathname.new(File.dirname(ncp_cfg['arm9']['target']))
305
+
306
+ Dir.chdir(code_root_dir.relative_path_from(root_dir))
307
+
308
+ timestamp_cache_path = File.join($config['gen_path'], 'timestamp_cache.json')
309
+ cache_exists = File.exist?(timestamp_cache_path)
310
+
311
+ if clear_gen && cache_exists
312
+ File.delete(timestamp_cache_path)
313
+ cache_exists = false
314
+ end
315
+
316
+ timestamp_cache = cache_exists ? JSON.load_file(timestamp_cache_path) : {}
317
+
318
+ if timestamp_cache['NCPP_VERSION'] != VERSION
319
+ timestamp_cache = {}
320
+ else
321
+ timestamp_cache.delete('NCPP_VERSION')
322
+ end
323
+
324
+ exts = $config['source_file_types'].join(',')
325
+
326
+ success = true
327
+ parsed_file_count = 0
328
+ lines_parsed = 0
329
+ start_time = Time.now
330
+
331
+ $config['sources'].each do |src|
332
+ extra_commands = {}
333
+ extra_variables = {}
334
+ command_cache = {}
335
+
336
+ defs_modified = false
337
+
338
+ unless puritan_mode # read ncpp_global/defs files
339
+
340
+ rb_def_files = [
341
+ File.join(src.sub(/^\/|\/$/, '').split('/').first, NCPP_GLB_DEFS_FILENAME+'.rb'),
342
+ File.join(src.sub('*', ''), NCPP_DEFS_FILENAME+'.rb'),
343
+ ]
344
+
345
+ if !safe_mode
346
+ rb_def_files.each do |file|
347
+ next unless File.exist?(file)
348
+ defs_modified = update_ts_cache_entry(timestamp_cache, file)
349
+ defs = eval_rb_defs(file)
350
+ extra_commands.merge!(defs[:commands])
351
+ extra_variables.merge!(defs[:variables])
352
+ end
353
+ else
354
+ rb_def_files.each do |file|
355
+ next unless File.exist?(file)
356
+ Utils.print_warning "'#{file}' is ignored in safe mode"
357
+ end
358
+ end
359
+
360
+ ncpp_def_files = rb_def_files.map { "#{it[..-4]}.ncpp" }
361
+ ncpp_def_files.each do |file|
362
+ next unless File.exist?(file)
363
+ defs_modified = update_ts_cache_entry(timestamp_cache, file)
364
+ defs = eval_ncpp_defs(file, extra_commands, extra_variables, safe_mode)
365
+ extra_commands.merge!(defs[:commands])
366
+ extra_variables.merge!(defs[:variables])
367
+ end
368
+
369
+ end
370
+
371
+ if File.file?(src)
372
+ files = [src]
373
+ else
374
+ if src.end_with?('/*')
375
+ base = src[0...-2] # drop trailing "/*"
376
+ pattern = File.join(base, '**', "*.{#{exts}}") # recursive directory search
377
+ else
378
+ base = src
379
+ pattern = File.join(base, "*.{#{exts}}")
380
+ end
381
+ files = Dir.glob(pattern)
382
+ end
383
+
384
+ timestamp_cache.delete_if do |file, _mtime|
385
+ if !File.exist?(file)
386
+ File.delete(File.join($config['gen_path'], file))
387
+ true
388
+ else
389
+ false
390
+ end
391
+ end
392
+
393
+ files.delete_if do |file|
394
+ last_modified = File.mtime(file).to_s
395
+ modified = !timestamp_cache[file]&.eql?(last_modified)
396
+ timestamp_cache[file] = last_modified
397
+ if file.end_with?('.s')
398
+ if modified || modified.nil?
399
+ dest = File.join($config['gen_path'], file)
400
+ FileUtils.mkdir_p(File.dirname(dest))
401
+ FileUtils.cp(file, dest)
402
+ end
403
+ true
404
+ elsif !modified && !defs_modified
405
+ true
406
+ else
407
+ false
408
+ end
409
+ end
410
+
411
+ parsed_file_count += files.count
412
+
413
+ files.each do |file|
414
+ interpreter = CFileInterpreter.new(
415
+ file, $config['gen_path'], $config['command_prefix'], extra_commands, extra_variables,
416
+ safe: safe_mode, puritan: puritan_mode, no_cache: no_cache, cmd_cache: no_cache_pass ? {} : command_cache
417
+ )
418
+ interpreter.run(verbose: !quiet, debug: debug)
419
+ lines_parsed += interpreter.lines_parsed
420
+
421
+ command_cache.merge!(interpreter.get_cacheable_cache) unless no_cache_pass
422
+
423
+ unless interpreter.incomplete_files.empty?
424
+ timestamp_cache.delete(file)
425
+ success = false
426
+ end
427
+ end
428
+
429
+ # interpreter = CFileInterpreter.new(
430
+ # files, $config['gen_path'], $config['command_prefix'], extra_commands, extra_variables,
431
+ # safe: safe_mode, puritan: puritan_mode
432
+ # )
433
+ # interpreter.run(verbose: !quiet)
434
+ # lines_parsed += interpreter.lines_parsed
435
+
436
+ # unless interpreter.incomplete_files.empty?
437
+ # interpreter.incomplete_files.each do |file|
438
+ # timestamp_cache.delete(file)
439
+ # success = false
440
+ # end
441
+ # end
442
+
443
+ end
444
+
445
+ timestamp_cache['NCPP_VERSION'] = VERSION
446
+
447
+ FileUtils.mkdir_p(File.dirname(timestamp_cache_path))
448
+ File.write(timestamp_cache_path, JSON.generate(timestamp_cache))
449
+
450
+ # FileUtils.mkdir_p(File.dirname(cmd_cache_path))
451
+ # File.write(cmd_cache_path, JSON.generate(command_cache))
452
+
453
+ unless quiet
454
+ if lines_parsed > 0
455
+ msg = "\nParsed #{lines_parsed} line#{'s' if lines_parsed != 1} across " \
456
+ "#{parsed_file_count} file#{'s' if parsed_file_count != 1}."
457
+ puts (success ? msg.green : msg.yellow)
458
+ if success
459
+ puts "Took ".green + String(Time.now - start_time).underline_green + " seconds.".green
460
+ else
461
+ puts "Took ".yellow + String(Time.now - start_time).underline_yellow + " seconds.".yellow
462
+ end
463
+ else
464
+ puts "Nothing to parse.".green
465
+ end
466
+ end
467
+
468
+ puts
469
+ ARGV.clear
470
+
471
+ unless success
472
+ puts 'NCPrePatcher execution was not successful.'.bold_red
473
+ exit(1)
474
+ end
475
+
476
+ end
477
+
478
+ end
Binary file