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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +1 -0
- data/LICENSE +14 -0
- data/README +135 -0
- data/bin/lb +69 -0
- data/gpl-3.0.txt +674 -0
- data/lib/litbuild/ascii_doc_visitor.rb +557 -0
- data/lib/litbuild/bash_script_visitor.rb +547 -0
- data/lib/litbuild/blueprint.rb +232 -0
- data/lib/litbuild/blueprint_library.rb +122 -0
- data/lib/litbuild/blueprint_parser.rb +210 -0
- data/lib/litbuild/commands.rb +37 -0
- data/lib/litbuild/driver.rb +66 -0
- data/lib/litbuild/errors.rb +39 -0
- data/lib/litbuild/logfile_namer.rb +23 -0
- data/lib/litbuild/multi_part_visitor.rb +35 -0
- data/lib/litbuild/narrative.rb +16 -0
- data/lib/litbuild/package.rb +143 -0
- data/lib/litbuild/section.rb +73 -0
- data/lib/litbuild/service_dir.rb +59 -0
- data/lib/litbuild/source_code_manager.rb +143 -0
- data/lib/litbuild/source_files_visitor.rb +26 -0
- data/lib/litbuild/url_visitor.rb +29 -0
- data/lib/litbuild/version.rb +5 -0
- data/lib/litbuild/visitor.rb +42 -0
- data/lib/litbuild.rb +16 -0
- data.tar.gz.sig +0 -0
- metadata +94 -0
- metadata.gz.sig +0 -0
@@ -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
|