daedalus-core 0.0.1

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