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,547 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/service_dir'
4
+ require 'litbuild/source_code_manager'
5
+ require 'litbuild/visitor'
6
+
7
+ module Litbuild
8
+ ##
9
+ # This class writes bash scripts to directories, rooted at the
10
+ # SCRIPT_DIR directory. It adds a sequential number prefix on each
11
+ # script written to a directory, and ensures that no duplicate scripts
12
+ # are written: if a script for a specific blueprint (or blueprint
13
+ # phase) has already been written to any directory, BashScriptVisitor
14
+ # will ignore subsequent requests to write that blueprint (phase).
15
+ class BashScriptVisitor < Visitor
16
+ INSTALL_GID = 9999
17
+
18
+ attr_reader :blueprint_dir
19
+
20
+ def initialize(parameters:)
21
+ @parameters = parameters
22
+ super(directory: @parameters['SCRIPT_DIR'])
23
+ @written = Hash.new { |hash, key| hash[key] = [] }
24
+ @all_written_targets = []
25
+ @all_commands = []
26
+ @blueprint_dir = quote(File.expand_path('.'))
27
+ @scm = SourceCodeManager.new(@parameters['TARFILE_DIR'],
28
+ @parameters['PATCH_DIR'])
29
+ end
30
+
31
+ def visit_commands(commands:)
32
+ write(blueprint: commands, location: cwd) do |script|
33
+ phase = commands.active_phase
34
+ restart_file = if phase
35
+ "#{commands.name}::#{phase.tr(' ', '_')}"
36
+ else
37
+ commands.name
38
+ end
39
+ render_restart_header(script, restart_file)
40
+ log = commands.logfile(commands.name, phase)
41
+ cmds = commands['commands'] || []
42
+ files = handle_file_directives(commands)
43
+ render_servicedirs(script: script,
44
+ dirs: commands['servicedir'],
45
+ pipelines: commands['service-pipeline'])
46
+ cmds = [files, cmds].flatten
47
+ cmds.each do |command|
48
+ render_command(script, command, log)
49
+ end
50
+ render_cfgrepo_trailer(script, commands, log)
51
+ render_restart_trailer(script, restart_file)
52
+ end
53
+ end
54
+
55
+ def visit_package(package:)
56
+ write(blueprint: package, location: cwd) do |script|
57
+ if (File.stat('/etc').gid == INSTALL_GID) &&
58
+ Process.uid.zero? &&
59
+ (ENV['LITBUILD_PKGUSR'] != 'false')
60
+ pkgusr = { 'name' => [package.name],
61
+ 'description' => package['full-name'] }
62
+ package.directives['package-user'] ||= [pkgusr]
63
+ end
64
+ if package.directives.include?('package-user')
65
+ render_package_user(package, script)
66
+ else
67
+ render_standard_package(package, script)
68
+ end
69
+ end
70
+ end
71
+
72
+ def visit_section(section:)
73
+ section_dir = File.join(cwd, section.name)
74
+ write(blueprint: section, location: cwd) do |script|
75
+ render_restart_header(script, section.name)
76
+ script.puts("cd $(dirname $0)/#{section.name}")
77
+ skip_line(script)
78
+ write_components(location: section_dir, script: script)
79
+ render_restart_trailer(script, section.name)
80
+ end
81
+ end
82
+
83
+ def write_sudoers
84
+ script_dir = @parameters['SCRIPT_DIR']
85
+ sudoers = sudoers_entries.sort
86
+ return if sudoers.empty?
87
+
88
+ full_path = File.join(script_dir, 'lb-sudoers')
89
+ File.open(full_path, 'w') do |f|
90
+ sudoers.each do |s|
91
+ f.puts(s)
92
+ end
93
+ end
94
+ end
95
+
96
+ def write_toplevel_script(target:)
97
+ script_dir = @parameters['SCRIPT_DIR']
98
+ return if @written[script_dir].empty?
99
+
100
+ script_name = File.join(script_dir, "#{target}.sh")
101
+ File.open(script_name, 'w') do |f|
102
+ f.puts("#!#{find_bash}/bash")
103
+ skip_line(f)
104
+ f.puts("trap 'echo UTTER FAILURE on line $LINENO' ERR")
105
+ f.puts('set -e -v')
106
+ skip_line(f)
107
+ write_components(location: script_dir, script: f)
108
+ f.puts('set +v')
109
+ f.puts('echo TOTAL SUCCESS')
110
+ end
111
+ FileUtils.chmod('ugo+x', script_name)
112
+ end
113
+
114
+ private
115
+
116
+ def write(blueprint:, location:, &block)
117
+ return if @all_written_targets.include?(blueprint.target_name)
118
+
119
+ FileUtils.mkdir_p(location)
120
+ script_name = format('%<count>02d-%<script>s',
121
+ count: @written[location].size,
122
+ script: blueprint.file_name + '.sh')
123
+ script_path = File.join(location, script_name)
124
+ File.open(script_path, 'w') do |f|
125
+ f.write(to_bash_script(blueprint, block))
126
+ end
127
+ FileUtils.chmod('ugo+x', script_path)
128
+ envcmds = environment_commands(blueprint).reject { |c| c =~ /^mkdir/ }
129
+ unless envcmds.empty?
130
+ envscript = File.join(location, "#{blueprint.file_name}_env.sh")
131
+ File.open(envscript, 'w') do |f|
132
+ envcmds.each { |cmd| f.puts(cmd) }
133
+ end
134
+ FileUtils.chmod('ugo+x', envscript)
135
+ end
136
+ @written[location] << script_name
137
+ @all_written_targets << blueprint.target_name
138
+ end
139
+
140
+ def to_bash_script(blueprint, block)
141
+ if blueprint.phases? && !blueprint.active_phase
142
+ raise(ParameterMissing,
143
+ "Phase must be set to render script for #{blueprint.name}")
144
+ end
145
+
146
+ script = StringIO.new
147
+ script.puts("#!#{find_bash}/bash")
148
+ skip_line(script)
149
+ script.puts("export LB_BLUEPRINT_DIR=#{blueprint_dir}")
150
+ script.puts("trap 'echo #{blueprint.failure_line} on line $LINENO' ERR")
151
+ script.puts('set -e -v')
152
+ skip_line(script)
153
+ env_cmds = environment_commands(blueprint)
154
+ unless env_cmds.empty?
155
+ env_cmds.each do |cmd|
156
+ script.puts(cmd)
157
+ end
158
+ skip_line(script)
159
+ end
160
+ block.call(script)
161
+ skip_line(script)
162
+ script.puts('set +v')
163
+ script.puts("echo #{blueprint.success_line}")
164
+ script.string
165
+ end
166
+
167
+ def write_components(location:, script:)
168
+ @written[location].map do |target|
169
+ script.puts("echo \"At $(date): Beginning #{target}:\"")
170
+ script.puts("./#{target}")
171
+ end
172
+ end
173
+
174
+ def find_bash
175
+ ENV['PATH'].split(':').detect { |pe| File.exist?(File.join(pe, 'bash')) }
176
+ end
177
+
178
+ def handle_file_directives(blueprint)
179
+ blueprint.files.keys.sort.map do |name|
180
+ accum = StringIO.new
181
+ accum.puts("cat > #{name} <<'LBEOF'")
182
+ accum.puts(blueprint.files[name].string)
183
+ accum.puts('LBEOF')
184
+ accum.string
185
+ end
186
+ end
187
+
188
+ def sudoers_entries
189
+ uid = `id -u`.strip
190
+ return [] if uid == '0'
191
+
192
+ raw_sudo_cmds = @all_commands.select do |c|
193
+ c =~ /sudo / && c.lines.size < 2
194
+ end.uniq
195
+ sudo_cmds = raw_sudo_cmds.map do |c|
196
+ command = c.sub(/.*sudo /, '')
197
+ unless command.start_with?('/')
198
+ base_command = command.sub(/ .*$/, '')
199
+ raise(RelativeSudo,
200
+ "Need absolute path for \"#{base_command}\" in \"#{c}\"")
201
+ end
202
+ sudoed_cmd = c.sub(/^.*sudo (.*)$/, '\\1')
203
+ sudoed_cmd = sudoed_cmd.sub(/;.*$/, '') if sudoed_cmd.match?(/;/)
204
+ sudoed_cmd.gsub(/([,:=\\])/, '\\\\\1')
205
+ end
206
+ username = `id -un`.strip
207
+ sudo_cmds.map do |c|
208
+ "#{username} ALL = NOPASSWD: #{c}"
209
+ end
210
+ end
211
+
212
+ def render_standard_package(package, script)
213
+ script.puts("export LB_SOURCE_DIR=#{quote(source_dir(package))}")
214
+ phase = package.active_phase
215
+ restart_file = if phase
216
+ "#{package.name}::#{phase.tr(' ', '_')}"
217
+ else
218
+ package.name
219
+ end
220
+ render_restart_header(script, restart_file, package.version)
221
+ render_prepare_source(package, script)
222
+ build_location = dir_for_build(package)
223
+
224
+ if package.build_dir
225
+ render_command(script, "mkdir -p #{build_location}", '/dev/null')
226
+ skip_line(script)
227
+ end
228
+
229
+ Package::BUILD_STAGES.each do |stage|
230
+ log = package.logfile(stage, phase)
231
+ render_in_dir(script, build_location) do
232
+ package.build_commands(stage).each do |command|
233
+ render_command(script, command, log)
234
+ end
235
+ end
236
+ end
237
+ render_restart_trailer(script, restart_file, package.version)
238
+ end
239
+
240
+ def dir_for_build(package)
241
+ if (dir = package.build_dir)
242
+ File.expand_path(File.join(source_dir(package), dir))
243
+ else
244
+ source_dir(package)
245
+ end
246
+ end
247
+
248
+ def render_prepare_source(package, script)
249
+ log = package.logfile('prepare')
250
+ render_command(script, "if [ ! -d #{source_dir(package)} ]; then", log)
251
+ render_command(script, "mkdir -p #{work_site}", log)
252
+ render_in_dir(script, work_site) do
253
+ render_command(script, @scm.untar_command_for(package), log)
254
+ end
255
+ render_in_tree_sources(package, script, log)
256
+ render_patch_commands(package, script, log)
257
+ render_command(script, 'fi', log)
258
+ end
259
+
260
+ def work_site
261
+ @parameters['WORK_SITE']
262
+ end
263
+
264
+ def source_dir(package)
265
+ File.join(work_site, package.name_and_version)
266
+ end
267
+
268
+ def render_in_tree_sources(package, script, log)
269
+ intree_commands = @scm.intree_untar_commands_for(package)
270
+ return if intree_commands.empty?
271
+
272
+ render_in_dir(script, source_dir(package)) do
273
+ intree_commands.each do |cmd|
274
+ render_command(script, cmd, log)
275
+ end
276
+ end
277
+ end
278
+
279
+ def render_patch_commands(package, script, log)
280
+ patch_commands = @scm.patch_commands_for(package)
281
+ return if patch_commands.empty?
282
+
283
+ render_in_dir(script, source_dir(package)) do
284
+ patch_commands.each do |command|
285
+ render_command(script, command, log)
286
+ end
287
+ end
288
+ end
289
+
290
+ def render_package_user(package, script)
291
+ pkgusr = package.pkgusr_name
292
+ log = package.logfile('pkgusr')
293
+ pkgusr_dir = "~#{pkgusr}"
294
+ package.directives['configuration-files'] ||= []
295
+ package.directives['configuration-files'] << "~#{pkgusr}/options"
296
+ restart_file = ".#{package.active_phase || 'default'}"
297
+ render_restart_header(script, restart_file, package.version, pkgusr_dir)
298
+ pkgusr_srcdir = File.join(pkgusr_dir, package.name_and_version)
299
+ render_add_package_user(package, script, log)
300
+ @scm.copy_source_files_commands(package).each do |cp_command|
301
+ render_command(script, cp_command, log)
302
+ end
303
+ script.puts("export LB_SOURCE_DIR=#{quote(pkgusr_srcdir)}")
304
+ skip_line(script)
305
+ render_command(script, generate_options_file(package, pkgusr_srcdir), log)
306
+ skip_line(script)
307
+ pre_build(package, script, log)
308
+ render_command(script,
309
+ "su -l -c /usr/libexec/pkgusr/build #{pkgusr}",
310
+ log)
311
+ post_build(package, script, log)
312
+ render_restart_trailer(script, restart_file, package.version, pkgusr_dir)
313
+ end
314
+
315
+ def pre_build(package, script, log)
316
+ render_servicedirs(script: script,
317
+ dirs: package['servicedir'],
318
+ pipelines: package['service-pipeline'])
319
+ return unless (bbar = package['before-build-as-root'])
320
+
321
+ bbar.each { |cmd| render_command(script, cmd, log) }
322
+ end
323
+
324
+ def post_build(package, script, log)
325
+ if (aiar = package['after-install-as-root'])
326
+ aiar.each { |cmd| render_command(script, cmd, log) }
327
+ end
328
+ render_cfgrepo_trailer(script, package, log)
329
+ render_command(script, 'set_install_dirs', log)
330
+ render_command(script, 'ldconfig', log)
331
+ end
332
+
333
+ def render_add_package_user(package, script, log)
334
+ pkgusr = package.value('package-user')
335
+ desc = pkgusr['description'].first || package.value('full_name')
336
+ render_command(script,
337
+ "add_package_user '#{desc}' #{package.pkgusr_name}",
338
+ log)
339
+ end
340
+
341
+ def generate_options_file(package, srcdir)
342
+ options = StringIO.new
343
+ options.puts("cat > ~#{package.pkgusr_name}/options <<'LBEOF'")
344
+ options.puts("export version=#{package.version}")
345
+ options.puts("export LB_SOURCE_DIR=#{quote(srcdir)}")
346
+ environment_commands(package).each { |cmd| options.puts(cmd) }
347
+ package.build_dir && options.puts("export build_dir=#{package.build_dir}")
348
+ render_intree_commands(package, options)
349
+ Package::BUILD_STAGES.each do |stage|
350
+ options.puts("function #{stage}_commands()\n{")
351
+ cmds = package.build_commands(stage)
352
+ if cmds.empty?
353
+ options.puts(':')
354
+ else
355
+ cmds.each { |cmd| options.puts(cmd) }
356
+ end
357
+ options.puts('}')
358
+ end
359
+ render_patches(package, options)
360
+ options.puts('LBEOF')
361
+ options.string
362
+ end
363
+
364
+ def render_intree_commands(package, options)
365
+ return if package.in_tree.empty?
366
+
367
+ options.puts('declare -A in_tree')
368
+ package.in_tree.sort.each do |basename, version, path|
369
+ options.puts("in_tree[#{basename}-#{version}]=#{path}")
370
+ end
371
+ end
372
+
373
+ def render_patches(package, options)
374
+ return if package.patch_files.empty?
375
+
376
+ options.puts('declare -a patches')
377
+ package.patch_files.each_with_index do |name, idx|
378
+ options.puts("patches[#{idx}]=#{name}")
379
+ end
380
+ end
381
+
382
+ def render_restart_header(script,
383
+ filename,
384
+ content = 'COMPLETE',
385
+ restart_dir = '"$LITBUILDDBDIR"')
386
+ path = "#{restart_dir}/#{filename}"
387
+ script.puts("if [ -d #{restart_dir} -a -w #{restart_dir} ]")
388
+ script.puts('then')
389
+ script.puts("if [ -f #{path} ]")
390
+ script.puts('then')
391
+ script.puts("grep -q '^#{content}$' #{path} && exit 0")
392
+ script.puts('fi')
393
+ script.puts('fi')
394
+ skip_line(script)
395
+ end
396
+
397
+ def render_restart_trailer(script,
398
+ filename,
399
+ content = 'COMPLETE',
400
+ restart_dir = '"$LITBUILDDBDIR"')
401
+ path = "#{restart_dir}/#{filename}"
402
+ skip_line(script)
403
+ script.puts("if [ -d #{restart_dir} -a -w #{restart_dir} ]")
404
+ script.puts('then')
405
+ script.puts("echo \"#{content}\" > #{path}")
406
+ script.puts('fi')
407
+ end
408
+
409
+ def render_command(script, command, log)
410
+ @all_commands << command
411
+ if command.match?(/>/)
412
+ # redirecting output of command, can't put stdout in log.
413
+ script.puts(command)
414
+ else
415
+ script.puts("#{command} >> #{log} 2>&1")
416
+ end
417
+ end
418
+
419
+ def render_in_dir(script, dir)
420
+ script.puts("pushd #{dir}")
421
+ yield
422
+ script.puts('popd')
423
+ end
424
+
425
+ def skip_line(script)
426
+ script.puts
427
+ end
428
+
429
+ def environment_commands(blueprint)
430
+ commands = []
431
+ if blueprint['environment']
432
+ env_directives = blueprint['environment']
433
+ merged_and_flattened = {}
434
+ env_directives.each do |env|
435
+ env.each { |key, val| merged_and_flattened[key] = val.first }
436
+ end
437
+ merged_and_flattened.each do |k, v|
438
+ commands << (v.empty? ? "unset #{k}" : "export #{k}=#{quote(v)}")
439
+ end
440
+ if merged_and_flattened.key?('LITBUILDDBDIR') &&
441
+ !merged_and_flattened['LITBUILDDBDIR'].empty?
442
+ commands << "mkdir -p #{merged_and_flattened['LITBUILDDBDIR']}"
443
+ end
444
+ end
445
+ commands
446
+ end
447
+
448
+ def render_servicedirs(script:, dirs:, pipelines:)
449
+ return unless dirs || pipelines
450
+
451
+ script.puts('pushd /etc/s6-rc/source')
452
+ skip_line(script)
453
+ pipelines&.each { |pipeline| render_service_pipeline(script, pipeline) }
454
+ dirs&.each { |sdir| render_service_dir(script, sdir) }
455
+ script.puts('popd')
456
+ skip_line(script)
457
+ end
458
+
459
+ def render_service_pipeline(script, spipe)
460
+ pname = spipe['name'].first
461
+ spipe['bundle']&.each do |bundle|
462
+ script.puts("grep -q '^#{pname}$' #{bundle}/contents || " \
463
+ "echo #{pname} >> #{bundle}/contents")
464
+ end
465
+ sdirs = spipe['servicedirs']
466
+ sdirs.each_with_index do |sdir, i|
467
+ render_service_dir(script, sdir)
468
+ if i == sdirs.size - 1
469
+ script.puts("echo #{pname} > #{sdir['name'].first}/pipeline-name")
470
+ end
471
+ if i < sdirs.size - 1
472
+ next_svc = sdirs[i + 1]['name'].first
473
+ script.puts("echo #{next_svc} > #{sdir['name'].first}/producer-for")
474
+ end
475
+ unless i.zero?
476
+ prev_svc = sdirs[i - 1]['name'].first
477
+ script.puts("echo #{prev_svc} > #{sdir['name'].first}/consumer-for")
478
+ end
479
+ skip_line(script)
480
+ end
481
+ end
482
+
483
+ def render_service_dir(script, sdir)
484
+ sd = ServiceDir.new(sdir)
485
+ if sd.bundle
486
+ script.puts("grep -q '^#{sd.name}$' #{sd.bundle}/contents || " \
487
+ "echo #{sd.name} >> #{sd.bundle}/contents")
488
+ end
489
+ script.puts("mkdir -p #{sd.name}")
490
+ sd.oneline_files.keys.sort.each do |fn|
491
+ script.puts("echo #{sd.oneline_files[fn]} > #{sd.name}/#{fn}")
492
+ end
493
+ multiline = sd.multiline_files
494
+ deps = sd.dependencies
495
+ multiline['dependencies'] = deps.join("\n") unless deps.empty?
496
+ multiline.keys.sort.each do |filename|
497
+ # I always terminate here documents in litbuild-generated
498
+ # scripts with "LBEOF", partly because I'm thinking "Litbuild
499
+ # End-of-File" but also partly because it makes me think of
500
+ # Shia LaBoeuf, and then I remember the Rob Cantor song of
501
+ # that name and giggle.
502
+ script.puts("cat > #{sd.name}/#{filename} <<'LBEOF'")
503
+ script.puts(multiline[filename])
504
+ script.puts('LBEOF')
505
+ end
506
+ env = sd.env
507
+ unless env.empty?
508
+ script.puts("mkdir -p #{sd.name}/env")
509
+ env.keys.sort.each do |envvar|
510
+ script.puts("echo '#{env[envvar]}' > #{sd.name}/env/#{envvar}")
511
+ end
512
+ end
513
+ skip_line(script)
514
+ end
515
+
516
+ def render_cfgrepo_trailer(script, blueprint, log)
517
+ cfgs = blueprint['configuration-files']
518
+ return unless cfgs
519
+
520
+ render_command(script, "cfggit add #{cfgs.join(' ')}", log)
521
+ render_command(script, 'cfggit stageall', log)
522
+ bp = "#{blueprint.class.name.split('::').last} #{blueprint.name}"
523
+ cmd = "cfggit as-default -m 'Configuration files for #{bp}'"
524
+ render_command(script, cmd, log)
525
+ end
526
+
527
+ # quote a string suitably for a bash script
528
+ def quote(value)
529
+ # if value contains embedded backslash and newline characters, get
530
+ # rid of those first.
531
+ oneline = value.gsub(/\\\n /m, '')
532
+
533
+ # now, if the value contains ' -- backslash-quote everything (incl spaces)
534
+ # if the value contains " -- single-quote the whole thing
535
+ # if the value contains other punctuation -- double-quote the whole thing
536
+ if oneline.match?(/'/)
537
+ oneline.gsub(/([\\ "'`$])/, '\\\\\1')
538
+ elsif oneline.match?(/"/)
539
+ "'#{oneline}'"
540
+ elsif oneline.match?(/[\\ `]/)
541
+ "\"#{oneline}\""
542
+ else
543
+ oneline
544
+ end
545
+ end
546
+ end
547
+ end