litbuild 1.0.9 → 1.0.14
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 +4 -4
- data/lib/litbuild/ascii_doc_visitor.rb +6 -3
- data/lib/litbuild/bash_script_visitor.rb +14 -5
- data/lib/litbuild/blueprint.rb +9 -0
- data/lib/litbuild/blueprint_parser.rb +31 -30
- data/lib/litbuild/package.rb +6 -1
- data/lib/litbuild/section.rb +4 -4
- data/lib/litbuild/source_code_manager.rb +34 -7
- data/lib/litbuild/string_indentation.rb +33 -0
- data/lib/litbuild/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f420862b3a3dd758ef05aed2b717defe89a48a34944b25b3af4042a0d47a749
|
4
|
+
data.tar.gz: bfb4c6364c57b9b9220d0bd43113f4a8b2ff0639b106b675a9e8af3c85982eab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22838c4497fdb70a5140507dcc0a10f511f9751bd36197ec3dc88a8c5e7bc6fda27b8678bd34f46ea7d21b9f06c944e52b3bcd7ffee00713d9aa34a56d168387
|
7
|
+
data.tar.gz: 01f7b1eab7fe96e96fbd04dd446743259786bb54a8556f379627c13153c0e917ea6af368fdc4bb3c4f201666f32d78380a9c416ee238e3e39a4c07305b200edd
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'stringio'
|
4
4
|
require 'litbuild/multi_part_visitor'
|
5
5
|
require 'litbuild/service_dir'
|
6
|
+
require 'litbuild/string_indentation'
|
6
7
|
|
7
8
|
module Litbuild
|
8
9
|
##
|
@@ -15,6 +16,8 @@ module Litbuild
|
|
15
16
|
# blueprint (phase). AsciiDocVisitor can also write a top-level
|
16
17
|
# AsciiDoc document that includes all top-level fragments.
|
17
18
|
class AsciiDocVisitor < MultiPartVisitor
|
19
|
+
include StringIndentation
|
20
|
+
|
18
21
|
def initialize(parameters:)
|
19
22
|
@parameters = parameters
|
20
23
|
super(directory: @parameters['DOCUMENT_DIR'])
|
@@ -274,7 +277,7 @@ module Litbuild
|
|
274
277
|
end
|
275
278
|
svc.multiline_files.keys.sort.each do |filename|
|
276
279
|
doc.puts("|#{filename.capitalize} script\nl|")
|
277
|
-
doc.puts(svc.multiline_files[filename])
|
280
|
+
doc.puts(strip_indentation_from_value(svc.multiline_files[filename]))
|
278
281
|
doc.puts
|
279
282
|
end
|
280
283
|
end
|
@@ -358,7 +361,7 @@ module Litbuild
|
|
358
361
|
end
|
359
362
|
doc.puts('[source,indent=0]')
|
360
363
|
doc.puts('----')
|
361
|
-
doc.puts(file['content'])
|
364
|
+
doc.puts(strip_indentation_from_value(file['content']))
|
362
365
|
doc.puts('----')
|
363
366
|
end
|
364
367
|
|
@@ -371,7 +374,7 @@ module Litbuild
|
|
371
374
|
doc.puts("=== #{filename}\n\n")
|
372
375
|
doc.puts('[source,indent=0]')
|
373
376
|
doc.puts('----')
|
374
|
-
doc.puts(files[filename])
|
377
|
+
doc.puts(strip_indentation_from_string(files[filename].join))
|
375
378
|
doc.puts('----')
|
376
379
|
end
|
377
380
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'litbuild/service_dir'
|
4
4
|
require 'litbuild/source_code_manager'
|
5
|
+
require 'litbuild/string_indentation'
|
5
6
|
require 'litbuild/visitor'
|
6
7
|
|
7
8
|
module Litbuild
|
@@ -13,6 +14,8 @@ module Litbuild
|
|
13
14
|
# phase) has already been written to any directory, BashScriptVisitor
|
14
15
|
# will ignore subsequent requests to write that blueprint (phase).
|
15
16
|
class BashScriptVisitor < Visitor
|
17
|
+
include StringIndentation
|
18
|
+
|
16
19
|
INSTALL_GID = 9999
|
17
20
|
|
18
21
|
attr_reader :blueprint_dir
|
@@ -164,9 +167,14 @@ module Litbuild
|
|
164
167
|
script.string
|
165
168
|
end
|
166
169
|
|
170
|
+
# Timestamps are written with default format plus
|
171
|
+
# seconds-since-epoch (in parentheses), to make it as easy as
|
172
|
+
# possible to calculate how long things take.
|
173
|
+
DATECMD = "date '+%a %b %e %H:%M:%S %Z %Y (%s)'"
|
174
|
+
|
167
175
|
def write_components(location:, script:)
|
168
176
|
@written[location].map do |target|
|
169
|
-
script.puts("echo \"At $(
|
177
|
+
script.puts("echo \"At $(#{DATECMD}): Beginning #{target}:\"")
|
170
178
|
script.puts("./#{target}")
|
171
179
|
end
|
172
180
|
end
|
@@ -179,7 +187,7 @@ module Litbuild
|
|
179
187
|
blueprint.files.keys.sort.map do |name|
|
180
188
|
accum = StringIO.new
|
181
189
|
accum.puts("cat > #{name} <<'LBEOF'")
|
182
|
-
accum.puts(blueprint.files[name].string)
|
190
|
+
accum.puts(strip_indentation_from_string(blueprint.files[name].string))
|
183
191
|
accum.puts('LBEOF')
|
184
192
|
accum.string
|
185
193
|
end
|
@@ -336,7 +344,7 @@ module Litbuild
|
|
336
344
|
render_restart_header(script, restart_file, package.version, pkgusr_dir)
|
337
345
|
pkgusr_srcdir = File.join(pkgusr_dir, package.name_and_version)
|
338
346
|
render_add_package_user(package, script, log)
|
339
|
-
@scm.
|
347
|
+
@scm.copy_files_commands(package).each do |cp_command|
|
340
348
|
render_command(script, cp_command, log)
|
341
349
|
end
|
342
350
|
script.puts("export LB_SOURCE_DIR=#{quote(pkgusr_srcdir)}")
|
@@ -502,6 +510,7 @@ module Litbuild
|
|
502
510
|
def render_service_pipeline(script, spipe)
|
503
511
|
pname = spipe['name'].first
|
504
512
|
spipe['bundle']&.each do |bundle|
|
513
|
+
script.puts("mkdir -p #{bundle}")
|
505
514
|
script.puts("grep -q '^#{pname}$' #{bundle}/contents || " \
|
506
515
|
"echo #{pname} >> #{bundle}/contents")
|
507
516
|
end
|
@@ -534,7 +543,7 @@ module Litbuild
|
|
534
543
|
end
|
535
544
|
multiline = sd.multiline_files
|
536
545
|
deps = sd.dependencies
|
537
|
-
multiline['dependencies'] = deps.join("\n") unless deps.empty?
|
546
|
+
multiline['dependencies'] = [deps.join("\n")] unless deps.empty?
|
538
547
|
multiline.keys.sort.each do |filename|
|
539
548
|
# I always terminate here documents in litbuild-generated
|
540
549
|
# scripts with "LBEOF", partly because I'm thinking "Litbuild
|
@@ -542,7 +551,7 @@ module Litbuild
|
|
542
551
|
# Shia LaBoeuf, and then I remember the Rob Cantor song of
|
543
552
|
# that name and giggle.
|
544
553
|
script.puts("cat > #{sd.name}/#{filename} <<'LBEOF'")
|
545
|
-
script.puts(multiline[filename])
|
554
|
+
script.puts(strip_indentation_from_value(multiline[filename]))
|
546
555
|
script.puts('LBEOF')
|
547
556
|
end
|
548
557
|
env = sd.env
|
data/lib/litbuild/blueprint.rb
CHANGED
@@ -7,6 +7,15 @@ require 'litbuild/logfile_namer'
|
|
7
7
|
require 'litbuild/visitor'
|
8
8
|
|
9
9
|
module Litbuild
|
10
|
+
# Blueprints are described in the `doc` directory.
|
11
|
+
#
|
12
|
+
# tl;dr: Blueprints are considered as "chunks" separated by blank
|
13
|
+
# lines. Each chunk can be either directives or narrative. Narrative
|
14
|
+
# is AsciiDoc and does not get parsed or transformed. Directives are
|
15
|
+
# basically a simplified version of YAML.
|
16
|
+
#
|
17
|
+
# After parsing, the narrative is found in the `grafs` variables and
|
18
|
+
# directives are found in `directive` hashes.
|
10
19
|
class Blueprint
|
11
20
|
class << self
|
12
21
|
# This should simply be the name of the directory where blueprints
|
@@ -1,20 +1,18 @@
|
|
1
1
|
# frozen_string_literal: false
|
2
2
|
|
3
3
|
require 'litbuild/errors'
|
4
|
+
require 'litbuild/string_indentation'
|
4
5
|
require 'json'
|
5
6
|
|
6
7
|
module Litbuild
|
7
8
|
# This is a kludgy hand-built parser. Blueprint structure is not (at
|
8
9
|
# least, at this point) complicated enough that I can see any point in
|
9
10
|
# defining a grammar and using a parser generator. The structure of
|
10
|
-
#
|
11
|
-
#
|
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.
|
11
|
+
# blueprints is described informally in `doc/blueprints.txt` (and
|
12
|
+
# blueprint-type-specific files under `doc` as well).
|
17
13
|
class BlueprintParser
|
14
|
+
include StringIndentation
|
15
|
+
|
18
16
|
# _directives_ are for use in scripts. _grafs_ are for use in
|
19
17
|
# documents.
|
20
18
|
attr_reader :base_directives, :phase_directives, :base_grafs, :phase_grafs
|
@@ -30,9 +28,8 @@ module Litbuild
|
|
30
28
|
# blueprint. If there are no phase directives, this is the entire
|
31
29
|
# blueprint, obvs.
|
32
30
|
@base_grafs = []
|
33
|
-
|
34
|
-
|
35
|
-
@base_directives = base
|
31
|
+
@base_directives = parse_phase(phase_chunks.shift, {}, @base_grafs)
|
32
|
+
@base_directives['full-name'] ||= @base_directives['name']
|
36
33
|
|
37
34
|
# The rest of the blueprint, if any, consists of directives and
|
38
35
|
# narrative for specific phases. The directives for each phase
|
@@ -42,13 +39,15 @@ module Litbuild
|
|
42
39
|
phase_name = phase_chunks.shift
|
43
40
|
phase_contents = phase_chunks.shift
|
44
41
|
grafs = []
|
45
|
-
@phase_directives[phase_name] = parse_phase(phase_contents,
|
42
|
+
@phase_directives[phase_name] = parse_phase(phase_contents,
|
43
|
+
@base_directives,
|
44
|
+
grafs)
|
46
45
|
@phase_grafs[phase_name] = grafs
|
47
46
|
end
|
48
47
|
|
49
48
|
# Any directives at the beginning of a blueprint are actually a
|
50
|
-
# file header that should not be
|
51
|
-
# for it.
|
49
|
+
# file header that should not be considered as part of the
|
50
|
+
# narrative for it.
|
52
51
|
@base_grafs.shift while @base_grafs[0].is_a?(Hash)
|
53
52
|
rescue StandardError => e
|
54
53
|
msg = "Cannot parse blueprint starting: #{@text.lines[0..3].join}"
|
@@ -122,8 +121,8 @@ module Litbuild
|
|
122
121
|
end
|
123
122
|
directive_name = md[1]
|
124
123
|
firstline_value = md[2]
|
125
|
-
value_lines = related_lines(directive_line, lines_to_process)
|
126
|
-
value = parse_directive_value(firstline_value, value_lines)
|
124
|
+
value_lines, indent = related_lines(directive_line, lines_to_process)
|
125
|
+
value = parse_directive_value(firstline_value, value_lines, indent)
|
127
126
|
graf_directives[directive_name] << if value == "''"
|
128
127
|
''
|
129
128
|
else
|
@@ -137,7 +136,9 @@ module Litbuild
|
|
137
136
|
# Regardless of what kind of directive structure we're dealing with,
|
138
137
|
# all of the lines that are indented more than the initial directive
|
139
138
|
# line are part of that directive. This method finds all those
|
140
|
-
# related lines and strips away the common initial indentation
|
139
|
+
# related lines and strips away the common initial indentation; it
|
140
|
+
# returns both the related lines and the amount of indentation that
|
141
|
+
# was removed.
|
141
142
|
def related_lines(directive_line, lines_to_process)
|
142
143
|
directive_indent = indent_for(directive_line)
|
143
144
|
related = []
|
@@ -145,16 +146,15 @@ module Litbuild
|
|
145
146
|
(indent_for(lines_to_process[0]) > directive_indent)
|
146
147
|
related << lines_to_process.shift
|
147
148
|
end
|
148
|
-
|
149
|
-
related.map { |l| l.slice(common_indent..-1) }
|
149
|
+
strip_indentation_from_array(related)
|
150
150
|
end
|
151
151
|
|
152
152
|
# What kind of directive are we dealing with? Could be a simple
|
153
153
|
# value (with zero or more continuation lines), or a multiline
|
154
154
|
# value, or an array, or a subdirective block.
|
155
|
-
def parse_directive_value(firstline_value, other_lines)
|
155
|
+
def parse_directive_value(firstline_value, other_lines, indentation)
|
156
156
|
if firstline_value == '|'
|
157
|
-
multiline_value(other_lines)
|
157
|
+
multiline_value(other_lines, indentation)
|
158
158
|
elsif other_lines.empty?
|
159
159
|
firstline_value
|
160
160
|
elsif other_lines[0].match?(/^ *- /)
|
@@ -175,8 +175,15 @@ module Litbuild
|
|
175
175
|
stripped.join(" \\\n ")
|
176
176
|
end
|
177
177
|
|
178
|
-
|
179
|
-
|
178
|
+
# While parsing multiline values, we want to restore the indentation
|
179
|
+
# that was previously stripped away -- this is typically used for
|
180
|
+
# `file` directives, which often appear in multiple directive blocks
|
181
|
+
# and should be treated as a whole. The indentation for multi-line
|
182
|
+
# values must be removed *later*, during rendering of scripts and
|
183
|
+
# documents.
|
184
|
+
def multiline_value(lines, indentation)
|
185
|
+
lines_with_indentation = lines.map { |l| (' ' * indentation) + l }
|
186
|
+
"#{lines_with_indentation.join("\n")}\n"
|
180
187
|
end
|
181
188
|
|
182
189
|
def array_value(lines)
|
@@ -184,8 +191,8 @@ module Litbuild
|
|
184
191
|
until lines.empty?
|
185
192
|
array_member = lines.shift
|
186
193
|
firstline_value = /^- *(.*)$/.match(array_member)[1]
|
187
|
-
related = related_lines(array_member, lines)
|
188
|
-
value << parse_directive_value(firstline_value, related)
|
194
|
+
related, indentation = related_lines(array_member, lines)
|
195
|
+
value << parse_directive_value(firstline_value, related, indentation)
|
189
196
|
end
|
190
197
|
value
|
191
198
|
rescue StandardError
|
@@ -198,11 +205,5 @@ module Litbuild
|
|
198
205
|
def subdirective_value(lines)
|
199
206
|
parse_lines(lines)
|
200
207
|
end
|
201
|
-
|
202
|
-
# Utility method to find the amount of blank space at the beginning
|
203
|
-
# of a line.
|
204
|
-
def indent_for(line)
|
205
|
-
/^([[:blank:]]*).*/.match(line)[1].size
|
206
|
-
end
|
207
208
|
end
|
208
209
|
end
|
data/lib/litbuild/package.rb
CHANGED
@@ -131,11 +131,16 @@ module Litbuild
|
|
131
131
|
end
|
132
132
|
|
133
133
|
# otherwise, the default version goes right *before* the *first
|
134
|
-
# directive of the next stage* (which
|
134
|
+
# directive of the next stage* (which *should* always be present)...
|
135
135
|
next_stage = STAGE_DIRECTIVES[STAGE_DIRECTIVES.index(stg) + 1]
|
136
136
|
first_idx = grafs.index do |graf|
|
137
137
|
graf.respond_to?(:key) && graf.key?(next_stage)
|
138
138
|
end
|
139
|
+
|
140
|
+
# ...but if, for whatever reason, we haven't found a place to
|
141
|
+
# put the default directives, just put them at the beginning.
|
142
|
+
first_idx ||= 0
|
143
|
+
|
139
144
|
grafs.insert(first_idx, stg => to_add[stg])
|
140
145
|
end
|
141
146
|
end
|
data/lib/litbuild/section.rb
CHANGED
@@ -35,8 +35,8 @@ module Litbuild
|
|
35
35
|
|
36
36
|
blueprints = self['blueprints'].clone || []
|
37
37
|
if $DEBUG
|
38
|
-
warn("Explicit blueprints for section #{name}:" \
|
39
|
-
"
|
38
|
+
warn("Explicit blueprints for section #{name}: " \
|
39
|
+
"#{blueprints.join(', ')}")
|
40
40
|
end
|
41
41
|
blueprints << automatic_inclusions
|
42
42
|
deduped = blueprints.flatten.uniq
|
@@ -56,8 +56,8 @@ module Litbuild
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
if $DEBUG
|
59
|
-
warn("Automatically-added blueprints for section #{name}:" \
|
60
|
-
"
|
59
|
+
warn("Automatically-added blueprints for section #{name}: " \
|
60
|
+
"#{auto_adds.sort.join(', ')}")
|
61
61
|
end
|
62
62
|
auto_adds.sort
|
63
63
|
end
|
@@ -13,7 +13,8 @@ module Litbuild
|
|
13
13
|
#
|
14
14
|
# Note, SourceCodeManager does not recurse into additional levels of
|
15
15
|
# subdirectories -- it turns out that makes the test suite really
|
16
|
-
# slow
|
16
|
+
# slow, and recursive search is not useful to me, so I'm just
|
17
|
+
# skipping it.
|
17
18
|
class SourceCodeManager
|
18
19
|
def initialize(*dirs)
|
19
20
|
all_pkgfiles = dirs.map do |d|
|
@@ -55,13 +56,39 @@ module Litbuild
|
|
55
56
|
end
|
56
57
|
|
57
58
|
##
|
58
|
-
# Package Users expect to have tarfiles for the top-level
|
59
|
-
# and any in-tree packages in their `src` directory, and
|
60
|
-
# their `patches` directory. This
|
61
|
-
#
|
62
|
-
#
|
63
|
-
|
59
|
+
# Typically, Package Users expect to have tarfiles for the top-level
|
60
|
+
# package and any in-tree packages in their `src` directory, and
|
61
|
+
# patches in their `patches` directory. This method arranges things
|
62
|
+
# that way: it produces commands that copy all the necessary files
|
63
|
+
# from TARFILE_DIR and/or PATCH_DIR to the destination directory,
|
64
|
+
# skipping any that are already present where the Package Users
|
65
|
+
# build script expects to find them.
|
66
|
+
#
|
67
|
+
# Binary package files are an exception to the typical case. A
|
68
|
+
# binary package file has name `binary-#{packagename}.tar.lz` (no
|
69
|
+
# version number, and always lzip-compressed), and contains the
|
70
|
+
# files produced by the compilation and installation process. If
|
71
|
+
# such a file is present in the Package User home directory, the
|
72
|
+
# build script simply unpacks it and does nothing else.
|
73
|
+
#
|
74
|
+
# So: if a binary package file is present in the tarfile directory,
|
75
|
+
# this method simply emits a command to copy it to the Package User
|
76
|
+
# home directory; and if there is already a binary package file
|
77
|
+
# present in the Package User home directory, this method does
|
78
|
+
# nothing.
|
79
|
+
def copy_files_commands(package)
|
64
80
|
pkgusr = pkgusr_name(package)
|
81
|
+
|
82
|
+
# copy binary file if available, then return
|
83
|
+
binfile = "binary-#{package.name}.tar.lz"
|
84
|
+
return ["cp #{find_file(binfile)} ~#{pkgusr}"] \
|
85
|
+
if @available_files.detect { |f| /#{binfile}/ =~ f }
|
86
|
+
|
87
|
+
# do nothing if binary file is present in home dir already
|
88
|
+
return [] if homedir(package) && File.exist?(
|
89
|
+
File.join(homedir(package), binfile)
|
90
|
+
)
|
91
|
+
|
65
92
|
mkdir_commands = %w[src patches].map do |dir|
|
66
93
|
"mkdir -p ~#{pkgusr}/#{dir}"
|
67
94
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module Litbuild
|
4
|
+
module StringIndentation
|
5
|
+
# Utility method to find the amount of blank space at the beginning
|
6
|
+
# of a line.
|
7
|
+
def indent_for(line)
|
8
|
+
/^([[:blank:]]*).*/.match(line)[1].size
|
9
|
+
end
|
10
|
+
|
11
|
+
# Utility method to strip the common indentation from all strings in
|
12
|
+
# an array. Returns both the stripped strings and the amount of
|
13
|
+
# indentation removed.
|
14
|
+
def strip_indentation_from_array(strings)
|
15
|
+
common_indent = strings.map { |l| indent_for(l) }.min
|
16
|
+
[strings.map { |l| l.slice(common_indent..-1) }, common_indent]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Utility method to strip the common indentation from all lines of a
|
20
|
+
# multi-line string. (Does not return the amount of indentation
|
21
|
+
# removed.)
|
22
|
+
def strip_indentation_from_string(string)
|
23
|
+
strip_indentation_from_array(string.lines)[0]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Utility method to strip the common indentation from all lines of a
|
27
|
+
# directive value (which is expected to be an array containing a
|
28
|
+
# single string element)
|
29
|
+
def strip_indentation_from_value(value)
|
30
|
+
strip_indentation_from_string(value[0])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/litbuild/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: litbuild
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brett Neumeier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A build system based on Knuth's idea of literate programming.
|
14
14
|
email:
|
@@ -39,6 +39,7 @@ files:
|
|
39
39
|
- lib/litbuild/service_dir.rb
|
40
40
|
- lib/litbuild/source_code_manager.rb
|
41
41
|
- lib/litbuild/source_files_visitor.rb
|
42
|
+
- lib/litbuild/string_indentation.rb
|
42
43
|
- lib/litbuild/url_visitor.rb
|
43
44
|
- lib/litbuild/version.rb
|
44
45
|
- lib/litbuild/visitor.rb
|
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
63
|
- !ruby/object:Gem::Version
|
63
64
|
version: '0'
|
64
65
|
requirements: []
|
65
|
-
rubygems_version: 3.
|
66
|
+
rubygems_version: 3.4.7
|
66
67
|
signing_key:
|
67
68
|
specification_version: 4
|
68
69
|
summary: A literate build system
|