squared 0.3.10 → 0.4.0

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.
@@ -15,8 +15,10 @@ module Squared
15
15
  include Rake::DSL
16
16
 
17
17
  VAR_SET = %i[parent global envname dependfile theme run script env pass].freeze
18
+ BLK_SET = %i[run depend doc lint test copy clean].freeze
18
19
  SEM_VER = /\b(\d+)(?:(\.)(\d+))?(?:(\.)(\d+)(\S+)?)?\b/.freeze
19
- private_constant :VAR_SET, :SEM_VER
20
+ URI_SCHEME = %r{^([a-z][a-z\d+-.]*)://[^@:\[\]\\^<>|\s]}i.freeze
21
+ private_constant :VAR_SET, :BLK_SET, :SEM_VER, :URI_SCHEME
20
22
 
21
23
  class << self
22
24
  def populate(*); end
@@ -25,7 +27,7 @@ module Squared
25
27
  def bannerargs(*); end
26
28
 
27
29
  def tasks
28
- %i[build depend graph doc lint test copy clean].freeze
30
+ (%i[build archive graph] + BLK_SET).freeze
29
31
  end
30
32
 
31
33
  def as_path(val)
@@ -51,7 +53,8 @@ module Squared
51
53
  end
52
54
 
53
55
  (@@tasks = {})[ref] = {
54
- 'graph' => %i[run print].freeze
56
+ 'graph' => %i[run print].freeze,
57
+ 'unpack' => %i[zip tar tgz tar.gz txz tar.xz].freeze
55
58
  }.freeze
56
59
  @@task_desc = Rake::TaskManager.record_task_metadata
57
60
  @@print_order = 0
@@ -59,8 +62,8 @@ module Squared
59
62
  attr_reader :name, :project, :workspace, :path, :theme, :exception, :pipe, :verbose,
60
63
  :group, :parent, :dependfile
61
64
 
62
- def initialize(workspace, path, name, *, group: nil, graph: nil, pass: nil, exclude: nil, verbose: nil,
63
- first: {}, last: {}, error: {}, common: ARG[:COMMON], **kwargs)
65
+ def initialize(workspace, path, name, *, group: nil, graph: nil, pass: nil, exclude: nil, release: nil,
66
+ archive: nil, first: {}, last: {}, error: {}, common: ARG[:COMMON], **kwargs)
64
67
  @path = path
65
68
  @workspace = workspace
66
69
  @name = name.to_s.freeze
@@ -73,6 +76,13 @@ module Squared
73
76
  @copy = kwargs[:copy]
74
77
  @clean = kwargs[:clean]
75
78
  @version = kwargs[:version]
79
+ @archive = case archive
80
+ when String, Array
81
+ { uri: archive }
82
+ when Hash
83
+ archive
84
+ end
85
+ @release = release
76
86
  @envname = @name.gsub(/[^\w]+/, '_').upcase.freeze
77
87
  @exception = env_bool(kwargs[:exception], workspace.exception, strict: true)
78
88
  @pipe = env_pipe(kwargs[:pipe], workspace.pipe, strict: true)
@@ -80,7 +90,7 @@ module Squared
80
90
  when nil
81
91
  workspace.verbose
82
92
  when String
83
- env_bool(verbose, workspace.verbose, strict: true, index: true)
93
+ env_pipe(verbose, workspace.verbose, strict: true, index: true)
84
94
  else
85
95
  verbose
86
96
  end
@@ -108,7 +118,7 @@ module Squared
108
118
  @parent = nil
109
119
  @global = false
110
120
  run_set(kwargs[:run], kwargs[:env], opts: kwargs.fetch(:opts, true))
111
- initialize_ref(Base.ref)
121
+ initialize_ref Base.ref
112
122
  end
113
123
 
114
124
  def initialize_ref(ref)
@@ -116,7 +126,7 @@ module Squared
116
126
  end
117
127
 
118
128
  def initialize_build(ref, **kwargs)
119
- initialize_ref(ref)
129
+ initialize_ref ref
120
130
  if (@script = @workspace.script_get(group: @group, ref: ref))
121
131
  if @script[:log] && !kwargs.key?(:log)
122
132
  kwargs[:log] = @script[:log]
@@ -166,43 +176,40 @@ module Squared
166
176
  def initialize_events(ref, **)
167
177
  return unless (events = @workspace.events_get(group: @group, ref: ref))
168
178
 
169
- events.each { |task, data| data.each { |ev, blk| (@events[ev] ||= {})[task] ||= blk } }
179
+ events.each do |task, data|
180
+ data.each { |ev, blk| (@events[ev] ||= {})[task] ||= [blk] }
181
+ end
170
182
  end
171
183
 
172
184
  def initialize_logger(log: nil, **)
173
185
  return if @log
174
186
 
175
187
  log = log.is_a?(Hash) ? log.dup : { file: log }
176
- if (file = env('LOG_FILE'))
177
- file = DateTime.now.strftime(file)
178
- elsif (val = env('LOG_AUTO'))
179
- file = "#{@name}-%s.log" % [case val
180
- when 'y', 'year'
181
- Date.today.year
182
- when 'm', 'month'
183
- Date.today.strftime('%Y-%m')
184
- when 'd', 'day', '1'
185
- Date.today
186
- else
187
- val.include?('%') ? DateTime.now.strftime(val) : DateTime.now.strftime
188
- end]
189
- elsif (val = log[:file])
190
- file = val.is_a?(String) ? DateTime.now.strftime(val) : "#{@name}-#{Date.today}.log"
191
- end
192
- if file
193
- file = (val = env('LOG_DIR')) ? @workspace.home.join(val, file) : @workspace.home.join(file)
188
+ unless (file = env('LOG_FILE'))
189
+ file = case env('LOG_AUTO')
190
+ when 'y', 'year'
191
+ "#{@name}-#{Date.today.year}.log"
192
+ when 'm', 'month'
193
+ "#{@name}-#{Date.today.strftime('%Y-%m')}.log"
194
+ when 'd', 'day', '1'
195
+ "#{@name}-#{Date.today}.log"
196
+ end
197
+ end
198
+ if file ||= log[:file]
199
+ file = Date.today.strftime(file)
200
+ file = (dir = env('LOG_DIR')) ? @workspace.home.join(dir, file) : @workspace.home.join(file)
194
201
  begin
195
202
  file = file.realdirpath
196
203
  rescue StandardError => e
197
204
  raise if @exception
198
205
 
199
206
  file = nil
200
- warn log_message(Logger::WARN, e) if warning?
207
+ warn log_message(Logger::WARN, e, pass: true) if warning?
201
208
  end
202
209
  end
203
210
  log[:progname] ||= @name
204
211
  if (val = env('LOG_LEVEL', ignore: false))
205
- log[:level] = val
212
+ log[:level] = val.match?(/^\d$/) ? log_sym(val.to_i) : val
206
213
  end
207
214
  log.delete(:file)
208
215
  @log = [file, log]
@@ -211,20 +218,18 @@ module Squared
211
218
  def initialize_env(dev: nil, prod: nil, **)
212
219
  @dev = env_match('BUILD', dev, suffix: 'DEV', strict: true)
213
220
  @prod = env_match('BUILD', prod, suffix: 'PROD', strict: true)
214
- cmd = @output[0]
215
- if @output[2] != false && (val = env('BUILD', suffix: 'ENV'))
216
- begin
217
- data = JSON.parse(val)
218
- raise_error('invalid JSON object', val, hint: "BUILD_#{@envname}_ENV") unless data.is_a?(Hash)
219
- rescue StandardError => e
220
- log&.warn e
221
- else
222
- @output[2] = data
221
+ unless @output[2] == false || !(val = env('BUILD', suffix: 'ENV'))
222
+ data = parse_json(val, hint: "BUILD_#{@envname}_ENV")
223
+ @output[2] = data if data
224
+ end
225
+ unless @output[0] == false || @output[0].is_a?(Array)
226
+ if (val = env('BUILD', suffix: 'OPTS'))
227
+ n = @output[0] ? 1 : 3
228
+ @output[n] = merge_opts(@output[n], shell_split(val, escape: false))
229
+ end
230
+ if (val = env(ref.to_s.upcase, suffix: 'OPTS'))
231
+ @output[4] = merge_opts(@output[4], shell_split(val, escape: false))
223
232
  end
224
- end
225
- if cmd != false && !cmd.is_a?(Array)
226
- @output[cmd ? 1 : 3] = shell_split(val, escape: false, join: true) if (val = env('BUILD', suffix: 'OPTS'))
227
- @output[4] = shell_split(val, escape: false, join: true) if !cmd && (val = env('SCRIPT', suffix: 'OPTS'))
228
233
  end
229
234
  @version = val if (val = env('BUILD', suffix: 'VERSION'))
230
235
  return unless (val = env('BUILD', strict: true))
@@ -259,14 +264,14 @@ module Squared
259
264
 
260
265
  format_desc action, flag, '(-)project*'
261
266
  task flag do |_, args|
262
- args = args.to_a.reject { |val| name == val.to_s }
267
+ args = param_guard(action, flag, args: args.to_a.reject { |val| name == val.to_s })
263
268
  if flag == :run
264
269
  graph args
265
270
  else
266
271
  out, done = graph(args, out: [])
267
272
  out.map! do |val|
268
273
  done.each_with_index do |proj, i|
269
- next unless val =~ / #{Regexp.escape(proj.name)}(?:@\d|\z)/
274
+ next unless val.match?(/ #{Regexp.escape(proj.name)}(?:@\d|\z)/)
270
275
 
271
276
  val += " (#{i.succ})"
272
277
  break
@@ -280,6 +285,25 @@ module Squared
280
285
  ])
