qrpm 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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