qrpm 0.0.3 → 0.3.0

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.
data/lib/qrpm/node.rb CHANGED
@@ -1,53 +1,394 @@
1
1
 
2
- module Qrpm
2
+ module Qrpm
3
3
  class Node
4
- attr_reader :directory # Destination directory
5
- attr_reader :name # Defaults to last element of file/link
6
- def path() "#{directory}/#{name}" end
4
+ # Parent node
5
+ attr_reader :parent
7
6
 
8
- def initialize(directory, name)
9
- @directory, @name = directory, name
7
+ # Path to node. This is a String expression that leads from the
8
+ # root node down to the current node. The expression may only contain
9
+ # variables if the node is a DirectoryNode but it can include integer
10
+ # indexes
11
+ attr_reader :path
12
+
13
+ # Name of node within its parent. ArrayNode element objects (this include
14
+ # files) has integer "names" and only DirectoryNode keys may contain
15
+ # references. It is initialized with a String or an Integer except for
16
+ # DirectoryNode objects that are initialized with a Fragment::Expression
17
+ # (directory nodes is not part of the dictionary). When computing directory
18
+ # paths, the source of the Fragment::Expression is used
19
+ attr_reader :name
20
+
21
+ # The value of this node. This can be a Fragment::Expression (ValueNode),
22
+ # Hash, or Array object
23
+ attr_reader :expr
24
+
25
+ # Interpolated #name. Initialized by Compile#analyze. Default an alias for #name
26
+ alias_method :key, :name
27
+
28
+ # Interpolated #expr. Initialized by Compile#analyze. Default an alias for #expr
29
+ alias_method :value, :expr
30
+
31
+ def initialize(parent, name, expr)
32
+ constrain parent, HashNode, ArrayNode, nil
33
+ constrain name, Fragment::Fragment, String, Integer, nil
34
+ constrain expr, Fragment::Fragment, Hash, Array
35
+ @parent = parent
36
+ @name = name
37
+ @expr = expr
38
+ @interpolated = false
39
+ @parent&.send(:add_node, self)
40
+ @path = Node.ref(@parent&.path, self.is_a?(DirectoryNode) ? @name.source : name) # FIXME
10
41
  end
11
42
 
12
- def file?() self.class == Qrpm::File end
13
- def link?() self.class == Qrpm::Link end
43
+ # Return list of variable names in #expr. Directory nodes may include
44
+ # variables in the key so they include key variables too
45
+ def variables = abstract_method
14
46
 
15
- def dump(&block)
16
- puts self.class
17
- indent {
18
- puts "directory: #{directory}"
19
- puts "name : #{name}"
20
- yield if block_given?
21
- }
47
+ # Interpolate variables in Node. Note that interpolation is not recursive
48
+ # except for DirectoryNode objects that interpolates both key and elements.
49
+ # #interpolate sets the interpolated flag and returns self
50
+ def interpolate(dict)
51
+ @interpolated = true
52
+ self
22
53
  end