281
286
  end
282
287
  end
288
+ when 'unpack'
289
+ format_desc action, flag, 'tag/url,dir,digest?,f/force?'
290
+ task flag, [:tag, :dir, :digest, :force] do |_, args|
291
+ tag = param_guard(action, flag, args: args, key: :tag)
292
+ dir = param_guard(action, flag, args: args, key: :dir)
293
+ unless tag.match?(URI_SCHEME)
294
+ raise_error 'no base uri' unless @release
295
+
296
+ tag = "#{@release.include?('??') ? @release.sub('??', tag) : @release + tag}.#{flag}"
297
+ end
298
+ case (digest = args.digest)
299
+ when 'f', 'force'
300
+ digest = nil
301
+ force = true
302
+ else
303
+ force = args.fetch(:force, false)
304
+ end
305
+ unpack(basepath(dir), uri: tag, digest: digest, ext: flag.to_s, force: force)
306
+ end
283
307
  end
284
308
  end
285
309
  end
@@ -348,18 +372,19 @@ module Squared
348
372
 
349
373
  out = obj.link(self, *args, **kwargs, &blk) if obj.respond_to?(:link)
350
374
  if !out
351
- warn log_message(Logger::WARN, 'link not compatible', subject: obj.to_s, hint: name)
375
+ warn log_message(Logger::WARN, 'link not compatible', subject: obj.to_s, hint: name, pass: true)
352
376
  elsif out.respond_to?(:build)
353
377
  out.build
354
378
  end
355
379
  self
356
380
  end
357
381
 
358
- def build(*args, sync: invoked_sync?('build'), from: :run, **)
382
+ def build(*args, sync: invoked_sync?('build'), from: @buildtype || :build, **)
359
383
  banner = verbose
360
384
  if args.empty?
361
- return unless from == :run
385
+ return unless from == (@buildtype || :build)
362
386
 
387
+ run_b(@run, sync: sync, from: from) if series?(@run)
363
388
  args = @output
364
389
  banner = verbose == 1 if task_invoked?('build', 'build:sync')
365
390
  end
@@ -370,7 +395,7 @@ module Squared
370
395
  a, b, c, d, e = val
371
396
  case b
372
397
  when Hash
373
- b = append_hash(b).join(' ')
398
+ b = append_hash(b, build: true).join(' ')
374
399
  when Enumerable
375
400
  b = b.to_a.join(' ')
376
401
  end
