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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Litbuild
4
+ class LogfileNamer
5
+ def initialize(log_dir)
6
+ @log_dir = log_dir
7
+ @counter = 0
8
+ end
9
+
10
+ def path_for(blueprint, phase = nil, stage = nil)
11
+ count = format('%03d', @counter)
12
+ @counter += 1
13
+ file_name = build_name(count, blueprint, phase, stage)
14
+ File.join(@log_dir, file_name)
15
+ end
16
+
17
+ protected
18
+
19
+ def build_name(*elements)
20
+ elements.uniq.compact.join('-') + '.log'
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/errors'
4
+ require 'litbuild/visitor'
5
+
6
+ module Litbuild
7
+ ##
8
+ # This is a base class for Visitors that can be sent to multiple Parts
9
+ # and/or Appendices. It is appropriate for Visitors like
10
+ # AsciiDocVisitors.
11
+ class MultiPartVisitor < Visitor
12
+ def initialize(directory:)
13
+ super
14
+ @other_parts = []
15
+ @appendices = []
16
+ end
17
+
18
+ def visit_part(part)
19
+ @other_parts << part.name
20
+ part.accept(visitor: self)
21
+ end
22
+
23
+ # We need to call some methods on the appendix blueprint, so we
24
+ # store the entire appendix rather than just the name.
25
+ def visit_appendix(appendix)
26
+ @appendices << appendix
27
+ appendix.accept(visitor: self)
28
+ end
29
+
30
+ protected
31
+
32
+ attr_reader :other_parts
33
+ attr_reader :appendices
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/blueprint'
4
+
5
+ module Litbuild
6
+ class Narrative < Blueprint
7
+ def self.directory_name
8
+ 'narratives'
9
+ end
10
+
11
+ def accept(visitor:)
12
+ super
13
+ visitor.visit_narrative(narrative: self)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/blueprint'
4
+
5
+ module Litbuild
6
+ class Package < Blueprint
7
+ BUILD_STAGES = %w[configure compile test install].freeze
8
+ STAGE_DIRECTIVES = BUILD_STAGES.map { |stg| "#{stg}-commands" }.freeze
9
+ STAGE_DEFAULTS = { 'configure-commands' => './configure --prefix=/usr',
10
+ 'compile-commands' => 'make',
11
+ 'test-commands' => 'make check',
12
+ 'install-commands' => 'make install' }.freeze
13
+ NONE = '(none)'
14
+
15
+ def self.directory_name
16
+ 'packages'
17
+ end
18
+
19
+ def initialize(text:)
20
+ super
21
+ if phases?
22
+ phases.each do |p|
23
+ synthesize_default_commands(@phase_directives[p], @phase_grafs[p])
24
+ end
25
+ else
26
+ synthesize_default_commands(@base_directives, @base_grafs)
27
+ end
28
+ end
29
+
30
+ def accept(visitor:)
31
+ super
32
+ visitor.visit_package(package: self)
33
+ end
34
+
35
+ def version
36
+ self['version'].first
37
+ end
38
+
39
+ def tar_files_needed
40
+ ["#{name_and_version}.tar"] + in_tree_packages
41
+ end
42
+
43
+ def in_tree
44
+ in_tree_files = self['in-tree-sources'] || []
45
+ in_tree_files.map(&:split)
46
+ end
47
+
48
+ def in_tree_packages
49
+ in_tree.map { |name, version, _path| "#{name}-#{version}.tar" }
50
+ end
51
+
52
+ def patch_files
53
+ patches = self['patches'] || []
54
+ patches.map { |basename| "#{name_and_version}-#{basename}.patch" }
55
+ end
56
+
57
+ def build_dir
58
+ value('build-dir')
59
+ end
60
+
61
+ def name_and_version
62
+ "#{name}-#{version}"
63
+ end
64
+
65
+ def build_commands(stage)
66
+ commands = directives["#{stage}-commands"]
67
+ return [] if commands == [NONE]
68
+
69
+ commands
70
+ end
71
+
72
+ def pkgusr_name
73
+ value('package-user')['name'].first
74
+ end
75
+
76
+ def header_text
77
+ name
78
+ end
79
+
80
+ ##
81
+ # Return the URL directive of the specified type, or `(unknown)` if
82
+ # there is none.
83
+ def url(url_type)
84
+ directives["#{url_type}-url"]&.first || '(unknown)'
85
+ end
86
+
87
+ private
88
+
89
+ # This is called from the constructor, be careful if modifying it.
90
+ def synthesize_default_commands(directives, grafs)
91
+ to_add = {}
92
+ STAGE_DIRECTIVES.each do |sdir|
93
+ unless directives.include?(sdir)
94
+ directives[sdir] = [STAGE_DEFAULTS[sdir]]
95
+ to_add[sdir] = [STAGE_DEFAULTS[sdir]]
96
+ end
97
+ end
98
+ add_to_narrative(grafs, to_add) unless to_add.empty?
99
+ end
100
+
101
+ # We have default commands to add to the narrative. Where do they
102
+ # go?
103
+ #
104
+ # The logic here is a little thorny (see comments embeddeed in this
105
+ # method for a description). For any occasion where it does not have
106
+ # a pleasing result, the blueprint can simply be written to have
107
+ # explicit directives rather than using defaults.
108
+ def add_to_narrative(grafs, to_add)
109
+ # *install* always goes at the end.
110
+ stages_to_consider = STAGE_DIRECTIVES.reverse
111
+ stg = stages_to_consider.shift
112
+ grafs << { stg => to_add[stg] } if to_add.key?(stg)
113
+
114
+ # For other stages...
115
+ until stages_to_consider.empty?
116
+ stg = stages_to_consider.shift
117
+ next unless to_add.key?(stg)
118
+
119
+ # if there is a previous stage *and* directives for that stage
120
+ # are present in the narrative, the default version goes right
121
+ # *after* the *last* of those directives;
122
+ previous_stage = stages_to_consider[0]
123
+ if previous_stage
124
+ last_idx = grafs.rindex do |graf|
125
+ graf.respond_to?(:key) && graf.key?(previous_stage)
126
+ end
127
+ if last_idx
128
+ grafs.insert(last_idx + 1, stg => to_add[stg])
129
+ next
130
+ end
131
+ end
132
+
133
+ # otherwise, the default version goes right *before* the *first
134
+ # directive of the next stage* (which will always be present).
135
+ next_stage = STAGE_DIRECTIVES[STAGE_DIRECTIVES.index(stg) + 1]
136
+ first_idx = grafs.index do |graf|
137
+ graf.respond_to?(:key) && graf.key?(next_stage)
138
+ end
139
+ grafs.insert(first_idx, stg => to_add[stg])
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/blueprint'
4
+ require 'litbuild/errors'
5
+ require 'litbuild/multi_part_visitor'
6
+
7
+ module Litbuild
8
+ class Section < Blueprint
9
+ def self.directory_name
10
+ 'sections'
11
+ end
12
+
13
+ def accept(visitor:)
14
+ super
15
+ visitor.in_subdirectory(name) do
16
+ components.each do |bp|
17
+ bp.accept(visitor: visitor)
18
+ end
19
+ end
20
+ visitor.visit_section(section: self)
21
+ return unless visitor.is_a?(MultiPartVisitor)
22
+
23
+ self['other-parts']&.each do |part|
24
+ part_bp = @bp_library.blueprint_for(target: part)
25
+ visitor.visit_part(part_bp)
26
+ end
27
+ self['appendices']&.each do |app|
28
+ app_bp = @bp_library.blueprint_for(target: app)
29
+ visitor.visit_appendix(app_bp)
30
+ end
31
+ end
32
+
33
+ def components
34
+ return @components if @components
35
+
36
+ blueprints = self['blueprints'].clone || []
37
+ if $DEBUG
38
+ warn("Explicit blueprints for section #{name}:" \
39
+ " #{blueprints.join(', ')}")
40
+ end
41
+ blueprints << automatic_inclusions
42
+ deduped = blueprints.flatten.uniq
43
+ @components = deduped.map do |bp|
44
+ @bp_library.blueprint_for(target: bp)
45
+ end
46
+ @components
47
+ end
48
+
49
+ def automatic_inclusions
50
+ auto_adds = []
51
+ @bp_library.blueprints.each_value do |bp|
52
+ if bp.phases.include?(name)
53
+ auto_adds << "#{bp.name}::#{name}"
54
+ elsif bp['in-section']&.include?(name)
55
+ auto_adds << bp.name
56
+ end
57
+ end
58
+ if $DEBUG
59
+ warn("Automatically-added blueprints for section #{name}:" \
60
+ " #{auto_adds.sort.join(', ')}")
61
+ end
62
+ auto_adds.sort
63
+ end
64
+
65
+ def success_line
66
+ 'HUGE SUCCESS'
67
+ end
68
+
69
+ def failure_line
70
+ 'DISMAL FAILURE'
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Litbuild
4
+ # Service definition directories are a bit complicated. This is just
5
+ # a helper class that knows how to deal with them.
6
+ class ServiceDir
7
+ def initialize(svcdef)
8
+ @svcdef = svcdef
9
+ end
10
+
11
+ def name
12
+ @svcdef['name'].first
13
+ end
14
+
15
+ def bundle
16
+ @svcdef['bundle']&.first
17
+ end
18
+
19
+ def type
20
+ return @svcdef['type'].first if @svcdef.key?('type')
21
+ return 'longrun' if @svcdef.key?('run')
22
+ return 'oneshot' if @svcdef.key?('up')
23
+
24
+ raise(InvalidDirective, "servicedir #{name} must specify type")
25
+ end
26
+
27
+ def oneline_files
28
+ file_contents = { 'type' => type }
29
+ %w[down-signal notification-fd].each do |fn|
30
+ file_contents[fn] = @svcdef[fn].first if @svcdef.key?(fn)
31
+ end
32
+ file_contents
33
+ end
34
+
35
+ def dependencies
36
+ @svcdef['dependencies'] || []
37
+ end
38
+
39
+ def multiline_files
40
+ file_contents = {}
41
+ %w[up down run].each do |fn|
42
+ next unless @svcdef.key?(fn)
43
+
44
+ file_contents[fn] = @svcdef[fn]
45
+ end
46
+ file_contents
47
+ end
48
+
49
+ def env
50
+ return [] unless @svcdef.key?('env')
51
+
52
+ flattened = {}
53
+ @svcdef['env'].first.each do |variable, value|
54
+ flattened[variable] = value.first
55
+ end
56
+ flattened
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/errors'
4
+
5
+ module Litbuild
6
+ ##
7
+ # Find all of the tar files and patch files, possibly compressed,
8
+ # present in any of the directories passed to the constructor or any
9
+ # direct subdirectories of those directories. Given a package
10
+ # blueprint, provide suitable commands to unpack tar files and in-tree
11
+ # source tarfiles, apply patch files, or copy those files to a package
12
+ # user home directory.
13
+ #
14
+ # Note, SourceCodeManager does not recurse into additional levels of
15
+ # subdirectories -- it turns out that makes the test suite really
16
+ # slow.
17
+ class SourceCodeManager
18
+ def initialize(*dirs)
19
+ all_pkgfiles = dirs.map do |d|
20
+ Dir.glob("#{d}/*.tar*") +
21
+ Dir.glob("#{d}/*/*.tar*") +
22
+ Dir.glob("#{d}/*.patch*") +
23
+ Dir.glob("#{d}/*/*.patch*")
24
+ end.flatten
25
+ abs_paths = all_pkgfiles.map { |f| File.expand_path(f) }
26
+ @available_files = abs_paths.sort.uniq
27
+ end
28
+
29
+ def untar_command_for(package)
30
+ unpack_tar(package.name_and_version)
31
+ end
32
+
33
+ def intree_untar_commands_for(package)
34
+ commands = []
35
+ package.in_tree.each do |basename, version, path|
36
+ intree = "#{basename}-#{version}"
37
+ commands << unpack_tar(intree)
38
+ if path
39
+ commands << "mkdir -p #{File.dirname(path)}"
40
+ commands << "mv #{intree} #{path}"
41
+ else
42
+ commands << "mv #{intree} #{basename}"
43
+ end
44
+ end
45
+ commands
46
+ end
47
+
48
+ def patch_commands_for(package)
49
+ return [] unless package.patch_files
50
+
51
+ package.patch_files.map do |patch_file|
52
+ full_fn = find_file(patch_file)
53
+ "#{decompress_command(full_fn)} < #{full_fn} | patch -p1"
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Package Users expect to have tarfiles for the top-level package
59
+ # and any in-tree packages in their `src` directory, and patches in
60
+ # their `patches` directory. This produces commands that copy needed
61
+ # files from the TARFILE_DIR and PATCH_DIR to those directories, if
62
+ # not already present there.
63
+ def copy_source_files_commands(package)
64
+ pkgusr = pkgusr_name(package)
65
+ mkdir_commands = %w[src patches].map do |dir|
66
+ "mkdir -p ~#{pkgusr}/#{dir}"
67
+ end
68
+ copy_commands = []
69
+ package.tar_files_needed.each do |filename|
70
+ unless pkgusr_file_available?(package, filename)
71
+ copy_commands << "cp #{find_tarfile(filename)} ~#{pkgusr}/src"
72
+ end
73
+ end
74
+ package.patch_files.each do |filename|
75
+ unless pkgusr_file_available?(package, filename)
76
+ copy_commands << "cp #{find_file(filename)} ~#{pkgusr}/patches"
77
+ end
78
+ end
79
+ mkdir_commands + copy_commands.sort
80
+ end
81
+
82
+ private
83
+
84
+ def pkgusr_file_available?(package, filename)
85
+ pkghome = homedir(package)
86
+ return false unless pkghome
87
+
88
+ dir = filename.match?(/.tar$/) ? 'src' : 'patches'
89
+ possible_names = DECOMPRESSORS.keys.map { |ext| "#{filename}.#{ext}" }
90
+ possible_names << filename
91
+ possible_names.any? { |f| File.exist?(File.join(pkghome, dir, f)) }
92
+ end
93
+
94
+ def find_file(filename)
95
+ @available_files.detect { |f| /#{filename}/ =~ f } ||
96
+ raise(Litbuild::MissingSource,
97
+ "File #{filename} is needed but not available.")
98
+ end
99
+
100
+ def find_tarfile(filename)
101
+ filename = "#{filename}.tar" unless filename.end_with?('.tar')
102
+ find_file(filename)
103
+ end
104
+
105
+ def unpack_tar(basename)
106
+ tarfile = find_tarfile(basename)
107
+ decompress_opt = tar_decompress_opt(tarfile)
108
+ "tar -x #{decompress_opt} -f #{tarfile}"
109
+ end
110
+
111
+ DECOMPRESSORS = { 'gz' => 'gzip', 'bz2' => 'bzip2', 'lzma' => 'lzma',
112
+ 'lz' => 'lzip', 'xz' => 'xz' }.freeze
113
+
114
+ def decompressor_for_file(file)
115
+ ext = DECOMPRESSORS.keys.find { |e| file =~ /#{e}$/ }
116
+ ext ? DECOMPRESSORS[ext] : nil
117
+ end
118
+
119
+ def decompress_command(filename)
120
+ decompressor = decompressor_for_file(filename)
121
+ decompressor ? "#{decompressor} -d" : 'cat'
122
+ end
123
+
124
+ def tar_decompress_opt(tarfile)
125
+ decompressor = decompressor_for_file(tarfile)
126
+ decompressor ? "--use-compress-program=#{decompressor}" : ''
127
+ end
128
+
129
+ def pkgusr_name(package)
130
+ pkgusr = package['package-user'].first
131
+ name = pkgusr['name'].first
132
+ raise(InvalidDirective, 'package-user missing name') unless name
133
+
134
+ name
135
+ end
136
+
137
+ def homedir(package)
138
+ Dir.home(pkgusr_name(package))
139
+ rescue ArgumentError
140
+ nil
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'litbuild/visitor'
4
+
5
+ module Litbuild
6
+ # This is a simple Visitor that just accumulates the files needed to
7
+ # build a set of Pacakge blueprints.
8
+ class SourceFilesVisitor < Visitor
9
+ def initialize
10
+ super
11
+ @files = []
12
+ end
13
+
14
+ def visit_package(package:)
15
+ @files << files_needed(package)
16
+ end
17
+
18
+ def files_needed(pkg)
19
+ ["#{pkg.name_and_version}.tar"] + pkg.patch_files + pkg.in_tree_packages
20
+ end
21
+
22
+ def files
23
+ @files.compact.flatten.sort.uniq
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'litbuild/visitor'
4
+
5
+ module Litbuild
6
+ # This is a simple Visitor that just accumulates the best download URL
7
+ # for a set of Package blueprints.
8
+ class UrlVisitor < Visitor
9
+ def initialize
10
+ super
11
+ @urls = []
12
+ end
13
+
14
+ def visit_package(package:)
15
+ @urls << download_url(package)
16
+ end
17
+
18
+ def download_url(pkg)
19
+ url = pkg.directives['download-url'] ||
20
+ pkg.directives['project-url'] ||
21
+ pkg.directives['scm-url']
22
+ url ? url.first : "#{name}: no download URL known"
23
+ end
24
+
25
+ def urls
26
+ @urls.compact.flatten.sort.uniq
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Litbuild
4
+ VERSION = '1.0.1'
5
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: false
2
+
3
+ module Litbuild
4
+ ##
5
+ # This is the base class for Litubild Visitors; it defines a
6
+ # no-operation Visitor. Each Visitor subclass encapsulates one of the
7
+ # operations that can be performed on a Blueprint along with its
8
+ # dependencies and components (and along with the dependencies and
9
+ # components of those Blueprints and so on).
10
+ class Visitor
11
+ # The main Visitor classes (bash script, asciidoc) write files to
12
+ # specific directories, and need to be able to switch to
13
+ # subdirectories tmeporarily; so Litbuild::Visitor has some
14
+ # functionality to support that.
15
+ #
16
+ # The default '/' value is just to avoid nil checking for Visitors
17
+ # that don't need this feature.
18
+ def initialize(directory: '/')
19
+ @dir_stack = [directory]
20
+ end
21
+
22
+ def in_subdirectory(subdir)
23
+ @dir_stack.push(File.join(cwd, subdir))
24
+ yield
25
+ @dir_stack.pop
26
+ end
27
+
28
+ def visit_commands(commands:); end
29
+
30
+ def visit_narrative(narrative:); end
31
+
32
+ def visit_package(package:); end
33
+
34
+ def visit_section(section:); end
35
+
36
+ protected
37
+
38
+ def cwd
39
+ @dir_stack.last
40
+ end
41
+ end
42
+ end
data/lib/litbuild.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'litbuild/blueprint'
4
+ require 'litbuild/blueprint_library'
5
+ require 'litbuild/blueprint_parser'
6
+ require 'litbuild/commands'
7
+ require 'litbuild/driver'
8
+ require 'litbuild/errors'
9
+ require 'litbuild/logfile_namer'
10
+ require 'litbuild/narrative'
11
+ require 'litbuild/package'
12
+ require 'litbuild/section'
13
+ require 'litbuild/source_files_visitor'
14
+ require 'litbuild/url_visitor'
15
+ require 'litbuild/version'
16
+ require 'litbuild/visitor'
data.tar.gz.sig ADDED
Binary file