class Litbuild::BashScriptVisitor
This class writes bash scripts to directories, rooted at the SCRIPT_DIR directory. It adds a sequential number prefix on each script written to a directory, and ensures that no duplicate scripts are written: if a script for a specific blueprint (or blueprint phase) has already been written to any directory, BashScriptVisitor
will ignore subsequent requests to write that blueprint (phase).
Constants
- INSTALL_GID
- SUDOPATH
Attributes
blueprint_dir[R]
Public Class Methods
new(parameters:)
click to toggle source
Calls superclass method
# File lib/litbuild/bash_script_visitor.rb, line 20 def initialize(parameters:) @parameters = parameters super(directory: @parameters['SCRIPT_DIR']) @written = Hash.new { |hash, key| hash[key] = [] } @all_written_targets = [] @all_commands = [] @blueprint_dir = quote(File.expand_path('.')) @scm = SourceCodeManager.new(@parameters['TARFILE_DIR'], @parameters['PATCH_DIR']) end
Public Instance Methods
visit_commands(commands:)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 31 def visit_commands(commands:) write(blueprint: commands, location: cwd) do |script| phase = commands.active_phase restart_file = if phase "#{commands.name}::#{phase.tr(' ', '_')}" else commands.name end render_restart_header(script, restart_file) log = commands.logfile(commands.name, phase) cmds = commands['commands'] || [] files = handle_file_directives(commands) render_servicedirs(script: script, dirs: commands['servicedir'], pipelines: commands['service-pipeline']) cmds = [files, cmds].flatten cmds.each do |command| render_command(script, command, log) end render_cfgrepo_trailer(script, commands, log) render_restart_trailer(script, restart_file) end end
visit_package(package:)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 55 def visit_package(package:) write(blueprint: package, location: cwd) do |script| if (File.stat('/etc').gid == INSTALL_GID) && Process.uid.zero? && (ENV['LITBUILD_PKGUSR'] != 'false') pkgusr = { 'name' => [package.name], 'description' => package['full-name'] } package.directives['package-user'] ||= [pkgusr] end if package.directives.include?('package-user') render_package_user(package, script) else render_standard_package(package, script) end end end
visit_section(section:)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 72 def visit_section(section:) section_dir = File.join(cwd, section.name) write(blueprint: section, location: cwd) do |script| render_restart_header(script, section.name) FileUtils.mkdir_p(section_dir) script.puts("cd $(dirname $0)/#{section.name}") skip_line(script) write_components(location: section_dir, script: script) render_restart_trailer(script, section.name) end end
write_sudoers()
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 84 def write_sudoers script_dir = @parameters['SCRIPT_DIR'] sudoers = sudoers_entries.sort return if sudoers.empty? full_path = File.join(script_dir, 'lb-sudoers') File.open(full_path, 'w') do |f| sudoers.each do |s| f.puts(s) end end end
write_toplevel_script(target:)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 97 def write_toplevel_script(target:) script_dir = @parameters['SCRIPT_DIR'] return if @written[script_dir].empty? script_name = File.join(script_dir, "#{target}.sh") File.open(script_name, 'w') do |f| f.puts("#!#{find_bash}/bash") skip_line(f) f.puts("trap 'echo UTTER FAILURE on line $LINENO' ERR") f.puts('set -e -v') skip_line(f) write_components(location: script_dir, script: f) f.puts('set +v') f.puts('echo TOTAL SUCCESS') end FileUtils.chmod('ugo+x', script_name) end
Private Instance Methods
convert_to_absolute(command)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 198 def convert_to_absolute(command) return command if command.start_with?('/') cmd_tokens = command.split program = cmd_tokens.shift possible_paths = SUDOPATH.map { |dir| File.join(dir, program) } fullpath = possible_paths.detect { |path| File.exist?(path) } unless fullpath msg = "Program #{program}, run via sudo, cannot be found in " \ 'any of the standard directories.' raise(SudoProgramNotFound, msg) end ([fullpath] + cmd_tokens).join(' ') end
dir_for_build(package)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 259 def dir_for_build(package) if (dir = package.build_dir) File.expand_path(File.join(source_dir(package), dir)) else source_dir(package) end end
environment_commands(blueprint)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 449 def environment_commands(blueprint) commands = [] if blueprint['environment'] env_directives = blueprint['environment'] merged_and_flattened = {} env_directives.each do |env| env.each { |key, val| merged_and_flattened[key] = val.first } end merged_and_flattened.each do |k, v| commands << (v.empty? ? "unset #{k}" : "export #{k}=#{quote(v)}") end if merged_and_flattened.key?('LITBUILDDBDIR') && !merged_and_flattened['LITBUILDDBDIR'].empty? commands << "mkdir -p #{merged_and_flattened['LITBUILDDBDIR']}" end end commands end
find_bash()
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 175 def find_bash ENV['PATH'].split(':').detect { |pe| File.exist?(File.join(pe, 'bash')) } end
generate_options_file(package, srcdir)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 360 def generate_options_file(package, srcdir) options = StringIO.new options.puts("cat > ~#{package.pkgusr_name}/options <<'LBEOF'") options.puts("export version=#{package.version}") options.puts("export LB_SOURCE_DIR=#{quote(srcdir)}") environment_commands(package).each { |cmd| options.puts(cmd) } package.build_dir && options.puts("export build_dir=#{package.build_dir}") render_intree_commands(package, options) Package::BUILD_STAGES.each do |stage| options.puts("function #{stage}_commands()\n{") cmds = package.build_commands(stage) if cmds.empty? options.puts(':') else cmds.each { |cmd| options.puts(cmd) } end options.puts('}') end render_patches(package, options) options.puts('LBEOF') options.string end
handle_file_directives(blueprint)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 179 def handle_file_directives(blueprint) blueprint.files.keys.sort.map do |name| accum = StringIO.new accum.puts("cat > #{name} <<'LBEOF'") accum.puts(blueprint.files[name].string) accum.puts('LBEOF') accum.string end end
post_build(package, script, log)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 343 def post_build(package, script, log) if (aiar = package['after-install-as-root']) aiar.each { |cmd| render_command(script, cmd, log) } end render_cfgrepo_trailer(script, package, log) render_command(script, 'set_install_dirs', log) render_command(script, 'ldconfig', log) end
pre_build(package, script, log)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 334 def pre_build(package, script, log) render_servicedirs(script: script, dirs: package['servicedir'], pipelines: package['service-pipeline']) return unless (bbar = package['before-build-as-root']) bbar.each { |cmd| render_command(script, cmd, log) } end
quote(value)
click to toggle source
quote a string suitably for a bash script
# File lib/litbuild/bash_script_visitor.rb, line 548 def quote(value) # if value contains embedded backslash and newline characters, get # rid of those first. oneline = value.gsub(/\\\n /m, '') # now, if the value contains ' -- backslash-quote everything (incl spaces) # if the value contains " -- single-quote the whole thing # if the value contains other punctuation -- double-quote the whole thing if oneline.match?(/'/) oneline.gsub(/([\\ "'`$])/, '\\\\\1') elsif oneline.match?(/"/) "'#{oneline}'" elsif oneline.match?(/[\\ `]/) "\"#{oneline}\"" else oneline end end
render_add_package_user(package, script, log)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 352 def render_add_package_user(package, script, log) pkgusr = package.value('package-user') desc = pkgusr['description'].first || package.value('full_name') render_command(script, "add_package_user '#{desc}' #{package.pkgusr_name}", log) end
render_cfgrepo_trailer(script, blueprint, log)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 536 def render_cfgrepo_trailer(script, blueprint, log) cfgs = blueprint['configuration-files'] return unless cfgs render_command(script, "cfggit add #{cfgs.join(' ')}", log) render_command(script, 'cfggit stageall', log) bp = "#{blueprint.class.name.split('::').last} #{blueprint.name}" cmd = "cfggit as-default -m 'Configuration files for #{bp}'" render_command(script, cmd, log) end
render_command(script, command, log)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 428 def render_command(script, command, log) unfolded_command = command.gsub(/ ?\\\n */, ' ') @all_commands << unfolded_command if unfolded_command.match?(/>/) # redirecting output of command, can't put stdout in log. script.puts(unfolded_command) else script.puts("#{unfolded_command} >> #{log} 2>&1") end end
render_in_dir(script, dir) { || ... }
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 439 def render_in_dir(script, dir) script.puts("pushd #{dir}") yield script.puts('popd') end
render_in_tree_sources(package, script, log)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 287 def render_in_tree_sources(package, script, log) intree_commands = @scm.intree_untar_commands_for(package) return if intree_commands.empty? render_in_dir(script, source_dir(package)) do intree_commands.each do |cmd| render_command(script, cmd, log) end end end
render_intree_commands(package, options)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 383 def render_intree_commands(package, options) return if package.in_tree.empty? options.puts('declare -A in_tree') package.in_tree.sort.each do |basename, version, path| options.puts("in_tree[#{basename}-#{version}]=#{path}") end end
render_package_user(package, script)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 309 def render_package_user(package, script) pkgusr = package.pkgusr_name log = package.logfile('pkgusr') pkgusr_dir = "~#{pkgusr}" package.directives['configuration-files'] ||= [] package.directives['configuration-files'] << "~#{pkgusr}/options" restart_file = ".#{package.active_phase || 'default'}" render_restart_header(script, restart_file, package.version, pkgusr_dir) pkgusr_srcdir = File.join(pkgusr_dir, package.name_and_version) render_add_package_user(package, script, log) @scm.copy_source_files_commands(package).each do |cp_command| render_command(script, cp_command, log) end script.puts("export LB_SOURCE_DIR=#{quote(pkgusr_srcdir)}") skip_line(script) render_command(script, generate_options_file(package, pkgusr_srcdir), log) skip_line(script) pre_build(package, script, log) render_command(script, "su -l -c /usr/libexec/pkgusr/build #{pkgusr}", log) post_build(package, script, log) render_restart_trailer(script, restart_file, package.version, pkgusr_dir) end
render_patch_commands(package, script, log)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 298 def render_patch_commands(package, script, log) patch_commands = @scm.patch_commands_for(package) return if patch_commands.empty? render_in_dir(script, source_dir(package)) do patch_commands.each do |command| render_command(script, command, log) end end end
render_patches(package, options)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 392 def render_patches(package, options) return if package.patch_files.empty? options.puts('declare -a patches') package.patch_files.each_with_index do |name, idx| options.puts("patches[#{idx}]=#{name}") end end
render_prepare_source(package, script)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 267 def render_prepare_source(package, script) log = package.logfile('prepare') render_command(script, "if [ ! -d #{source_dir(package)} ]; then", log) render_command(script, "mkdir -p #{work_site}", log) render_in_dir(script, work_site) do render_command(script, @scm.untar_command_for(package), log) end render_in_tree_sources(package, script, log) render_patch_commands(package, script, log) render_command(script, 'fi', log) end
render_restart_header(script, filename, content = 'COMPLETE', restart_dir = '"$LITBUILDDBDIR"')
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 401 def render_restart_header(script, filename, content = 'COMPLETE', restart_dir = '"$LITBUILDDBDIR"') path = "#{restart_dir}/#{filename}" script.puts("if [ -d #{restart_dir} -a -w #{restart_dir} ]") script.puts('then') script.puts("if [ -f #{path} ]") script.puts('then') script.puts("grep -q '^#{content}$' #{path} && exit 0") script.puts('fi') script.puts('fi') skip_line(script) end
render_restart_trailer(script, filename, content = 'COMPLETE', restart_dir = '"$LITBUILDDBDIR"')
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 416 def render_restart_trailer(script, filename, content = 'COMPLETE', restart_dir = '"$LITBUILDDBDIR"') path = "#{restart_dir}/#{filename}" skip_line(script) script.puts("if [ -d #{restart_dir} -a -w #{restart_dir} ]") script.puts('then') script.puts("echo \"#{content}\" > #{path}") script.puts('fi') end
render_service_dir(script, sdir)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 503 def render_service_dir(script, sdir) sd = ServiceDir.new(sdir) if sd.bundle script.puts("grep -q '^#{sd.name}$' #{sd.bundle}/contents || " \ "echo #{sd.name} >> #{sd.bundle}/contents") end script.puts("mkdir -p #{sd.name}") sd.oneline_files.keys.sort.each do |fn| script.puts("echo #{sd.oneline_files[fn]} > #{sd.name}/#{fn}") end multiline = sd.multiline_files deps = sd.dependencies multiline['dependencies'] = deps.join("\n") unless deps.empty? multiline.keys.sort.each do |filename| # I always terminate here documents in litbuild-generated # scripts with "LBEOF", partly because I'm thinking "Litbuild # End-of-File" but also partly because it makes me think of # Shia LaBoeuf, and then I remember the Rob Cantor song of # that name and giggle. script.puts("cat > #{sd.name}/#{filename} <<'LBEOF'") script.puts(multiline[filename]) script.puts('LBEOF') end env = sd.env unless env.empty? script.puts("mkdir -p #{sd.name}/env") env.keys.sort.each do |envvar| script.puts("echo '#{env[envvar]}' > #{sd.name}/env/#{envvar}") end end skip_line(script) end
render_service_pipeline(script, spipe)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 479 def render_service_pipeline(script, spipe) pname = spipe['name'].first spipe['bundle']&.each do |bundle| script.puts("grep -q '^#{pname}$' #{bundle}/contents || " \ "echo #{pname} >> #{bundle}/contents") end sdirs = spipe['servicedirs'] sdirs.each_with_index do |sdir, i| render_service_dir(script, sdir) if i == sdirs.size - 1 script.puts("echo #{pname} > #{sdir['name'].first}/pipeline-name") end if i < sdirs.size - 1 next_svc = sdirs[i + 1]['name'].first script.puts("echo #{next_svc} > #{sdir['name'].first}/producer-for") end unless i.zero? prev_svc = sdirs[i - 1]['name'].first script.puts("echo #{prev_svc} > #{sdir['name'].first}/consumer-for") end skip_line(script) end end
render_servicedirs(script:, dirs:, pipelines:)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 468 def render_servicedirs(script:, dirs:, pipelines:) return unless dirs || pipelines script.puts('pushd /etc/s6-rc/source') skip_line(script) pipelines&.each { |pipeline| render_service_pipeline(script, pipeline) } dirs&.each { |sdir| render_service_dir(script, sdir) } script.puts('popd') skip_line(script) end
render_standard_package(package, script)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 231 def render_standard_package(package, script) script.puts("export LB_SOURCE_DIR=#{quote(source_dir(package))}") phase = package.active_phase restart_file = if phase "#{package.name}::#{phase.tr(' ', '_')}" else package.name end render_restart_header(script, restart_file, package.version) render_prepare_source(package, script) build_location = dir_for_build(package) if package.build_dir render_command(script, "mkdir -p #{build_location}", '/dev/null') skip_line(script) end Package::BUILD_STAGES.each do |stage| log = package.logfile(stage, phase) render_in_dir(script, build_location) do package.build_commands(stage).each do |command| render_command(script, command, log) end end end render_restart_trailer(script, restart_file, package.version) end
running_as_root()
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 189 def running_as_root uid = `id -u`.strip uid == '0' end
skip_line(script)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 445 def skip_line(script) script.puts end
source_dir(package)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 283 def source_dir(package) File.join(work_site, package.name_and_version) end
sudoers_entries()
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 213 def sudoers_entries return [] if running_as_root raw_sudo_cmds = @all_commands.select do |c| c =~ /sudo / && c.lines.size < 2 end.uniq sudo_cmds = raw_sudo_cmds.map do |c| sudoed_cmd = c.sub(/^.*sudo (.*)$/, '\\1') sudoed_cmd = sudoed_cmd.sub(/;.*$/, '') if sudoed_cmd.match?(/;/) sudoed_cmd = convert_to_absolute(sudoed_cmd) sudoed_cmd.gsub(/([,:=\\])/, '\\\\\1') end username = `id -un`.strip sudo_cmds.map do |c| "#{username} ALL = NOPASSWD: #{c}" end end
to_bash_script(blueprint, block)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 141 def to_bash_script(blueprint, block) if blueprint.phases? && !blueprint.active_phase raise(ParameterMissing, "Phase must be set to render script for #{blueprint.name}") end script = StringIO.new script.puts("#!#{find_bash}/bash") skip_line(script) script.puts("export LB_BLUEPRINT_DIR=#{blueprint_dir}") script.puts("trap 'echo #{blueprint.failure_line} on line $LINENO' ERR") script.puts('set -e -v') skip_line(script) env_cmds = environment_commands(blueprint) unless env_cmds.empty? env_cmds.each do |cmd| script.puts(cmd) end skip_line(script) end block.call(script) skip_line(script) script.puts('set +v') script.puts("echo #{blueprint.success_line}") script.string end
work_site()
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 279 def work_site @parameters['WORK_SITE'] end
write(blueprint:, location:, &block)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 117 def write(blueprint:, location:, &block) return if @all_written_targets.include?(blueprint.target_name) FileUtils.mkdir_p(location) script_name = format('%<count>02d-%<script>s', count: @written[location].size, script: blueprint.file_name + '.sh') script_path = File.join(location, script_name) File.open(script_path, 'w') do |f| f.write(to_bash_script(blueprint, block)) end FileUtils.chmod('ugo+x', script_path) envcmds = environment_commands(blueprint).reject { |c| c =~ /^mkdir/ } unless envcmds.empty? envscript = File.join(location, "env_#{blueprint.file_name}.sh") File.open(envscript, 'w') do |f| envcmds.each { |cmd| f.puts(cmd) } end FileUtils.chmod('ugo+x', envscript) end @written[location] << script_name @all_written_targets << blueprint.target_name end
write_components(location:, script:)
click to toggle source
# File lib/litbuild/bash_script_visitor.rb, line 168 def write_components(location:, script:) @written[location].map do |target| script.puts("echo \"At $(date): Beginning #{target}:\"") script.puts("./#{target}") end end