@@ -386,7 +411,7 @@ module Squared
386
411
  end
387
412
  cmd = cmd.join(' && ')
388
413
  else
389
- cmd, opts, var, flags, scr = args
414
+ cmd, opts, var, flags, extra = args
390
415
  end
391
416
  if cmd
392
417
  cmd = as_get(cmd)
@@ -394,7 +419,7 @@ module Squared
394
419
  flags = append_hash(flags).join(' ') if flags.is_a?(Hash)
395
420
  case opts
396
421
  when Hash
397
- opts = append_hash(opts)
422
+ opts = append_hash(opts, build: true)
398
423
  cmd = as_a(cmd).push(flags).concat(opts).compact.join(' ')
399
424
  when Enumerable
400
425
  cmd = as_a(cmd).concat(opts.to_a)
@@ -404,10 +429,9 @@ module Squared
404
429
  cmd = [cmd, flags, opts].compact.join(' ') if opts || flags
405
430
  end
406
431
  else
407
- return unless respond_to?(:compose)
432
+ return unless (opts || extra) && respond_to?(:compose)
408
433
 
409
- cmd = compose(as_get(opts), flags, script: true, args: scr, from: from)
410
- from = :script if from == :run && script?
434
+ cmd = compose(as_get(opts), flags, script: true, args: extra, from: from)
411
435
  end
412
436
  run(cmd, var, from: from, banner: banner, sync: sync)
413
437
  end
@@ -416,6 +440,12 @@ module Squared
416
440
  run_b(@depend, sync: sync, from: :depend)
417
441
  end
418
442
 
443
+ def archive(*, sync: invoked_sync?('archive'), **)
444
+ return unless @archive.is_a?(Array)
445
+
446
+ unpack(path, **@archive, sync: sync, from: :archive)
447
+ end
448
+
419
449
  def doc(*, sync: invoked_sync?('doc'), **)
420
450
  run_b(@doc, sync: sync, from: :doc)
421
451
  end
@@ -445,27 +475,31 @@ module Squared
445
475
  rescue StandardError => e
446
476
  log&.error e
447
477
  ret = on(:error, from, e)
448
- raise if @exception && ret != true
478
+ raise if exception && ret != true
449
479
  end
450
- when Enumerable
451
- as_a(@clean).each do |val|
452
- val = val.to_s
453
- path = basepath(val)
454
- if path.directory? && val =~ %r{[\\/]\z}
455
- log&.warn "rm -rf #{path}"
456
- FileUtils.rm_rf(path, verbose: verbose)
457
- else
458
- log&.warn "rm #{path}"
459
- (val.include?('*') ? Dir[path] : [path]).each do |file|
460
- next unless File.file?(file)
461
-
462
- begin
463
- File.delete(file)
464
- rescue StandardError => e
465
- log&.error e
480
+ else
481
+ if @clean.is_a?(Enumerable) && !series?(@clean)
482
+ as_a(@clean).each do |val|
483
+ val = val.to_s
484
+ path = basepath(val)
485
+ if path.directory? && val.match?(%r{[\\/]$})
486
+ log&.warn "rm -rf #{path}"
487
+ path.rmtree(verbose: verbose)
488
+ else
489
+ log&.warn "rm #{path}"
490
+ (val.include?('*') ? Dir[path] : [path]).each do |file|
491
+ next unless File.file?(file)
492
+
493
+ begin
494
+ File.delete(file)
495
+ rescue StandardError => e
496
+ log&.error e
497
+ end
466
498
  end
467
499
  end
468
500
  end
501
+ else
502
+ run_b(@clean, sync: sync)
469
503
  end
470
504
  end
471
505
  on :last, :clean
@@ -502,6 +536,119 @@ module Squared
502
536
  end
503
537
  end
504
538
 
539
+ def unpack(target, sync: true, uri: nil, digest: nil, ext: nil, force: false, depth: 1, headers: {},
540
+ from: :unpack)
541
+ if !target.exist?
542
+ target.mkpath
543
+ elsif !target.directory?
544
+ raise_error('invalid location', hint: target)
545
+ elsif !target.empty?
546
+ raise_error('directory not empty', hint: target) unless force || env('UNPACK_FORCE')
547
+ create = true
548
+ end
549
+ if digest
550
+ if (n = digest.index(':').to_i) > 0
551
+ size = digest[0, n].downcase
552
+ digest = digest[n + 1..-1]
553
+ else
554
+ size = digest.size
555
+ end
556
+ algo = case size
557
+ when 32, 'md5'
558
+ Digest::MD5
559
+ when 'rmd160'
560
+ Digest::RMD160
561
+ when 40, 'sha1'
562
+ Digest::SHA1
563
+ when 64, 'sha256'
564
+ Digest::SHA256
565
+ when 96, 'sha384'
566
+ Digest::SHA384
567
+ when 128, 'sha512'
568
+ Digest::SHA512
569
+ else
570
+ raise_error("invalid checksum: #{digest}", hint: name)
571
+ end
572
+ end
573
+ if (val = env('HEADERS')) && (out = parse_json(val, hint: "HEADERS_#{@envname}"))
574
+ headers = out
575
+ end
576
+ require 'open-uri'
577
+ data = nil
578
+ (uri = as_a(uri)).each_with_index do |url, index|
579
+ last = index == uri.size - 1
580
+ URI.open(url, headers) do |f|
581
+ data = f.read
582
+ if algo && algo.hexdigest(data) != digest
583
+ data = nil
584
+ raise_error("checksum failed: #{digest}", hint: url) if last
585
+ end
586
+ next if ext && index == 0
587
+
588
+ case f.content_type
589
+ when 'application/zip'
590
+ ext = 'zip'
591
+ when 'application/x-gzip'
592
+ ext = 'tgz'
593
+ when 'application/x-xz'
594
+ ext = 'txz'
595
+ end
596
+ end
597
+ if data
598
+ uri = url
599
+ break
600
+ elsif last
601
+ raise_error('no content', hint: url)
602
+ end
603
+ end
604
+ ext ||= URI.parse(uri).path[/^.+?\.((?:tar\.)?\w+)$/i, 1]
605
+ if (n = env("#{ext == 'zip' ? 'ZIP' : 'TAR'}_DEPTH", ignore: false))
606
+ depth = n.to_i
607
+ end
608
+ begin
609
+ require 'tempfile'
610
+ file = Tempfile.new("#{name}-")
611
+ file.write(data)
612
+ file.close
613
+ if create
614
+ warn log_message(Logger::WARN, name, 'force remove', hint: target, pass: true)
615
+ target.rmtree(verbose: true)
616
+ target.mkpath
617
+ end
618
+ case ext
619
+ when 'zip', 'aar'
620
+ session 'unzip', shell_quote(file.path), quote_option('d', target)
621
+ when 'tar', 'tgz', 'tar.gz', 'tar.xz'
622
+ flags = +(verbose ? 'v' : '')
623
+ case ext
624
+ when 'tgz', 'tar.gz'
625
+ flags += 'z'
626
+ when 'txz', 'tar.xz'
627
+ flags += 'J'
628
+ end
629
+ session 'tar', "-x#{flags}", basic_option('strip-components', depth), quote_option('f', file.path),
630
+ quote_option('C', target)
631
+ depth = 0
632
+ else
633
+ raise_error("unsupported format: #{ext}", hint: uri)
634
+ end
635
+ run(sync: sync, from: from)
636
+ while depth > 0 && target.children.size == 1
637
+ entry = target.children.first
638
+ break unless entry.directory?
639
+
640
+ dest = target.join(File.basename(file.path))
641
+ FileUtils.mv(entry, dest)
642
+ dest.children.each { |file| FileUtils.mv(file, target) }
643
+ dest.rmdir
644
+ target = entry
645
+ depth -= 1
646
+ end
647
+ ensure
648
+ file&.unlink
649
+ end
650
+ end
651
+
505
652
  def first(key, *args, **kwargs, &blk)
