litbuild 1.0.1

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.
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'stringio'
4
+ require 'litbuild/blueprint_parser'
5
+ require 'litbuild/errors'
6
+ require 'litbuild/logfile_namer'
7
+ require 'litbuild/visitor'
8
+
9
+ module Litbuild
10
+ class Blueprint
11
+ class <<self
12
+ # This should simply be the name of the directory where blueprints
13
+ # of each specific type are expected to be found by Driver.
14
+ def self.directory_name
15
+ raise(SubclassResponsibility)
16
+ end
17
+ end
18
+
19
+ attr_reader :active_phase
20
+ attr_reader :base_grafs
21
+ attr_writer :logfile_namer
22
+
23
+ def self.descendants
24
+ ObjectSpace.each_object(Class).select { |c| c < self }
25
+ end
26
+
27
+ # WARNING: in most cases, a freshly-created blueprint cannot be used
28
+ # until it has also been _prepared_. (See below.)
29
+ def initialize(text:)
30
+ parser = BlueprintParser.new(text)
31
+ @base_directives = parser.base_directives
32
+ @phase_directives = parser.phase_directives
33
+ @base_grafs = parser.base_grafs
34
+ @phase_grafs = parser.phase_grafs
35
+ @active_phase = @base_directives['default-phase']&.first
36
+ end
37
+
38
+ # This prepares a blueprint for use. This is separate from
39
+ # initialize because parameters are not available until after all
40
+ # blueprints have been parsed, and the logfile namer is not
41
+ # available until after the LOGFILE_DIR parameter can be resolved.
42
+ #
43
+ # Parameters is a set of configuration parameters
44
+ # Logfile_namer is used to generate log file names
45
+ # Library is the BlueprintLibrary, used (for example) to find
46
+ # dependencies.
47
+ def prepare(parameters:, logfile_namer:, library:)
48
+ @logfile_namer = logfile_namer
49
+ @bp_library = library
50
+ [@base_directives,
51
+ @phase_directives,
52
+ @base_grafs,
53
+ @phase_grafs].each { |component| resolve_all(parameters, component) }
54
+ end
55
+
56
+ # Dependencies need to be handled before their dependants; that's
57
+ # handled here. Subclasses should run `super` before doing anything
58
+ # else with the Visitor, or call the send_to_dependencies method
59
+ # directly, so that this happens properly!
60
+ def accept(visitor:)
61
+ send_to_dependencies(visitor: visitor)
62
+ end
63
+
64
+ # Return a copy of this blueprint that is initialized to operate on
65
+ # the specified phase.
66
+ def for_phase(new_active_phase)
67
+ return self unless phases?
68
+
69
+ phased_blueprint = clone
70
+ phased_blueprint.phase = new_active_phase if new_active_phase
71
+ phased_blueprint
72
+ end
73
+
74
+ def phases
75
+ @phase_directives.keys
76
+ end
77
+
78
+ def phases?
79
+ !@phase_directives.empty?
80
+ end
81
+
82
+ def phase_grafs
83
+ @phase_grafs[active_phase]
84
+ end
85
+
86
+ def directives
87
+ if active_phase
88
+ @phase_directives[active_phase]
89
+ else
90
+ @base_directives
91
+ end
92
+ end
93
+
94
+ def [](directive)
95
+ directives[directive]
96
+ end
97
+
98
+ # This is just like the [] operator method except that it always
99
+ # returns only the first value for a directive -- which is helpful
100
+ # for all the directives that are only ever supposed to have a
101
+ # single value, like `name` or `full-name`.
102
+ def value(directive)
103
+ self[directive]&.first
104
+ end
105
+
106
+ def name
107
+ value('name')
108
+ end
109
+
110
+ def name_with_phase
111
+ if active_phase
112
+ "#{name}::#{active_phase}"
113
+ else
114
+ name
115
+ end
116
+ end
117
+
118
+ def full_name
119
+ value('full-name')
120
+ end
121
+
122
+ def file_name
123
+ if active_phase
124
+ "#{name}-#{active_phase}"
125
+ else
126
+ name
127
+ end.tr(' ', '-')
128
+ end
129
+
130
+ def header_text
131
+ value('full-name')
132
+ end
133
+
134
+ def header_text_with_phase
135
+ "#{header_text} (#{active_phase} phase)"
136
+ end
137
+
138
+ def logfile(stage_name, phase_name = nil)
139
+ phase = phase_name&.tr(' ', '_')
140
+ @logfile_namer.path_for(name, phase, stage_name)
141
+ end
142
+
143
+ def parameter_defaults
144
+ values = {}
145
+ all_directive_sets = [@base_directives, @phase_directives.values].flatten
146
+ all_directive_sets.each do |dirset|
147
+ next unless dirset.key?('parameter')
148
+
149
+ dirset['parameter'].each do |a_param|
150
+ val = a_param['default'].first
151
+ values[a_param['name'].first] = if val == '(empty)'
152
+ ''
153
+ else
154
+ val
155
+ end
156
+ end
157
+ end
158
+ values
159
+ end
160
+
161
+ def target_name
162
+ if active_phase
163
+ "#{name}::#{active_phase}"
164
+ else
165
+ name
166
+ end
167
+ end
168
+
169
+ def success_line
170
+ 'SUCCESS'
171
+ end
172
+
173
+ def failure_line
174
+ 'FAILURE'
175
+ end
176
+
177
+ # If a dependency is declared both without a phase (typically in the
178
+ # base directives for the blueprint) and with a phase (typically in
179
+ # a phase of that blueprint), throw away the phaseless declaration
180
+ # to avoid including two versions of the dependency.
181
+ def deduped_dependency_names
182
+ dep_names = self['depends-on'].clone || []
183
+ deps_with_phase = dep_names.select { |dep| dep.match?(/::/) }
184
+ deps_with_phase.each do |dep|
185
+ dep_without_phase = dep.split('::')[0]
186
+ dep_names.delete(dep_without_phase)
187
+ end
188
+ dep_names
189
+ end
190
+
191
+ protected
192
+
193
+ def send_to_dependencies(visitor:)
194
+ @bp_library.dependencies_for(self).each do |bp|
195
+ bp.accept(visitor: visitor)
196
+ end
197
+ end
198
+
199
+ def phase=(phase)
200
+ bptype = self.class.name.split('::').last
201
+ msg = "#{bptype} #{name} does not have a phase '#{phase}'"
202
+ raise(InvalidParameter, msg) unless phase.nil? || phases.include?(phase)
203
+
204
+ @active_phase = phase
205
+ end
206
+
207
+ private
208
+
209
+ def resolve_all(parameters, something)
210
+ case something
211
+ when Hash
212
+ something.values.each { |element| resolve_all(parameters, element) }
213
+ when Array
214
+ something.each { |element| resolve_all(parameters, element) }
215
+ when String
216
+ resolve(parameters: parameters, a_string: something)
217
+ end
218
+ end
219
+
220
+ def resolve(parameters:, a_string:)
221
+ params = a_string.scan(/PARAM\[([A-Z_]+)\]/).flatten.sort.uniq
222
+ params.each do |p|
223
+ unless parameters[p]
224
+ raise(ParameterMissing, "Parameter #{p} is not defined")
225
+ end
226
+
227
+ a_string.gsub!(/PARAM\[#{p}\]/, parameters[p])
228
+ end
229
+ a_string
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/commands.rb'
4
+ require 'litbuild/narrative.rb'
5
+ require 'litbuild/package.rb'
6
+ require 'litbuild/section.rb'
7
+
8
+ module Litbuild
9
+ # BlueprintLibrary initializes and sets configuration parameters for
10
+ # blueprints, and provides a common access point for blueprints and
11
+ # parameters. It always loads blueprints from the *current working
12
+ # directory* and uses config parameters from *the environment*.
13
+ class BlueprintLibrary
14
+ REQUIRED_PARAMS = %w[DOCUMENT_DIR LOGFILE_DIR PATCH_DIR
15
+ SCRIPT_DIR TARFILE_DIR WORK_SITE].freeze
16
+
17
+ attr_reader :blueprints, :parameters
18
+
19
+ def initialize(logfile_namer_class:)
20
+ @blueprints = {}
21
+ Blueprint.descendants.each do |blueprint_type|
22
+ load_blueprints_of_type(blueprint_type)
23
+ end
24
+ @parameters = resolve_parameter_values
25
+ log_namer = logfile_namer_class.new(@parameters['LOGFILE_DIR'])
26
+ @blueprints.each_value do |bp|
27
+ bp.prepare(parameters: @parameters,
28
+ logfile_namer: log_namer,
29
+ library: self)
30
+ end
31
+ end
32
+
33
+ def blueprint_for(target:)
34
+ name, phase = split_name_and_phase(target: target)
35
+ bp = blueprints[name]
36
+ unless bp
37
+ raise(UnknownBlueprint, "Blueprint #{name} not found in library")
38
+ end
39
+
40
+ bp.for_phase(phase)
41
+ end
42
+
43
+ # Convert the dependency declarations found in a `depends-on`
44
+ # directive to actual blueprints. See the `Dependencies` section of
45
+ # doc/blueprints.txt for details on how this works.
46
+ def dependencies_for(blueprint)
47
+ dep_names = blueprint.deduped_dependency_names
48
+ dep_names.map do |dep|
49
+ if dep.match?(/::/) # Explicit phase specified
50
+ blueprint_for(target: dep)
51
+ else
52
+ bp = blueprint_for(target: dep)
53
+ if bp.phases&.include?(blueprint.active_phase)
54
+ bp.for_phase(blueprint.active_phase)
55
+ else
56
+ bp
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # split a target name, like linux::headers, into a blueprint name
65
+ # (linux) and a phase name (headers).
66
+ def split_name_and_phase(target:)
67
+ if target.match?(/::/)
68
+ target.strip.match(/(.*)::(.*)/)[1..2]
69
+ else
70
+ [target.strip, nil]
71
+ end
72
+ end
73
+
74
+ # These methods are used during initialization, be cautious when
75
+ # modifying them.
76
+
77
+ def load_blueprints_of_type(blueprint_type)
78
+ # Sometimes, starting in ruby 2.3, there is an extra singleton
79
+ # descendant of Blueprint, or something. (This happens about a
80
+ # thrid of the time, so I really mean "soemtime"s.) IDK what's
81
+ # going on and I don't feel like fussing with it; this kludge
82
+ # works around the issue, whatever it is.
83
+ return unless blueprint_type.name
84
+
85
+ Dir.glob("./#{blueprint_type.directory_name}/*.txt").each do |a_file|
86
+ bp_text = File.read(a_file)
87
+ begin
88
+ # All blueprints have a name and a full-name. If a blueprint
89
+ # has no `name:` or `full-name:` directive, they will be
90
+ # provided at load time.
91
+ %w[name full-name].each do |directive|
92
+ unless bp_text.match?(/^#{directive}:/)
93
+ bp_text = "#{directive}: #{File.basename(a_file, '.txt')}\n\n" +
94
+ bp_text
95
+ end
96
+ end
97
+ bp = blueprint_type.new(text: bp_text)
98
+ rescue ArgumentError => e
99
+ raise(Litbuild::ParseError, "Could not parse #{a_file}: #{e.message}")
100
+ end
101
+ if @blueprints[bp.name]
102
+ raise(DuplicateBlueprint,
103
+ "Duplicate blueprint #{bp.name} found in #{a_file}")
104
+ else
105
+ @blueprints[bp.name] = bp
106
+ end
107
+ end
108
+ end
109
+
110
+ def resolve_parameter_values
111
+ values = {}
112
+ REQUIRED_PARAMS.each { |key| values[key] = 'UNSET' }
113
+ @blueprints.each_value do |bp|
114
+ values.merge!(bp.parameter_defaults)
115
+ end
116
+ ENV.each do |k, v|
117
+ values[k] = v if values.key?(k)
118
+ end
119
+ @parameters = values
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'litbuild/errors'
4
+ require 'json'
5
+
6
+ module Litbuild
7
+ # This is a kludgy hand-built parser. Blueprint structure is not (at
8
+ # least, at this point) complicated enough that I can see any point in
9
+ # defining a grammar and using a parser generator. The structure of
10
+ # blueprint directives is described informally in
11
+ # `doc/blueprints.txt` (and blueprint-type-specific files under `doc`
12
+ # as well).
13
+ #
14
+ # tl;dr: each paragraph can be either directives or narrative.
15
+ # Narrative is AsciiDoc and does not get parsed or transformed.
16
+ # Directives are basically a simplified version of YAML.
17
+ class BlueprintParser
18
+ # _directives_ are for use in scripts. _grafs_ are for use in
19
+ # documents.
20
+ attr_reader :base_directives, :phase_directives, :base_grafs, :phase_grafs
21
+
22
+ def initialize(file_text)
23
+ @text = file_text
24
+ @phase_directives = {}
25
+ @phase_grafs = {}
26
+ phase_chunks = @text.split(/^phase: ([a-z -_]+)$/)
27
+
28
+ # The first part of the blueprint, before any phase directive,
29
+ # becomes the base directives and base narrative for the
30
+ # blueprint. If there are no phase directives, this is the entire
31
+ # blueprint, obvs.
32
+ @base_grafs = []
33
+ base = parse_phase(phase_chunks.shift, {}, @base_grafs)
34
+ base['full-name'] ||= base['name']
35
+ @base_directives = base
36
+
37
+ # The rest of the blueprint, if any, consists of directives and
38
+ # narrative for specific phases. The directives for each phase
39
+ # include all the base directives, as well as those specific to
40
+ # the phase.
41
+ until phase_chunks.empty?
42
+ phase_name = phase_chunks.shift
43
+ phase_contents = phase_chunks.shift
44
+ grafs = []
45
+ @phase_directives[phase_name] = parse_phase(phase_contents, base, grafs)
46
+ @phase_grafs[phase_name] = grafs
47
+ end
48
+
49
+ # Any directives at the beginning of a blueprint are actually a
50
+ # file header that should not be rendered as part of the narrative
51
+ # for it.
52
+ @base_grafs.shift while @base_grafs[0].is_a?(Hash)
53
+ rescue StandardError => e
54
+ msg = "Cannot parse blueprint starting: #{@text.lines[0..3].join}"
55
+ raise(Litbuild::ParseError, "#{msg} -- #{e}")
56
+ end
57
+
58
+ private
59
+
60
+ # Parse all directive paragraphs found in blueprint_text. Also add
61
+ # all parsed directive paragraphs, and all narrative paragraphs, to
62
+ # a collecting parameter.
63
+ def parse_phase(blueprint_text, start_with_directives, graf_collector)
64
+ directives = JSON.parse(JSON.generate(start_with_directives))
65
+ paragraphs = blueprint_text.split(/\n\n+/m)
66
+ paragraphs.each do |paragraph|
67
+ if directives?(paragraph)
68
+ parsed = parse_paragraph(paragraph)
69
+ add_directives(directives, parsed)
70
+ graf_collector << parsed
71
+ else
72
+ graf_collector << paragraph
73
+ end
74
+ end
75
+ handle_servicedirs(directives)
76
+ directives
77
+ end
78
+
79
+ # All s6-rc service directories should be tracked in the
80
+ # configuration file repository, so add them to
81
+ # `configuration-files`.
82
+ def handle_servicedirs(directives)
83
+ cfgs = directives['configuration-files'] || []
84
+ if (spipes = directives['service-pipeline'])
85
+ spipes.each do |a_pipe|
86
+ a_pipe['servicedirs'].each do |a_svc|
87
+ cfgs << "/etc/s6-rc/source/#{a_svc['name'].first}"
88
+ end
89
+ end
90
+ end
91
+ if (sdirs = directives['servicedir'])
92
+ sdirs.each do |a_svc|
93
+ cfgs << "/etc/s6-rc/source/#{a_svc['name'].first}"
94
+ end
95
+ end
96
+ directives['configuration-files'] = cfgs unless cfgs.empty?
97
+ end
98
+
99
+ def directives?(paragraph)
100
+ paragraph.split(' ').first =~ /^[a-z-]+:/
101
+ end
102
+
103
+ def add_directives(directives, parsed)
104
+ directives.merge!(parsed) do |_key, old_val, new_val|
105
+ [old_val, new_val].flatten
106
+ end
107
+ end
108
+
109
+ def parse_paragraph(paragraph)
110
+ lines_to_process = paragraph.lines.map(&:rstrip)
111
+ parse_lines(lines_to_process)
112
+ end
113
+
114
+ def parse_lines(lines_to_process)
115
+ graf_directives = Hash.new { |h, k| h[k] = [] }
116
+ until lines_to_process.empty?
117
+ directive_line = lines_to_process.shift
118
+ md = /^([A-Za-z0-9_-]+): *(.*)/.match(directive_line)
119
+ unless md
120
+ raise(Litbuild::ParseError,
121
+ "Expected '#{directive_line}' to be a directive")
122
+ end
123
+ directive_name = md[1]
124
+ firstline_value = md[2]
125
+ value_lines = related_lines(directive_line, lines_to_process)
126
+ value = parse_directive_value(firstline_value, value_lines)
127
+ if value == "''"
128
+ # two literal apostrophes is a special case, we treat it as an
129
+ # empty string. Probably ought to document that.
130
+ graf_directives[directive_name] << ''
131
+ else
132
+ graf_directives[directive_name] << value
133
+ end
134
+ graf_directives[directive_name].flatten!
135
+ end
136
+ graf_directives
137
+ end
138
+
139
+ # Regardless of what kind of directive structure we're dealing with,
140
+ # all of the lines that are indented more than the initial directive
141
+ # line are part of that directive. This method finds all those
142
+ # related lines and strips away the common initial indentation.
143
+ def related_lines(directive_line, lines_to_process)
144
+ directive_indent = indent_for(directive_line)
145
+ related = []
146
+ while !lines_to_process.empty? &&
147
+ (indent_for(lines_to_process[0]) > directive_indent)
148
+ related << lines_to_process.shift
149
+ end
150
+ common_indent = related.map { |l| indent_for(l) }.min
151
+ related.map { |l| l.slice(common_indent..-1) }
152
+ end
153
+
154
+ # What kind of directive are we dealing with? Could be a simple
155
+ # value (with zero or more continuation lines), or a multiline
156
+ # value, or an array, or a subdirective block.
157
+ def parse_directive_value(firstline_value, other_lines)
158
+ if firstline_value == '|'
159
+ multiline_value(other_lines)
160
+ elsif other_lines.empty?
161
+ firstline_value
162
+ elsif other_lines[0].match?(/^ *- /)
163
+ array_value(other_lines)
164
+ elsif other_lines[0].match?(/^[A-Za-z_-]+: /)
165
+ subdirective_value(other_lines)
166
+ else
167
+ folded_continuation_lines(firstline_value, other_lines)
168
+ end
169
+ end
170
+
171
+ # any time a directive line is indented more than the previous line,
172
+ # without being a part of an array or sub-directive or multi-line
173
+ # directive, it's a continuation of the previous line.
174
+ def folded_continuation_lines(first_line, related)
175
+ stripped = related.map(&:strip)
176
+ stripped.unshift(first_line)
177
+ stripped.join(" \\\n ")
178
+ end
179
+
180
+ def multiline_value(lines)
181
+ lines.join("\n") + "\n"
182
+ end
183
+
184
+ def array_value(lines)
185
+ value = []
186
+ until lines.empty?
187
+ array_member = lines.shift
188
+ firstline_value = /^- *(.*)$/.match(array_member)[1]
189
+ related = related_lines(array_member, lines)
190
+ value << parse_directive_value(firstline_value, related)
191
+ end
192
+ value
193
+ rescue StandardError
194
+ raise(Litbuild::ParseError, "Problem parsing: #{lines}")
195
+ end
196
+
197
+ # It turns out that sub-directives are the easiest thing in the
198
+ # world to parse, since we can just take the value and parse it as a
199
+ # new directive block.
200
+ def subdirective_value(lines)
201
+ parse_lines(lines)
202
+ end
203
+
204
+ # Utility method to find the amount of blank space at the beginning
205
+ # of a line.
206
+ def indent_for(line)
207
+ /^([[:blank:]]*).*/.match(line)[1].size
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/blueprint'
4
+
5
+ module Litbuild
6
+ class Commands < Blueprint
7
+ def self.directory_name
8
+ 'commands'
9
+ end
10
+
11
+ def accept(visitor:)
12
+ super
13
+ visitor.visit_commands(commands: self)
14
+ end
15
+
16
+ def files
17
+ return @files if @files
18
+
19
+ @files = {}
20
+ (directives['file'] || []).each do |a_file|
21
+ add_file_content(a_file)
22
+ end
23
+ @files
24
+ end
25
+
26
+ protected
27
+
28
+ def add_file_content(directive)
29
+ unless directive['name'] && directive['content']
30
+ raise(InvalidDirective, 'file directive missing name or content')
31
+ end
32
+
33
+ content = @files[directive['name'].first] ||= StringIO.new
34
+ content.puts(directive['content'].first)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/blueprint_library'
4
+ require 'litbuild/ascii_doc_visitor'
5
+ require 'litbuild/bash_script_visitor'
6
+ require 'litbuild/source_files_visitor'
7
+ require 'litbuild/url_visitor'
8
+
9
+ module Litbuild
10
+ # This is what the command-line program `lb` uses to do all the work,
11
+ # aside from parsing the command line. It initializes a blueprint
12
+ # library and provides methods for exercising all the functionality of
13
+ # litbuild: writing bash scripts, writing documents, querying package
14
+ # details, etc.
15
+ #
16
+ # All the actual work is done by Visitors that are handed to the
17
+ # top-level blueprint target (typically specified on the command line).
18
+ class Driver
19
+ REQUIRED_PARAMS = %w[DOCUMENT_DIR LOGFILE_DIR PATCH_DIR
20
+ SCRIPT_DIR TARFILE_DIR WORK_SITE].freeze
21
+
22
+ def initialize(logfile_namer_class: Litbuild::LogfileNamer)
23
+ @bplib = BlueprintLibrary.new(logfile_namer_class: logfile_namer_class)
24
+ end
25
+
26
+ def library
27
+ @bplib
28
+ end
29
+
30
+ def params
31
+ @bplib.parameters
32
+ end
33
+
34
+ def download_urls_for(target:)
35
+ uv = UrlVisitor.new
36
+ dispatch(visitor: uv, target: target)
37
+ uv.urls
38
+ end
39
+
40
+ def source_files_for(target:)
41
+ sfv = SourceFilesVisitor.new
42
+ dispatch(visitor: sfv, target: target)
43
+ sfv.files
44
+ end
45
+
46
+ def write_scripts_for(target:)
47
+ bsv = BashScriptVisitor.new(parameters: params)
48
+ dispatch(visitor: bsv, target: target)
49
+ bsv.write_sudoers
50
+ bsv.write_toplevel_script(target: target)
51
+ end
52
+
53
+ def write_document_for(target:)
54
+ adv = AsciiDocVisitor.new(parameters: params)
55
+ bp = library.blueprint_for(target: target)
56
+ bp.accept(visitor: adv)
57
+ adv.write_toplevel_doc(blueprint: bp)
58
+ end
59
+
60
+ private
61
+
62
+ def dispatch(visitor:, target:)
63
+ library.blueprint_for(target: target).accept(visitor: visitor)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Litbuild
4
+ # Superclass for all Litbuild errors
5
+ class Error < RuntimeError; end
6
+
7
+ # One or more required configuration parameters has not been provided
8
+ class ParameterMissing < Error; end
9
+
10
+ # A child class of Blueprint is expected to have defined a method, but
11
+ # has not done so.
12
+ class SubclassResponsibility < Error; end
13
+
14
+ # A blueprint was asked to build a phase that has not been defined.
15
+ class InvalidParameter < Error; end
16
+
17
+ # A blueprint directive is missing the expected structure
18
+ class InvalidDirective < Error; end
19
+
20
+ # The source tarfile and/or patches for a package were not found in
21
+ # the expected location.
22
+ class MissingSource < Error; end
23
+
24
+ # One of the blueprints referenced in the build set is not defined.
25
+ class UnknownBlueprint < Error; end
26
+
27
+ # A blueprint directive block could not be parsed
28
+ class ParseError < Error; end
29
+
30
+ # More than one blueprint exists with some name.
31
+ class DuplicateBlueprint < Error; end
32
+
33
+ # A command executed through sudo does not have an absolute path.
34
+ class RelativeSudo < Error; end
35
+
36
+ # A blueprints directive specifies a blueprint that has already been
37
+ # included elsewhere.
38
+ class UnrenderedComponent < Error; end
39
+ end