Class: Asgard::Base

Inherits:
Thor
  • Object
show all
Includes:
Shell
Defined in:
lib/asgard/base.rb

Direct Known Subclasses

Tasks

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shell

#sh, #shebang

Class Method Details

._build_dep_graph(stages) ⇒ Object

Translate stages into a DependencyGraph-compatible hash.

stages: [[:one], [:two, :three], [:four]]
→ { one: [], two: [:one], three: [:one], four: [:two, :three] }


61
62
63
64
65
66
67
68
# File 'lib/asgard/base.rb', line 61

def _build_dep_graph(stages)
  graph = {}
  stages.each_with_index do |stage, i|
    prev_stage = i.positive? ? stages[i - 1] : []
    stage.each { |task| graph[task] = prev_stage.dup }
  end
  graph
end

._condObject



40
41
42
# File 'lib/asgard/base.rb', line 40

def _cond
  @_cond ||= Hash.new { |h, k| h[k] = ConditionVariable.new }
end

._depsObject



28
29
30
# File 'lib/asgard/base.rb', line 28

def _deps
  @_deps ||= {}
end

._doneObject



36
37
38
# File 'lib/asgard/base.rb', line 36

def _done
  @_done ||= Set.new
end

._ran_mutexObject



44
45
46
# File 'lib/asgard/base.rb', line 44

def _ran_mutex
  @_ran_mutex ||= Mutex.new
end

._reset_ran!Object

Reset execution tracking for a fresh asgard invocation.



49
50
51
52
53
54
55
# File 'lib/asgard/base.rb', line 49

def _reset_ran!
  _ran_mutex.synchronize do
    @_running = Set.new
    @_done    = Set.new
    @_cond    = Hash.new { |h, k| h[k] = ConditionVariable.new }
  end
end

._runningObject



32
33
34
# File 'lib/asgard/base.rb', line 32

def _running
  @_running ||= Set.new
end

.default_task(meth = nil) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/asgard/base.rb', line 127

def default_task(meth = nil)
  if meth && meth != :none && @_default_task_location
    here = caller_locations(1, 1).first
    # rubocop:disable Style/StderrPuts -- warn bypasses $stderr in Ruby 4.0, breaking capture_io in tests
    $stderr.puts "asgard: default_task :#{meth} at #{here.path}:#{here.lineno} " \
                 "overrides default_task :#{@_default_task_name} set at " \
                 "#{@_default_task_location.path}:#{@_default_task_location.lineno}"
    # rubocop:enable Style/StderrPuts
  end
  if meth && meth != :none
    @_default_task_location = caller_locations(1, 1).first
    @_default_task_name     = meth
  end
  super
end

.depends_on(*tasks) ⇒ Object

Declare dependencies for the next task. Bare symbols run sequentially; arrays within the splat run in parallel.

depends_on :build                          # sequential
depends_on :build, :lint                   # both sequential
depends_on [:build, :lint]                 # build and lint in parallel
depends_on :setup, [:build, :lint], :test  # setup, then build+lint, then test


77
78
79
# File 'lib/asgard/base.rb', line 77

def depends_on(*tasks)
  @_pending_deps = tasks
end

.desc(usage_or_desc, description = nil, options = {}) ⇒ Object

Allow single-argument desc: desc “Run the tests” The usage string defaults to the method name when the description is the only arg.



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/asgard/base.rb', line 83

def desc(usage_or_desc, description = nil, options = {})
  if description.nil? || description.is_a?(Hash)
    options = description if description.is_a?(Hash)
    @_pending_single_desc      = usage_or_desc
    @_pending_single_desc_opts = options
  else
    @_pending_single_desc      = nil
    @_pending_single_desc_opts = nil
    super
  end
end

.dotenv(path = ".env") ⇒ Object



117
118
119
120
# File 'lib/asgard/base.rb', line 117

def dotenv(path = ".env")
  require "dotenv"
  Dotenv.load(path) if File.exist?(path)
end


112
113
114
115
# File 'lib/asgard/base.rb', line 112

def footer(text = nil)
  return @_footer if text.nil?
  (@_footer ||= []).unshift(text)
end

.header(text = nil) ⇒ Object



107
108
109
110
# File 'lib/asgard/base.rb', line 107

def header(text = nil)
  return @_header if text.nil?
  (@_header ||= []) << text
end

.helper(name) ⇒ Object



122
123
124
125
# File 'lib/asgard/base.rb', line 122

def helper(name, &)
  define_singleton_method(name, &)
  no_commands { private define_method(name) { |*args, **kwargs, &blk| self.class.send(name, *args, **kwargs, &blk) } }
end

.inherited(subclass) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/asgard/base.rb', line 15

def inherited(subclass)
  super
  Asgard::Base.subclasses << subclass
  subclass.instance_variable_set(:@_deps,              {})
  subclass.instance_variable_set(:@_pending_deps,      [])
  subclass.instance_variable_set(:@_pending_single_desc,      nil)
  subclass.instance_variable_set(:@_pending_single_desc_opts, nil)
  subclass.instance_variable_set(:@_running,    Set.new)
  subclass.instance_variable_set(:@_done,       Set.new)
  subclass.instance_variable_set(:@_cond,       Hash.new { |h, k| h[k] = ConditionVariable.new })
  subclass.instance_variable_set(:@_ran_mutex,  Mutex.new)
end

.method_added(method_name) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/asgard/base.rb', line 193

def method_added(method_name)
  if @_pending_single_desc && !no_commands?
    pending_desc = @_pending_single_desc
    pending_opts = @_pending_single_desc_opts || {}
    @_pending_single_desc      = nil
    @_pending_single_desc_opts = nil
    desc(method_name.to_s, pending_desc, pending_opts)
  end

  return super unless @usage

  pending = Array(@_pending_deps).dup
  @_pending_deps = []

  return super if pending.empty?
  return super if method_name.to_s.start_with?("_")

  # Each element is a Symbol (sequential) or Array (parallel group).
  _deps[method_name.to_sym] = pending.map { |d| Array(d).map(&:to_sym) }
  super
end

.no_negate(*names) ⇒ Object

Suppress [–no-name] / [–skip-name] from help for boolean class options where negation is meaningless. Call after class_option declarations.



97
98
99
100
101
102
103
104
105
# File 'lib/asgard/base.rb', line 97

def no_negate(*names)
  names.each do |name|
    opt = class_options[name]
    next unless opt
    opt.define_singleton_method(:usage) do |padding = 0|
      aliases_for_usage.ljust(padding) + "[#{switch_name}]"
    end
  end
end

.subclassesObject



11
12
13
# File 'lib/asgard/base.rb', line 11

def subclasses
  @subclasses ||= []
end

.validate_deps!Object

Validate the full dep graph for cycles using Dagwood::DependencyGraph.



144
145
146
147
148
149
150
151
152
153
154
# File 'lib/asgard/base.rb', line 144

def validate_deps!
  _check_orphaned_deps!
  return if _deps.empty?

  all_task_names = all_commands.keys.map(&:to_sym)
  _check_undefined_deps!(all_task_names)
  _check_dep_arities!
  _build_and_sort_graph(all_task_names)
rescue TSort::Cyclic => e
  raise Asgard::CircularDependencyError, e.message
end

Instance Method Details

#help(command = nil, subcommand = false) ⇒ Object

rubocop:disable Style/OptionalBooleanParameter



216
217
218
219
220
221
# File 'lib/asgard/base.rb', line 216

def help(command = nil, subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
  say self.class.header.join("\n\n") if self.class.header && command.nil?
  say "\n"
  super
  say self.class.footer.join("\n\n") if self.class.footer && command.nil?
end