506
653
  event(:first, key, *args, **kwargs, &blk)
507
654
  end
@@ -514,17 +661,38 @@ module Squared
514
661
  event(:error, key, *args, **kwargs, &blk)
515
662
  end
516
663
 
517
- def event(name, key, *args, **kwargs, &blk)
518
- (@events[name.to_sym] ||= {})[key.to_sym] = [block_given? ? [blk] + args : args, kwargs]
664
+ def event(name, key, *args, override: false, **kwargs, &blk)
665
+ data = @events[name.to_sym] ||= {}
666
+ items = if override
667
+ data[key.to_sym] = []
668
+ else
669
+ data[key.to_sym] ||= []
670
+ end
671
+ items << [block_given? ? [blk] + args : args, kwargs]
672
+ self
519
673
  end
520
674
 
521
675
  def as(cmd, script, to = nil)
522
676
  script = { "#{script}": to } if to
523
677
  data = (@as ||= {})[cmd.to_sym] ||= {}
524
678
  script.each { |key, val| data[key.to_s] = val }
679
+ self
680
+ end
681
+
682
+ def series(key, override: false, &blk)
683
+ if blocks.include?(key.to_sym) && block_given?
684
+ if !override && series?(target = instance_variable_get(:"@#{key}"))
685
+ target << blk
686
+ else
687
+ instance_variable_set :"@#{key}", [blk]
688
+ end
689
+ else
690
+ log&.warn "series: @#{key} (invalid)"
691
+ end
692
+ self
525
693
  end
526
694
 
527
- def variable_set(key, *val, **kwargs)
695
+ def variable_set(key, *val, **kwargs, &blk)
528
696
  if variables.include?(key)
529
697
  case key
530
698
  when :build, :run
@@ -545,17 +713,22 @@ module Squared
545
713
  when :dependfile
546
714
  @dependfile = basepath(*val)
547
715
  else
548
- instance_variable_set :"@#{key}", val.first
716
+ if blocks.include?(key) && block_given?
717
+ series key, &blk
718
+ else
719
+ instance_variable_set(:"@#{key}", block_given? && val.empty? ? blk : val.first)
720
+ end
549
721
  end
550
722
  else
551
723
  log&.warn "variable_set: @#{key} (private)"
552
724
  end
725
+ self
553
726
  end
554
727
 
555
728
  def enabled?(ref = nil, **)
556
729
  return false if ref && !ref?(ref)
557
730
 
558
- path.directory? && !path.empty?
731
+ (path.directory? && !path.empty?) || archive?
559
732
  end
560
733
 
561
734
  def has?(meth, ref = nil)
@@ -569,7 +742,7 @@ module Squared
569
742
  end
570
743
 
571
744
  def build?
572
- !!@output[0] || script?
745
+ !!@output[0] || script? || series?(@run)
573
746
  end
574
747
 
575
748
  def script?
@@ -580,6 +753,10 @@ module Squared
580
753
  !!@depend
581
754
  end
582
755
 
756
+ def archive?
757
+ @archive.is_a?(Hash) && (!path.exist? || path.empty?)
758
+ end
759
+
583
760
  def graph?
584
761
  @graph.is_a?(Array) && !@graph.empty?
585
762
  end
@@ -640,8 +817,12 @@ module Squared
640
817
  @ref.reverse_each
641
818
  end
642
819
 
643
- def basepath(*args, ascend: nil)
644
- ret = path.join(*args)
820
+ def basepath(*args)
821
+ path.join(*args)
822
+ end
823
+
824
+ def rootpath(*args, ascend: nil)
825
+ ret = basepath(*args)
645
826
  return ret unless ascend && !ret.exist?
646
827
 
647
828
  path.parent.ascend.each do |dir|
@@ -678,7 +859,7 @@ module Squared
678
859
  unless cmd
679
860
  if warning?
680
861
  from &&= from.to_s
681
- warn log_message(Logger::WARN, from || 'unknown', subject: project, hint: 'no command given')
862
+ warn log_message(Logger::WARN, from || 'unknown', subject: project, hint: 'no command given', pass: true)
682
863
  end
683
864
  return
684
865
  end
@@ -690,7 +871,7 @@ module Squared
690
871
  log&.warn "ENV was discarded: #{var}" if var
