Class: Babushka::Dep

Inherits:
Object show all
Extended by:
LogHelpers
Includes:
LogHelpers
Defined in:
lib/babushka/dep.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from LogHelpers

debug, deprecated!, log, log_block, log_error, log_ok, log_stderr, log_warn, removed!

Constructor Details

#initialize(name, source, params, opts, block) ⇒ Dep

Create a new dep named name within source, whose implementation is found in block. To define deps yourself, you should call dep (which is Dep::Helpers#dep).



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/babushka/dep.rb', line 35

def initialize name, source, params, opts, block
  if !name.is_a?(String)
    raise InvalidDepName, "The dep name #{name.inspect} isn't a string."
  elsif name.empty?
    raise InvalidDepName, "Deps can't have empty names."
  elsif /[[:cntrl:]]/mu =~ name
    raise InvalidDepName, "The dep name '#{name}' contains nonprintable characters."
  elsif /\// =~ name
    raise InvalidDepName, "The dep name '#{name}' contains '/', which isn't allowed (logs are named after deps, and filenames can't contain '/')."
  elsif /\:/ =~ name
    raise InvalidDepName, "The dep name '#{name}' contains ':', which isn't allowed (colons separate dep and template names from source prefixes)."
  elsif !params.all? {|param| param.is_a?(Symbol) }
    non_symbol_params = params.reject {|p| p.is_a?(Symbol) }
    raise DepParameterError, "The dep '#{name}' has #{'a ' if non_symbol_params.length == 1}non-symbol param#{'s' if non_symbol_params.length > 1} #{non_symbol_params.map(&:inspect).to_list}, which #{non_symbol_params.length == 1 ? "isn't" : "aren't"} allowed."
  else
    @name, @dep_source, @params, @block = name, source, params, block
    @args = {}
    @opts = Base.sources.current_load_opts.merge(opts)
    @load_path = Base.sources.current_load_path
    @dep_source.deps.register(self)
  end
end

Instance Attribute Details

#argsObject (readonly)

Returns the value of attribute args



30
31
32
# File 'lib/babushka/dep.rb', line 30

def args
  @args
end

#callstackObject (readonly)

Returns the value of attribute callstack



30
31
32
# File 'lib/babushka/dep.rb', line 30

def callstack
  @callstack
end

#dep_sourceObject (readonly)

Returns the value of attribute dep_source



30
31
32
# File 'lib/babushka/dep.rb', line 30

def dep_source
  @dep_source
end

#load_pathObject (readonly)

Returns the value of attribute load_path



30
31
32
# File 'lib/babushka/dep.rb', line 30

def load_path
  @load_path
end

#nameObject (readonly)

Returns the value of attribute name



30
31
32
# File 'lib/babushka/dep.rb', line 30

def name
  @name
end

#optsObject (readonly)

Returns the value of attribute opts



30
31
32
# File 'lib/babushka/dep.rb', line 30

def opts
  @opts
end

#paramsObject (readonly)

Returns the value of attribute params



30
31
32
# File 'lib/babushka/dep.rb', line 30

def params
  @params
end

Instance Method Details

#basenameObject

Return this dep's name, first removing the template suffix if one is present.

Note that this only removes the suffix when it was used to define the dep. Dep names that end in something that looks like a template suffix, but didn't match a template and result in a templated dep, won't be touched.

Some examples:

Dep('benhoskings:Chromium.app').basename #=> 'Chromium'
Dep('generated report.pdf').basename     #=> "generated report.pdf"


98
99
100
# File 'lib/babushka/dep.rb', line 98

def basename
  suffixed? ? name.sub(/\.#{Regexp.escape(template.name)}$/, '') : name
end

#cache_keyObject



109
110
111
# File 'lib/babushka/dep.rb', line 109

def cache_key
  DepRequirement.new(name, @params.map {|p| @args[p].try(:current_value) })
end

#contextObject



58
59
60
# File 'lib/babushka/dep.rb', line 58

def context
  @context ||= template.context_class.new(self, &@block)
end

#contextual_nameObject

Returns this dep's name, including its source name as a prefix if the source is remote.

The contextual name is the name you can use to refer to unambiguously refer to this dep on your system; i.e. the name that properly identifies the dep, taking your (possibly customised) source names into account.



82
83
84
85
# File 'lib/babushka/dep.rb', line 82

def contextual_name
  # TODO This isn't quite right; it should be dep_source.default? instead."
  dep_source.remote? ? "#{dep_source.name}:#{name}" : name
end

#inspectObject



418
419
420
# File 'lib/babushka/dep.rb', line 418

def inspect
  "#<Dep:#{object_id} '#{dep_source.name}:#{name}'>"
end

#meet(*args) ⇒ Object

Entry point for a full met?/meet #process run.



127
128
129
# File 'lib/babushka/dep.rb', line 127

def meet *args
  with(*args).process(true)
end

#met?(*args) ⇒ Boolean

The entry point for a dry run, where only met? blocks will be evaluated. This is useful to inspect the current state of a dep tree, without altering the system.

Returns:

  • (Boolean)


122
123
124
# File 'lib/babushka/dep.rb', line 122

def met? *args
  with(*args).process(false)
end

#process(and_meet = true) ⇒ Object

Run this dep and its subdeps recursively.

The overall flow for a single dep is [met? [, meet, met?]]. That is, met? is checked; if it's false, meet is run and then met? is checked again.

The [meet, met?] component for unmet deps is only performed if and_meet is true. If it's false, then babushka will just check met? down the tree, returning true if this dep and all its subdeps are already met.

Running the dep involves the following steps:

  • First, the setup block is run.

  • Next, the dep's dependencies (i.e. the contents of requires) are run recursively; this dep early-exits if any of the subdeps couldn't be met (or if this is a dry run, if any of the subdeps are unmet).

  • If the dependencies are all met, the met? block is run. If met? returns true, or any true-like value, the dep is already met and there is nothing to do. Otherwise, the dep is unmet, and the following happens:

    - The +prepare+ task is run.
    - The +before+ task is run.
    - If +before+ returned a true-like value, the +meet+ task is run.
      This is where the actual work of achieving the dep's aim is done.
    - If +meet+ returned a true-like value, the +after+ task is run.
  • Finally, the met? task is run again, to check whether running meet achieved the dep's goal.

The final step is important to understand: the before/meet/after blocks' return values are ignored. The result of a dep is always that of its met? block, whether it was already met, became met after the meet block was run, or couldn't be met.

Sometimes there are conditions under which a dep is unmeetable. For example, if a dep detects that the existing version of a package is broken in some way that requires manual intervention, then there's no use running the meet block. In this circumstance, the dep can call #unmeetable!, which raises an UnmeetableDep exception. Babushka will rescue it and consider the dep unmeetable (that is, it will just allow the dep to fail without attempting to meet it).

The following describes the return values of the defining components, and of the dep itself.

  • A '-' means the corresponding block wouldn't be run at all.

  • An 'X' means the corresponding value doesn't matter, and is discarded.

    Scenario            | met?         | -> meet      | -> met? | dep returns
    --------------------+--------------+--------------+---------+------------
    already met         | true         | -            | -       | true
    met during run      | false        | X            | true    | true
    couldn't be met     | false        | X            | false   | false
    failure during meet | false        | #unmeetable! | -       | nil
    unmeetable          | #unmeetable! | -            | -       | nil

Wherever possible, the met? test shouldn't directly test any work done in the meet block, only that its overall purpose has been achieved. Just like normal test-driven development, you should test the “what” and not the “how”. Tests involving the “how” are brittle and don't correctly express the dep's intent.

For example, if the purpose of a given dep is to make sure the webserver is running, the contents of the meet block would probably involve `/etc/init.d/nginx start` or similar, on a Linux system at least. In this case, the met? block shouldn't test anything involving `/etc/init.d` directly; instead, it should separately test that the webserver is running, for example by using `lsof` to check that something is listening on port 80.



197
198
199
# File 'lib/babushka/dep.rb', line 197

def process and_meet = true
  process_as_requirement(and_meet, [], Babushka::DepCache.new)
end

#process_as_requirement(and_meet, callstack, cache) ⇒ Object

Process this dep as a requirement of another dep – that is, not as the top-level dep in a tree. The difference is that the callstack and dep cache are supplied by the calling dep, instead of created anew.

This method is intended to be called only from deps themselves, as they invoke their requirements (via Dep#run_requirement); to process a dep directly, call Dep#process instead.



208
209
210
211
212
213
214
# File 'lib/babushka/dep.rb', line 208

def process_as_requirement and_meet, callstack, cache
  @callstack = callstack
  @cache = cache
  process_with_caching(and_meet)
ensure
  @callstack = @cache = nil
end

#suffixObject

Returns the portion of the end of the dep name that looks like a template suffix, if any. Unlike #basename, this method will return anything that looks like a template suffix, even if it doesn't match a template.



105
106
107
# File 'lib/babushka/dep.rb', line 105

def suffix
  name.scan(DepTemplate::TEMPLATE_NAME_MATCH).flatten.first
end

#templateObject

Attempt to retrieve the template specified in +opts+. If the template name includes a source prefix, it is searched for within the corresponding source. Otherwise, it is searched for in the current source and the core sources.



66
67
68
69
70
71
72
73
74
# File 'lib/babushka/dep.rb', line 66

def template
  @template ||= if opts[:template]
    Base.sources.template_for(opts[:template], :from => dep_source).tap {|t|
      raise TemplateNotFound, "There is no template named '#{opts[:template]}' to define '#{name}' against." if t.nil?
    }
  else
    Base.sources.template_for(suffix, :from => dep_source) || self.class.base_template
  end
end

#with(*args) ⇒ Object



113
114
115
116
117
# File 'lib/babushka/dep.rb', line 113

def with *args
  @args = parse_arguments(args)
  @context = nil # To re-run param.default() calls, etc, inside deps.
  self
end