daedalus-core 0.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,448 @@
1
+ # A simple processor to create a list of dependencies for a given source code
2
+ # file based on the #include files in the source file.
3
+ #
4
+ # The process involves three phases:
5
+ #
6
+ # 1. Parsing the source file for all C/C++ preprocessor control directives
7
+ # (#include, #ifdef, #ifndef, #else, #endif, #define, #undef, #if, #elif)
8
+ # 2. Executing the tree derived from step 1 in the context of a set of
9
+ # macro definitions.
10
+ # 3. Collecting the dependency list for each source file.
11
+ #
12
+ # In step 1, the tree resulting from parsing each included file is cached. If
13
+ # a later file of the same expanded relative path is included, the parse tree
14
+ # from the cache is returned.
15
+ #
16
+ # The system level macro definitions are retrieved using `cpp -dM`. This
17
+ # system set may be updated with new definitions or removed definitions while
18
+ # the parse tree is executed.
19
+
20
+ require 'set'
21
+
22
+ module Daedalus
23
+ class DependencyGrapher
24
+ DEV_NULL = RUBY_PLATFORM =~ /mingw|mswin/ ? 'NUL' : '/dev/null'
25
+
26
+ class ExpressionEvaluator
27
+ def initialize(expression)
28
+ @expression = expression
29
+ end
30
+
31
+ # TODO: when melbourne is running in MRI, use it to parse
32
+ # the expression and then evaluate it.
33
+ def evaluate(defines)
34
+ @expression == "0" ? false : true
35
+ end
36
+ end
37
+
38
+ class Node
39
+ def initialize(parser)
40
+ @parser = parser
41
+ end
42
+
43
+ def close
44
+ message = "unbalanced \#endif for #{@parser.stack_top.class} at line #{@parser.line}"
45
+ raise ParseError, message
46
+ end
47
+
48
+ def add_else
49
+ "invalid \#else for #{@parser.stack_top.class} at line #{@parser.line}"
50
+ end
51
+
52
+ # TODO: remove
53
+ def execute(defines, node)
54
+ puts "#execute not implemented for #{self.class}"
55
+ end
56
+ end
57
+
58
+ module Container
59
+ attr_accessor :body
60
+
61
+ def initialize(parser)
62
+ super parser
63
+ @body = []
64
+ end
65
+
66
+ def close
67
+ @parser.stack_pop
68
+ end
69
+
70
+ def execute_body(defines, node)
71
+ body.each { |x| x.execute(defines, node) }
72
+ end
73
+ end
74
+
75
+ module Conditional
76
+ def initialize(parser)
77
+ super parser
78
+ @else = nil
79
+ end
80
+
81
+ def add_else(node)
82
+ @else = node
83
+ end
84
+ end
85
+
86
+
87
+ class SourceFile < Node
88
+ include Container
89
+
90
+ attr_reader :name, :object_name, :includes, :dependencies
91
+
92
+ def initialize(name, parser)
93
+ super parser
94
+ @name = name
95
+ @object_name = name.sub(/((c(pp)?)|S)$/, 'o')
96
+ @includes = []
97
+ end
98
+
99
+ def execute(defines)
100
+ execute_body defines, self
101
+ end
102
+
103
+ def collect_dependencies
104
+ set = Set.new
105
+
106
+ set << @name
107
+ @includes.each { |x| x.collect_dependencies(set) }
108
+
109
+ @dependencies = set.to_a.sort
110
+ end
111
+
112
+ def print_dependencies(out, max)
113
+ str = "#{@object_name}:"
114
+ out.print str
115
+
116
+ width = str.size
117
+ @dependencies.each do |name|
118
+ size = name.size + 1
119
+ if width + size > max
120
+ width = 0
121
+ out.print " \\\n "
122
+ end
123
+
124
+ out.print " ", name
125
+ width += size
126
+ end
127
+
128
+ out.print "\n"
129
+ end
130
+ end
131
+
132
+ class IncludedFile < Node
133
+ include Container
134
+
135
+ attr_reader :name, :includes
136
+
137
+ def self.cache
138
+ @cache ||= {}
139
+ end
140
+
141
+ def initialize(name, parser)
142
+ super parser
143
+ @name = name
144
+ @includes = []
145
+ end
146
+
147
+ def expand_filename(node)
148
+ return if File.exist? @name
149
+
150
+ @parser.directories.each do |dir|
151
+ path = File.join dir, @name
152
+ return @name = path if File.file? path
153
+ end
154
+
155
+ # Try to find the file in the same directory as where we're looking from
156
+ dir = File.dirname(node.name)
157
+ path = File.join dir, @name
158
+
159
+ return @name = path if File.file?(path)
160
+
161
+ raise Errno::ENOENT, "unable to find file to include: #{@name} from #{node.name}"
162
+ end
163
+
164
+ def execute(defines, node)
165
+ expand_filename(node)
166
+
167
+ if cached = self.class.cache[@name.to_sym]
168
+ @body = cached.body
169
+ else
170
+ parser = FileParser.new self, @parser.directories
171
+ parser.parse_file @name
172
+ self.class.cache[@name.to_sym] = self
173
+ end
174
+
175
+ execute_body defines, self
176
+
177
+ node.includes << self
178
+ end
179
+
180
+ def collect_dependencies(set)
181
+ return if set.include? @name
182
+
183
+ set << @name
184
+ @includes.each { |x| x.collect_dependencies(set) }
185
+ end
186
+ end
187
+
188
+ class IfDefined < Node
189
+ include Conditional, Container
190
+
191
+ def initialize(macro, parser)
192
+ super parser
193
+ @macro = macro.strip
194
+ end
195
+
196
+ def execute(defines, node)
197
+ if defines.key? @macro
198
+ execute_body defines, node
199
+ elsif @else
200
+ @else.execute(defines, node)
201
+ end
202
+ end
203
+ end
204
+
205
+ class IfNotDefined < Node
206
+ include Conditional, Container
207
+
208
+ def initialize(macro, parser)
209
+ super parser
210
+ @macro = macro.strip
211
+ end
212
+
213
+ def execute(defines, node)
214
+ if !defines.key? @macro
215
+ execute_body defines, node
216
+ elsif @else
217
+ @else.execute(defines, node)
218
+ end
219
+ end
220
+ end
221
+
222
+ class If < Node
223
+ include Conditional, Container
224
+
225
+ def initialize(expression, parser)
226
+ super parser
227
+ @value = nil
228
+ @expression = expression.strip
229
+ end
230
+
231
+ def execute(defines, node)
232
+ @value = ExpressionEvaluator.new(@expression).evaluate defines
233
+
234
+ if @value
235
+ execute_body(defines, node)
236
+ elsif @else
237
+ @else.execute(defines, node)
238
+ end
239
+ end
240
+ end
241
+
242
+ class Else < Node
243
+ include Container
244
+
245
+ def execute(defines, node)
246
+ execute_body defines, node
247
+ end
248
+ end
249
+
250
+ class ElseIf < If; end
251
+
252
+ class Define < Node
253
+ def initialize(macro, parser)
254
+ super parser
255
+
256
+ macro.strip!
257
+ if index = macro.index(" ")
258
+ @name = macro[0..index-1]
259
+ @value = macro[index+1..-1]
260
+ @name, @value = macro.strip.split
261
+ else
262
+ @name = macro
263
+ @value = "1"
264
+ end
265
+ end
266
+
267
+ def execute(defines, node)
268
+ defines[@name] = @value
269
+ end
270
+ end
271
+
272
+ class Undefine < Node
273
+ def initialize(macro, parser)
274
+ super parser
275
+ @macro = macro.strip
276
+ end
277
+
278
+ def execute(defines, node)
279
+ defines.delete @macro
280
+ end
281
+ end
282
+
283
+ class ParseError < Exception; end
284
+
285
+ # Parses a file for all preprocessor control directives into a tree of Node
286
+ # objects. The parser can operate on a file or an array of lines.
287
+ class FileParser
288
+ attr_reader :line, :directories
289
+
290
+ def initialize(root, directories)
291
+ @stack = [root]
292
+ @directories = directories
293
+ end
294
+
295
+ # Parser operations
296
+
297
+ def add_body(node)
298
+ @stack.last.body << node
299
+ end
300
+
301
+ def add_else(node)
302
+ @stack.last.add_else node
303
+ end
304
+
305
+ def stack_push(node)
306
+ @stack.push node
307
+ end
308
+
309
+ def stack_pop
310
+ @stack.pop
311
+ end
312
+
313
+ def stack_top
314
+ @stack.last
315
+ end
316
+
317
+ def close
318
+ @stack.last.close
319
+ end
320
+
321
+ # Events
322
+
323
+ def process_include(name)
324
+ # We do not process any <files>. This could be enabled as
325
+ # an option, but we don't need it or want it now.
326
+ name =~ /\s*"([^"]+)".*$/
327
+ return unless $1
328
+
329
+ node = IncludedFile.new $1, self
330
+ add_body node
331
+ end
332
+
333
+ def process_ifdef(macro)
334
+ node = IfDefined.new macro, self
335
+ add_body node
336
+ stack_push node
337
+ end
338
+
339
+ def process_ifndef(macro)
340
+ node = IfNotDefined.new macro, self
341
+ add_body node
342
+ stack_push node
343
+ end
344
+
345
+ def process_endif(rest)
346
+ close
347
+ end
348
+
349
+ def process_else(rest)
350
+ node = Else.new self
351
+ add_else node
352
+ end
353
+
354
+ def process_define(macro)
355
+ node = Define.new macro, self
356
+ add_body node
357
+ end
358
+
359
+ def process_undef(macro)
360
+ node = Undefine.new macro, self
361
+ add_body node
362
+ end
363
+
364
+ def process_if(expression)
365
+ node = If.new expression, self
366
+ add_body node
367
+ stack_push node
368
+ end
369
+
370
+ def process_elif(expression)
371
+ node = ElseIf.new expression, self
372
+ add_else node
373
+ add_body node
374
+ stack_push node
375
+ end
376
+
377
+ # Parse methods
378
+
379
+ if defined? Encoding
380
+ def parse_file(name)
381
+ parse IO.readlines(name, :encoding => "ascii-8bit")
382
+ end
383
+ else
384
+ def parse_file(name)
385
+ parse IO.readlines(name)
386
+ end
387
+ end
388
+
389
+ def parse(lines)
390
+ @line = 0
391
+
392
+ lines.each do |line|
393
+ @line += 1
394
+ m = line.match(/^\s*#(include|ifdef|ifndef|endif|else|define|undef|if|elif)(.*)$/)
395
+
396
+ # TODO: continue reading if line ends in \
397
+
398
+ send :"process_#{m[1]}", m[2] if m
399
+ end
400
+ end
401
+ end
402
+
403
+ attr_accessor :file_names, :directories, :defines, :system_defines
404
+ attr_reader :sources
405
+
406
+ def initialize(cc, files, directories=[], defines=nil)
407
+ @cc = cc
408
+ @file_names = files
409
+ @directories = directories
410
+ @defines = defines
411
+
412
+ @system_defines = {}
413
+ @sources = []
414
+ end
415
+
416
+ def get_system_defines
417
+ lines = `#{@cc} -dM -E #{@defines} - < #{DEV_NULL}`.split("\n")
418
+
419
+ source = SourceFile.new "sytem_defines", self
420
+ parser = FileParser.new source, @directories
421
+ parser.parse lines
422
+
423
+ source.execute @system_defines
424
+ end
425
+
426
+ def process
427
+ get_system_defines
428
+
429
+ @file_names.each do |name|
430
+ source = SourceFile.new name, self
431
+ parser = FileParser.new source, @directories
432
+
433
+ parser.parse_file name
434
+ source.execute @system_defines.dup
435
+ source.collect_dependencies
436
+
437
+ @sources << source
438
+ end
439
+ end
440
+
441
+ def print_dependencies(out, max=72)
442
+ @sources.each do |source|
443
+ source.print_dependencies out, max
444
+ end
445
+ end
446
+ end
447
+ end
448
+
@@ -0,0 +1,3 @@
1
+ module Daedalus
2
+ VERSION = "0.0.1"
3
+ end