691
872
  task_invoke(cmd, exception: exception, warning: warning?)
692
873
  else
693
- print_item format_banner(cmd, banner: banner) if sync && !env('BANNER', equals: '0')
874
+ print_item format_banner(cmd, banner: banner) if sync
694
875
  args = var.is_a?(Hash) ? [var, cmd] : [cmd]
695
876
  shell(*args, chdir: chdir, exception: exception)
696
877
  end
@@ -715,18 +896,23 @@ module Squared
715
896
  end
716
897
 
717
898
  def run_b(obj, from: nil, sync: true)
718
- return unless obj
719
-
720
- if obj.is_a?(Array) && obj.any? { |val| !val.is_a?(String) }
721
- build(*obj, from: from, sync: sync)
899
+ case obj
900
+ when Proc, Method
901
+ obj.call
722
902
  else
723
- run_s(obj.is_a?(Enumerable) ? obj.to_a : obj, from: from, sync: sync)
903
+ if series?(obj)
904
+ obj.each(&:call)
905
+ elsif obj.is_a?(Array) && obj.any? { |val| !val.is_a?(String) }
906
+ build(*obj, from: from, sync: sync)
907
+ elsif obj
908
+ run_s(obj.is_a?(Enumerable) ? obj.to_a : obj, from: from, sync: sync)
909
+ end
724
910
  end
725
911
  end
726
912
 
727
913
  def graph_branch(target, data, tasks = nil, out = nil, sync: true, pass: [], done: [], depth: 0,
728
914
  single: false, last: false, context: nil)
