1#!/usr/bin/env ruby -Ku 2# vim:ts=2:sw=2:expandtab: 3# Copyright (c) 2006-2007, The RubyCocoa Project. 4# Copyright (c) 2007 Chris Mcgrath. (from nb_nibtool.rb) 5# Copyright (c) 2007 cho45. (rubycocoa command) 6# All Rights Reserved. 7# 8# RubyCocoa is free software, covered under either the Ruby's license or the 9# LGPL. See the COPYRIGHT file for more information. 10# 11# 12# This command does NOT require rubygems/rake 13# but the generated templates may include Rakefile and gem dependency. 14 15require "optparse" 16require "pathname" 17require 'fileutils' 18require "osx/cocoa" 19require "osx/xcode" 20require "osx/standaloneify" 21require "nkf" 22require "iconv" 23require "erb" 24include FileUtils 25include OSX 26 27class RubyCocoaCommand 28 VERSION = "$Revision: 1846 $" 29 CONFIG_DIR = Pathname.new("#{ENV["HOME"]}/.rubycocoa") 30 ORGANIZATIONNAME = (ENV['ORGANIZATIONNAME'].nil? ? '«ORGANIZATIONNAME»' : ENV['ORGANIZATIONNAME']) 31 32 def self.run(argv) 33 new(argv.dup).run 34 end 35 36 def initialize(argv) 37 @argv = argv 38 39 @subparsers = { 40 "help" => OptionParser.new { |opts| 41 opts.banner = <<-EOB.gsub(/^ {10}/, "") 42 Usage: rubycocoa help <subcommand> 43 44 Show help of subcommand. 45 EOB 46 }, 47 48 "new" => OptionParser.new {|opts| 49 opts.banner = <<-EOB.gsub(/^ {10}/, "") 50 Usage: rubycocoa new [options] "Application Name" 51 52 Create new application skelton. 53 EOB 54 55 opts.separator "" 56 57 opts.separator "Options:" 58 opts.on("--template TEMPLATE", "specify templates (template dir.)") {|@template|} 59 }, 60 61 "create" => OptionParser.new {|opts| 62 opts.banner = <<-EOB.gsub(/^ {10}/, "") 63 Usage: rubycocoa create [options] ClassName[<SuperClass=NSObject] 64 65 Create a ruby class skelton. 66 EOB 67 68 opts.separator "" 69 70 @actions = [] 71 @outlets = [] 72 opts.separator "Options:" 73 opts.on("-a NAME", "--action NAME", "With action") {|action| @actions << action} 74 opts.on("-o NAME", "--outlet NAME", "With outlet") {|outlet| @outlets << outlet} 75 }, 76 77 "update" => OptionParser.new {|opts| 78 opts.banner = <<-EOB.gsub(/^ {10}/, "") 79 Usage: rubycocoa update [options] <Nib File> <Ruby File> 80 81 Update nib with ruby class difinition. 82 EOB 83 opts.separator "" 84 85 opts.separator "Options:" 86 opts.on("-a", "--add_class", "Add class if the class is not in nib.") {|@add_class|} 87 }, 88 89 "convert" => OptionParser.new {|opts| 90 opts.banner = <<-EOB.gsub(/^ {10}/, "") 91 Usage: rubycocoa convert [options] <Nib File | Obj-c Header File> 92 93 Generate files including ruby class from a nib or header. 94 EOB 95 96 opts.separator "" 97 98 opts.separator "Options:" 99 opts.on("--overwrite", "overwite all files") {|@overwrite|} 100 }, 101 102 "add" => OptionParser.new {|opts| 103 opts.banner = <<-EOB.gsub(/^ {10}/, "") 104 Usage: rubycocoa add [options] <Add Files> <Xcode Project> 105 106 Add files to Xcode project and resource build phase. 107 EOB 108 opts.separator "" 109 110 opts.separator "Options:" 111 opts.on("-t", "--type", "Specify file type (eg. text.script.ruby)") {|@type|} 112 opts.on("-p", "--pathtype", "Specify path type (<group>)") {|@pathtype|} 113 opts.on("-g", "--group", "Specify group (Classes)") {|@group|} 114 opts.on("--only-add", "Only add files (not adding to build phase)") {|@only_add|} 115 }, 116 117 "standaloneify" => OptionParser.new {|opts| 118 @extra_libs = [] 119 opts.banner = <<-EOB.gsub(/^ {10}/, "") 120 Usage: rubycocoa standaloneify [options] <Ruby Cocoa.app> <Dest.app> 121 122 Creates a new application that should have dependencies resolved. 123 EOB 124 opts.separator "" 125 126 opts.separator "Options:" 127 opts.on("-f","--force","Delete target app if it exists already") { |@force| } 128 opts.on("-l LIBRARY","--lib","Extra library to bundle") { |lib| @extra_libs << lib } 129 }, 130 } 131 132 @parser = OptionParser.new do |parser| 133 parser.banner = <<-EOB.gsub(/^ {8}/, "") 134 Usage: rubycocoa <subcommand> [options] <files> 135 136 The 'rubycocoa' command is a part of RubyCocoa. 137 It's a tool for creating new application skeltons. 138 139 Intro: 140 You can create a RubyCocoa application skelton with the 'new' subcommand: 141 142 $ rubycocoa new "New RubyCocoa Application" 143 144 After selecting the template, 'rubycocoa' will create the application directory in the current directory. 145 EOB 146 147 parser.separator "" 148 149 parser.separator "Subcommands:" 150 @subparsers.keys.sort.each do |k| 151 parser.separator "#{parser.summary_indent} #{k}" 152 end 153 154 parser.separator "" 155 156 parser.separator "Options:" 157 parser.on('--version', "Show version string `#{VERSION}'") do 158 puts VERSION 159 exit 160 end 161 end 162 end 163 164 def run 165 @parser.order!(@argv) 166 if @argv.empty? 167 puts @parser.help 168 exit 169 else 170 @subcommand = @argv.shift 171 method_name = "cmd_#{@subcommand}" 172 if self.respond_to?(method_name) 173 @subparsers[@subcommand].parse!(@argv) 174 self.send(method_name) 175 else 176 puts "Not implemented subcommand: `#{@subcommand}'." 177 puts 178 puts @parser.help 179 end 180 end 181 end 182 183 def cmd_new 184 abort @subparsers[@subcommand].help if @argv.empty? 185 186 appname, = @argv 187 abort "Application Name is required" if appname.nil? || appname.empty? 188 189 if @template 190 template = Pathname.new @template 191 abort "#{template} is not exists." unless template.exist? 192 else 193 templates = Pathname.glob(CONFIG_DIR + "templates/*") 194 templates.concat Pathname.glob("/Library/Application Support/Apple/Developer Tools/Project Templates/Application/Cocoa-Ruby*") 195 templates.each_with_index do |f,i| 196 puts "% 2d: %s %s" % [i, f.basename, i == 0 ? "(default)" : ""] 197 end 198 print "Select Template> " 199 num = $stdin.gets.chomp 200 num = "0" if num.empty? 201 abort "Canceled because the input was not a number" unless num =~ /^\d/ 202 203 num = num.to_i 204 template = templates[num] 205 end 206 puts "Creating `#{appname}' using `#{template}'..." 207 208 dest = Pathname.new(appname) 209 abort "#{appname} already exists. Exiting." if dest.exist? 210 cp_r template, dest 211 Pathname.glob(dest + "*Cocoa*App*") do |f| 212 f.rename(f.parent + f.basename.to_s.sub(/Cocoa(?:Doc)?App/, appname)) 213 end 214 plist = read_plist(dest + "#{appname}.xcodeproj/TemplateInfo.plist") 215 (dest + "#{appname}.xcodeproj/TemplateInfo.plist").unlink 216 217 (plist["FilesToRename"] || {}).each do |k, v| 218 (dest + k).rename(apply_template(v, :project_name => appname)) 219 puts "#{k} => #{v}" 220 end 221 ((plist["FilesToMacroExpand"].to_a || []) << "#{appname}.xcodeproj/project.pbxproj").each do |name| 222 f = dest + apply_template(name, :project_name => appname) 223 #next unless f.exist? 224 puts "Expanding Macro: #{f}" 225 f.open("rb+") do |g| 226 str = g.read 227 str = Iconv.conv("ISO-8859-1", "UTF-16", str) if f.basename.to_s == 'InfoPlist.strings' 228 if ['InfoPlist.strings', 'rb_main.rb', 'main.m'].include?(f.basename.to_s) 229 str = str.gsub(/\307|\253/, '��').gsub(/\310|\273/, '��') 230 end 231 content = apply_template(str, :project_name => appname, :file => f) 232 content = Iconv.conv("UTF-16", "ISO-8859-1", content) if f.basename.to_s == 'InfoPlist.strings' 233 g.rewind 234 g << content 235 g.truncate g.tell 236 end 237 end 238 end 239 240 def cmd_convert 241 abort @subparsers[@subcommand].help if @argv.empty? 242 243 file = Pathname.new @argv[0] 244 tmpl = class_skelton 245 246 if file.extname == ".nib" 247 plist_path = file + "classes.nib" 248 plist = read_plist(plist_path) 249 plist["IBClasses"].each do |l| 250 class_name = l["CLASS"].to_s 251 next if class_name == "FirstResponder" 252 super_class = l["SUPERCLASS"].to_s 253 actions = l["ACTIONS"] ? l["ACTIONS"].allKeys.map {|i| i.to_s } : [] 254 outlets = l["OUTLETS"] ? l["OUTLETS"].allKeys.map {|i| i.to_s } : [] 255 result = ERB.new(tmpl, $SAFE, '-').result(binding) 256 257 path = Pathname.new(class_name + ".rb") 258 if path.exist? 259 puts "#{path} exists. skip." 260 else 261 puts "-> #{path}" 262 path.open("w") do |f| 263 f.puts result 264 end 265 end 266 end 267 else 268 header = file.read 269 _, class_name, super_class = */@interface ([^\s]+) : ([^\s]+)/.match(header) 270 271 outlets = header.scan(/IBOutlet id (.*?);/) 272 actions = header.scan(/^- \(IBAction\)(.*?):\(id\)sender;/) 273 result = ERB.new(tmpl, $SAFE, '-').result(binding) 274 275 path = Pathname.new(class_name + ".rb") 276 if path.exist? 277 puts "#{path} exists. skip." 278 else 279 puts "-> #{path}" 280 path.open("w") do |f| 281 f.puts result 282 end 283 end 284 end 285 end 286 287 def cmd_create 288 abort @subparsers[@subcommand].help if @argv.empty? 289 290 _, class_name, super_class = */^([^<]+)(?:<(.+))?$/.match(@argv[0]) 291 tmpl = class_skelton 292 293 super_class = "NSObject" unless super_class 294 295 outlets = @outlets 296 actions = @actions 297 result = ERB.new(tmpl, $SAFE, '-').result(binding) 298 299 path = Pathname.new(class_name + ".rb") 300 if path.exist? 301 puts "#{path} exists. skip." 302 else 303 puts "-> #{path}" 304 path.open("w") do |f| 305 f.puts result 306 end 307 end 308 end 309 310 def cmd_update 311 abort @subparsers[@subcommand].help if @argv.empty? 312 313 nib, rb, = *@argv 314 nib = Pathname.pwd + nib 315 rb = Pathname.pwd + rb 316 puts "Update `#{nib.basename}' with `#{rb.basename}'" 317 318 class << NSObject 319 @@collect_child_classes = false 320 @@subklasses = {} 321 @@current_class = nil 322 323 def ib_outlets(*args) 324 args.each do |arg| 325 puts "found outlet #{arg} in #{@@current_class}" 326 ((@@subklasses[@@current_class] ||= {})[:outlets] ||= []) << arg 327 end 328 end 329 330 alias_method :ns_outlet, :ib_outlets 331 alias_method :ib_outlet, :ib_outlets 332 alias_method :ns_outlets, :ib_outlets 333 334 def ib_action(name, &blk) 335 puts "found action #{name} in #{@@current_class}" 336 ((@@subklasses[@@current_class] ||= {})[:actions] ||= []) << name 337 end 338 339 alias_method :_before_classes_nib_inherited, :inherited 340 def inherited(subklass) 341 if @@collect_child_classes 342 unless subklass.to_s == "" 343 puts "current class: #{subklass.to_s}" 344 @@current_class = subklass.to_s 345 end 346 end 347 _before_classes_nib_inherited(subklass) 348 end 349 end 350 NSObject.instance_eval { @@collect_child_classes = true } 351 load rb 352 NSObject.instance_eval { @@collect_child_classes = false } 353 354 plist_path = nib + "classes.nib" 355 plist = read_plist(plist_path) 356 357 NSObject.instance_eval { @@subklasses }.each do |k, v| 358 class_def = plist["IBClasses"].find {|i| i["CLASS"] == k } 359 unless class_def 360 if @add_class 361 class_def = NSMutableDictionary.alloc.init 362 class_def['CLASS'] = k 363 class_def['LANGUAGE'] = 'ObjC' 364 plist['IBClasses'].addObject(class_def) 365 else 366 puts "Ruby class `#{k}' is not in nib." 367 next 368 end 369 end 370 371 klass = k.split("::").inject(Object) { |par, const| par.const_get(const) } 372 superklass = klass.superclass.to_s.sub(/^OSX::/, '') 373 class_def.setObject_forKey(superklass, "SUPERCLASS") 374 375 %w(outlets actions).each do |t| 376 next unless v[t.to_sym] 377 updated = NSMutableDictionary.dictionary 378 v[t.to_sym].each do |item| 379 puts "adding #{t} #{item}" 380 updated.setObject_forKey('id', item) 381 end 382 class_def[t.upcase] = updated unless updated.count == 0 383 end 384 end 385 386 plist_path.open("wb") {|f| f.puts plist } 387 end 388 389 def cmd_help 390 subcommand, = @argv 391 if subcommand 392 if @subparsers.key? subcommand 393 puts @subparsers[subcommand].help 394 else 395 puts "No such subcommand `#{subcommand}'" 396 puts @parser.help 397 end 398 else 399 puts @parser.help 400 end 401 end 402 403 def cmd_package 404 exec("rake package") 405 end 406 407 def cmd_add 408 abort @subparsers[@subcommand].help if @argv.empty? 409 410 proj_path = @argv.pop 411 files = @argv 412 413 @group = "Classes" unless @group 414 @pathtype = "<group>" unless @pathtype 415 416 proj = XcodeProject.new(proj_path) 417 files.each do |f| 418 f = Pathname.new(f) 419 if f.directory? 420 puts "#{f} is directory. skip." 421 end 422 type = "text.script.ruby" unless @type 423 puts "Adding `#{f}' as `#{type}' to `#{@group} in `#{proj_path}'." 424 if proj.objects.find {|k,v| v["isa"] == "PBXFileReference" and v["path"] == f } 425 puts "`#{f}' is already in the Xcode project" 426 else 427 id = proj.groups[@group].add_file(type, f, @pathtype) 428 unless @only_add 429 puts "Adding `#{f}' to resource build phase." 430 proj.add_file_to_resouce_phase(id) 431 end 432 end 433 end 434 proj.save 435 end 436 437 def cmd_standaloneify 438 src, dest, = *@argv 439 if @force 440 rm_rf dest 441 end 442 Standaloneify.make_standalone_application(src, dest, @extra_libs) 443 end 444 445 def read_plist(path) 446 plist = NSPropertyListSerialization.objc_send( 447 :propertyListFromData, NSData.alloc.initWithContentsOfFile(path.to_s), 448 :mutabilityOption, NSPropertyListMutableContainersAndLeaves, 449 :format, nil, 450 :errorDescription, nil 451 ) 452 unless plist 453 abort "Error while reading `#{path}'" 454 end 455 plist 456 end 457 458 def apply_template(str, opts) 459 str.to_s.gsub(/��([A-Z]+)��/) do 460 m = Regexp.last_match 461 case m[1] 462 when "DATE" 463 now = NSCalendarDate.calendarDate 464 now.setCalendarFormat("%x") 465 now.description 466 when "TIME" 467 now = NSCalendarDate.calendarDate 468 now.setCalendarFormat("%X") 469 now.description 470 when "YEAR" 471 Time.now.year 472 when "DIRECTORY" 473 opts[:file].parent.basename 474 when "FILEEXTENSION" 475 opts[:file].extname 476 when "FILENAME" 477 opts[:file].basename 478 when "FILEBASENAME" 479 opts[:file].basename(".*") 480 when "FILEBASENAMEASIDENTIFIER" 481 opts[:file].basename(".*").gsub(/\s+/, "_") 482 when "FULLUSERNAME" 483 OSX.NSFullUserName.to_s 484 when "USERNAME" 485 OSX.NSUserName.to_s 486 when "PROJECTNAME" 487 opts[:project_name] 488 when "PROJECTNAMEASIDENTIFIER" 489 opts[:project_name].gsub(/\s+/, "_") 490 when "PROJECTNAMEASXML" 491 opts[:project_name].gsub(/&/, "&").gsub(/>/, ">").gsub(/</, "<") 492 when "ORGANIZATIONNAME" 493 ORGANIZATIONNAME 494 else 495 m[0] 496 end 497 end 498 end 499 500 def class_skelton 501 unless @class_skelton 502 @class_skelton = <<-EOS.gsub(/ /, "") 503 require 'osx/cocoa' 504 include OSX 505 506 class <%=class_name%> < <%=super_class%> 507 <%- outlets.each do |outlet| -%> 508 ib_outlets :<%=outlet%> 509 <%- end %> 510 <%- actions.each do |action| -%> 511 512 ib_action :<%=action%> do |sender| 513 end 514 <%- end %> 515 516 def awakeFromNib 517 end 518 end 519 EOS 520 end 521 users = CONFIG_DIR + "class.rb" 522 if users.exist? 523 users.read 524 else 525 @class_skelton 526 end 527 end 528end 529 530if $0 == __FILE__ 531 RubyCocoaCommand.run(ARGV) 532end 533 534 535