54
+
55
+ # True if object has been interpolated
56
+ def interpolated? = @interpolated
57
+
58
+ # Name of class
59
+ def class_name = self.class.to_s.sub(/.*::/, "")
60
+
61
+ # Signature. Used in tests
62
+ def signature(klass: self.class_name, key: self.name) = abstract_method
63
+
64
+ def inspect = "#<#{self.class} #{path.inspect}>"
65
+ def dump = abstract_method
66
+
67
+ # Traverse node and its children recursively and execute block on each
68
+ # node. The nodes are traversed in depth-first order. The optional
69
+ # +klasses+ argument is a list of Class objects and the block is only
70
+ # executed on matching nodes. The default is to execute the block on all
71
+ # nodes. Returns an array of traversed nodes if no block is given
72
+ def traverse(*klasses, &block)
73
+ klasses = klasses.flatten
74
+ if block_given?
75
+ yield self if klasses.empty? || klasses.include?(self.class)
76
+ traverse_recursively(&block)
77
+ else
78
+ l = klasses.empty? || klasses.include?(self.class) ? [self] : []
79
+ traverse_recursively { |n| l << n }
80
+ l
81
+ end
82
+ end
83
+
84
+ def dot(expr)
85
+ curr = self
86
+ src = ".#{expr}" if expr[0] != "["
87
+ while src =~ /^\.(#{IDENT_RE})|\[(\d+)\]/
88
+ member, index = $1, $2
89
+ src = $'
90
+ if member
91
+ curr.is_a?(HashNode) or raise ArgumentError, "#{member} is not a hash in '#{expr}'"
92
+ curr.key?(member) or raise ArgumentError, "Unknown member '#{member}' in '#{expr}'"
93
+ curr = curr[member]
94
+ else
95
+ curr.is_a?(ArrayNode) or raise ArgumentError, "#{curr.key_source} is not an array in '#{expr}'"
96
+ curr.size > index.to_i or raise "Out of range index '#{index}' in '#{expr}'"
97
+ curr = curr[index]
98
+ end
99
+ end
100
+ src.empty? or raise ArgumentError, "Illegal expression: #{expr}"
101
+ curr
102
+ end
103
+
104
+ protected
105
+ def self.ref(parent_ref, element)
106
+ if parent_ref
107
+ parent_ref + (element.is_a?(Integer) ? "[#{element}]" : ".#{element}")
108
+ else
109
+ element
110
+ end
111
+ end
112
+
113
+ def traverse_recursively(&block) = abstract_method
114
+ def signature_content = abstract_method
23
115
  end
24
116
 
25
- class File < Node
26
- attr_reader :file # Path to file in the source repository
27
- attr_reader :perm # Defaults to nil - using the current permissions
28
- def initialize(directory, name, file, perm = nil)
29
- super(directory, name || file.sub(/.*\//, ""))
30
- @file, @perm = file, perm
117
+ class ValueNode < Node
118
+ # Source code of expression
119
+ def source() @expr.source end
120
+
121
+ # Override Qrpm#value. Initially nil, initialized by #interpolate
122
+ attr_reader :value
123
+
124
+ def initialize(parent, name, expr)
125
+ expr ||= Fragment::NilFragment.new
126
+ constrain expr, Fragment::Fragment
127
+ super
128
+ end
129
+
130
+ # Override Qrpm methods
131
+ def variables() @variables ||= expr.variables end
132
+
133
+ def interpolate(dict)
134
+ @value ||= expr.interpolate(dict) # Allows StandardDirNode to do its own assignment
135
+ super
136
+ end
137
+
138
+ def signature() "#{class_name}(#{name},#{expr.source})" end
139
+ def dump() puts value ? value : source end
140
+
141
+ protected
142
+ def traverse_recursively(&block) [] end
143
+ end
144
+
145
+ # Pre-defined standard directory node. #setsys and #setpck is used to point
146
+ # the value at either the corresponding system or package directory depending
147
+ # on the number of files in that directory
148
+ class StandardDirNode < ValueNode
149
+ def initialize(parent, name)
150
+ super parent, name, nil
151
+ end
152
+
153
+ def signature() "#{class_name}(#{name},#{value.inspect})" end
154
+
155
+ def setsys() @expr = Fragment::Fragment.parse("$sys#{name}") end
156
+ def setpck() @expr = Fragment::Fragment.parse("$pck#{name}") end
157
+ end
158
+
159
+ class ContainerNode < Node
160
+ # Return list of Node objects in the container. Hash keys are not included
161
+ def exprs = abstract_method
162
+
163
+ forward_to :expr, :empty?, :size, :[], :[]=
164
+
165
+ def variables() @variables ||= exprs.map(&:variables).flatten.uniq end
166
+
167
+ # Can't be defined as an alias because #exprs is redefined in derived
168
+ # classes, otherwise #values would refer to the derived version of #exprs
169
+ def values() exprs end
170
+
171
+ def signature = "#{self.class_name}(#{name},#{exprs.map { |v| v.signature }.join(",")})"
172
+
173
+ protected
174
+ def traverse_recursively(&block)
175
+ values.map { |value| value.traverse(&block) }.flatten
176
+ end
177
+
178
+ def add_node(node) = abstract_method
179
+ end
180
+
181
+ # A HashNode has a hash as expr. It doesn't forward #interpolate to its
182
+ # members (FileNode overrides that)
183
+ #
184
+ class HashNode < ContainerNode
185
+ # Override ContainerNode#exprs
186
+ def exprs() expr.values end
187
+
188
+ def initialize(parent, name, hash = {})
189
+ constrain hash, Hash
190
+ super(parent, name, hash.dup)
31
191
  end
192
+
193
+ forward_to :expr, :key?, :keys
194
+
32
195
  def dump
33
- super {
34
- puts "file : #{file}"
35
- puts "perm : #{perm}"
196
+ puts "{"
197
+ indent {
198
+ expr.each { |k,v|
199
+ print "#{k}: "
200
+ v.dump
201
+ }
36
202
  }
203
+ puts "}"
37
204
  end
205
+
206
+ protected
207
+ # Override ContainerNode#add_node
208
+ def add_node(node) self[node.name] = node end
38
209
  end
39
210
 
40
- class Link < Node
41
- attr_reader :link # Destination file of link
211
+ class RootNode < HashNode
212
+ def initialize() super(nil, nil, {}) end
42
213
 
43
- def initialize(directory, name, link)
44
- super(directory, name || link.sub(/.*\//, ""))
45
- @link = link
214
+ # Override Node#interpolate. Only interpolates contained DirectoryNode
215
+ # objects (TODO doubtfull - this is a Qrpm-level problem not a Node problem)
216
+ def interpolate(dict)
217
+ exprs.each { |e| e.is_a?(DirectoryNode) and e.interpolate(dict) }
218
+ super
46
219
  end
220
+
221
+ def signature = "#{self.class_name}(#{values.map { |v| v.signature }.join(",")})"
222
+ end
223
+
224
+ # A file. Always an element of a DirectoryNode object
225
+ #
226
+ # A file is a hash with an integer key and with the following expressions as members:
227
+ #
228
+ # file Source file. The source file path is prefixed with $srcdir if
229
+ # defined. May be nil
230
+ # name Basename of the destination file. This defaults to the
231
+ # basename of the source file/symlink/reflink. The full path of
232
+ # the destination file is computed by prefixing the path of the
233
+ # parent directory
234
+ # reflink Path on the target filesystem to the source of the reflink
235
+ # (hard-link). May be nil
236
+ # symlink Path on the target filesystem to the source of the symlink.
237
+ # May be nil
238
+ # perm Permissions of the target file in chmod(1) octal or rwx
239
+ # notation. May be nil
240
+ #
241
+ # Exactly one of 'file', 'symlink', and 'reflink' must be defined. 'perm'
242
+ # can't be used together with 'symlink' or 'reflink'
243
+ #
244
+ # When interpolated the following methods are defined on a FileNode:
245
+ #
246
+ # srcpath Path to source file
247
+ # dstpath Path to destination file
248
+ # dstname Basename of destination file
249
+ # reflink Path to source link
250
+ # symlink Path to source link
251
+ # perm Permissions
252
+ #
253
+ class FileNode < HashNode
254
+ # Source file. This is the relative path to the file in the build directory
255
+ # except for link files. Link files have a path on the target filesystem as
256
+ # path. It is the interpolated value of #expr["file/reflink/symlink"]
257
+ attr_reader :srcpath
258
+
259
+ # Destination file path
260
+ attr_reader :dstpath
261
+
262
+ # Destination file name
263
+ attr_reader :dstname
264
+
265
+ # Hard-link file
266
+ attr_reader :reflink
267
+
268
+ # Symbolic link file
269
+ attr_reader :symlink
270
+
271
+ # Permissions of destination file. Perm is always a string
272
+ attr_reader :perm
273
+
274
+ # Directory
275
+ def directory = parent.directory
276
+
277
+ # Query methods
278
+ def file? = !link?
279
+ def link? = symlink? || reflink?
280
+ def reflink? = @expr.key?("reflink")
281
+ def symlink? = @expr.key?("symlink")
282
+
283
+ def initialize(parent, name)
284
+ constrain parent, DirectoryNode
285
+ constrain name, Integer
286
+ super
287
+ end
288
+
289
+ def interpolate(dict)
290
+ super
291
+ exprs.each { |e| e.interpolate(dict) }
292
+ @srcpath = value[%w(file symlink reflink).find { |k| expr.key?(k) }].value
293
+ @dstname = value["name"]&.value || File.basename(srcpath)
294
+ @dstpath = "#{parent.directory}/#{@dstname}"
295
+ @reflink = value["reflink"]&.value
296
+ @symlink = value["symlink"]&.value
297
+ @perm = value["perm"]&.value
298
+ self
299
+ end
300
+
301
+ # :call-seq:
302
+ # FileNode.make(directory_node, filename)
303
+ # FileNode.make(directory_node, hash)
304
+ #
305
+ # Shorthand for creating file object
306
+ def self.make(parent, arg)
307
+ file = FileNode.new(parent, parent.size)
308
+ hash = arg.is_a?(String) ? { "file" => arg } : arg
309
+ hash.each { |k,v| ValueNode.new(file, k.to_s, Fragment::Fragment.parse(v)) }
310
+ file
311
+ end
312
+
313
+ # Signature. Used in tests
314
+ def signature = "FileNode(#{name},#{expr["file"].source})"
315
+
316
+ # Path to source file. Returns the QRPM source expression or the
317
+ # interpolated result if the FileNode object has been interpolated. Used by
318
+ # Qrpm#dump
319
+ def src
320
+ e = expr["file"] || expr["reflink"] || expr["symlink"]
321
+ interpolated? ? e.value : e.source
322
+ end
323
+
324
+ # Name of destination. Returns the QRPM source expression or the
325
+ # interpolated result if the FileNode object has been interpolated. Used by
326
+ # Qrpm#dump
327
+ def dst
328
+ if expr["name"]
329
+ interpolated? ? expr["name"].value : expr["name"].expr.source
330
+ else
331
+ File.basename(src)
332
+ end
333
+ end
334
+ end
335
+
336
+ class ArrayNode < ContainerNode
337
+ # Override ContainerNode#exprs
338
+ def exprs() expr end
339
+
340
+ def initialize(parent, name, array = [])
341
+ constrain array, Array
342
+ super(parent, name, array.dup)
343
+ end
344
+
345
+ forward_to :expr, :first, :last
346
+
347
+ # Array forwards #interpolate to its children
348
+ def interpolate(dict)
349
+ exprs.each { |e| e.interpolate(dict) }
350
+ super
351
+ end
352
+
47
353
  def dump
48
- super {
49
- puts "link : #{link}"
354
+ puts "["
355
+ indent {
356
+ expr.each { |n|
357
+ print "- "
358
+ n.dump
359
+ }
50
360
  }
361
+ puts "]"
362
+ end
363
+
364
+ protected
365
+ def add_node(node)
366
+ node.instance_variable_set(:"@name", expr.size)
367
+ expr << node
368
+ end
369
+ end
370
+
371
+ class DirectoryNode < ArrayNode
372
+ # Override Qrpm#key
373
+ attr_reader :key
374
+
375
+ # File system path to the directory. An alias for uuid/key
376
+ def directory() key end
377
+
378
+ def initialize(parent, name, array = [])
379
+ constrain name, Fragment::Fragment
380
+ super
381
+ end
382
+
383
+ def variables() (super + key_variables).uniq end
384
+ def key_variables() @key_variables ||= name.variables end
385
+
386
+ # A directory node also interpolates its key
387
+ def interpolate(dict)
388
+ # #key is used by the embedded files to compute their paths so it has be
389
+ # interpolated before we interpolate the files through the +super+ method
390
+ @key = name.interpolate(dict)
391
+ super
51
392
  end
52
393
  end
53
394
  end
data/lib/qrpm/qrpm.rb CHANGED
@@ -1,63 +1,192 @@
1
+ module Qrpm
2
+ class Qrpm
3
+ # Definitions. Maps from path to Node object
4
+ attr_reader :defs
1
5
 
2
- __END__
3
- # Returns array of variables in the string. Variables can be either '$name' or
4
- # '${name}'. The variables are returned in left-to-right order
5
- #
6
- def collect_exprs(expr)
7
- expr.scan(/\$([\w_]+)|\$\{([\w_]+)\}/).flatten.compact
8
- end
6
+ # Dependencies. Maps from variable name to list of variables it depends on
7
+ attr_reader :deps
9
8
 
10
- # Expand variables in the given string
11
- #
12
- # The method takes case to substite left-to-rigth to avoid a variable expansion
13
- # to infer with the name of an immediately preceding variable. Eg. $a$b; if $b
14
- # is resolved to 'c' then a search would otherwise be made for a variable named
15
- # '$ac'
16
- #
17
- def expand_expr(dict, value)
18
- value = value.dup
19
- collect_exprs(value).each { |k| value.sub!(/\$#{k}|\$\{#{k}\}/, dict[k]) }
20
- value
21
- end
9
+ # Variable definitions. This is ValueNode, HashNode, and ArrayNode objects in #defs
10
+ attr_reader :vars
11
+
12
+ # Directory definitions. This is the Directory nodes in #defs
13
+ attr_reader :dirs
14
+
15
+ # File definitions. This is the File nodes in #defs
16
+ attr_reader :files
17
+
18
+ # Dictionary. Maps from path to interpolated value. Only evaluated nodes
19
+ # have entries in #dict
20
+ attr_reader :dict
21
+
22
+ # Source directory
23
+ def srcdir() @dict["srcdir"] end
24
+
25
+ def initialize(defs, deps)
26
+ constrain defs, String => Node
27
+ @defs, @deps = defs, deps
28
+ @vars, @dirs, @files = {}, {}, {}
29
+ @defs.values.map(&:traverse).flatten.each { |object|
30
+ case object
31
+ when FileNode; @files[object.path] = object
32
+ when DirectoryNode; @dirs[object.path] = object
33
+ else @vars[object.path] = object
34
+ end
35
+ }
36
+ @dict = {}
37
+ @evaluated = false
38
+ end
39
+
40
+ # True if object has been evaluated
41
+ def evaluated? = @evaluated
22
42
 
23
- def expand_dict(dict)
24
- expressions = dict.map { |k,v| [k, collect_exprs(v)] }.to_h
25
- result = expressions.select { |k, v| v.empty? }.map { |k,v| [k, dict[k]] }.to_h
26
- unresolved = dict.keys - result.keys
43
+ # Evaluate object. Returns self
44
+ def evaluate
45
+ @evaluated ||= begin
46
+ unresolved = @defs.dup # Queue of unresolved definitions
27
47
 
28
- changed = true
29
- while changed && !unresolved.empty?
30
- changed = false
31
- unresolved.delete_if { |k|
32
- if expressions[k].all? { |var| result.key? var }
33
- result[k] = expand_expr(result, dict[k])
34
- changed = true
48
+ # Find objects. Built-in RPM fields and directories are evaluated recursively
49
+ paths = FIELDS.keys.select { |k| @defs.key? k } + dirs.keys #+ DEFAULTS.keys
50
+
51
+ # Find dependency order of objects
52
+ ordered_deps = find_evaluation_order(paths)
53
+
54
+ # Evaluate objects and remove them from the @unresolved queue
55
+ ordered_deps.each { |path|
56
+ node = @defs[path]
57
+ node.interpolate(dict) if !node.interpolated? && !dict.key?(path)
58
+ unresolved.delete(path)
59
+ @dict[path] = node.value if !node.is_a?(DirectoryNode) && !node.is_a?(FileNode)
60
+ }
61
+ self
35
62
  end
36
- }
37
- end
38
- unresolved.empty? or raise "Unresolved variables: #{unresolved.join(", ")}"
63
+ end
39
64
 
40
- result
41
- end
65
+ # Evaluate and return Rpm object
66
+ def rpm(**rpm_options)
67
+ evaluate
68
+ used_vars = dict.keys.map { |k| [k, @defs[k]] }.to_h
69
+ Rpm.new(dict["srcdir"], used_vars, files.values, **rpm_options)
70
+ end
71
+
72
+ def [](name) @dict[name] end
73
+ def key?(name) @dict.key? name end
42
74
 
43
- # +value+ will typically be a dirs hash
44
- def expand_object(dict, object)
45
- case object
46
- when Array; object.map { |v| expand_object(dict, v) }
47
- when Hash
48
- object.map { |k,v|
49
- key = expand_expr(dict, k)
50
- object = expand_object(dict, v)
51
- [key, object]
52
- }.to_h
53
- when String
54
- expand_expr(dict, object)
55
- else
56
- object
57
- end
58
- end
75
+ def inspect
76
+ "#<#{self.class}>"
77
+ end
78
+
79
+ def dump
80
+ FIELDS.keys.each { |f|
81
+ puts if f == "make"
82
+ obj = self[f]
83
+ if obj.is_a?(Array)
84
+ puts "#{f.capitalize}:"
85
+ self[f].each { |e| indent.puts "- #{e.value}" }
86
+ else
87
+ puts "#{f.capitalize}: #{self[f]}" if key? f
88
+ end
89
+ }
90
+ puts
91
+ puts "Directories:"
92
+ indent {
93
+ dirs.values.each { |d|
94
+ puts d.key
95
+ indent {
96
+ d.values.each { |f|
97
+ if f.file? && File.basename(f.src) == f.dst
98
+ print f.src
99
+ else
100
+ joiner = f.file? ? "->" : (f.reflink? ? "~>" : "~~>")
101
+ print "#{f.src} #{joiner} #{f.dst}"
102
+ end
103
+ print ", perm: #{f.perm}" if f.perm
104
+ puts
105
+ }
106
+ }
107
+ }
108
+ }
109
+ end
59
110
 
60
- def expand_dirs(dict, dirs)
61
- expand_object(dict, dirs)
111
+ def dump_parts(parts = [:defs, :deps, :vars, :dirs, :files, :dict])
112
+ parts = Array(parts)
113
+ if parts.include? :defs
114
+ puts "Defs"
115
+ indent { defs.each { |k,v| puts "#{k}: #{v.value.inspect}" if v.value } }
116
+ end
117
+ if parts.include? :deps
118
+ puts "Deps"
119
+ indent { deps.each { |k,v| puts "#{k}: #{v.join(", ")}" if !v.empty? } }
120
+ end
121
+ if parts.include? :vars
122
+ puts "Vars"
123
+ indent { vars.each { |k,v| puts "#{k}: #{v.inspect}" if !v.value.nil? } }
124
+ end
125
+ if parts.include? :dict
126
+ puts "Dict"
127
+ indent { dict.each { |k,v| puts "#{k}: #{v.inspect}" } }
128
+ end
129
+ if parts.include? :dirs
130
+ puts "Dirs"
131
+ indent { dirs.each { |k,v| puts "#{k}: #{v.directory}" } }
132
+ end
133
+ if parts.include? :files
134
+ puts "Files"
135
+ indent { files.each { |k,v| puts "#{k}: #{v.srcpath}" } }
136
+ end
137
+ end
138
+
139
+ private
140
+ # Assumes all required variables have been defined
141
+ def interpolate(node)
142
+ constrain node, Node
143
+ node.interpolate(defs)
144
+ end
145
+
146
+ def find_evaluation_order(paths)
147
+ paths.map { |path|
148
+ @deps[path].empty? ? [path] : find_evaluation_order_recusively([], path).flatten.reverse + [path]
149
+ }.flatten.uniq
150
+ end
151
+
152
+ def find_evaluation_order_recusively(stack, object)
153
+ if stack.include? object
154
+ cycle = stack.drop_while { |e| e != object } + [object]
155
+ raise "Cyclic definition: #{cycle.join(' -> ')}"
156
+ end
157
+ @deps[object].map { |e|
158
+ if @deps.key?(e)
159
+ [e] + find_evaluation_order_recusively(stack + [object], e)
160
+ else
161
+ []
162
+ end
163
+ }
164
+ end
165
+
166
+ def find_dependencies_recusively(stack, object)
167
+ if stack.include? object
168
+ cycle = stack.drop_while { |e| e != object } + [object]
169
+ raise "Cyclic definition: #{cycle.join(' -> ')}"
170
+ end
171
+ @deps[object].map { |e|
172
+ [e] + find_dependencies_recusively(stack + [object], e)
173
+ }
174
+ end
175
+
176
+ def to_val(obj)
177
+ case obj
178
+ when HashNode
179
+ obj.exprs.map { |node| [node.key, to_val(node)] }.to_h
180
+ when ArrayNode
181
+ obj.exprs.map { |node| to_val(node.value) }
182
+ when ValueNode
183
+ obj.value
184
+ when String
185
+ obj
186
+ else
187
+ raise StandardError.new "Unexpected object class: #{obj.class}"
188
+ end
189
+ end
190
+ end
62
191
  end
63
192