Mercurial > projects > dstep
diff sciprts/dstepgen.rb @ 0:c7db221de6e8
First upload of dgen.rb and dstepgen.rb
author | Jacob Carlborg <doob@me.com> |
---|---|
date | Sat, 31 Jan 2009 17:22:44 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sciprts/dstepgen.rb Sat Jan 31 17:22:44 2009 +0100 @@ -0,0 +1,1388 @@ +#!/usr/bin/env ruby + +# Copyright (c) 2008-2009, Jacob Carlborg. All rights reserved. +# Copyright (c) 2006-2007, Apple Inc. All rights reserved. +# Copyright (c) 2005-2006 FUJIMOTO Hisakuni +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of Apple Inc. ("Apple") nor the names of +# its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +require "rubygems" +gem "builder", "~> 2.0" +require "builder" +require "tmpdir" +require "optparse" +include Builder + +$KCODE = "UTF8" + +# Extensions that adds support for member access syntax +class Hash + def type + result = self["type"] + return result unless result.nil? + self[:type] + end + + def type= (type) + self[:type] = type + end + + def id + result = self["id"] + return result unless result.nil? + self[:id] + end + + def id= (id) + self[:id] = id + end + + def methods + result = self["methods"] + return result unless result.nil? + self[:methods] + end + + def methods= (methods) + self[:methods] = methods + end + + def method_missing (method, *args) + self.class.instance_eval do + define_method(method) do |*args| + if args.length > 0 + self[method[0 ... -1]] = args[0] + self[eval(":#{method}"[0 ... -1])] = args[0] + else + result = self[method] + return result unless result.nil? + self[eval(":#{method}")] + end + end + end + + if (method = method.id2name) =~ /=/ + eval("self.#{method} (args.length < 2 ? args[0] : args)") + else + eval("self.#{method}") + end + end +end + +# This Struct represents an Objective-C Framework +Framework = Struct.new(:name, :headers) do + def initialize + self.headers = [] + self.name = "" + end +end + +# This Struct represents a C/Objective-C header +HeaderFile = Struct.new(:name, :framework, :cftypes, :constants, :defines, :enums, :externs, :functions, + :imports, :inline_functions, :opaques, :path, :structs, :typedefs) do + def initialize + self.name = "" + self.cftypes = [] + self.constants = [] + self.defines = [] + self.enums = [] + self.externs = [] + self.framework = "" + self.functions = [] + self.imports = [] + self.inline_functions = [] + self.opaques = [] + self.path = "" + self.structs = [] + self.typedefs = [] + end +end + +# This class scans the headers +class HeaderScaner + CPP = ['/usr/bin/cpp-4.0', '/usr/bin/cpp-3.3', '/usr/bin/cpp3'].find { |x| File.exist?(x) } + raise "cpp not found" if CPP.nil? + CPPFLAGS = "-D__APPLE_CPP__ -include /usr/include/AvailabilityMacros.h" + CPPFLAGS << "-D__GNUC__" unless /\Acpp-4/.match(File.basename(CPP)) + + attr_accessor :frameworks, :headers, :do_64bit + + def initialize + @extern_name = 'extern' + @frameworks = [] + @file_content = nil + @headers = [] + @classes = {} + @informal_protocols = {} + @function_pointer_types = {} + @do_64bit = false + end + + def classes + @classes + end + + def protocols + @informal_protocols + end + + def cftypes (header) + re = /typedef\s+(const\s+)?(struct\s*\w+\s*\*\s*)([^\s]+Ref)\s*;/ + @cpp_result.scan(re).each do |m| + header.cftypes << { :name => m[2], :type => m[1], :const => m[0] =~ /const/ ? true : false} + end + end + + def constants (header) + tmp = header.externs.map do |i| + constant?(i, true) + end + + header.constants = tmp.flatten.compact + end + + def defines (header) + re = /#define\s+([^\s]+)\s+(\([^)]+\)|[^\s]+)\s*$/ + @file_content.scan(re).each do |m| + next unless !m[0].include?('(') && m[1] != '\\' + header.defines << { :name => m[0], :value => m[1] } + end + end + + def enums (header) + re = /\benum\b\s*(\w+\s+)?\{([^}]*)\}/ + @cpp_result.scan(re).each do |m| + enum = { :name => m[0], :members => [] } + + m[1].split(",").map do |i| + name, value = i.split("=", 2).map do |x| + x.strip + end + + enum[:members] << { :name => name, :value => value } unless name.empty? || name[0] == ?# + end + + header.enums << enum + end + end + + def function_pointer_types (header) + re = /typedef\s+([\w\s]+)\s*\(\s*\*\s*(\w+)\s*\)\s*\(([^)]*)\)\s*;/ + data = @cpp_result.scan(re) + re = /typedef\s+([\w\s]+)\s*\(([^)]+)\)\s*;/ + data |= @cpp_result.scan(re).map do |m| + ary = m[0].split(/(\w+)$/) + ary[1] << " *" + ary << m[1] + ary + end + + data.each do |m| + name = m[1] + args = m[2].split(",").map do |x| + if x.include?(" ") + ptr = x.sub!(/\[\]\s*$/, "") + x = x.sub(/\w+\s*$/, "").strip + ptr ? x + "*" : x + else + x.strip + end + end + + type = "#{m[0]}(*)(#{args.join(', ')})" + @function_pointer_types[name] = type + end + end + + def typedefs (header) + re = /^\s*typedef\s+(.+)\s+([\w\*]+)\s*;$/ + data = @cpp_result + data.scan(re).each do |m| + var = get_var_info(m[0] + " " + m[1]) + header.typedefs << get_var_info(m[0] + " " + m[1]) if var + end + end + + def externs (header) + re = /^\s*#{@extern_name}\s+\b(.*)\s*;.*$/ + header.externs = @cpp_result.scan(re).map do |m| + m[0].strip + end + end + + def imports (header) + tmp = [] + + @file_content.each do |line| + if line =~ /#(include|import) <(.+)\.h>/ + next if $2 == header.name + tmp << $2 + elsif line =~ /@class(\s+)(\w+)(,|;)/ + next if $2 == header.name + + if $3 == ";" + tmp << header.framework + "/" + $2 unless header.framework == "" + tmp << $2 if header.framework == "" + elsif + str = line[6 + $1.length ... -2] + str.gsub!(" ", "") + arr = str.split(",") + + arr.each do |s| + tmp << header.framework + "/" + s unless header.framework == "" + tmp << s if header.framework == "" + end + end + end + end + + header.imports = tmp.compact.uniq + end + + def informal_protocols (header) + self.methods(header) + end + + def methods (header) + interface_re = /^@(interface|protocol)\s+(\w+)\s*:?\s*(\w*)\s*(\([^)]+\))?/ + + end_re = /^@end/ + body_re = /^[-+]\s*(\([^)]+\))?\s*([^:\s;]+)/ + args_re = /\w+\s*:/ + prop_re = /^@property\s*(\([^)]+\))?\s*([^;]+);$/ + current_interface = current_category = nil + i = 0 + parent = nil + + @cpp_result.each_line do |line| + size = line.size + line.strip! + + if md = interface_re.match(line) + parent = nil + current_interface = md[1] == "protocol" ? "NSObject" : md[2] + current_category = md[4].delete("()").strip if md[4] + parent = md[3] unless md[3] == "" + + elsif end_re.match(line) + current_interface = current_category = nil + + elsif current_interface && md = prop_re.match(line) + # Parsing Objective-C 2.0 properties + if (a = md[2].split(/\s/)).length >= 2 && /^\w+$/.match(name = a[-1]) && (type = a[0 .. -2].join(" ")).index(",").nil? + getter, setter = name, "set#{name[0].chr.upcase + name[1 .. -1]}" + readonly = false + + if attributes = md[1] + if md = /getter\s*=\s*(\w+)/.match(attributes) + getter = md[1] + end + + if md = /setter\s*=\s*(\w+)/.match(attributes) + setter = md[1] + end + + readonly = true if attributes.index("readonly") + end + + typeinfo = VarInfo.new(type, "", "") + + @classes[current_interface] ||= {} + methods = (@classes[current_interface].methods ||= []) + methods << MethodInfo.new(typeinfo, getter, false, [], line) + + unless readonly + methods << MethodInfo.new(VarInfo.new("void", "", ""), setter + ":", false, [typeinfo], line) + end + end + + elsif current_interface && (line[0] == ?+ || line[0] == ?-) + mtype = line[0] + data = @cpp_result[i .. -1] + body_md = body_re.match(data) + + next if body_md.nil? + + rettype = body_md[1] ? body_md[1].delete("()") : "id" + retval = VarInfo.new(rettype, "", "") + args = [] + selector = "" + data = data[0 .. data.index(";")] + args_data = [] + + data.scan(args_re) do |x| + args_data << [$`, x, $'] + end + + variadic = false + args_data.each_with_index do |ary, n| + before, argname, argtype = ary + arg_nameless = (n > 0 && /\)\s*$/.match(before)) + argname = ":" if arg_nameless + realargname = nil + + if n < args_data.length - 1 + argtype.sub!(args_data[n + 1][2], "") + + if arg_nameless + argtype.sub!(/(\w+\s*)?\w+\s*:\s*$/, "") + else + unless argtype.sub!(/(\w+)\s+\w+:\s*$/) { |s| realargname = $1; "" } + # maybe the next argument is nameless + argtype.sub!(/\w+\s*:\s*$/, "") + end + end + else + argtype.sub!(/\s+__attribute__\(\(.+\)\)/, "") + + if arg_nameless + argtype.sub!(/\w+\s*;$/, "") + else + unless argtype.sub!(/(\w+)\s*;$/) { |s| realargname = $1; "" } + variadic = argtype.sub!(/,\s*\.\.\.\s*;/, "") != nil + argtype.sub!(/\w+\s*$/, "") if variadic + end + end + end + + selector << argname + realargname ||= argname.sub(/:/, "") + args << VarInfo.new(argtype, realargname, "") unless argtype.empty? + end + + selector = body_md[2] if selector.empty? + args << VarInfo.new("...", "vararg", "") if variadic + method = MethodInfo.new(retval, selector, line[0] == ?+, args, data) + + if current_category && current_interface == "NSObject" + (@informal_protocols[current_category] ||= []) << method + end + + @classes[current_interface] ||= {} + + if header.name == current_interface + @classes[current_interface].file = header.name + else + @classes[current_interface].file ||= header.name + end + + unless parent == current_interface || parent =~ /\s+/ || parent.nil? + @classes[current_interface].parent = parent + end + + (@classes[current_interface].methods ||= []) << method + end + i += size + end + end + + def structs (header) + re = /typedef\s+struct\s*\w*\s*((\w+)|\{([^{}]*(\{[^}]+\})?)*\}\s*([^\s]+))\s*(__attribute__\(.+\))?\s*;/ + i = 0 + body = nil + @cpp_result.scan(re).each do |m| + struct = { :name => m[4], :members => [] } + + unless struct[:name].nil? + if struct[:name][0, 1] == "*" + struct[:name].sub!("*", "") + end + end + + return_type = nil + stripped_return_type = nil + body = m[2] + + if m[2] + m[2].split(/,|;/).map do |i| + str, bytes = i.split(":", 2).map do |x| + x.strip + end + + var = get_var_info(str, true) + + if var + if var.return_type == "***dummy***" + var.return_type = return_type + var.stripped_return_type = stripped_return_type + else + return_type = var.return_type + stripped_return_type = var.stripped_return_type + end + + struct[:members] << { :name => var.name, :bytes => bytes, :declaredType => var.return_type, :type => "", :type64 => "" } unless str.empty? || str[0] == ?# + + names = [] + + tmp = struct[:members].collect do |member| + unless names.include?(member[:name]) + names << member[:name] + member + end + end + + struct[:members] = tmp.compact + end + end + end + + header.structs << struct if body + end + end + + def functions (header, inline = false) + if inline + inline_func_re = /(inline|__inline__)\s+((__attribute__\(\([^)]*\)\)\s+)?([\w\s\*<>]+)\s*\(([^)]*)\)\s*)\{/ + res = @cpp_result.scan(inline_func_re) + res.each do |x| + x.delete_at(0) + x.delete_at(1) + end + else + skip_inline_re = /(static)?\s(inline|__inline__)[^{;]+(;|\{([^{}]*(\{[^}]+\})?)*\})\s*/ + func_re = /(^([\w\s\*<>]+)\s*\(([^)]*)\)\s*)(__attribute__[^;]+)?;/ + res = @cpp_result.gsub(skip_inline_re, '').scan(func_re) + end + + funcs = res.map do |m| + orig, base, args = m + base.sub!(/^.*extern\s/, "") + func = constant?(base) + + if func + args = args.strip.split(",").map do |i| + constant?(i) + end + + next if args.any? do |x| + x.nil? + end + + args = [] if args.size == 1 && args[0].return_type == "void" + FunctionInfo.new(func, args, orig, inline) + end + end.compact + + if inline + header.inline_functions = funcs + else + header.functions = funcs + end + end + + def prepare (path) + @file_content = File.read(path) + @file_content.gsub!(%r{(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)}, ""); + @complete_cpp_result, @cpp_result = do_cpp(path, false, true, "") + end + + def scan (frameworks, headers) + @frameworks = frameworks + @headers = headers + + @frameworks.each do |framework| + framework.headers.each do |header| + prepare(header.path) + + imports(header) + cftypes(header) + externs(header) + constants(header) + enums(header) + structs(header) + typedefs(header) + functions(header) + functions(header, true) + defines(header) + methods(header) + function_pointer_types(header) + end + end + + @headers.each do |header| + prepare(header.path) + + imports(header) + cftypes(header) + externs(header) + constants(header) + enums(header) + structs(header) + typedefs(header) + functions(header) + functions(header, true) + defines(header) + methods(header) + function_pointer_types(header) + end + end + + def get_var_info (str, multi = false) + str.strip! + + return nil if str.empty? + + if str == "..." + VarInfo.new("...", "...", str) + else + str = "***dummy*** " + str if str[-1].chr == '*' || str.index(/\s/).nil? + tokens = multi ? str.split(',') : [str] + part = tokens.first + re = /^([^()]*)\b(\w+)\b\s*(\[[^\]]*\])*$/ + m = re.match(part) + + if m + return nil if m[1].split(/\s+/).any? do |x| + ['end', 'typedef'].include?(x) + end + + m = m.to_a[1..-1].compact.map do |i| + i.strip + end + + m[0] += m[2] if m.size == 3 + m[0] = 'void' if m[1] == 'void' + + var = begin + VarInfo.new(m[0], m[1], part) + rescue + return nil + end + + if tokens.size > 1 + [var, *tokens[1..-1].map { |x| constant?(m[0] + x.strip.sub(/^\*+/, '')) }] + else + var + end + end + end + end + + def constant? (str, multi = false) + str.strip! + return nil if str.empty? + + if str == '...' + VarInfo.new('...', '...', str) + else + str << " dummy" if str[-1].chr == '*' or str.index(/\s/).nil? + tokens = multi ? str.split(',') : [str] + part = tokens.first + re = /^([^()]*)\b(\w+)\b\s*(\[[^\]]*\])*$/ + m = re.match(part) + + if m + return nil if m[1].split(/\s+/).any? do |x| + ['end', 'typedef'].include?(x) + end + + m = m.to_a[1..-1].compact.map do |i| + i.strip + end + + m[0] += m[2] if m.size == 3 + m[0] = 'void' if m[1] == 'void' + + var = begin + VarInfo.new(m[0], m[1], part) + rescue + return nil + end + + if tokens.size > 1 + [var, *tokens[1..-1].map { |x| constant?(m[0] + x.strip.sub(/^\*+/, '')) }] + else + var + end + end + end + end + + class VarInfo + + attr_reader :name, :orig, :const, :type_modifier + attr_accessor :octype, :resolved_type, :resolved_type64, :return_type, :stripped_return_type + + def initialize (type, name, orig) + @return_type = type.clone + @name = name + @orig = orig + @const = false + @type_modifier = "" + + @return_type.gsub!(/\[[^\]]*\]/, "*") + + if @return_type =~ /\bconst\b/ + @const = true + @return_type.gsub!("const", "") + end + + if @return_type =~ /\b(in|out|inout|oneway|bycopy|byref)\b/ + + case $1 + when "in" + @type_modifier << "n" + when "out" + @type_modifier << "o" + when "inout" + @type_modifier << "N" + when "oneway" + @type_modifier << "w" + when "bycopy" + @type_modifier << "c" + when "byref" + @type_modifier << "r" + end + + @return_type.gsub!("#{$1}", "") + end + + @return_type.gsub!(/\s?\*\s?/, "*") + @return_type.gsub!(/^\(|\)$/, "") + @return_type.strip! + + t = type.gsub(/\b(__)?const\b/, "") + t.gsub!(/<[^>]*>/, '') + t.gsub!(/\b(in|out|inout|oneway|const)\b/, "") + t.gsub!(/\b__private_extern__\b/, "") + t.gsub!(/^\s*\(?\s*/, "") + t.gsub!(/\s*\)?\s*$/, "") + + raise "Empty type (was '#{type}')" if t.empty? + + @stripped_return_type = t + end + + def function_pointer? (function_pointer_types) + type = @function_pointer_types[@stripped_return_type] || @stripped_return_type + @function_pointer_type ||= FunctionPointerInfo.new_from_type(type) + end + + def <=>(x) + self.name <=> x.name + end + + def hash + @name.hash + end + + def eql? (o) + @name == o.name + end + end + + class FunctionInfo < VarInfo + + attr_reader :args, :argc + + def initialize (function, args, orig, inline = false) + super(function.return_type, function.name, orig) + + @args = args + @argc = @args.size + @variadic = false + + if @args[-1] && @args[-1].return_type == "..." + @argc -= 1 + @variadic = true + @args.pop + end + + @inline = inline + self + end + + def variadic? + @variadic + end + + def inline? + @inline + end + end + + class FunctionPointerInfo < FunctionInfo + def initialize (return_type, arg_types, orig) + args = arg_types.map do |x| + VarInfo.new(x, "", "") + end + + super(VarInfo.new(return_type, "", ""), args, orig) + end + + def self.new_from_type (type) + @cache ||= {} + info = @cache[type] + + return info if info + + tokens = type.split(/\(\*\)/) + return nil if tokens.size != 2 + + return_type = tokens.first.strip + rest = tokens.last.sub(/^\s*\(\s*/, "").sub(/\s*\)\s*$/, "") + + arg_types = rest.split(/,/).map do |x| + x.strip + end + + @cache[type] = self.new(return_type, arg_types, type) + end + end + + class MethodInfo < FunctionInfo + + attr_reader :selector + + def initialize (method, selector, is_class, args, orig) + super(method, args, orig) + + @selector = selector + @is_class = is_class + self + end + + def class_method? + @is_class + end + + def <=>(o) + @selector <=> o.selector + end + + def hash + @selector.hash + end + + def eql? (o) + @selector == o.selector + end + end + + def do_cpp (path, fails_on_error = true, do_64 = true, flags = "") + f_on = false + err_file = '/tmp/.cpp.err' + cpp_line = "#{CPP} #{CPPFLAGS} #{flags} #{do_64 ? '-D__LP64__' : ''} \"#{path}\" 2>#{err_file}" + complete_result = `#{cpp_line}` + + if $?.to_i != 0 && fails_on_error + $stderr.puts File.read(err_file) + File.unlink(err_file) + raise "#{CPP} returned #{$?.to_int/256} exit status\nline was: #{cpp_line}" + end + + result = complete_result.select do |s| + # First pass to only grab non-empty lines and the pre-processed lines + # only from the target header (and not the entire pre-processing result). + next if s.strip.empty? + m = %r{^#\s*\d+\s+"([^"]+)"}.match(s) + f_on = (File.basename(m[1]) == File.basename(path)) if m + f_on + end.select do |s| + # Second pass to ignore all pro-processor comments that were left. + /^#/.match(s) == nil + end.join + + File.unlink(err_file) + return [complete_result, result] + end +end + +class DStepGenerator + + VERSION = 1.0 + + attr_accessor :out_file, :scaner + + def initialize + @do_64bit = false + @frameworks = [] + @framework_paths = [] + @headers = [] + @import_directives = "" + @informal_protocols = [] + @classes = [] + @scaner = HeaderScaner.new + @scaner.do_64bit = @do_64bit + end + + def do_64bit + return @do_64bit + end + + def do_64bit= (do_64bit) + @do_64bit = do_64bit + @scaner.do_64bit = do_64bit + end + + def collect + scaner.scan(@frameworks, @headers) + @classes = scaner.classes + @informal_protocols = scaner.protocols + end + + def handle_framework (framework) + val = framework.name + path = framework_path(val) + + raise "Can't locate framework '#{val}'" if path.nil? + @framework_paths << File.dirname(path) + raise "Can't find framework '#{val}'" if path.nil? + + parent_path, name = path.scan(/^(.+)\/(\w+)\.framework\/?$/)[0] + + if @private + headers_path = File.join(path, "PrivateHeaders") + raise "Can't locate private framework headers at '#{headers_path}'" unless File.exist?(headers_path) + + headers = Dir.glob(File.join(headers_path, "**", "*.h")) + public_headers_path = File.join(path, "Headers") + public_headers = if File.exist?(public_headers_path) + HeaderScaner::CPPFLAGS << " -I#{public_headers_path} " + Dir.glob(File.join8(headers_path, "**", "*.h")) + else + [] + end + else + headers_path = File.join(path, "Headers") + raise "Can't locate public framework headers at '#{headers_path}'" unless File.exist?(headers_path) + public_headers = headers = Dir.glob(File.join(headers_path, "**", "*.h")) + end + + # We can't just "#import <x/x.h>" as the main Framework header might not include _all_ headers. + # So we are tricking this by importing the main header first, then all headers. + header_basenames = (headers | public_headers).map do |x| + x.sub(/#{headers_path}\/*/, "") + end + + if idx = header_basenames.index("#{name}.h") + header_basenames.delete_at(idx) + header_basenames.unshift("#{name}.h") + end + + @import_directives = header_basenames.map do |x| + "#import <#{name}/#{x}>" + end.join("\n") + + @import_directives << "\n" + + @compiler_flags ||= "-F\"#{parent_path}\" -framework #{name}" + @cpp_flags ||= "" + @cpp_flags << "-F\"#{parent_path}\" " + + headers.each do |header| + header_file = HeaderFile.new + header_file.path = header + header_file.name = File.basename(header, File.extname(header)) + header_file.framework = framework.name + + framework.headers << header_file + end + + # Memorize the dependencies. + @dependencies = DStepGenerator.dependencies_of_framework(path) + end + + def framework_path (val) + return path if File.exist?(val) + + val += ".framework" unless /\.framework$/.match(val) + paths = ["/System/Library/Frameworks", "/Library/Frameworks", "#{ENV['HOME']}/Library/Frameworks"] + paths << "/System/Library/PrivateFrameworks" if @private + + paths.each do |dir| + path = File.join(dir, val) + return path if File.exist?(path) + end + + return nil + end + + def self.dependencies_of_framework (path) + @dependencies ||= {} + name = File.basename(path, ".framework") + path = File.join(path, name) + deps = @dependencies[path] + + if deps.nil? + deps = `otool -L "#{path}"`.scan(/\t([^\s]+)/).map do |m| + dpath = m[0] + next if File.basename(dpath) == name + next if dpath.include?("PrivateFrameworks") + next unless dpath.sub!(/\.framework\/Versions\/\w+\/\w+$/, "") + dpath + ".framework" + end.compact + + @dependencies[path] = deps + end + + return deps + end + + def compile_and_execute (code, enable_64bit = false) + compiler_line = "gcc " + src = File.new(unique_tmp_path("src", ".m"), "w") + src << code + src.close + + arch_flag = if enable_64bit + "-arch x86_64 -arch ppc64" + else + "-arch i386 -arch ppc" + end + + compiler_line << arch_flag + + bin = unique_tmp_path "bin" + log = unique_tmp_path "log" + + line = "#{compiler_line} -o #{bin} #{src.path} #{@compiler_flags}> #{log}" + + unless system(line) + msg = "Cannot compile Objective-C code ...aborting\nCommand was: #{line}\n\nLog:\n#{File.read(log)}\n\n" + $stderr << msg + File.delete src.path + raise msg + end + + result = `#{bin}` + + unless $?.success? + raise "Cannot execute compiled Objective-C code ... aborting\nCommand was: #{line}\nBinary is: #{bin}" + end + + File.delete bin + File.delete log + File.delete src.path + + return result + end + + def unique_tmp_path (base, extension = "", dir = Dir.tmpdir) + i = 0 + loop do + path = File.join(dir, "#{base}-#{Process.pid}-#{i}#{extension}") + return path unless File.exists?(path) + i += 1 + end + end + + def add_header (path) + header = HeaderFile.new + header.path = path + header.name = File.basename(path, File.extname(path)) + @import_directives << "#include <#{path}>\n" + @headers << header + end + + def add_framework (name) + framework = Framework.new + framework.name = name + handle_framework(framework) + #@import_directives << "#import <#{framework.name}/#{framework.name}.h>\n" + @frameworks << framework + end + + def collect_header_types (header, enable_64bit) + types = [] + + header.cftypes.each do |cftype| + types << cftype[:type] + end + + header.constants.each do |constant| + types << constant.stripped_return_type + end + + header.structs.each do |struct| + types << struct[:name] + + struct[:members].each do |member| + types << member[:declaredType] + end + end + + header.typedefs.each do |typedef| + types << typedef.stripped_return_type + end + + header.functions.each do |function| + types << function.stripped_return_type + + function.args.each do |arg| + types << arg.stripped_return_type + end + end + + types + end + + def collect_classes_types (enable_64bit) + types = [] + + @classes.each do |clazz, value| + value.methods.each do |method| + types << method.stripped_return_type + + method.args.each do |arg| + types << arg.stripped_return_type + end + end + end + + types + end + + def collect_informal_protocols_types (enable_64bit) + types = [] + + @informal_protocols.each do |name, methods| + methods.each do |method| + types << method.stripped_return_type + + method.args.each do |arg| + types << arg.stripped_return_type + end + end + end + + types + end + + def resolve_header_types (header, enable_64bit, resolved_types, x) + i = x + + header.cftypes.each do |cftype| + cftype[enable_64bit ? :type64 : :type] = resolved_types[i] + i += 1 + end + + header.constants.each do |constant| + constant.resolved_type = resolved_types[i] unless enable_64bit + constant.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + end + + header.structs.each do |struct| + struct[enable_64bit ? :type64 : :type] = resolved_types[i] + i += 1 + + struct[:members].each do |member| + member[enable_64bit ? :type64 : :type] = resolved_types[i] + i += 1 + end + end + + header.typedefs.each do |typedef| + typedef.resolved_type = resolved_types[i] unless enable_64bit + typedef.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + end + + header.functions.each do |function| + function.resolved_type = resolved_types[i] unless enable_64bit + function.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + + function.args.each do |arg| + arg.resolved_type = resolved_types[i] unless enable_64bit + arg.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + end + end + + i + end + + def resolve_methods_types (enable_64bit, resolved_types, x) + i = x + + @classes.each do |clazz, value| + value.methods.each do |method| + method.resolved_type = resolved_types[i] unless enable_64bit + method.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + + method.args.each do |arg| + arg.resolved_type = resolved_types[i] unless enable_64bit + arg.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + end + end + end + + i + end + + def resolve_informal_protocols_types (enable_64bit, resolved_types, x) + i = x + + @informal_protocols.each do |name, methods| + methods.each do |method| + method.resolved_type = resolved_types[i] unless enable_64bit + method.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + + method.args.each do |arg| + arg.resolved_type = resolved_types[i] unless enable_64bit + arg.resolved_type64 = resolved_types[i] if enable_64bit + i += 1 + end + end + end + + i + end + + def resolve_types (enable_64bit = false) + code = "#include <stdio.h>\n" + code << @import_directives + types = [] + + @frameworks.each do |framework| + framework.headers.each do |header| + types << collect_header_types(header, enable_64bit) + end + end + + @headers.each do |header| + types << collect_header_types(header, enable_64bit) + end + + types << collect_classes_types(enable_64bit) + types << collect_informal_protocols_types(enable_64bit) + + code << "int main () \n{\n" + types.flatten! + + types.each do |type| + code << ' printf("%s\n", ' + "@encode(#{type}));\n" + end + + code << " return 0;\n}" + + resolved_types = [] + + compile_and_execute(code, enable_64bit).split("\n").each do |line| + resolved_types << line + end + + i = 0 + + @frameworks.each do |framework| + framework.headers.each do |header| + i = resolve_header_types(header, enable_64bit, resolved_types, i) + end + end + + @headers.each do |header| + i = resolve_header_types(header, enable_64bit, resolved_types, i) + end + + i = resolve_methods_types(enable_64bit, resolved_types, i) + i = resolve_informal_protocols_types(enable_64bit, resolved_types, i) + end + + def generate_header (xml, header) + xml.file :name => header.name do + header.imports.each do |import| + xml.import import + end + + header.defines.each do |define| + xml.define define + end + + header.cftypes.each do |cftype| + xml.cftype cftype + end + + header.constants.each do |constant| + xml.constant :name => constant.name, :declaredType => constant.return_type, :type => constant.resolved_type, :type64 => constant.resolved_type64, :const => constant.const + end + + header.enums.each do |enum| + xml.enum :name => enum[:name] do + enum[:members].each do |member| + xml.member member + end + end + end + + header.structs.each do |struct| + xml.struct :name => struct[:name], :type => struct[:type], :type64 => struct[:type64] do + struct[:members].each do |member| + xml.member member + end + end + end + + header.typedefs.each do |typedef| + xml.typedef :name => typedef.name, :declaredType => typedef.return_type, :type => typedef.resolved_type, :type64 => typedef.resolved_type64, :const => typedef.const + end + + header.functions.each do |function| + xml.function :name => function.name, :inline => function.inline?, :variadic => function.variadic? do + function.args.each do |arg| + xml.arg :name => arg.name, :declaredType => arg.return_type, :type => arg.resolved_type, :type64 => arg.resolved_type64, :const => arg.const, :typeModifier => arg.type_modifier + end + + xml.returnValue :declaredType => function.return_type, :type => function.resolved_type, :type64 => function.resolved_type64, :const => function.const, :typeModifier => function.type_modifier + end + end + end + end + + def generate_classes (xml) + @classes.each do |clazz, value| + xml.class :name => clazz, :parent => value.parent, :file => value.file do + value.methods.each do |method| + xml.method :selector => method.selector, :classMethod => method.class_method?, :variadic => method.variadic? do + method.args.each do |arg| + xml.arg :name => arg.name, :declaredType => arg.return_type, :type => arg.resolved_type, :type64 => arg.resolved_type64, :const => arg.const, :typeModifier => arg.type_modifier + end + + xml.returnValue :declaredType => method.return_type, :type => method.resolved_type, :type64 => method.resolved_type64, :const => method.const, :typeModifier => method.type_modifier + end + end + end + end + end + + def generate_informal_protocols (xml) + @informal_protocols.each do |name, methods| + xml.informalProtocol :name => name do + methods.each do |method| + xml.method :selector => method.selector, :classMethod => method.class_method?, :variadic => method.variadic? do + method.args.each do |arg| + xml.arg :name => arg.name, :declaredType => arg.return_type, :type => arg.resolved_type, :type64 => arg.resolved_type64, :const => arg.const, :typeModifier => arg.type_modifier + end + + xml.returnValue :declaredType => method.return_type, :type => method.resolved_type, :type64 => method.resolved_type64, :const => method.const, :typeModifier => method.type_modifier + end + end + end + end + end + + def generate + resolve_types + resolve_types(true) if @do_64bit + + file = STDOUT if @out_file == nil + file = File.open @out_file, "w" unless @out_file == nil + + xml = XmlMarkup.new(:target => file, :indent => 4) + xml.instruct! + + xml.dstep :xmlns => "http://www.dsource.org/projects/dstep", "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation" => "http://www.dsource.org/projects/dstep/trunk/scripts/dstep.xsd" do + @frameworks.each do |framework| + xml.framework :name => framework.name do + framework.headers.each do |header| + generate_header(xml, header) + end + end + end + + @headers.each do |header| + generate_header(xml, header) + end + + generate_classes(xml) + generate_informal_protocols(xml) + end + + file.close unless file == STDOUT + end +end + +def die (*msg) + $stderr.puts msg + exit 1 +end + +if __FILE__ == $0 + dstep_gen = DStepGenerator.new + + OptionParser.new do |opts| + opts.banner = "Usage: #{File.basename(__FILE__)} [options] <headers...>" + opts.separator "" + opts.separator "Options:" + + opts.on("-f", "--framework FRAMEWORK", "Generate metadata for the given framework.") do |opt| + dstep_gen.add_framework(opt) + end + + opts.on(nil, "--64-bit", "Write 64-bit annotations.") do + dstep_gen.do_64bit = true + end + + opts.on("-o", "--output FILE", "Write output to the given file.") do |opt| + die "Output file can't be specified more than once" if dstep_gen.out_file + dstep_gen.out_file = opt + end + + # opts.on("-d", "--output-dir PATH", "Write ouptut to the given paht, use this with the --framework option") do |opt| + # die "Output directory can't be specified more than once" if dstep_gen.out_dir + # dstep_gen.out_dir = opt + # end + + + + help_msg = "Use the `-h' flag or for help." + + opts.on("-h", "--help", "Show this message.") do + puts opts, help_msg + exit + end + + opts.on('-v', '--version', 'Show version.') do + puts DStepGenerator::VERSION + exit + end + + opts.separator "" + + if ARGV.empty? + die opts.banner + else + begin + opts.parse!(ARGV) + + ARGV.each do |header| + dstep_gen.add_header(header) + end + + dstep_gen.collect + dstep_gen.generate + rescue => e + msg = e.message + msg = "Internal error" if msg.empty? + + die msg, opts.banner, help_msg + end + end + end +end \ No newline at end of file