729
- tag = ->(proj) { "#{proj.name}#{SEM_VER =~ proj.version ? "@#{proj.version}" : ''}" }
915
+ tag = ->(proj) { "#{proj.name}#{SEM_VER.match?(proj.version) ? "@#{proj.version}" : ''}" }
730
916
  check = ->(deps) { deps.reject { |val| done.include?(val) } }
731
917
  dedupe = lambda do |name|
732
918
  next [] unless (ret = data[name])
@@ -784,13 +970,7 @@ module Squared
784
970
  next if pass.include?(meth)
785
971
 
786
972
  if workspace.task_defined?(cmd = task_join(proj.name, meth))
787
- if ENV.key?(key = "BANNER_#{proj.name.upcase}")
788
- key = nil
789
- else
790
- ENV[key] = '0'
791
- end
792
973
  run(cmd, sync: false, banner: false)
793
- ENV.delete(key) if key
794
974
  elsif proj.has?(meth, tasks || group ? nil : workspace.baseref)
795
975
  proj.__send__(meth.to_sym, sync: sync)
796
976
  end
@@ -859,7 +1039,7 @@ module Squared
859
1039
  end
860
1040
 
861
1041
  def session(*cmd, prefix: cmd.first, main: true, options: true)
862
- prefix = stripext(prefix.to_s).upcase
1042
+ prefix = prefix.to_s.upcase
863
1043
  if (val = PATH[prefix] || PATH[prefix.to_sym])
864
1044
  cmd[0] = shell_quote(val, force: false)
865
1045
  end
@@ -873,8 +1053,7 @@ module Squared
873
1053
  def session_delete(*list, target: @session)
874
1054
  ret = []
875
1055
  list.each do |val|
876
- pat = /^#{Regexp.escape(shell_option(val))}(?: |=|$)/
877
- if (key = target.find { |opt| opt =~ pat })
1056
+ if (key = target.find { |opt| opt.match?(/^#{Regexp.escape(shell_option(val))}(?: |=|$)/o) })
878
1057
  target.delete(key)
879
1058
  ret << key
880
1059
  end
@@ -889,30 +1068,33 @@ module Squared
889
1068
  def session_done(cmd)
890
1069
  return cmd unless cmd.respond_to?(:done)
891
1070
 
892
- raise_error('no args were added', hint: cmd.first || name) unless cmd.size > 1
1071
+ raise_error('no args added', hint: cmd.first || name) unless cmd.size > 1
893
1072
  @session = nil if cmd == @session
894
1073
  cmd.done
895
1074
  end
896
1075
 
897
- def option(*args, target: @session, prefix: target&.first, **kwargs)
1076
+ def option(*args, prefix: @session&.first, **kwargs)
898
1077
  if prefix
899
1078
  args.each do |val|
900
- ret = env("#{stripext(prefix)}_#{val.gsub(/\W/, '_')}".upcase, **kwargs)
1079
+ ret = env("#{prefix}_#{val.gsub(/\W/, '_')}".upcase, **kwargs)
901
1080
  return ret if ret
902
1081
  end
903
1082
  end
904
1083
  nil
905
1084
  end
906
1085
 
907
- def option_sanitize(opts, list, target: @session, no: nil, args: false, first: false, pass: ['='])
1086
+ def option_sanitize(opts, list, target: @session, no: nil, first: false, pass: ['='])
908
1087
  ret = []
909
1088
  reg = []
910
1089
  bare = []
911
1090
  e = []
912
1091
  b = []
1092
+ m = []
913
1093
  p = []
914
1094
  q = []
1095
+ qq = []
915
1096
  i = []
1097
+ f = []
916
1098
  list = list.map do |val|
917
1099
  x, y = val.split('|')
918
1100
  if y
@@ -932,16 +1114,19 @@ module Squared
932
1114
  e << flag
933
1115
  when 'b'
934
1116
  b << flag
1117
+ when 'm'
1118
+ m << flag
935
1119
  when 'q'
1120
+ qq << flag if val[n + 2] == 'q'
936
1121
  q << flag
937
1122
  when 'p'
938
1123
  p << flag
939
1124
  when 'i'
940
1125
  i << flag
941
- when 'v'
942
- reg << Regexp.escape(flag)
1126
+ when 'f'
1127
+ f << flag
943
1128
  else
944
- next
1129
+ reg << Regexp.escape(flag)
945
1130
  end
946
1131
  bare << flag if val.end_with?('?')
947
1132
  else
@@ -959,36 +1144,37 @@ module Squared
959
1144
  elsif opt.start_with?('no-') && no.include?(name = opt[3..-1])
960
1145
  target << "--no-#{name}"
961
1146
  else
962
- if opt =~ /\A([^=]+)=(.+)\z/
963
- a = $1
964
- if e.include?(a)
965
- target << shell_option(a, $2)
966
- elsif q.include?(a)
967
- target << quote_option(a, $2)
968
- elsif p.include?(a)
969
- target << quote_option(a, basepath($2))
970
- elsif b.include?(a) || (i.include?(a) && /^\d+$/.match?($2))
971
- target << basic_option(a, $2)
1147
+ if opt =~ /^([^=]+)=(.+)$/
1148
+ key = $1
1149
+ val = $2
1150
+ match = ->(flag, pat) { flag.include?(key) && pat.match?(val) }
1151
+ if e.include?(key)
1152
+ target << shell_option(key, val)
1153
+ elsif q.include?(key)
1154
+ target << quote_option(key, val, double: qq.include?(key))
1155
+ elsif p.include?(key)
1156
+ target << quote_option(key, basepath(val))
1157
+ elsif m.include?(key)
1158
+ target << basic_option(key, val, merge: true)
1159
+ elsif b.include?(key) || match.(i, /^\d+$/) || match.(f, /^\d*(?:\.\d+)?$/)
1160
+ target << basic_option(key, val)
972
1161
  else
973
1162
  ret << opt
974
1163
  end
975
- opt = a
1164
+ opt = key
976
1165
  else
977
1166
  ret << opt
978
- found = true if args
979
1167
  end
980
- found = true if first && pass.none? { |val| opt.include?(val) }
1168
+ found = true if first && pass.none? { |s| opt.include?(s) }
981
1169
  end
982
1170
  end
983
- [ret, reg.empty? ? /\A\s+\z/ : /\A(#{reg.join('|')})=(.+)\z/]
1171
+ [ret, reg.empty? ? /\A\s+\z/ : /^(#{reg.join('|')})=(.+)$/]
984
1172
  end
985
1173
 
986
1174
  def option_clear(opts, target: @session, **kwargs)
987
- return if opts.empty?
988
-
989
1175
  kwargs[:subject] ||= target&.first
990
- kwargs[:hint] ||= 'unrecognized'
991
- warn log_message(:warn, opts.join(', '), **kwargs)
1176
+ kwargs[:hint] ||= 'not used'
1177
+ warn log_message(Logger::WARN, opts.join(', '), pass: true, **kwargs) unless opts.empty?
992
1178
  end
993
1179
 
994
1180
  def print_item(*val)
@@ -1018,7 +1204,7 @@ module Squared
1018
1204
  val
1019
1205
  end
1020
1206
  end
1021
- out << sub_style('-' * n, styles: border)
1207
+ out << sub_style(ARG[:BORDER][1] * n, styles: border)
1022
1208
  out.join("\n")
1023
1209
  end
1024
1210
 
@@ -1030,7 +1216,7 @@ module Squared
1030
1216
  sub.each { |h| s = sub_style(s, **h) }
1031
1217
  s
1032
1218
  end
1033
- ret = [sub_style('-' * n, styles: kwargs.key?(:border) ? kwargs[:border] : borderstyle), *lines]
1219
+ ret = [sub_style(ARG[:BORDER][1] * n, styles: kwargs.key?(:border) ? kwargs[:border] : borderstyle), *lines]
1034
1220
  ret.reverse! if reverse
1035
1221
  ret.join("\n")
1036
1222
  end
@@ -1056,7 +1242,7 @@ module Squared
1056
1242
  if verbose
1057
1243
  out = []
1058
1244
  if data[:command]
1059
- if /\A(?:"((?:[^"]|(?<=\\)")+)"|'((?:[^']|(?<=\\)')+)'|(\S+)) /.match(cmd)
1245
+ if cmd =~ /\A(?:"((?:[^"]|(?<=\\)")+)"|'((?:[^']|(?<=\\)')+)'|(\S+)) /
1060
1246
  path = $3 || $2 || $1
1061
1247
  cmd = cmd.sub(path, File.basename(path).upcase)
1062
1248
  end
@@ -1094,9 +1280,9 @@ module Squared
1094
1280
  unless items.empty?
1095
1281
  pad = items.size.to_s.size
1096
1282
  items.each_with_index do |val, i|
1097
- next unless reg.empty? || reg.any? { |pat| val[0] =~ pat }
1283
+ next unless reg.empty? || reg.any? { |pat| val[0].match?(pat) }
1098
1284
 
1099
- out << "#{(i + 1).to_s.rjust(pad)}. #{each ? each.(val) : val[0]}"
1285
+ out << "#{i.succ.to_s.rjust(pad)}. #{each ? each.(val) : val[0]}"
1100
1286
  end
1101
1287
  end
1102
1288
  sub = [headerstyle]
@@ -1130,9 +1316,9 @@ module Squared
1130
1316
  opts.each { |val| target << shell_option(flag, val) }
1131
1317
  end
1132
1318
 
1133
- def append_hash(data, target: @session)
1319
+ def append_hash(data, target: @session, build: false)
1134
1320
  target ||= []
1135
- if (type = env('BUILD', suffix: 'TYPE') || ENV['BUILD_TYPE'])
1321
+ if build && (type = env('BUILD', suffix: 'TYPE') || ENV['BUILD_TYPE'])
1136
1322
  type = "__#{type}__"
1137
1323
  if (extra = data[type] || data[type.to_sym]).is_a?(Hash)
1138
1324
  data = data.merge(extra)
@@ -1161,7 +1347,7 @@ module Squared
1161
1347
  target
1162
1348
  end
1163
1349
 
1164
- def append_any(val, target: @session, delim: false)
1350
+ def append_any(val, target: @session, build: false, delim: false)
1165
1351
  if delim && !target.include?('--')
1166
1352
  target << '--'
1167
1353
  else
@@ -1171,7 +1357,7 @@ module Squared
1171
1357
  when String
1172
1358
  target << val
1173
1359
  when Hash
1174
- append_hash(val, target: target)
1360
+ append_hash(val, target: target, build: build)
1175
1361
  when Enumerable
1176
1362
  if target.is_a?(Array)
1177
1363
  target.concat(val.to_a)
@@ -1180,6 +1366,7 @@ module Squared
1180
1366
  end
1181
1367
  else
1182
1368
  target.delete('--') if delim
1369
+ nil
1183
1370
  end
1184
1371
  end
1185
1372
 
@@ -1188,8 +1375,9 @@ module Squared
1188
1375
 
1189
1376
  target << '--' if delim && !target.include?('--')
1190
1377
  list.map do |val|
1191
- target << (val = escape ? shell_escape(val, quote: quote) : shell_quote(val))
1192
- val
1378
+ item = escape ? shell_escape(val, quote: quote) : shell_quote(val)
1379
+ target << item
1380
+ item
1193
1381
  end
1194
1382
  end
1195
1383
 
@@ -1197,8 +1385,8 @@ module Squared
1197
1385
  **kwargs)
1198
1386
  return if (list = list.flatten).empty?
1199
1387
 
1200
- list.flatten.each do |opt|
1201
- next unless (val = option(opt, target: target, **kwargs))
1388
+ list.each do |opt|
1389
+ next unless (val = option(opt, **kwargs))
1202
1390
 
1203
1391
  return target << (if flag
1204
1392
  shell_option(opt, equals ? val : nil, quote: quote, escape: escape, force: force)
@@ -1214,7 +1402,7 @@ module Squared
1214
1402
  return if (list = list.flatten).empty?
1215
1403
 
1216
1404
  ret = []
1217
- list.flatten.each do |flag|
1405
+ list.each do |flag|
1218
1406
  next unless (val = option(flag, **kwargs))
1219
1407
 
1220
1408
  if val == '0' && no
@@ -1227,7 +1415,45 @@ module Squared
1227
1415
  end
1228
1416
 
1229
1417
  def append_nocolor(target: @session)
1230
- target << '--no-color' if !ARG[:COLOR] || stdin? || option('no-color', target: target, ignore: false)
1418
+ target << '--no-color' if !ARG[:COLOR] || stdin? || option('no-color', ignore: false)
1419
+ end
1420
+
1421
+ def merge_opts(base, data)
1422
+ return data unless base
1423
+ return base unless data
1424
+
1425
+ ret = case data
1426
+ when String
1427
+ case base
1428
+ when String
1429
+ "#{base} #{data}"
1430
+ when Hash
1431
+ "#{append_hash(base).join(' ')} #{data}"
1432
+ when Enumerable
1433
+ "#{base.to_a.join(' ')} #{data}"
1434
+ end
1435
+ when Hash
1436
+ case base
1437
+ when String
1438
+ "#{base} #{append_hash(data).join(' ')}"
1439
+ when Hash
1440
+ base.merge(data)
1441
+ when Enumerable
1442
+ base.to_a + append_hash(data)
1443
+ end
1444
+ when Enumerable
1445
+ case base
1446
+ when String
1447
+ "#{base} #{data.to_a.join(' ')}"
1448
+ when Hash
1449
+ "#{append_hash(base).join(' ')} #{data.to_a.join(' ')}"
1450
+ when Enumerable
1451
+ base.to_a + data.to_a
1452
+ end
1453
+ else
1454
+ base
1455
+ end
1456
+ ret || data
1231
1457
  end
1232
1458
 
1233
1459
  def collect_hash(data, pass: [])
@@ -1236,10 +1462,19 @@ module Squared
1236
1462
  ret
1237
1463
  end
1238
1464
 
1239
- def param_guard(action, flag, args: nil, key: nil, pat: nil)
1465
+ def parse_json(val, hint: nil)
1466
+ ret = JSON.parse(val)
1467
+ raise_error('invalid JSON object', val, hint: hint) unless ret.is_a?(Hash)
1468
+ rescue StandardError => e
1469
+ log&.warn e
1470
+ else
1471
+ ret
1472
+ end
1473
+
1474
+ def param_guard(action, flag, args: nil, key: nil, pat: nil, values: nil)
1240
1475
  if args && key
1241
- val = args.fetch(key, nil)
1242
- return val unless val.nil? || (pat && !val.match?(pat))
1476
+ val = args[key]
1477
+ return val unless val.nil? || (pat && !val.match?(pat)) || (values && !values.include?(val))
1243
1478
 
1244
1479
  @session = nil
1245
1480
  raise_error(action, "#{flag}[#{key}]", hint: val.nil? ? 'missing' : 'invalid')
@@ -1250,6 +1485,17 @@ module Squared
1250
1485
  args
1251
1486
  end
1252
1487
 
1488
+ def relativepath(*files, all: false)
1489
+ return [] if files.empty?
1490
+
1491
+ files.flatten.map { |val| Pathname.new(val) }.select { |val| projectpath?(val) }.map do |val|
1492
+ val = val.absolute? ? val.to_s.sub(/^#{Regexp.escape(File.join(path, ''))}/o, '') : val.to_s
1493
+ val = val[2..-1] if val.start_with?('./')
1494
+ val = "#{val}*" if all && val.end_with?('/')
1495
+ val
1496
+ end
1497
+ end
1498
+
1253
1499
  def projectmap(files, parent: false)
1254
1500
  files = files.select { |val| projectpath?(val) } unless parent
1255
1501
  files.map { |val| val == '.' ? '.' : shell_quote(basepath(val.strip)) }
@@ -1291,22 +1537,28 @@ module Squared
1291
1537
  val.compact.map { |s| color(s) }.flatten
1292
1538
  end
1293
1539
 
1540
+ def epochtime
1541
+ DateTime.now.strftime('%Q').to_i
1542
+ end
1543
+
1294
1544
  def on(event, from, *args, **kwargs)
1295
- return unless from && (obj = @events[event][from])
1545
+ return unless from
1296
1546
 
1297
- if obj.is_a?(Array) && obj[1].is_a?(Hash)
1298
- opts = kwargs.empty? ? obj[1] : obj[1].merge(kwargs)
1299
- target = obj[0]
1300
- else
1301
- opts = kwargs
1302
- target = obj
1303
- end
1304
- as_a(target, flat: true).each do |cmd|
1305
- case cmd
1306
- when Proc, Method
1307
- cmd.call(*args, **opts)
1308
- when String
1309
- run(cmd, **opts)
1547
+ @events[event][from]&.each do |obj|
1548
+ if obj.is_a?(Array) && obj[1].is_a?(Hash)
1549
+ opts = kwargs.empty? ? obj[1] : obj[1].merge(kwargs)
1550
+ target = obj[0]
1551
+ else
1552
+ opts = kwargs
1553
+ target = obj
1554
+ end
1555
+ as_a(target, flat: true).each do |cmd|
1556
+ case cmd
1557
+ when Proc, Method
1558
+ cmd.call(*args, **opts)
1559
+ when String
1560
+ run(cmd, **opts)
1561
+ end
1310
1562
  end
1311
1563
  end
1312
1564
  end
@@ -1315,7 +1567,7 @@ module Squared
1315
1567
  pwd = Pathname.pwd
1316
1568
  if block_given?
1317
1569
  begin
1318
- if path == pwd || pass == true || (pass.is_a?(String) && semscan(pass).join <= RUBY_VERSION)
1570
+ if path == pwd || pass == true || (pass.is_a?(String) && semscan(pass).join >= RUBY_VERSION)
1319
1571
  ret = instance_eval(&blk)
1320
1572
  else
1321
1573
  Dir.chdir(path)
@@ -1347,23 +1599,53 @@ module Squared
1347
1599
  end
1348
1600
 
1349
1601
  def run_set(cmd, val = nil, opts: nil, **)
1350
- return @output = cmd.dup if cmd.is_a?(Array)
1351
-
1352
- unless @output[1] == false && !@output[0].nil?
1602
+ diso = @output[1] == false && !@output[0].nil?
1603
+ dise = @output[2] == false
1604
+ parse = lambda do |data|
1605
+ ret = []
1606
+ if data[:command]
1607
+ ret[0] = data[:command]
1608
+ ret[1] = data[:opts] unless diso
1609
+ ret[3] = data[:args]
1610
+ elsif data[:script]
1611
+ ret[1] = data[:script]
1612
+ ret[3] = data[:opts]
1613
+ ret[4] = data[:args]
1614
+ else
1615
+ ret[0] = false
1616
+ end
1617
+ ret[2] = data[:env] unless dise
1618
+ ret
1619
+ end
1620
+ case cmd
1621
+ when Array
1622
+ @output = if cmd.all? { |data| data.is_a?(Hash) }
1623
+ diso = false
1624
+ dise = false
1625
+ cmd.map { |data| parse.(data) }
1626
+ else
1627
+ cmd.dup
1628
+ end
1629
+ return
1630
+ when Hash
1631
+ @output = parse.(data)
1632
+ else
1633
+ @output[0] = cmd
1634
+ end
1635
+ unless diso
1353
1636
  if opts == false
1354
1637
  @output[1] = false
1355
1638
  elsif opts && opts != true
1356
1639
  @output[1] = opts
1357
1640
  end
1358
1641
  end
1359
- unless @output[2] == false
1360
- if val.is_a?(Hash)
1361
- @output[2] = val
1362
- elsif val == false
1363
- @output[2] = false
1364
- end
1642
+ return if dise
1643
+
1644
+ if val.is_a?(Hash)
1645
+ @output[2] = val
1646
+ elsif val == false
1647
+ @output[2] = false
1365
1648
  end
1366
- @output[0] = cmd
1367
1649
  end
1368
1650
 
1369
1651
  def script_set(cmd, prod: nil, args: nil, **)
@@ -1379,6 +1661,8 @@ module Squared
1379
1661
  end
1380
1662
 
1381
1663
  def as_get(val)
1664
+ return unless val
1665
+
1382
1666
  @global && (ret = @as && @as[from] && @as[from][val]) ? ret : val
1383
1667
  end
1384
1668
 
@@ -1404,8 +1688,7 @@ module Squared
1404
1688
  end
1405
1689
 
1406
1690
  def projectpath?(val)
1407
- val = Pathname.new(val).cleanpath
1408
- val.absolute? ? val.to_s.start_with?(File.join(path, '')) : !val.to_s.start_with?(File.join('..', ''))
1691
+ Pathname.new(val).absolute? ? val.to_s.start_with?(File.join(path, '')) : !val.to_s.start_with?('..')
1409
1692
  end
1410
1693
 
1411
1694
  def semmajor?(cur, want)
@@ -1414,19 +1697,20 @@ module Squared
1414
1697
 
1415
1698
  def runnable?(val)
1416
1699
  case val
1417
- when String
1700
+ when String, Enumerable, Proc, Method
1418
1701
  true
1419
- when Enumerable
1420
- !val.is_a?(Hash)
1421
1702
  else
1422
1703
  false
1423
1704
  end
1424
1705
  end
1425
1706
 
1707
+ def series?(val)
1708
+ val.is_a?(Array) && val.all? { |p| p.is_a?(Proc) || p.is_a?(Method) }
1709
+ end
1710
+
1426
1711
  def session_arg?(*list, target: @session, value: false)
1427
1712
  list.any? do |val|
1428
- pat = /^#{Regexp.escape(shell_option(val))}#{value ? '[ =].' : '(?:[ =]|$)'}/
1429
- target.any? { |opt| opt =~ pat }
1713
+ target.any? { |opt| opt.match?(/^#{Regexp.escape(shell_option(val))}#{value ? '[ =].' : '(?:[ =]|$)'}/o) }
1430
1714
  end
1431
1715
  end
1432
1716
 
@@ -1463,6 +1747,10 @@ module Squared
1463
1747
  Base.tasks + VAR_SET
1464
1748
  end
1465
1749
 
1750
+ def blocks
1751
+ BLK_SET
1752
+ end
1753
+
1466
1754
  def borderstyle
1467
1755
  ((data = workspace.banner_get(*@ref, group: group)) && data[:border]) || theme[:border]
1468
1756
  end