From cd7aecde450157ae2ec0c07a2171d7149bebb74a Mon Sep 17 00:00:00 2001 From: jnunemaker Date: Fri, 15 Dec 2006 01:32:12 +0000 Subject: [PATCH] initial import of all my public projects git-svn-id: http://svn.addictedtonew.com/public/gems/twitter@1 fe7eae16-9a24-0410-a59d-9e59979e88be --- CHANGELOG | 2 + Manifest.txt | 12 + README | 46 + Rakefile | 57 + bin/twitter | 35 + doc/classes/Twitter/BadResponse.html | 113 ++ doc/classes/Twitter/Base.html | 250 +++ doc/classes/Twitter/Base.src/M000015.html | 18 + doc/classes/Twitter/Base.src/M000016.html | 18 + doc/classes/Twitter/Base.src/M000017.html | 20 + doc/classes/Twitter/Base.src/M000018.html | 18 + doc/classes/Twitter/Base.src/M000019.html | 18 + doc/classes/Twitter/Base.src/M000020.html | 25 + doc/classes/Twitter/CantConnect.html | 113 ++ doc/classes/Twitter/Command.html | 261 +++ doc/classes/Twitter/Command.src/M000007.html | 32 + doc/classes/Twitter/Command.src/M000008.html | 18 + doc/classes/Twitter/Command.src/M000009.html | 32 + doc/classes/Twitter/Command.src/M000010.html | 31 + doc/classes/Twitter/Command.src/M000011.html | 28 + doc/classes/Twitter/Command.src/M000012.html | 43 + doc/classes/Twitter/Command.src/M000013.html | 28 + doc/classes/Twitter/Command.src/M000014.html | 43 + doc/classes/Twitter/EasyClassMaker.html | 179 ++ .../Twitter/EasyClassMaker.src/M000001.html | 18 + .../Twitter/EasyClassMaker.src/M000002.html | 18 + .../Twitter/EasyClassMaker/ClassMethods.html | 156 ++ .../ClassMethods.src/M000003.html | 19 + .../ClassMethods.src/M000004.html | 16 + doc/classes/Twitter/Status.html | 147 ++ doc/classes/Twitter/Status.src/M000005.html | 24 + doc/classes/Twitter/UnknownTimeline.html | 111 ++ doc/classes/Twitter/Untwitterable.html | 111 ++ doc/classes/Twitter/User.html | 147 ++ doc/classes/Twitter/User.src/M000006.html | 23 + doc/created.rid | 1 + doc/files/bin/twitter.html | 148 ++ doc/files/lib/twitter/base_rb.html | 123 ++ doc/files/lib/twitter/command_rb.html | 115 ++ .../lib/twitter/easy_class_maker_rb.html | 108 ++ doc/files/lib/twitter/status_rb.html | 123 ++ doc/files/lib/twitter/user_rb.html | 122 ++ doc/files/lib/twitter/version_rb.html | 101 ++ doc/files/lib/twitter_rb.html | 154 ++ doc/fr_class_index.html | 36 + doc/fr_file_index.html | 34 + doc/fr_method_index.html | 47 + doc/index.html | 24 + doc/rdoc-style.css | 208 +++ lib/twitter.rb | 45 + lib/twitter/base.rb | 109 ++ lib/twitter/command.rb | 195 ++ lib/twitter/easy_class_maker.rb | 43 + lib/twitter/status.rb | 33 + lib/twitter/user.rb | 32 + lib/twitter/version.rb | 9 + pkg/twitter-0.0.1.gem | Bin 0 -> 17920 bytes pkg/twitter-0.0.1.tgz | Bin 0 -> 29092 bytes pkg/twitter-0.0.1/CHANGELOG | 2 + pkg/twitter-0.0.1/Manifest.txt | 12 + pkg/twitter-0.0.1/README | 46 + pkg/twitter-0.0.1/Rakefile | 57 + pkg/twitter-0.0.1/bin/twitter | 35 + pkg/twitter-0.0.1/lib/twitter.rb | 45 + pkg/twitter-0.0.1/lib/twitter/base.rb | 109 ++ pkg/twitter-0.0.1/lib/twitter/command.rb | 195 ++ .../lib/twitter/easy_class_maker.rb | 43 + pkg/twitter-0.0.1/lib/twitter/status.rb | 33 + pkg/twitter-0.0.1/lib/twitter/user.rb | 32 + pkg/twitter-0.0.1/lib/twitter/version.rb | 9 + pkg/twitter-0.0.1/setup.rb | 1585 +++++++++++++++++ pkg/twitter-0.0.2.gem | Bin 0 -> 18432 bytes pkg/twitter-0.0.2.tgz | Bin 0 -> 15653 bytes pkg/twitter-0.0.2/CHANGELOG | 2 + pkg/twitter-0.0.2/README | 46 + pkg/twitter-0.0.2/Rakefile | 57 + pkg/twitter-0.0.2/bin/twitter | 35 + pkg/twitter-0.0.2/lib/twitter.rb | 45 + pkg/twitter-0.0.2/lib/twitter/base.rb | 109 ++ pkg/twitter-0.0.2/lib/twitter/command.rb | 195 ++ .../lib/twitter/easy_class_maker.rb | 43 + pkg/twitter-0.0.2/lib/twitter/status.rb | 33 + pkg/twitter-0.0.2/lib/twitter/user.rb | 32 + pkg/twitter-0.0.2/lib/twitter/version.rb | 9 + pkg/twitter-0.0.2/setup.rb | 1585 +++++++++++++++++ setup.rb | 1585 +++++++++++++++++ 86 files changed, 10019 insertions(+) create mode 100644 CHANGELOG create mode 100644 Manifest.txt create mode 100644 README create mode 100644 Rakefile create mode 100644 bin/twitter create mode 100644 doc/classes/Twitter/BadResponse.html create mode 100644 doc/classes/Twitter/Base.html create mode 100644 doc/classes/Twitter/Base.src/M000015.html create mode 100644 doc/classes/Twitter/Base.src/M000016.html create mode 100644 doc/classes/Twitter/Base.src/M000017.html create mode 100644 doc/classes/Twitter/Base.src/M000018.html create mode 100644 doc/classes/Twitter/Base.src/M000019.html create mode 100644 doc/classes/Twitter/Base.src/M000020.html create mode 100644 doc/classes/Twitter/CantConnect.html create mode 100644 doc/classes/Twitter/Command.html create mode 100644 doc/classes/Twitter/Command.src/M000007.html create mode 100644 doc/classes/Twitter/Command.src/M000008.html create mode 100644 doc/classes/Twitter/Command.src/M000009.html create mode 100644 doc/classes/Twitter/Command.src/M000010.html create mode 100644 doc/classes/Twitter/Command.src/M000011.html create mode 100644 doc/classes/Twitter/Command.src/M000012.html create mode 100644 doc/classes/Twitter/Command.src/M000013.html create mode 100644 doc/classes/Twitter/Command.src/M000014.html create mode 100644 doc/classes/Twitter/EasyClassMaker.html create mode 100644 doc/classes/Twitter/EasyClassMaker.src/M000001.html create mode 100644 doc/classes/Twitter/EasyClassMaker.src/M000002.html create mode 100644 doc/classes/Twitter/EasyClassMaker/ClassMethods.html create mode 100644 doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000003.html create mode 100644 doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000004.html create mode 100644 doc/classes/Twitter/Status.html create mode 100644 doc/classes/Twitter/Status.src/M000005.html create mode 100644 doc/classes/Twitter/UnknownTimeline.html create mode 100644 doc/classes/Twitter/Untwitterable.html create mode 100644 doc/classes/Twitter/User.html create mode 100644 doc/classes/Twitter/User.src/M000006.html create mode 100644 doc/created.rid create mode 100644 doc/files/bin/twitter.html create mode 100644 doc/files/lib/twitter/base_rb.html create mode 100644 doc/files/lib/twitter/command_rb.html create mode 100644 doc/files/lib/twitter/easy_class_maker_rb.html create mode 100644 doc/files/lib/twitter/status_rb.html create mode 100644 doc/files/lib/twitter/user_rb.html create mode 100644 doc/files/lib/twitter/version_rb.html create mode 100644 doc/files/lib/twitter_rb.html create mode 100644 doc/fr_class_index.html create mode 100644 doc/fr_file_index.html create mode 100644 doc/fr_method_index.html create mode 100644 doc/index.html create mode 100644 doc/rdoc-style.css create mode 100644 lib/twitter.rb create mode 100644 lib/twitter/base.rb create mode 100644 lib/twitter/command.rb create mode 100644 lib/twitter/easy_class_maker.rb create mode 100644 lib/twitter/status.rb create mode 100644 lib/twitter/user.rb create mode 100644 lib/twitter/version.rb create mode 100644 pkg/twitter-0.0.1.gem create mode 100644 pkg/twitter-0.0.1.tgz create mode 100644 pkg/twitter-0.0.1/CHANGELOG create mode 100644 pkg/twitter-0.0.1/Manifest.txt create mode 100644 pkg/twitter-0.0.1/README create mode 100644 pkg/twitter-0.0.1/Rakefile create mode 100644 pkg/twitter-0.0.1/bin/twitter create mode 100644 pkg/twitter-0.0.1/lib/twitter.rb create mode 100644 pkg/twitter-0.0.1/lib/twitter/base.rb create mode 100644 pkg/twitter-0.0.1/lib/twitter/command.rb create mode 100644 pkg/twitter-0.0.1/lib/twitter/easy_class_maker.rb create mode 100644 pkg/twitter-0.0.1/lib/twitter/status.rb create mode 100644 pkg/twitter-0.0.1/lib/twitter/user.rb create mode 100644 pkg/twitter-0.0.1/lib/twitter/version.rb create mode 100644 pkg/twitter-0.0.1/setup.rb create mode 100644 pkg/twitter-0.0.2.gem create mode 100644 pkg/twitter-0.0.2.tgz create mode 100644 pkg/twitter-0.0.2/CHANGELOG create mode 100644 pkg/twitter-0.0.2/README create mode 100644 pkg/twitter-0.0.2/Rakefile create mode 100644 pkg/twitter-0.0.2/bin/twitter create mode 100644 pkg/twitter-0.0.2/lib/twitter.rb create mode 100644 pkg/twitter-0.0.2/lib/twitter/base.rb create mode 100644 pkg/twitter-0.0.2/lib/twitter/command.rb create mode 100644 pkg/twitter-0.0.2/lib/twitter/easy_class_maker.rb create mode 100644 pkg/twitter-0.0.2/lib/twitter/status.rb create mode 100644 pkg/twitter-0.0.2/lib/twitter/user.rb create mode 100644 pkg/twitter-0.0.2/lib/twitter/version.rb create mode 100644 pkg/twitter-0.0.2/setup.rb create mode 100644 setup.rb diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 000000000..3afcf0cc4 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,2 @@ +0.0.2 - added the command line options i forgot to add (friend and follower); improved some docs +0.0.1 - initial release \ No newline at end of file diff --git a/Manifest.txt b/Manifest.txt new file mode 100644 index 000000000..8630e46f9 --- /dev/null +++ b/Manifest.txt @@ -0,0 +1,12 @@ +README +CHANGELOG +Rakefile +setup.rb +bin/twitter +lib/twitter.rb +lib/twitter/base.rb +lib/twitter/command.rb +lib/twitter/easy_class_maker.rb +lib/twitter/status.rb +lib/twitter/user.rb +lib/twitter/version.rb \ No newline at end of file diff --git a/README b/README new file mode 100644 index 000000000..eeec08d39 --- /dev/null +++ b/README @@ -0,0 +1,46 @@ +addicted to twitter +================== + +... a sweet little diddy that helps you twitter your life away + + +== Command Line Use + +$ twitter + +That will show the commands and each command will either run or show you the options it needs to run + +$ twitter post "releasing my new twitter gem" + +That will post a status update to your twitter + +== Examples + + Twitter::Base.new('your email', 'your password').update('watching veronica mars') + + # or you can use post + Twitter::Base.new('your email', 'your password').post('post works too') + + puts "Public Timeline", "=" * 50 + Twitter::Base.new('your email', 'your password').timeline(:public).each do |s| + puts s.text, s.user.name + puts + end + + puts '', "Friends Timeline", "=" * 50 + Twitter::Base.new('your email', 'your password').timeline.each do |s| + puts s.text, s.user.name + puts + end + + puts '', "Friends", "=" * 50 + Twitter::Base.new('your email', 'your password').friends.each do |u| + puts u.name, u.status.text + puts + end + + puts '', "Followers", "=" * 50 + Twitter::Base.new('your email', 'your password').followers.each do |u| + puts u.name, u.status.text + puts + end \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..0c32f7d3b --- /dev/null +++ b/Rakefile @@ -0,0 +1,57 @@ +require 'rubygems' +require 'rake' +require 'rake/clean' +require 'rake/testtask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/rdoctask' +require 'rake/contrib/rubyforgepublisher' +require 'fileutils' +require 'hoe' +include FileUtils +require File.join(File.dirname(__FILE__), 'lib', 'twitter', 'version') + +AUTHOR = "John Nunemaker" # can also be an array of Authors +EMAIL = "nunemaker@gmail.com" +DESCRIPTION = "a command line interface for twitter, also a library which wraps the twitter api" +GEM_NAME = "twitter" # what ppl will type to install your gem +RUBYFORGE_PROJECT = "twitter" # The unix name for your project +HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org" +RELEASE_TYPES = %w( gem ) # can use: gem, tar, zip + + +NAME = "twitter" +REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil +VERS = ENV['VERSION'] || (Twitter::VERSION::STRING + (REV ? ".#{REV}" : "")) +CLEAN.include ['**/.*.sw?', '*.gem', '.config'] +RDOC_OPTS = ['--quiet', '--title', "twitter documentation", + "--opname", "index.html", + "--line-numbers", + "--main", "README", + "--inline-source"] + +# Generate all the Rake tasks +# Run 'rake -T' to see list of generated tasks (from gem root directory) +hoe = Hoe.new(GEM_NAME, VERS) do |p| + p.author = AUTHOR + p.description = DESCRIPTION + p.email = EMAIL + p.summary = DESCRIPTION + p.url = HOMEPATH + p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT + p.test_globs = ["test/**/*_test.rb"] + p.clean_globs = CLEAN #An array of file patterns to delete on clean. + + # == Optional + #p.changes - A description of the release's latest changes. + p.extra_deps = %w( hpricot ) + #p.spec_extras - A hash of extra values to set in the gemspec. +end + + + +desc "Package and Install Gem" +task :package_and_install do + `rake package` + `sudo gem install pkg/#{NAME}-#{VERS}.gem` +end \ No newline at end of file diff --git a/bin/twitter b/bin/twitter new file mode 100644 index 000000000..cbc7441db --- /dev/null +++ b/bin/twitter @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# = addicted to twitter +# +# ... a sweet little diddy that helps you twitter your life away from the command line +# +# == Install +# +# $ sudo gem install twitter +# +# == Command Line Use +# +# $ twitter +# +# Usage: twitter [options] +# +# Available Commands: +# - post +# - timeline +# - friends +# - friend +# - followers +# - follower +# +# That will show the commands and each command will either run or show you the options it needs to run +# +# $ twitter post "releasing my new twitter gem" +# +# Got it! New twitter created at: Mon Nov 27 00:22:27 UTC 2006 +# +# That will post a status update to your twitter +require 'rubygems' +require 'twitter' +require 'twitter/command' + +Twitter::Command.process(ARGV) \ No newline at end of file diff --git a/doc/classes/Twitter/BadResponse.html b/doc/classes/Twitter/BadResponse.html new file mode 100644 index 000000000..59bd2be84 --- /dev/null +++ b/doc/classes/Twitter/BadResponse.html @@ -0,0 +1,113 @@ + + + + + + Class: Twitter::BadResponse + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::BadResponse
In: + + lib/twitter/base.rb + +
+
Parent: + + Untwitterable + +
+
+ + +
+ + + +
+ + + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/Base.html b/doc/classes/Twitter/Base.html new file mode 100644 index 000000000..1c80cc1d3 --- /dev/null +++ b/doc/classes/Twitter/Base.html @@ -0,0 +1,250 @@ + + + + + + Class: Twitter::Base + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::Base
In: + + lib/twitter/base.rb + +
+
Parent: + Object +
+
+ + +
+ + + +
+ + + +
+ +
+

Methods

+ +
+ followers   + friends   + new   + post   + timeline   + timelines   + update   +
+
+ +
+ + + + +
+ + + + + + + + + +
+

Public Class methods

+ +
+ + + + +
+

+Initializes the configuration for making requests to twitter +

+
+
+ +
+ + + + +
+
+
+ +

Public Instance methods

+ +
+ + + + +
+

+Returns an array of users who are following you +

+
+
+ +
+ + + + +
+

+Returns an array of users who are in your friends list +

+
+
+ +
+ + + + +
+

+Updates your twitter with whatever status string is passed in +

+
+
+ +
+ + + + +
+

+Returns an array of statuses for a timeline; Available timelines are determined from the @@timelines variable Defaults to your friends timeline +

+
+
+ +
+ + +
+ update(status) +
+ +
+

+Alias for post +

+
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/Base.src/M000015.html b/doc/classes/Twitter/Base.src/M000015.html new file mode 100644 index 000000000..79c08b437 --- /dev/null +++ b/doc/classes/Twitter/Base.src/M000015.html @@ -0,0 +1,18 @@ + + + + + + timelines (Twitter::Base) + + + + +
# File lib/twitter/base.rb, line 24
+    def self.timelines
+      @@timelines
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Base.src/M000016.html b/doc/classes/Twitter/Base.src/M000016.html new file mode 100644 index 000000000..e39879f41 --- /dev/null +++ b/doc/classes/Twitter/Base.src/M000016.html @@ -0,0 +1,18 @@ + + + + + + new (Twitter::Base) + + + + +
# File lib/twitter/base.rb, line 29
+    def initialize(email, password)
+      @config, @config[:email], @config[:password] = {}, email, password
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Base.src/M000017.html b/doc/classes/Twitter/Base.src/M000017.html new file mode 100644 index 000000000..af578d5dd --- /dev/null +++ b/doc/classes/Twitter/Base.src/M000017.html @@ -0,0 +1,20 @@ + + + + + + timeline (Twitter::Base) + + + + +
# File lib/twitter/base.rb, line 36
+    def timeline(which=:friends)
+      raise UnknownTimeline unless @@timelines.include?(which)
+      auth = which.to_s.include?('public') ? false : true
+      statuses(call("#{which}_timeline", :auth => auth))
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Base.src/M000018.html b/doc/classes/Twitter/Base.src/M000018.html new file mode 100644 index 000000000..153f15b97 --- /dev/null +++ b/doc/classes/Twitter/Base.src/M000018.html @@ -0,0 +1,18 @@ + + + + + + friends (Twitter::Base) + + + + +
# File lib/twitter/base.rb, line 43
+    def friends
+      users(call(:friends))
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Base.src/M000019.html b/doc/classes/Twitter/Base.src/M000019.html new file mode 100644 index 000000000..13f156cdb --- /dev/null +++ b/doc/classes/Twitter/Base.src/M000019.html @@ -0,0 +1,18 @@ + + + + + + followers (Twitter::Base) + + + + +
# File lib/twitter/base.rb, line 48
+    def followers
+      users(call(:followers))
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Base.src/M000020.html b/doc/classes/Twitter/Base.src/M000020.html new file mode 100644 index 000000000..27cd41375 --- /dev/null +++ b/doc/classes/Twitter/Base.src/M000020.html @@ -0,0 +1,25 @@ + + + + + + post (Twitter::Base) + + + + +
# File lib/twitter/base.rb, line 53
+    def post(status)
+      url = URI.parse("http://#{@@api_url}/statuses/update.xml")
+      req = Net::HTTP::Post.new(url.path)
+      
+      req.basic_auth(@config[:email], @config[:password])
+      req.set_form_data({'status' => status})
+      
+      res = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) }
+      Status.new_from_xml(Hpricot.parse(res.body).at('status'))
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/CantConnect.html b/doc/classes/Twitter/CantConnect.html new file mode 100644 index 000000000..fb7b17464 --- /dev/null +++ b/doc/classes/Twitter/CantConnect.html @@ -0,0 +1,113 @@ + + + + + + Class: Twitter::CantConnect + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::CantConnect
In: + + lib/twitter/base.rb + +
+
Parent: + + Untwitterable + +
+
+ + +
+ + + +
+ + + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.html b/doc/classes/Twitter/Command.html new file mode 100644 index 000000000..cd86d25d8 --- /dev/null +++ b/doc/classes/Twitter/Command.html @@ -0,0 +1,261 @@ + + + + + + Class: Twitter::Command + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::Command
In: + + lib/twitter/command.rb + +
+
Parent: + Object +
+
+ + +
+ + + +
+ + + +
+ +
+

Methods

+ +
+ commands   + follower   + followers   + friend   + friends   + post   + process   + timeline   +
+
+ +
+ + + + +
+ + + + + + + + + +
+

Public Class methods

+ +
+ + + + +
+
+
+ +
+ + + + +
+

+Shows last updated status and time for a follower Needs a screen name +

+
+
+ +
+ + + + +
+

+Shows all followers and their last +updated status +

+
+
+ +
+ + + + +
+

+Shows last updated status and time for a friend Needs a screen name +

+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+

+Posts an updated status to twitter +

+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+

+Shows status, time and user for the specified timeline +

+
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000007.html b/doc/classes/Twitter/Command.src/M000007.html new file mode 100644 index 000000000..97fc0df78 --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000007.html @@ -0,0 +1,32 @@ + + + + + + process (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 23
+      def process(args)
+        command = args.shift
+
+        case command
+        when "post"       then Command.post(args)
+        when "timeline"   then Command.timeline(args)
+        when "friends"    then Command.friends
+        when "friend"     then Command.friend(args)
+        when "followers"  then Command.followers
+        when "follower"   then Command.follower(args)
+        else
+          puts "\nUsage: twitter <command> [options]\n\nAvailable Commands:"
+          Twitter::Command.commands.each do |com|
+            puts "    - #{com}"
+          end
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000008.html b/doc/classes/Twitter/Command.src/M000008.html new file mode 100644 index 000000000..0239859c0 --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000008.html @@ -0,0 +1,18 @@ + + + + + + commands (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 41
+      def commands
+        @@commands
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000009.html b/doc/classes/Twitter/Command.src/M000009.html new file mode 100644 index 000000000..5bf437634 --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000009.html @@ -0,0 +1,32 @@ + + + + + + post (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 46
+      def post(args)
+        config = create_or_find_config
+        
+        if args.size == 0
+          puts %(\n  You didn't enter a message to post.\n\n  Usage: twitter post "You're fabulous message"\n)
+          exit(0)
+        end
+        
+        post = args.shift
+        
+        begin
+          status = Twitter::Base.new(config['email'], config['password']).post(post)
+          puts "\nGot it! New twitter created at: #{status.created_at}\n"
+        rescue
+          puts error_msg
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000010.html b/doc/classes/Twitter/Command.src/M000010.html new file mode 100644 index 000000000..d02615311 --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000010.html @@ -0,0 +1,31 @@ + + + + + + timeline (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 65
+      def timeline(args)
+        config = create_or_find_config
+        
+        timeline = :friends
+        timeline = args.shift.intern if args.size > 0 && Twitter::Base.timelines.include?(args[0].intern)
+        
+        begin
+          puts
+          Twitter::Base.new(config['email'], config['password']).timeline(timeline).each do |s|
+            puts "#{s.text}\n-- #{s.relative_created_at} by #{s.user.name}"
+            puts
+          end
+        rescue
+          puts error_msg
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000011.html b/doc/classes/Twitter/Command.src/M000011.html new file mode 100644 index 000000000..b6627cd44 --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000011.html @@ -0,0 +1,28 @@ + + + + + + friends (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 82
+      def friends
+        config = create_or_find_config
+        
+        begin
+          puts
+          Twitter::Base.new(config['email'], config['password']).friends.each do |u|
+            puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}"
+            puts
+          end
+        rescue
+          puts error_msg
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000012.html b/doc/classes/Twitter/Command.src/M000012.html new file mode 100644 index 000000000..e94fde52c --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000012.html @@ -0,0 +1,43 @@ + + + + + + friend (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 98
+      def friend(args)
+        config = create_or_find_config
+        
+        if args.size == 0
+          puts %(\n  You forgot to enter a screen name.\n\n  Usage: twitter friend jnunemaker\n)
+          exit(0)
+        end
+        
+        screen_name = args.shift
+                
+        begin
+          puts
+          found = false
+          Twitter::Base.new(config['email'], config['password']).friends.each do |u|
+            if u.screen_name == screen_name
+              puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}"
+              found = true
+            end
+          end
+          
+          unless found
+            puts "Sorry couldn't find a friend of yours with #{screen_name} as a screen name"
+          end
+          puts
+        rescue
+          puts error_msg
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000013.html b/doc/classes/Twitter/Command.src/M000013.html new file mode 100644 index 000000000..937c2f6bc --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000013.html @@ -0,0 +1,28 @@ + + + + + + followers (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 128
+      def followers
+        config = create_or_find_config
+        
+        begin
+          puts
+          Twitter::Base.new(config['email'], config['password']).followers.each do |u|
+            puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}"
+            puts
+          end
+        rescue
+          puts error_msg
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Command.src/M000014.html b/doc/classes/Twitter/Command.src/M000014.html new file mode 100644 index 000000000..78f265fe7 --- /dev/null +++ b/doc/classes/Twitter/Command.src/M000014.html @@ -0,0 +1,43 @@ + + + + + + follower (Twitter::Command) + + + + +
# File lib/twitter/command.rb, line 144
+      def follower(args)
+        config = create_or_find_config
+        
+        if args.size == 0
+          puts %(\n  You forgot to enter a screen name.\n\n  Usage: twitter follower jnunemaker\n)
+          exit(0)
+        end
+        
+        screen_name = args.shift
+        
+        begin
+          puts
+          found = false
+          Twitter::Base.new(config['email'], config['password']).follower.each do |u|
+            if u.screen_name == screen_name
+              puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}"
+              found = true
+            end
+          end
+          
+          unless found
+            puts "Sorry couldn't find a follower of yours with #{screen_name} as a screen name"
+          end
+          puts
+        rescue
+          puts error_msg
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/EasyClassMaker.html b/doc/classes/Twitter/EasyClassMaker.html new file mode 100644 index 000000000..820b4d89d --- /dev/null +++ b/doc/classes/Twitter/EasyClassMaker.html @@ -0,0 +1,179 @@ + + + + + + Module: Twitter::EasyClassMaker + + + + + + + + + + +
+ + + + + + + + + + +
ModuleTwitter::EasyClassMaker
In: + + lib/twitter/easy_class_maker.rb + +
+
+
+ + +
+ + + +
+ + + +
+ +
+

Methods

+ +
+ included   + new   +
+
+ +
+ + + + +
+ +
+

Classes and Modules

+ + Module Twitter::EasyClassMaker::ClassMethods
+ +
+ + + + + + + + +
+

Public Class methods

+ +
+ + + + +
+
+
+ +
+ + + + +
+

+allows for any class that includes this to use a block to initialize +variables instead of assigning each one seperately +

+

+Example: +

+

+instead of… +

+

+s = Status.new s.foo = ‘thing’ s.bar = ‘another +thing‘ +

+

+you can … +

+

+Status.new do |s| +

+
+  s.foo = 'thing'
+  s.bar = 'another thing'
+
+

+end +

+
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/EasyClassMaker.src/M000001.html b/doc/classes/Twitter/EasyClassMaker.src/M000001.html new file mode 100644 index 000000000..4d4f58aed --- /dev/null +++ b/doc/classes/Twitter/EasyClassMaker.src/M000001.html @@ -0,0 +1,18 @@ + + + + + + included (Twitter::EasyClassMaker) + + + + +
# File lib/twitter/easy_class_maker.rb, line 7
+    def self.included(base)
+      base.extend(ClassMethods)
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/EasyClassMaker.src/M000002.html b/doc/classes/Twitter/EasyClassMaker.src/M000002.html new file mode 100644 index 000000000..cec934ecb --- /dev/null +++ b/doc/classes/Twitter/EasyClassMaker.src/M000002.html @@ -0,0 +1,18 @@ + + + + + + new (Twitter::EasyClassMaker) + + + + +
# File lib/twitter/easy_class_maker.rb, line 39
+    def initialize
+      yield self if block_given?
+    end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/EasyClassMaker/ClassMethods.html b/doc/classes/Twitter/EasyClassMaker/ClassMethods.html new file mode 100644 index 000000000..1f8ed4f62 --- /dev/null +++ b/doc/classes/Twitter/EasyClassMaker/ClassMethods.html @@ -0,0 +1,156 @@ + + + + + + Module: Twitter::EasyClassMaker::ClassMethods + + + + + + + + + + +
+ + + + + + + + + + +
ModuleTwitter::EasyClassMaker::ClassMethods
In: + + lib/twitter/easy_class_maker.rb + +
+
+
+ + +
+ + + +
+ + + +
+ +
+

Methods

+ +
+ attributes   + attributes   +
+
+ +
+ + + + +
+ + + + + + + + + +
+

Public Class methods

+ +
+ + + + +
+

+read method for attributes class +variable +

+
+
+ +

Public Instance methods

+ +
+ + + + +
+

+creates the attributes class +variable and creates each attribute‘s accessor methods +

+
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000003.html b/doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000003.html new file mode 100644 index 000000000..aff4e8f5e --- /dev/null +++ b/doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000003.html @@ -0,0 +1,19 @@ + + + + + + attributes (Twitter::EasyClassMaker::ClassMethods) + + + + +
# File lib/twitter/easy_class_maker.rb, line 13
+      def attributes(*attrs)
+        @@attributes = attrs
+        @@attributes.each { |a| attr_accessor a }
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000004.html b/doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000004.html new file mode 100644 index 000000000..dc42003e9 --- /dev/null +++ b/doc/classes/Twitter/EasyClassMaker/ClassMethods.src/M000004.html @@ -0,0 +1,16 @@ + + + + + + attributes (Twitter::EasyClassMaker::ClassMethods) + + + + +
# File lib/twitter/easy_class_maker.rb, line 19
+      def self.attributes; @@attributes end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/Status.html b/doc/classes/Twitter/Status.html new file mode 100644 index 000000000..c94d180f1 --- /dev/null +++ b/doc/classes/Twitter/Status.html @@ -0,0 +1,147 @@ + + + + + + Class: Twitter::Status + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::Status
In: + + lib/twitter/status.rb + +
+
Parent: + Object +
+
+ + +
+ + + +
+ + + +
+ +
+

Methods

+ +
+ new_from_xml   +
+
+ +
+ + + +
+

Included Modules

+ + +
+ +
+ + + + + + + + + +
+

Public Class methods

+ +
+ + + + +
+

+Creates a new status from a piece of xml +

+
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/Status.src/M000005.html b/doc/classes/Twitter/Status.src/M000005.html new file mode 100644 index 000000000..c9f9a956f --- /dev/null +++ b/doc/classes/Twitter/Status.src/M000005.html @@ -0,0 +1,24 @@ + + + + + + new_from_xml (Twitter::Status) + + + + +
# File lib/twitter/status.rb, line 22
+      def new_from_xml(xml)
+        Status.new do |s|
+          s.id                  = (xml).at('id').innerHTML
+          s.created_at          = (xml).at('created_at').innerHTML
+          s.text                = (xml).at('text').innerHTML
+          s.relative_created_at = (xml).at('relative_created_at').innerHTML
+          s.user                = User.new_from_xml(xml) if (xml).at('user')
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/classes/Twitter/UnknownTimeline.html b/doc/classes/Twitter/UnknownTimeline.html new file mode 100644 index 000000000..7a693a4ca --- /dev/null +++ b/doc/classes/Twitter/UnknownTimeline.html @@ -0,0 +1,111 @@ + + + + + + Class: Twitter::UnknownTimeline + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::UnknownTimeline
In: + + lib/twitter/base.rb + +
+
Parent: + ArgumentError +
+
+ + +
+ + + +
+ + + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/Untwitterable.html b/doc/classes/Twitter/Untwitterable.html new file mode 100644 index 000000000..519fa7e2c --- /dev/null +++ b/doc/classes/Twitter/Untwitterable.html @@ -0,0 +1,111 @@ + + + + + + Class: Twitter::Untwitterable + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::Untwitterable
In: + + lib/twitter/base.rb + +
+
Parent: + StandardError +
+
+ + +
+ + + +
+ + + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/User.html b/doc/classes/Twitter/User.html new file mode 100644 index 000000000..fd9d07a90 --- /dev/null +++ b/doc/classes/Twitter/User.html @@ -0,0 +1,147 @@ + + + + + + Class: Twitter::User + + + + + + + + + + +
+ + + + + + + + + + + + + + +
ClassTwitter::User
In: + + lib/twitter/user.rb + +
+
Parent: + Object +
+
+ + +
+ + + +
+ + + +
+ +
+

Methods

+ +
+ new_from_xml   +
+
+ +
+ + + +
+

Included Modules

+ + +
+ +
+ + + + + + + + + +
+

Public Class methods

+ +
+ + + + +
+

+Creates a new user from a piece of xml +

+
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/doc/classes/Twitter/User.src/M000006.html b/doc/classes/Twitter/User.src/M000006.html new file mode 100644 index 000000000..09fb010c9 --- /dev/null +++ b/doc/classes/Twitter/User.src/M000006.html @@ -0,0 +1,23 @@ + + + + + + new_from_xml (Twitter::User) + + + + +
# File lib/twitter/user.rb, line 22
+      def new_from_xml(xml)
+        User.new do |u|
+          u.id            = (xml).at('id').innerHTML
+          u.name          = (xml).at('name').innerHTML
+          u.screen_name   = (xml).at('screen_name').innerHTML
+          u.status        = Status.new_from_xml(xml) if (xml).at('status')
+        end
+      end
+ + \ No newline at end of file diff --git a/doc/created.rid b/doc/created.rid new file mode 100644 index 000000000..5e48b7b6f --- /dev/null +++ b/doc/created.rid @@ -0,0 +1 @@ +Sun Nov 26 19:22:48 -0500 2006 diff --git a/doc/files/bin/twitter.html b/doc/files/bin/twitter.html new file mode 100644 index 000000000..6a980841e --- /dev/null +++ b/doc/files/bin/twitter.html @@ -0,0 +1,148 @@ + + + + + + File: twitter + + + + + + + + + + +
+

twitter

+ + + + + + + + + +
Path:bin/twitter +
Last Update:Sun Nov 26 19:22:40 -0500 2006
+
+ + +
+ + + +
+ +
+

addicted to twitter

+

+… a sweet little diddy that helps you twitter your life away from the +command line +

+

Install

+
+  $ sudo gem install twitter
+
+

Command Line Use

+
+  $ twitter
+
+    Usage: twitter <command> [options]
+
+    Available Commands:
+        - post
+        - timeline
+        - friends
+        - friend
+        - followers
+        - follower
+
+

+That will show the commands and each command will either run or show you +the options it needs to run +

+
+  $ twitter post "releasing my new twitter gem"
+
+    Got it! New twitter created at: Mon Nov 27 00:22:27 UTC 2006
+
+

+That will post a status update to your twitter +

+ +
+ +
+

Required files

+ +
+ rubygems   + twitter   + twitter/command   +
+
+ +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/files/lib/twitter/base_rb.html b/doc/files/lib/twitter/base_rb.html new file mode 100644 index 000000000..b6b546a69 --- /dev/null +++ b/doc/files/lib/twitter/base_rb.html @@ -0,0 +1,123 @@ + + + + + + File: base.rb + + + + + + + + + + +
+

base.rb

+ + + + + + + + + +
Path:lib/twitter/base.rb +
Last Update:Sun Nov 26 19:11:06 -0500 2006
+
+ + +
+ + + +
+ +
+

+This is the base class for the twitter library. It makes all the requests +to twitter, parses the xml (using hpricot) and returns ruby objects to play +with. +

+

+The private methods in this one are pretty fun. Be sure to check out users, +statuses and call. +

+ +
+ +
+

Required files

+ +
+ uri   + net/http   + rubygems   + hpricot   +
+
+ +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/files/lib/twitter/command_rb.html b/doc/files/lib/twitter/command_rb.html new file mode 100644 index 000000000..2e9b95acb --- /dev/null +++ b/doc/files/lib/twitter/command_rb.html @@ -0,0 +1,115 @@ + + + + + + File: command.rb + + + + + + + + + + +
+

command.rb

+ + + + + + + + + +
Path:lib/twitter/command.rb +
Last Update:Sun Nov 26 18:57:28 -0500 2006
+
+ + +
+ + + +
+ +
+

+The command class is used for the command line interface. It is only used +and included in the bin/twitter file. +

+ +
+ +
+

Required files

+ +
+ yaml   +
+
+ +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/files/lib/twitter/easy_class_maker_rb.html b/doc/files/lib/twitter/easy_class_maker_rb.html new file mode 100644 index 000000000..564eb4275 --- /dev/null +++ b/doc/files/lib/twitter/easy_class_maker_rb.html @@ -0,0 +1,108 @@ + + + + + + File: easy_class_maker.rb + + + + + + + + + + +
+

easy_class_maker.rb

+ + + + + + + + + +
Path:lib/twitter/easy_class_maker.rb +
Last Update:Sun Nov 26 19:09:56 -0500 2006
+
+ + +
+ + + +
+ +
+

+This is pretty much just a macro for creating a class that allows using a +block to initialize stuff and to define getters and setters really quickly. +

+ +
+ + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/files/lib/twitter/status_rb.html b/doc/files/lib/twitter/status_rb.html new file mode 100644 index 000000000..a0070630b --- /dev/null +++ b/doc/files/lib/twitter/status_rb.html @@ -0,0 +1,123 @@ + + + + + + File: status.rb + + + + + + + + + + +
+

status.rb

+ + + + + + + + + +
Path:lib/twitter/status.rb +
Last Update:Sun Nov 26 19:10:12 -0500 2006
+
+ + +
+ + + +
+ +
+

+The attributes are created_at, id, text, relative_created_at, and user +(which is a user object) new_from_xml expects xml along the lines of: +<status> +

+
+  <id>1</id>
+  <created_at>a date</created_at>
+  <text>some text</text>
+  <relative_created_at>about 1 min ago</relative_created_at>
+  <user>
+    <id>1</id>
+    <name>John Nunemaker</name>
+    <screen_name>jnunemaker</screen_name>
+  </user>
+
+

+</status> +

+ +
+ + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/files/lib/twitter/user_rb.html b/doc/files/lib/twitter/user_rb.html new file mode 100644 index 000000000..6ec910118 --- /dev/null +++ b/doc/files/lib/twitter/user_rb.html @@ -0,0 +1,122 @@ + + + + + + File: user.rb + + + + + + + + + + +
+

user.rb

+ + + + + + + + + +
Path:lib/twitter/user.rb +
Last Update:Sun Nov 26 19:05:38 -0500 2006
+
+ + +
+ + + +
+ +
+

+The attributes for user are id, name, screen_name, status (which is a +status object) new_from_xml expects xml along the lines of: <user> +

+
+  <id>1</id>
+  <name>John Nunemaker</name>
+  <screen_name>jnunemaker</screen_name>
+  <status>
+    <id>1</id>
+    <created_at>a date</created_at>
+    <text>some text</text>
+    <relative_created_at>about 1 min ago</relative_created_at>
+  </status>
+
+

+</user> +

+ +
+ + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/files/lib/twitter/version_rb.html b/doc/files/lib/twitter/version_rb.html new file mode 100644 index 000000000..2859c67dd --- /dev/null +++ b/doc/files/lib/twitter/version_rb.html @@ -0,0 +1,101 @@ + + + + + + File: version.rb + + + + + + + + + + +
+

version.rb

+ + + + + + + + + +
Path:lib/twitter/version.rb +
Last Update:Sun Nov 26 18:55:09 -0500 2006
+
+ + +
+ + + +
+ + + +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/files/lib/twitter_rb.html b/doc/files/lib/twitter_rb.html new file mode 100644 index 000000000..f709fe1f1 --- /dev/null +++ b/doc/files/lib/twitter_rb.html @@ -0,0 +1,154 @@ + + + + + + File: twitter.rb + + + + + + + + + + +
+

twitter.rb

+ + + + + + + + + +
Path:lib/twitter.rb +
Last Update:Sun Nov 26 18:44:38 -0500 2006
+
+ + +
+ + + +
+ +
+

addicted to twitter

+

+… a sweet little diddy that helps you twitter your life away +

+

Install

+

+$ sudo gem install twitter +

+

Examples

+
+      Twitter::Base.new('your email', 'your password').update('watching veronica mars')
+
+      # or you can use post
+      Twitter::Base.new('your email', 'your password').post('post works too')
+
+      puts "Public Timeline", "=" * 50
+      Twitter::Base.new('your email', 'your password').timeline(:public).each do |s|
+        puts s.text, s.user.name
+        puts
+      end
+
+      puts '', "Friends Timeline", "=" * 50
+      Twitter::Base.new('your email', 'your password').timeline.each do |s|
+        puts s.text, s.user.name
+        puts
+      end
+
+      puts '', "Friends", "=" * 50
+      Twitter::Base.new('your email', 'your password').friends.each do |u|
+        puts u.name, u.status.text
+        puts
+      end
+
+      puts '', "Followers", "=" * 50
+      Twitter::Base.new('your email', 'your password').followers.each do |u|
+        puts u.name, u.status.text
+        puts
+      end
+
+ +
+ +
+

Required files

+ +
+ twitter/version   + twitter/easy_class_maker   + twitter/base   + twitter/user   + twitter/status   +
+
+ +
+ + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/doc/fr_class_index.html b/doc/fr_class_index.html new file mode 100644 index 000000000..f260dcdfd --- /dev/null +++ b/doc/fr_class_index.html @@ -0,0 +1,36 @@ + + + + + + + + Classes + + + + + + + + \ No newline at end of file diff --git a/doc/fr_file_index.html b/doc/fr_file_index.html new file mode 100644 index 000000000..0ea1f63a4 --- /dev/null +++ b/doc/fr_file_index.html @@ -0,0 +1,34 @@ + + + + + + + + Files + + + + + + + + \ No newline at end of file diff --git a/doc/fr_method_index.html b/doc/fr_method_index.html new file mode 100644 index 000000000..6fda5c03a --- /dev/null +++ b/doc/fr_method_index.html @@ -0,0 +1,47 @@ + + + + + + + + Methods + + + + + + + + \ No newline at end of file diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 000000000..d209f0b17 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,24 @@ + + + + + + + twitter's twitter-0.0.2 Documentation + + + + + + + + + + + \ No newline at end of file diff --git a/doc/rdoc-style.css b/doc/rdoc-style.css new file mode 100644 index 000000000..44c7b3d13 --- /dev/null +++ b/doc/rdoc-style.css @@ -0,0 +1,208 @@ + +body { + font-family: Verdana,Arial,Helvetica,sans-serif; + font-size: 90%; + margin: 0; + margin-left: 40px; + padding: 0; + background: white; +} + +h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } +h1 { font-size: 150%; } +h2,h3,h4 { margin-top: 1em; } + +a { background: #eef; color: #039; text-decoration: none; } +a:hover { background: #039; color: #eef; } + +/* Override the base stylesheet's Anchor inside a table cell */ +td > a { + background: transparent; + color: #039; + text-decoration: none; +} + +/* and inside a section title */ +.section-title > a { + background: transparent; + color: #eee; + text-decoration: none; +} + +/* === Structural elements =================================== */ + +div#index { + margin: 0; + margin-left: -40px; + padding: 0; + font-size: 90%; +} + + +div#index a { + margin-left: 0.7em; +} + +div#index .section-bar { + margin-left: 0px; + padding-left: 0.7em; + background: #ccc; + font-size: small; +} + + +div#classHeader, div#fileHeader { + width: auto; + color: white; + padding: 0.5em 1.5em 0.5em 1.5em; + margin: 0; + margin-left: -40px; + border-bottom: 3px solid #006; +} + +div#classHeader a, div#fileHeader a { + background: inherit; + color: white; +} + +div#classHeader td, div#fileHeader td { + background: inherit; + color: white; +} + + +div#fileHeader { + background: #057; +} + +div#classHeader { + background: #048; +} + + +.class-name-in-header { + font-size: 180%; + font-weight: bold; +} + + +div#bodyContent { + padding: 0 1.5em 0 1.5em; +} + +div#description { + padding: 0.5em 1.5em; + background: #efefef; + border: 1px dotted #999; +} + +div#description h1,h2,h3,h4,h5,h6 { + color: #125;; + background: transparent; +} + +div#validator-badges { + text-align: center; +} +div#validator-badges img { border: 0; } + +div#copyright { + color: #333; + background: #efefef; + font: 0.75em sans-serif; + margin-top: 5em; + margin-bottom: 0; + padding: 0.5em 2em; +} + + +/* === Classes =================================== */ + +table.header-table { + color: white; + font-size: small; +} + +.type-note { + font-size: small; + color: #DEDEDE; +} + +.xxsection-bar { + background: #eee; + color: #333; + padding: 3px; +} + +.section-bar { + color: #333; + border-bottom: 1px solid #999; + margin-left: -20px; +} + + +.section-title { + background: #79a; + color: #eee; + padding: 3px; + margin-top: 2em; + margin-left: -30px; + border: 1px solid #999; +} + +.top-aligned-row { vertical-align: top } +.bottom-aligned-row { vertical-align: bottom } + +/* --- Context section classes ----------------------- */ + +.context-row { } +.context-item-name { font-family: monospace; font-weight: bold; color: black; } +.context-item-value { font-size: small; color: #448; } +.context-item-desc { color: #333; padding-left: 2em; } + +/* --- Method classes -------------------------- */ +.method-detail { + background: #efefef; + padding: 0; + margin-top: 0.5em; + margin-bottom: 1em; + border: 1px dotted #ccc; +} +.method-heading { + color: black; + background: #ccc; + border-bottom: 1px solid #666; + padding: 0.2em 0.5em 0 0.5em; +} +.method-signature { color: black; background: inherit; } +.method-name { font-weight: bold; } +.method-args { font-style: italic; } +.method-description { padding: 0 0.5em 0 0.5em; } + +/* --- Source code sections -------------------- */ + +a.source-toggle { font-size: 90%; } +div.method-source-code { + background: #262626; + color: #ffdead; + margin: 1em; + padding: 0.5em; + border: 1px dashed #999; + overflow: hidden; +} + +div.method-source-code pre { color: #ffdead; overflow: hidden; } + +/* --- Ruby keyword styles --------------------- */ + +.standalone-code { background: #221111; color: #ffdead; overflow: hidden; } + +.ruby-constant { color: #7fffd4; background: transparent; } +.ruby-keyword { color: #00ffff; background: transparent; } +.ruby-ivar { color: #eedd82; background: transparent; } +.ruby-operator { color: #00ffee; background: transparent; } +.ruby-identifier { color: #ffdead; background: transparent; } +.ruby-node { color: #ffa07a; background: transparent; } +.ruby-comment { color: #b22222; font-weight: bold; background: transparent; } +.ruby-regexp { color: #ffa07a; background: transparent; } +.ruby-value { color: #7fffd4; background: transparent; } \ No newline at end of file diff --git a/lib/twitter.rb b/lib/twitter.rb new file mode 100644 index 000000000..e4127597b --- /dev/null +++ b/lib/twitter.rb @@ -0,0 +1,45 @@ +# = addicted to twitter +# +# ... a sweet little diddy that helps you twitter your life away +# +# +# == Install +# +# $ sudo gem install twitter +# +# == Examples +# +# Twitter::Base.new('your email', 'your password').update('watching veronica mars') +# +# # or you can use post +# Twitter::Base.new('your email', 'your password').post('post works too') +# +# puts "Public Timeline", "=" * 50 +# Twitter::Base.new('your email', 'your password').timeline(:public).each do |s| +# puts s.text, s.user.name +# puts +# end +# +# puts '', "Friends Timeline", "=" * 50 +# Twitter::Base.new('your email', 'your password').timeline.each do |s| +# puts s.text, s.user.name +# puts +# end +# +# puts '', "Friends", "=" * 50 +# Twitter::Base.new('your email', 'your password').friends.each do |u| +# puts u.name, u.status.text +# puts +# end +# +# puts '', "Followers", "=" * 50 +# Twitter::Base.new('your email', 'your password').followers.each do |u| +# puts u.name, u.status.text +# puts +# end + +require 'twitter/version' +require 'twitter/easy_class_maker' +require 'twitter/base' +require 'twitter/user' +require 'twitter/status' \ No newline at end of file diff --git a/lib/twitter/base.rb b/lib/twitter/base.rb new file mode 100644 index 000000000..db3473b81 --- /dev/null +++ b/lib/twitter/base.rb @@ -0,0 +1,109 @@ +# This is the base class for the twitter library. It makes all the requests +# to twitter, parses the xml (using hpricot) and returns ruby objects to play with. +# +# The private methods in this one are pretty fun. Be sure to check out users, statuses and call. +require 'uri' +require 'net/http' +require 'rubygems' +require 'hpricot' + +module Twitter + class Untwitterable < StandardError; end + class CantConnect < Untwitterable; end + class BadResponse < Untwitterable; end + class UnknownTimeline < ArgumentError; end + + class Base + + # Twitter's url, duh! + @@api_url = 'twitter.com' + + # Timelines exposed by the twitter api + @@timelines = [:friends, :public] + + def self.timelines + @@timelines + end + + # Initializes the configuration for making requests to twitter + def initialize(email, password) + @config, @config[:email], @config[:password] = {}, email, password + end + + # Returns an array of statuses for a timeline; + # Available timelines are determined from the @@timelines variable + # Defaults to your friends timeline + def timeline(which=:friends) + raise UnknownTimeline unless @@timelines.include?(which) + auth = which.to_s.include?('public') ? false : true + statuses(call("#{which}_timeline", :auth => auth)) + end + + # Returns an array of users who are in your friends list + def friends + users(call(:friends)) + end + + # Returns an array of users who are following you + def followers + users(call(:followers)) + end + + # Updates your twitter with whatever status string is passed in + def post(status) + url = URI.parse("http://#{@@api_url}/statuses/update.xml") + req = Net::HTTP::Post.new(url.path) + + req.basic_auth(@config[:email], @config[:password]) + req.set_form_data({'status' => status}) + + res = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) } + Status.new_from_xml(Hpricot.parse(res.body).at('status')) + end + alias :update :post + + private + # Converts xml to an array of statuses + def statuses(res) + statuses = [] + doc = Hpricot.parse(res) + (doc/:status).each do |status| + statuses << Status.new_from_xml(status) + end + statuses + end + + # Converts xml to an array of users + def users(res) + users = [] + doc = Hpricot.parse(res) + (doc/:user).each do |user| + users << User.new_from_xml(user) + end + users + end + + # Calls whatever api method requested + # + # ie: call(:public_timeline, :auth => false) + def call(method, arg_options={}) + options = { :auth => true }.merge(arg_options) + + path = "/statuses/#{method.to_s}.xml" + headers = { "User-Agent" => @config[:email] } + + begin + response = Net::HTTP.start(@@api_url, 80) do |http| + req = Net::HTTP::Get.new(path, headers) + req.basic_auth(@config[:email], @config[:password]) if options[:auth] + http.request(req) + end + + raise BadResponse unless response.message == 'OK' + response.body + rescue + raise CantConnect + end + end + end +end \ No newline at end of file diff --git a/lib/twitter/command.rb b/lib/twitter/command.rb new file mode 100644 index 000000000..81c5a3305 --- /dev/null +++ b/lib/twitter/command.rb @@ -0,0 +1,195 @@ +# The command class is used for the command line interface. +# It is only used and included in the bin/twitter file. +require 'yaml' + +module Twitter + class Command + @@commands = [:post, :timeline, :friends, :friend, :followers, :follower] + + @@template = < [options]\n\nAvailable Commands:" + Twitter::Command.commands.each do |com| + puts " - #{com}" + end + end + end + + def commands + @@commands + end + + # Posts an updated status to twitter + def post(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You didn't enter a message to post.\n\n Usage: twitter post "You're fabulous message"\n) + exit(0) + end + + post = args.shift + + begin + status = Twitter::Base.new(config['email'], config['password']).post(post) + puts "\nGot it! New twitter created at: #{status.created_at}\n" + rescue + puts "\nTwitter what?. Something went wrong and your status could not be updated.\n" + end + end + + # Shows status, time and user for the specified timeline + def timeline(args) + config = create_or_find_config + + timeline = :friends + timeline = args.shift.intern if args.size > 0 && Twitter::Base.timelines.include?(args[0].intern) + + begin + puts + Twitter::Base.new(config['email'], config['password']).timeline(timeline).each do |s| + puts "#{s.text}\n-- #{s.relative_created_at} by #{s.user.name}" + puts + end + rescue + puts error_msg + end + end + + def friends + config = create_or_find_config + + begin + puts + Twitter::Base.new(config['email'], config['password']).friends.each do |u| + puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + puts + end + rescue + puts error_msg + end + end + + # Shows last updated status and time for a friend + # Needs a screen name + def friend(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You forgot to enter a screen name.\n\n Usage: twitter friend jnunemaker\n) + exit(0) + end + + screen_name = args.shift + + begin + puts + found = false + Twitter::Base.new(config['email'], config['password']).friends.each do |u| + if u.screen_name == screen_name + puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + found = true + end + end + + unless found + puts "Sorry couldn't find a friend of yours with #{screen_name} as a screen name" + end + puts + rescue + puts error_msg + end + end + + # Shows all followers and their last updated status + def followers + config = create_or_find_config + + begin + puts + Twitter::Base.new(config['email'], config['password']).followers.each do |u| + puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + puts + end + rescue + puts error_msg + end + end + + # Shows last updated status and time for a follower + # Needs a screen name + def follower(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You forgot to enter a screen name.\n\n Usage: twitter follower jnunemaker\n) + exit(0) + end + + screen_name = args.shift + + begin + puts + found = false + Twitter::Base.new(config['email'], config['password']).follower.each do |u| + if u.screen_name == screen_name + puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + found = true + end + end + + unless found + puts "Sorry couldn't find a follower of yours with #{screen_name} as a screen name" + end + puts + rescue + puts error_msg + end + end + + private + # Checks for the config, creates it if not found + def create_or_find_config + begin + config = YAML::load open(ENV['HOME'] + "/.twitter") + rescue + open(ENV["HOME"] + '/.twitter','w').write(@@template) + config = YAML::load open(ENV['HOME'] + "/.twitter") + end + + if config['email'] == nil or config['password'] == nil + puts "Please edit ~/.twitter to include your twitter email and password\nTextmate users: mate ~/.twitter" + exit(0) + end + + config + end + + def error_msg + "\nTwitter what?. Something went wrong and your status could not be updated.\n" + end + end + end +end \ No newline at end of file diff --git a/lib/twitter/easy_class_maker.rb b/lib/twitter/easy_class_maker.rb new file mode 100644 index 000000000..1537e3849 --- /dev/null +++ b/lib/twitter/easy_class_maker.rb @@ -0,0 +1,43 @@ +# This is pretty much just a macro for creating a class that allows +# using a block to initialize stuff and to define getters and setters +# really quickly. +module Twitter + module EasyClassMaker + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + # creates the attributes class variable and creates each attribute's accessor methods + def attributes(*attrs) + @@attributes = attrs + @@attributes.each { |a| attr_accessor a } + end + + # read method for attributes class variable + def self.attributes; @@attributes end + end + + # allows for any class that includes this to use a block to initialize + # variables instead of assigning each one seperately + # + # Example: + # + # instead of... + # + # s = Status.new + # s.foo = 'thing' + # s.bar = 'another thing' + # + # you can ... + # + # Status.new do |s| + # s.foo = 'thing' + # s.bar = 'another thing' + # end + def initialize + yield self if block_given? + end + end +end \ No newline at end of file diff --git a/lib/twitter/status.rb b/lib/twitter/status.rb new file mode 100644 index 000000000..4a84a0242 --- /dev/null +++ b/lib/twitter/status.rb @@ -0,0 +1,33 @@ +# The attributes are created_at, id, text, relative_created_at, and user (which is a user object) +# new_from_xml expects xml along the lines of: +# +# 1 +# a date +# some text +# about 1 min ago +# +# 1 +# John Nunemaker +# jnunemaker +# +# +module Twitter + class Status + include EasyClassMaker + + attributes :created_at, :id, :text, :relative_created_at, :user + + class << self + # Creates a new status from a piece of xml + def new_from_xml(xml) + Status.new do |s| + s.id = (xml).at('id').innerHTML + s.created_at = (xml).at('created_at').innerHTML + s.text = (xml).at('text').innerHTML + s.relative_created_at = (xml).at('relative_created_at').innerHTML + s.user = User.new_from_xml(xml) if (xml).at('user') + end + end + end + end +end \ No newline at end of file diff --git a/lib/twitter/user.rb b/lib/twitter/user.rb new file mode 100644 index 000000000..337a4712e --- /dev/null +++ b/lib/twitter/user.rb @@ -0,0 +1,32 @@ +# The attributes for user are id, name, screen_name, status (which is a status object) +# new_from_xml expects xml along the lines of: +# +# 1 +# John Nunemaker +# jnunemaker +# +# 1 +# a date +# some text +# about 1 min ago +# +# +module Twitter + class User + include EasyClassMaker + + attributes :id, :name, :screen_name, :status + + class << self + # Creates a new user from a piece of xml + def new_from_xml(xml) + User.new do |u| + u.id = (xml).at('id').innerHTML + u.name = (xml).at('name').innerHTML + u.screen_name = (xml).at('screen_name').innerHTML + u.status = Status.new_from_xml(xml) if (xml).at('status') + end + end + end + end +end \ No newline at end of file diff --git a/lib/twitter/version.rb b/lib/twitter/version.rb new file mode 100644 index 000000000..351cefe00 --- /dev/null +++ b/lib/twitter/version.rb @@ -0,0 +1,9 @@ +module Twitter #:nodoc: + module VERSION #:nodoc: + MAJOR = 0 + MINOR = 0 + TINY = 2 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/pkg/twitter-0.0.1.gem b/pkg/twitter-0.0.1.gem new file mode 100644 index 0000000000000000000000000000000000000000..ddc71f1d90f53e051ea5ed1a806437825103bae5 GIT binary patch literal 17920 zcmeI3Wo+f%lcvMW%*@Qm4KvdXb)dtX4l{FynVFe6=`dr5nVHk!q{DZAv!m6H=F5JX zon7f)mVC-C**^99$g<9<$IRH(n8nrDnZ?o@?7zy`{xKddF0g-V|FQqna&mFY@f z{~l-Od=3@ubaz1+5hu+4=ZzX+y5B&PyYWY{y$Uu56S(f%l}_JB#njx zW1+W^0)xEr^D!ipX*&Drdcz{#Cz6vS2qL|Er60{$iyEqn^#eb(_s=MfDM43EuxR0= zG=;db?9*Jn$s-~E*qHvcVjsixS#fcFapCPY?H_QuKJ{fNd}#?Oyt~_{60m>x)zkHQ z>+_l`vfJI|1sX718$FeZ4#4q3w*dS4;EF0!xY*XXM!8nGhaWri)Q~jQDyiX%kEWQk z(JNJ{#eo7x)bS^gg3cCiwH_Y5^Yk5vT5K>XpLfk`j&>z~@4vy<$dP0cL}j(%K1&f& zvTSCrh3z7ylT z{POjVc?8eC=}|B37-jTo4`3S29i3S-+Y}-5l`FDLG$ip)c)cU^!e{dS2K{C+$Pijf zJ~{e8qq$XI;8D$&H+#+~r90ki5j~Sm&-8_!TiXnYtuI`Mxa;E8bmgv=r~<$8x~4xg zF^9D3LqWt2uH6fw=~OUPj#L#|ujE7r4Z1B@#+!-`5qYey36luDGil^v=-(osE*>2- zTr|(eaf=)tiwr}q^5O&CyH$&s2x;6}U-rA-r{JfT<&76;r1~@;eH8grL>3Hn`vLSd z6kC4^N`JA1A{?N(Y~YKG=F8#mCzA%*xAgk=&RL2GM$_CqHnY4Szk_xl(VlZZLAN_S z_3t1B-=?$tXufD|N~U)d%> zFG8`&G=79*_>(Nt1AFD$xSsXcl71fuO~pAP5N zgU>kdvPzafElb{=Az4Nc@%c1RWKH9Vz{Alu({cYfxa(q#Eis4HV8${pXG{bZ<^cx-)@t|5Bn4qYU zmnkccLLWK|e0Bl=Ynxu{V>?n}#0|nS%a+3V&QIkX8OfcPv<>=uyc59TqYYk}`_6ajSF9p*^F74~2cSKyO9tm?( zthWM@<#QL)JB!~~4WIv_tY53x_JIO+uCD*)Kk*rx0r~7#LO%2tViUfKx_9_54)~2^ z4wZ3wHmi8Yc3UsDc{T;ZIJrofDXu!66#_`_E^j`am-=rSqGCn1Xd)6+jN8iE6+gA`j$dk_3gT?20M4|2fer zGWJhCe3_e&MH8OoQrX~KGj>8}b8%sq1Eq!u;q^4vC%!@$09rp?73r$Yk1MYfN&Kn> zSq<4NPZIX6H$cjDh>Z$iz8eamtiKl)I51=oVe?dyi}>N91~9RJ4-`u)gANfwf=aW% zttGJa&j`v_uUnS&cHqo;#W`nwVC;7y32mV)tXrFXbVRDo95GW3#4r_5qqmmn80?5V zm+KAugwL+~?vZG$BzONPJO32;Jch$~Q|_teFKd^0iK1L~&NlW`dpOIIF#HDA2fdDF zfme+_ru7sN7RdMqF9r^uhR$6U7CuvclniPxt3R;P3G8zzA=%EZ@f#QVK30IlZn?2D z#<8L=zioKtBDCLO7D8rIVzYHb2m3tJ9_LH%_}~Cv)~N|){!Ftc6yw?f+r1YTfokdZ zK|bUM^{bq`CXanr^y3^sggsR#%y_s<%H#dqYjdxMK8R*39Nzk32SCOuj}v)EL1g@GaJ-NE8N%PyEdS88y3ZNh;hdk zfH6}lh=;c~puwq~3MBMW1@cRrC1}u*gXI3WcC}_StcWSy5uQGJ+MVXRNPjx0po%ULw>i>6R%b_H=$t*yK`^mMLcB=L$V`XW8Fl$hTR>?gSo=#hjWwQZy&Tq zihkFfeCc!aEkAv2V^_45&W$T0 zcc&zT+nb{qY-R^n&iTo%$i=Mp=#Pc&;?RuuXG`8fl%T)U${~If{3+Oq0IvED^>8>3 zLI|8J*K8<^9r>E-`$O-FY#LFiPFqGX5*f@I8KU2Vy#gepm{Ave`6fL?l1hhF!*)q< z@r(uGFc050fj(GItmQsEoc`mffHz$ny0E5-Tt7b*Mv!o+s0CV_wym*7Bq$BBYb-KLa zTbOm+C-!>q;@@N{;+(K`q&y{2yNKc70a7%$CNVw&@WcU)zqZ=P&d5M+;aQUuW*>-} z9Gp9s&B2Kv#%zQuGxdLbaedwq2AQ$x=Ck102QH;q9GAD)ch212=qxX;+?FjGJ?-+? zBFSIm^4K@B2kcp7wI`!V(BtGR^#_bN$=iB%nm|&eqiGO<^5VSCl(6 zpH;T+*P;)Rb*ehT|}q@Pm?&mp@gFP8%c1=z8c4yF0Qt@r&#%ug|oCnk3Q zCJ7V|n<&Ep-+6_Z zITn<${t(a#$ga`9F9yegZtB@ux6)TQ$Qjl#SgN)h-&RffF3X8FrQEX!D5eER$$kiP zJIc!4gBtO6rvX2smSZ=5a(Jk9cxbOU7#$p=76Z?XsNXCxjUb(&(tlq;Ju@Y;I1FlF zQmDB(%JS;LP+M2ndv^FD=@?{otE~IpvYKV`GBXIN{g5Th?=X?oD zEvb{_w>`vL!9;iio&5zlt?}94%~Eu*2Z3}hPL=+tb zrxQQF`}Ad*tKDy!Jhdu7I1=~cULk8|@Ufzc)!;Xb=}}#5pN7NQ0*SVaP^Pn$;z*V{ zHX$r3SHAn1AsbO=Gk@Q3#;%co3^E{j@tJok$bVl#{c(>jJPP|#DHldb<3WIG-Il8T zjOY_{Z|0~XC`L&d2lTh%DPR{VCmdVdu58Fy`Gdno-}I9-tq#{h)V7<%pPTX6QQCeT$ga1<(V{~Y_$o&VIyX+z9V3ij!xmSyt| z*UgzZVUU6;$Y7scralOdOSjF;sm1F`En~ioof0=C2jk6|s_JQ1rdU6Tmt8}ONUVX; zxyA8xBm<|)Cu=Zq>ABIQW>Nh_z&;GE>Gp$59!Mmw;{+1HR1dsLtd%j) z?Q91&J#FlK0v(HxtMN+k=oy;Mj?MZ_v>id|y^)kq?sEaUxu%Y@>o%nA%VzPio><3u zu$5|PwZWA87=b9r%88a&wkpZ<9X>sjqwu~slGq)uq9Socgn(Iuj{KLkDop^qwCGfs z@9MpT%w|qg0URSz{9+9RBg!jbzdX>$ykV;gKFJn)%Sk(w5iD(4D?BGZOoz*2tpR~Y zd3?G+s&<)N0`ly<#QXQFRGgqM^qTavUd#uDOsYN!=GJ}qq{h=hIT%{3^E?BWk!CM6 zy^#RNrFPQ7V=#Sx*=j7O~)PMwPa?HGAaLj#^X>9ACW0Ge2}{IxtQ3 zyuD`_qt|h$^B^k8YLw0lF)>m|SkwK3&!{1u*%FR3J;lGUL9S&R^uM^R_Ieb^Em3G& zKL;NW16Fzq8}J~Z{&E0R_sGS` zT}F^qsmKUI?{(&5!HKfbz#~yvf*7Q9A**~_ur6Y@W*``1P?a|T{FjX7k7VMwsf|(P z^K6Q6ECmy8nN%2hKX0@|eAko0@wr!ft=ab`ptncvd!Z`sUcQcRz3Wp>lw=03oh zb*Td>rH(a5zrjeGsHBxd@O)uHfq#vH+zq-bbO}uFp}t?o?tOE*wHWuxz>;oB`*oPM z4}tjY=gdxZ&sX>%B#jAIJB?+3ST15Aqucj-1w5cInGdXwoVH; z%NERBOFIqJ{KWrAL_rj93UTIWfUo+IllG!xDgw@nKbSCkH$Kt>CTNZqQs%qwy2vKB z5x$GC-4DJTK9=0i_oRt_iclZil%)?}P4`QmRX$u93NeMB<+tv* zsX}YS-GwfV^q1~rgYi>oW=EaowZd{0JR~9q{M-`GmLk2Z_Cuy4LA!My9WK-=+djZ1 zy^L^{0~uZgI!ynAoQP}-(?S@1=0GfLrTBU?SGoL`d!EqEzo9y**}T6xqgQ>gk8X>zll<@bjl4?PVf*P9 zF%*`+SxH!Ys{fp#^Hu5{;S;QL;un#S|8zkI(ORCxVohi zveT{#56m=Dx_tgjF};H4V0eL;4_jfBsTI?)%&I6OLTq+kRi3=V&3b3yS?qN>Ntkb6 z{xw>t$yz(q)M&5mtb{$ZD)zph8n@AZb3V$5IOUz-7|18B(gX&f-f1NbU?(;Ty=}IF z;J)3(gQf9y*`S(Uvf{8dIg*dgir^u0h@uHI_j+}{W#`;NF2HT%$~0hO>31epVu`={ z#%3~`B|BpAle>`8w$z%Z7)1*&EWG9@s=^WST99ueVP3_9fd3`(d++=^V07?+60+A} zSocuJe6HKQxQe%Sq$4+#&xJ*59bqMh^(66eSXgd$4nU~~Cj<4P$Vf?b&<2OBNPr$r*-P zRawJ}ZNf$NIo=g$al-@B+x0DE=rIFBvo*z1qb;@0_8kh2;mv0jmsz=%*3J+gU|OcX z_q4Gfkb;~$sJuhYsOIO`E-Zs^-&+M_2ZsX*TeS!9LMe}urew>r%nqQuqsA}*(KZRn z+L)0OC1j|_5kJY2nS5v)YAsyN{qCis?%~wSQuf#TY%8&7(AyO03%YJHjT<3>XB7?4jDr4GB_YEyaDrrYI-tn{kRfo0DXQ99fdT5ec2 zq3nVeKF264iOE%Cq9PA1Y}8)c!|?Gf8-)djKuA|p1#-5PEad?N_DB;Y;STj#K%b(i zPT{IU8w0?Ik0e?+wnJ%~dZSt6$fB~4-OFU?&5fj(a|(v8s3Y}U<$BzX9Ot=%eeSF# zG?;z4y2xlf2^wKA>dN{O;S(a`2$E;6l<{D7C8rst*kJmuLn!UQMAOH9jvTne6@@Q3 zp%aB8>SAYrMEyel?>{j$# zmC-&?+%>w##{|kQc&~LC)fX)fCL2J~h`jrrZ4CXkfr)@l;|{~<(iBGj;!XjDM8eAm zjHNs*z=}n%9qH&Np_7*c!e2+UaicdLJ@tXRA{-Jx{R#Tt1%~FADjV1FlYKV1X8Z_cjN|VxRc~`x7^m&)#V5~N?N8bxZXs`;SngkZ z*NM*^PplW`RuV9gF^5Fd-+b|o%I`{4XDe~A#bR%2Ebax%w5-kv=l@9xtNVHV`^Tae7H5?p)q>J+!xIwxn{_&tw(yg_aENEgpmo4LeS8^GIJ z%FfSOuxBzQ>*syJUzBv_7j!5N3qZgYE3Ohv?Z*_eaAQ9wa~qVP7Dy|TYncDL1h&zqq) ztfc{h_-yF0!AQ-r@4(Yh0om+w^$#1=Z9Suqh`PH6Ey1Z7CYXL!_N)RziJ_ucJaA&% ztLe-~Lp;qlqhwLFIL*&_HB+$ZxS3aS`fbC~Nt|(q6J}lTtsB4X$B(RF&&?>dYC=hK zXb9xs0p6%4F^HRm;uKawNz{_aXBx@uhSKXQwNT(|di)>6QiO`2Ia_^?5onNAOe&j* z6s0TqW>fTV7@fFy(VDqIuvAF!& z1uQo8`fByfj{aWQ290PABh&0PAG#8q_Cb#*9r3x05od+miK%jqp=zZ<9ko2gHr#;4 zq@UrIR(~jsl^emh5oMZ4k9+J|>1)TGM1>!L`@czZ+FLI7NJC5`H7k=_e7605HMhU3 zgZ3Jf)sL#dx5>;DSN6dQGUV4n2q5xYJ{$vSMMq6fvtIc2PBiJn69>8*=msK_`>18- zJHnrBJpvkbabJDd;AQ%Q1BE`11BnIwy!)T`MP9(Oo@0zjh*lPsrs5Y~)QzU5O{vXv z#`zT3^z$tV;k#s-70WxAjDa8W7GH}0gZMr`)xBZSH zE)JEdY;K%Px~AuBDtF!t?I(m;bYZSQ7A%3Hwi5!HoXrUugf0IXbREhF-#vX{la-@a zeYrlIS76DrP07SSTapk{lWqSySzrUrs0M|LeizmQtVm9ed@R6heQp1@0s$nR3{JlQ zfPi=>=Lc$O8g-r3HK6)btFkr`tKV8hY*W|KZBSaT$i`~>$SIiWc!wr9Itp|KX`-GrN?3?|$ z|06XI5r0J^n9d4}znLvEql1z$|_f!UHP+CvuO!v=?Z73Uoqh;i7&~( z=d*K8p$m(Ez={Bm)>L(4-lb8#5yihP&4%8#cNg*&S6uWZBj`k|Q^o3g)s!7s0XS$} zETXzI55Ob0mDvb4W<-xW?_XRrB{8L!*i<&}6$i^@Z4eM@e4k33u$mAZL6 zcjr>jBCI80+@EZpE$mw{D z?SwjMcnxCuCNlv_I>XHCJ*(>-;2w&#O%A2`i0E#>DJwW*#mR z)n&&kJHH|e{>hPy5%jZH2ox8N@Svi}hfv2N%_IuDYGGS!M72p1t*KT6Sv&tqtMYx) zB_j<^leTRET%^icCWYlknYgMOV+_)~65Z>Rkr@1sIgKP1B*NME4kJSIf)o<%zoKE% z4ta!`6aTzgT-r@BsZ&aNXZ~$Q;L@(IHB$-F%!vkhaRh!4s` zbM1#mQOrZf+Sq;vWyj(`h6&Yd$1p<;%CEo*6ra&B9jrWnKxkwGH@;*qOV`3G{Xtq- zy{DSFY(iR@Srj*8SIV#U5h|zihw=Wyj!-H^()zMGqzYyhexQeDjQ^4ywxMcFjK`7Y zW>eDgD}j%^@~5ZCkHQR@q_*DgdXU|ak8}V74kv7TCn>cu$FhSt`Cn*mI`pe1#m0F# z%co^SoyjdBY?I-MFxMo~l37I8X~GUad#E01HP~%lBz{d>ITAzcFdu3Xc&6gEGl8i} z%(mt?V0CI6(E9E0?AIJu!y9%46ghda$KTC>>YTuFi|1tKJrkvR!t{Gks_=LX`Nmkg zn#r4gFFP!ThUwZjYHf@2<7J;Yr!kP4GmN3BRrq7HUZLcoi$G@L2vJ@k&qP{=rWTOu z(7qjVY@{;BUzNQcnm)s!Hjb%^%M^RF+}A2ujFS{jIS6>%oW?91F%vGiRQtH73a*ko zhMVk)Z1VdE5j5hskVzNlXNsZe8U4;_FGOLMGp z*B}tND8;ibTfpL@7S3H_w|?EH1eYP z)#b#k#4au3gg#R0*ECclwwDlX#WC5}8M6^xa?`h)HC`mkv9;{tZeN|KO97PCA1^Wy zhV_@#9%?o=DkwD21ATfQN{q!O1@^bx=EF$T?C3MhiIPX7Zy8pSTwa4e0=2u$XE0`! z3y6AYS~K!#o@pYG%;DcE>n5E7SY0Zm{8hb+TBwL9%k?W0v*&ua#Cv$f#T3;ze|U_z zhd!G zbRp9x{#;OzYZ^K(TB~{iTzBj3RWMf?2-e2c>H}iKR}5;v^3=#Y<-SxLK)rwkp~ zJV2FJGDgXFH08f-ECM$i4y^G$>xhLS$=n%o5{@9z#t)IsF8#nrm$Z1tm zge?dQ#3?q$)+jGoN)P&RLwsWQ!xCxtUP)=96DoQNSjhT&$S05Aoo1TRSH&0hZ+Jt+ z1owsY@&THU6a>*33MxXd;}n=q_|&u4$c5-_kU%mmloZ(o*PY)*=knLqqlQVXTMD%l z;^`fg`4!`{2;W+XI--}=odC&6=yF_NNM;UVV?Qu8HoLJyeEa@5@ps9#G2d`I5e`V) zOO>gSr*;DCMH@Y1(R$obzUn4A)NT(>+1_-;s-z&zOCZ!-<1Jzyz;KD%>D4mpk=j{P zmuogb+E87(3)JFeXr7%tG{G0OXVWVfFT(^?Xaxq<2#Ki~Yw%3N;QZaUlEj3KZ+3sC z7VX7uek&w8)ru!PPIsxi9Wn%GxCnV&qy-9@zjd@ynB%%+G+2>w_@K}=m$SBc9#921 znI3$n3vIYEnTh;l0aG11<$@5-u}HvI5CFPmCy4T56> zrr%z2)g$mHWLtmE9q02>9>qzdJ%nkYUGl3`AS0?(ww}hXU>{3?7k=X>w8j@qe_^Zf zN5eU1B8lXkhR;7c#`Zwb892rYoi!cLO~+gyK~gTFz~%h?^0eTKWx@X>a+5^KT;8Z5 zTSt7T8DHZo7p_dT%YSzUEq@(7fcQ+bXA)~+BbBb=p_eG9z)}}O%H$?Hg-?A$Ac*!o z30G=dHS<+GRC9pD4n|A{W-d(*vD{PhUN^Tv(P>l#-+iOngr|INlKF(37wJ1nfpjqy zjFkxn@$F`Uy*%?nTSL_uxCRdL@2N%%8QKqZ!RHu*5`)8J=@C9s?C-HoIHiWn=l0l^ z$q^gA_1cfMX*jz>EdT7x9*`3`+Ax>t9E&4S1p(iwvh1nxR&1H-pf0{`0_&LXCUe@s zCVW|AIn2=mY}8y#MKVN{eu`@qTT$ljnupOselU_oFdT#x&F7m3q$AgaFG&yZc*^?y zAymA8jtyM&8_R-{B#>%1wtD&UlzujOP%F2)Vvc6nSTRFnO+!YgB}9HNvMW|=xXVs9 z^OTe8NQ9<3ifGTdRZ!?>RkcU=(WRnE`#V`DCb;SOvM0ukg{D_kAex8@HT%O>?B(n0 z(*`c>#WX)ERXiaYp^3q7J&Z_6c&ImsTBTM|TeqDa4YTi?(cwUom-y@D^AveAZ@sYI zn%dAm6V89W4$b>u%e^}@oH2!FG4{Ue9W2yaEn*?LbU!QWg`Ia147c z1MwVWbo|K3O1AxbhXstZLIwpycY`x`L?hM(8{<8%<9Di@ENs0Gn6SmNLTDLoCCpW| z7mUxbQa2;y*+bzv*8V9}vGVuX`Hsr6=G{L+(rIp|s5p^qV+Zc`3y*{hw>0ioWTTED zY67e?me-_J&V<^HVu{?uA3no)#L?bAG~XwLSeC(fHGxL4_m^^juxD)~#ZWb_v09aF zgb(fT!tGfR+$Ley8d@CFojBuX=i^V*HABkaZQz9lQp-UFZU0KcI1c{7m3 zEb{Ehaid13u&J4`_P4rXQ1eF=TZKms<|Bb`HYdmZq2XI(q5J{7za2b8=5MSZBlQ81 zU$FA_Ue^){%u$SK4-siJQrOrG)}m~g!QU|eK)MqHsMq>wuEGT=cBa`I3*ZBsd^|Ej z+aj3Jb^v;+fzKQ61H91DGFZdXS$@0D2Iv8|-oLwRH(&=8?%yogZ@75Nld%4E69MaS z6vBOHq$I3;iW=<3_|yl>>J={r-)2KOw95D~?UL>hzXRiN)2;8*<-aB6&qq3S8`2a39X{PG(oOjk9U zCx6_%>;VVs*_kPl{aYEBqK@>}9=~;7G|;1nR4D1v3}=w^=3YL(*z`~{Pqsetn-5Vs z9|L2k?8k8X*{b`I7ui&CBgirmWR>)?Sq)7q?P$rL#bra0nJuwMZ z(YZK}`eIo5t(2qC;J3ga&cj2a&9nqg36Vpxe)*!8qL?9Ay`)(fEMe4nqPxyT9<-Ut zO?<(-rZ+1-r%hbi8I(rE4;2}MKhz}YisabKbBkkfZYOXFh@)D%%)VqJCO$f;&6`-N zvUAGP^I-L3a*XvYRg+ZQ*lEw>He6kC~ZfAId zI?7D0tX2A@0Uz&R#tC!7B#n@XwW0o?et|h4eIQ49P) z5s}!U^CWNH)UJ^CWZ#wEbC&y>XfrhGoAEPEJqgqiPa<_~o(jHT;XS|y=_4CX(}!S` zs8sN2#sd#YJnPFQiPM9sFvEI;PigrbmC3%B=fL}#+WgJOU*a^@J#FDgi%9}<*N%UBod~bMDb#w_tQsIp7ZCdm@;ZTbfZ_Ao`G`;mkrp3D9j_V zk}%oCZX&LOrb4i6>$=*+GbMB-<0I?PUajWrF%{PoGD_m!I!_e1jqqy%N-0DMGw@J| z5IuoL6;FBww-NjoRE2Jd>zd-xl?}A+JLtlMeuY0}C zpc@Clm#@n&5JvuN@2W%lw)yARyCaXxk5hsn+5pYR4@%kokJwfyrmO*YB?p4CJzO;1 znMKt^YtLnYh}rzJW8fDY-x!VLw|?e- zs%%|UnM4Q#*5}9ezX>yG_!p_c=a}O{E`Y&Ujo;;RRYZ7`93FU)xcnvn*&Fs1-isk(;*ZwjSb0&Kqrr+* z5*d#K%`fR^+m$l)f>y2Ii>4vlJo|1e)qP*d@`lPEm`F;cq9vW}xzRJt7sdc(fHTu(ubBGI-OXCL!?PhAB=A8+NmhJbaSjTjlBbfVgHcp8r&hPL{r9VS9DYEEZYR}^5g6$JkDkjDMF$IAsM#+;`UA%4rZ(&HR5 z=UO!5^HkPH`;2L=jNNfnrP}$O&fY?vu0*N;Fxg2SIh~+MZA3{DAGg@XFXJYf*FDS1 zA^TKu(0C`x11Ou?wmW$|lJGZFb&9Y?#haKVrgSck z{fhGiZvl_v0_r?ZnP1!|DB`%aT+l7qF?SE!=CQ#J3#j;jHm=cFk!j#VobkAWGs#vV ztRi#I5I6dBY%0fD^&jMH;P;i3aAPhlx8-!c3-%dRzIV-!C8P4;SiMn2Amk3ORP&T; zj$ZmIfr~Z%&qJVx6f0dPiUrVJoPN<A+=&taF6#;6N%D(Bj37@yc< zAKrHxiGsBM`b-Tyf1EPw!nAxA!-ZpVc^Lvc55-fR2Rpng2lbq)ST*!pfP-gQOoq<1 zXH{+rK`1$`Pldh@>3&~8`~Aa{={ac7?{-27Z0ZR#?Q@%73=-)8eTLM({Zpx5@Kyg| zCO!+5=#&`EOHOFO<;93CYmO0D1oq_TkDYptt%g}cLj`K|T~66IDZ{grDDQ)_T(mj< z{!$l;QswJa5;7xabBSM`wIZJiXGd37Feuslni5*c%vbPU?=KD;e(d2HYaply;>@ME z#ze9ZlM)N{c((a*7Dwj4Z8~KA0`(*8k0aK%i zH?u6V>-;g6vx*rf(2xj|KFp0C-!A6YAnGx48CYqjr}!Ock~jh(5IMA6)h2yY{5-x_ z%jEi!ny<~o{9Aqc`~kE-+GN3Xn^OvXyO=dqQ$P4&_iSR9?GBCLW}R9qNo1t|@3`R! z-w&Pvko8Z4{s|wq;;tkKt)%8O>itM&>+#3#7&p8f+${ao+=reEUrjTIo7|*m{@b2={!d3Ij`=g^BL|K;a zV0|xwm~lOM4frC4hs3nMPlfip&_}u~u|vBdv$#_45`bm6tOYL2jg`0Tn?|4~!xCP|+@GAh zKyhjCjBWcriOjmbe+>5mPDs{&tPIx!3W%s7y%$UzFL0ys)!28Fdd<;(n7z97oym`H z+mbVDmrJ*97DrSj+znjfDr8i@A1fn@-cgleZQS>>-QJVp5J^4zG|5sp z2L<%M8r5`zGU!@6f^2EegUv)aWw88(F#$tJdPmXCwjZux`xqeFGFKt*Ave`L-@obB znTIPV7o*K~s0&P8fnK_baxJ18>aMZUQZ4PLTXQJg^J_~?u5>-}(k?3xnD(lj4a}uR zz1%9rd=GZtT?o^TPC#fpm&l&<^=p>JtstU01`89-UYlKtAAL{vg?qmYoxbo}8N7r-Wkc^Lvg#R=IS%0E+Rg#sWV7u+06dS6WdX!x96 z?c6ym@dziVa{uy(%FvI^P~^k(Enur-SLGP4ll;r&aQ|-|=yboQ{wtsLVg4@vfr%js zk!Ofk<)gPiK)%^ArPTFf-YM*Z7OM`i6CC3pD31I@u!YksWh~*r5I+ib%bGpYah^vi zEj+C~84f-Uu^RwF=>dy5e}vez;^EIWfHH4S@SKC?_lwdtA^>N(UZD~x-IF*EvM~{? zHe1EoR(#DFRocn2?0yc_f#@x5~d#}IWP?jzKSp;d7uN2t)gU_J^ypzcDb z_e|?cSTIOlV>QLIb+?NFs?=3XJS-f}-Px8(;mt)4Zlx=xzV`eAm1?|JQuaHnf&TeJ z|8=*EdhKBAzYQ`x82apkSvFZJ(xTl!QfIyG0+84h*VeuZl3?M>(K)ND@_AHG{_-jK zgRZ7sy|D`8E`;fh>U8OJg?0Efq0Sz`I4amdf+xNh7>Y3UzP@Ax zu4RP#Q4R=1abwKhe;^}iUa*V?qGD0t9meM4Eq;9XzTE#@)m#-IHzrL6B@dlW?Sj^~x;<8pL4+QC zJD^vJd<>9`b^J3e^mP0&qY*NplHR>+@^Q2bFWVF=qN{pbrj#&`?l2odaFJgFm{qXWHzu8?l1%~X*G|8?^pxSm;eJaES;w($E0eg$)oTgHds z7Tnk1T|Ob9$IXt*D=5Ur@Ik~kF@CyYU6)mE?7X5p^^f7(J8W0|^KXn%gX@n!*fgle zBt_|6KBM(R+dYPf)wcI=Bmd41;Wf*}Zgjh6R}n{Vl?F{hzMQI5JU%`A`$qfvAp-sn zc?kkRg0YJmUx7XTzlvf0xAn`;+|}6Z-x%QkHvX55ot=&MKl8s_|B$r*;(z}yjEeuw z|JG=1Ms2d8`K^9a{QJv2=O>E>YN-NmNs2q7-r1B|D3gkE1^{y#yRyf027yoenuQEr^fo@_aZmuUASQ}0SwACfJbq!G)F2u^NSQO8! zU)_o`q+u~IN)g#^4?jvU$qILuvl*L;KJ_$*FD3EU)fj4O&B?R~P#WPLI=MDd4&dc* z6$=r?HP!ygUWUw#!s@IRiQT;WN!c)5hA{^;qz1iP_t}>JAc8>6-t1B#)Evz z_TQ1F4WbBDj4%A5o}MiCp|nZr%K3v_#I45p{BBF2?n^Lmfi$$L^l#D44Ut&$&zbyp zfdF`+Bs@s)$ZV&rF^8CuW5msBr^h`P$0pnn3|>a7J!^uTIjfHxKZd|F`UWZW0-(2| z5bnS>6hM*~$uOnLO?RD5L6k|}rQ_-^sAw6{Mrirckp4N?BE9Mp`rp5*__n5zZvXiI UvVSS?F9rUkz`qpu|4f1Z0mb!>p8x;= literal 0 HcmV?d00001 diff --git a/pkg/twitter-0.0.1.tgz b/pkg/twitter-0.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..81385086f60b7f30ab21332f048446fa4e97450b GIT binary patch literal 29092 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 literal 0 HcmV?d00001 diff --git a/pkg/twitter-0.0.1/CHANGELOG b/pkg/twitter-0.0.1/CHANGELOG new file mode 100644 index 000000000..3afcf0cc4 --- /dev/null +++ b/pkg/twitter-0.0.1/CHANGELOG @@ -0,0 +1,2 @@ +0.0.2 - added the command line options i forgot to add (friend and follower); improved some docs +0.0.1 - initial release \ No newline at end of file diff --git a/pkg/twitter-0.0.1/Manifest.txt b/pkg/twitter-0.0.1/Manifest.txt new file mode 100644 index 000000000..8630e46f9 --- /dev/null +++ b/pkg/twitter-0.0.1/Manifest.txt @@ -0,0 +1,12 @@ +README +CHANGELOG +Rakefile +setup.rb +bin/twitter +lib/twitter.rb +lib/twitter/base.rb +lib/twitter/command.rb +lib/twitter/easy_class_maker.rb +lib/twitter/status.rb +lib/twitter/user.rb +lib/twitter/version.rb \ No newline at end of file diff --git a/pkg/twitter-0.0.1/README b/pkg/twitter-0.0.1/README new file mode 100644 index 000000000..eeec08d39 --- /dev/null +++ b/pkg/twitter-0.0.1/README @@ -0,0 +1,46 @@ +addicted to twitter +================== + +... a sweet little diddy that helps you twitter your life away + + +== Command Line Use + +$ twitter + +That will show the commands and each command will either run or show you the options it needs to run + +$ twitter post "releasing my new twitter gem" + +That will post a status update to your twitter + +== Examples + + Twitter::Base.new('your email', 'your password').update('watching veronica mars') + + # or you can use post + Twitter::Base.new('your email', 'your password').post('post works too') + + puts "Public Timeline", "=" * 50 + Twitter::Base.new('your email', 'your password').timeline(:public).each do |s| + puts s.text, s.user.name + puts + end + + puts '', "Friends Timeline", "=" * 50 + Twitter::Base.new('your email', 'your password').timeline.each do |s| + puts s.text, s.user.name + puts + end + + puts '', "Friends", "=" * 50 + Twitter::Base.new('your email', 'your password').friends.each do |u| + puts u.name, u.status.text + puts + end + + puts '', "Followers", "=" * 50 + Twitter::Base.new('your email', 'your password').followers.each do |u| + puts u.name, u.status.text + puts + end \ No newline at end of file diff --git a/pkg/twitter-0.0.1/Rakefile b/pkg/twitter-0.0.1/Rakefile new file mode 100644 index 000000000..0c32f7d3b --- /dev/null +++ b/pkg/twitter-0.0.1/Rakefile @@ -0,0 +1,57 @@ +require 'rubygems' +require 'rake' +require 'rake/clean' +require 'rake/testtask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/rdoctask' +require 'rake/contrib/rubyforgepublisher' +require 'fileutils' +require 'hoe' +include FileUtils +require File.join(File.dirname(__FILE__), 'lib', 'twitter', 'version') + +AUTHOR = "John Nunemaker" # can also be an array of Authors +EMAIL = "nunemaker@gmail.com" +DESCRIPTION = "a command line interface for twitter, also a library which wraps the twitter api" +GEM_NAME = "twitter" # what ppl will type to install your gem +RUBYFORGE_PROJECT = "twitter" # The unix name for your project +HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org" +RELEASE_TYPES = %w( gem ) # can use: gem, tar, zip + + +NAME = "twitter" +REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil +VERS = ENV['VERSION'] || (Twitter::VERSION::STRING + (REV ? ".#{REV}" : "")) +CLEAN.include ['**/.*.sw?', '*.gem', '.config'] +RDOC_OPTS = ['--quiet', '--title', "twitter documentation", + "--opname", "index.html", + "--line-numbers", + "--main", "README", + "--inline-source"] + +# Generate all the Rake tasks +# Run 'rake -T' to see list of generated tasks (from gem root directory) +hoe = Hoe.new(GEM_NAME, VERS) do |p| + p.author = AUTHOR + p.description = DESCRIPTION + p.email = EMAIL + p.summary = DESCRIPTION + p.url = HOMEPATH + p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT + p.test_globs = ["test/**/*_test.rb"] + p.clean_globs = CLEAN #An array of file patterns to delete on clean. + + # == Optional + #p.changes - A description of the release's latest changes. + p.extra_deps = %w( hpricot ) + #p.spec_extras - A hash of extra values to set in the gemspec. +end + + + +desc "Package and Install Gem" +task :package_and_install do + `rake package` + `sudo gem install pkg/#{NAME}-#{VERS}.gem` +end \ No newline at end of file diff --git a/pkg/twitter-0.0.1/bin/twitter b/pkg/twitter-0.0.1/bin/twitter new file mode 100644 index 000000000..cbc7441db --- /dev/null +++ b/pkg/twitter-0.0.1/bin/twitter @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# = addicted to twitter +# +# ... a sweet little diddy that helps you twitter your life away from the command line +# +# == Install +# +# $ sudo gem install twitter +# +# == Command Line Use +# +# $ twitter +# +# Usage: twitter [options] +# +# Available Commands: +# - post +# - timeline +# - friends +# - friend +# - followers +# - follower +# +# That will show the commands and each command will either run or show you the options it needs to run +# +# $ twitter post "releasing my new twitter gem" +# +# Got it! New twitter created at: Mon Nov 27 00:22:27 UTC 2006 +# +# That will post a status update to your twitter +require 'rubygems' +require 'twitter' +require 'twitter/command' + +Twitter::Command.process(ARGV) \ No newline at end of file diff --git a/pkg/twitter-0.0.1/lib/twitter.rb b/pkg/twitter-0.0.1/lib/twitter.rb new file mode 100644 index 000000000..e4127597b --- /dev/null +++ b/pkg/twitter-0.0.1/lib/twitter.rb @@ -0,0 +1,45 @@ +# = addicted to twitter +# +# ... a sweet little diddy that helps you twitter your life away +# +# +# == Install +# +# $ sudo gem install twitter +# +# == Examples +# +# Twitter::Base.new('your email', 'your password').update('watching veronica mars') +# +# # or you can use post +# Twitter::Base.new('your email', 'your password').post('post works too') +# +# puts "Public Timeline", "=" * 50 +# Twitter::Base.new('your email', 'your password').timeline(:public).each do |s| +# puts s.text, s.user.name +# puts +# end +# +# puts '', "Friends Timeline", "=" * 50 +# Twitter::Base.new('your email', 'your password').timeline.each do |s| +# puts s.text, s.user.name +# puts +# end +# +# puts '', "Friends", "=" * 50 +# Twitter::Base.new('your email', 'your password').friends.each do |u| +# puts u.name, u.status.text +# puts +# end +# +# puts '', "Followers", "=" * 50 +# Twitter::Base.new('your email', 'your password').followers.each do |u| +# puts u.name, u.status.text +# puts +# end + +require 'twitter/version' +require 'twitter/easy_class_maker' +require 'twitter/base' +require 'twitter/user' +require 'twitter/status' \ No newline at end of file diff --git a/pkg/twitter-0.0.1/lib/twitter/base.rb b/pkg/twitter-0.0.1/lib/twitter/base.rb new file mode 100644 index 000000000..db3473b81 --- /dev/null +++ b/pkg/twitter-0.0.1/lib/twitter/base.rb @@ -0,0 +1,109 @@ +# This is the base class for the twitter library. It makes all the requests +# to twitter, parses the xml (using hpricot) and returns ruby objects to play with. +# +# The private methods in this one are pretty fun. Be sure to check out users, statuses and call. +require 'uri' +require 'net/http' +require 'rubygems' +require 'hpricot' + +module Twitter + class Untwitterable < StandardError; end + class CantConnect < Untwitterable; end + class BadResponse < Untwitterable; end + class UnknownTimeline < ArgumentError; end + + class Base + + # Twitter's url, duh! + @@api_url = 'twitter.com' + + # Timelines exposed by the twitter api + @@timelines = [:friends, :public] + + def self.timelines + @@timelines + end + + # Initializes the configuration for making requests to twitter + def initialize(email, password) + @config, @config[:email], @config[:password] = {}, email, password + end + + # Returns an array of statuses for a timeline; + # Available timelines are determined from the @@timelines variable + # Defaults to your friends timeline + def timeline(which=:friends) + raise UnknownTimeline unless @@timelines.include?(which) + auth = which.to_s.include?('public') ? false : true + statuses(call("#{which}_timeline", :auth => auth)) + end + + # Returns an array of users who are in your friends list + def friends + users(call(:friends)) + end + + # Returns an array of users who are following you + def followers + users(call(:followers)) + end + + # Updates your twitter with whatever status string is passed in + def post(status) + url = URI.parse("http://#{@@api_url}/statuses/update.xml") + req = Net::HTTP::Post.new(url.path) + + req.basic_auth(@config[:email], @config[:password]) + req.set_form_data({'status' => status}) + + res = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) } + Status.new_from_xml(Hpricot.parse(res.body).at('status')) + end + alias :update :post + + private + # Converts xml to an array of statuses + def statuses(res) + statuses = [] + doc = Hpricot.parse(res) + (doc/:status).each do |status| + statuses << Status.new_from_xml(status) + end + statuses + end + + # Converts xml to an array of users + def users(res) + users = [] + doc = Hpricot.parse(res) + (doc/:user).each do |user| + users << User.new_from_xml(user) + end + users + end + + # Calls whatever api method requested + # + # ie: call(:public_timeline, :auth => false) + def call(method, arg_options={}) + options = { :auth => true }.merge(arg_options) + + path = "/statuses/#{method.to_s}.xml" + headers = { "User-Agent" => @config[:email] } + + begin + response = Net::HTTP.start(@@api_url, 80) do |http| + req = Net::HTTP::Get.new(path, headers) + req.basic_auth(@config[:email], @config[:password]) if options[:auth] + http.request(req) + end + + raise BadResponse unless response.message == 'OK' + response.body + rescue + raise CantConnect + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.1/lib/twitter/command.rb b/pkg/twitter-0.0.1/lib/twitter/command.rb new file mode 100644 index 000000000..81c5a3305 --- /dev/null +++ b/pkg/twitter-0.0.1/lib/twitter/command.rb @@ -0,0 +1,195 @@ +# The command class is used for the command line interface. +# It is only used and included in the bin/twitter file. +require 'yaml' + +module Twitter + class Command + @@commands = [:post, :timeline, :friends, :friend, :followers, :follower] + + @@template = < [options]\n\nAvailable Commands:" + Twitter::Command.commands.each do |com| + puts " - #{com}" + end + end + end + + def commands + @@commands + end + + # Posts an updated status to twitter + def post(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You didn't enter a message to post.\n\n Usage: twitter post "You're fabulous message"\n) + exit(0) + end + + post = args.shift + + begin + status = Twitter::Base.new(config['email'], config['password']).post(post) + puts "\nGot it! New twitter created at: #{status.created_at}\n" + rescue + puts "\nTwitter what?. Something went wrong and your status could not be updated.\n" + end + end + + # Shows status, time and user for the specified timeline + def timeline(args) + config = create_or_find_config + + timeline = :friends + timeline = args.shift.intern if args.size > 0 && Twitter::Base.timelines.include?(args[0].intern) + + begin + puts + Twitter::Base.new(config['email'], config['password']).timeline(timeline).each do |s| + puts "#{s.text}\n-- #{s.relative_created_at} by #{s.user.name}" + puts + end + rescue + puts error_msg + end + end + + def friends + config = create_or_find_config + + begin + puts + Twitter::Base.new(config['email'], config['password']).friends.each do |u| + puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + puts + end + rescue + puts error_msg + end + end + + # Shows last updated status and time for a friend + # Needs a screen name + def friend(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You forgot to enter a screen name.\n\n Usage: twitter friend jnunemaker\n) + exit(0) + end + + screen_name = args.shift + + begin + puts + found = false + Twitter::Base.new(config['email'], config['password']).friends.each do |u| + if u.screen_name == screen_name + puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + found = true + end + end + + unless found + puts "Sorry couldn't find a friend of yours with #{screen_name} as a screen name" + end + puts + rescue + puts error_msg + end + end + + # Shows all followers and their last updated status + def followers + config = create_or_find_config + + begin + puts + Twitter::Base.new(config['email'], config['password']).followers.each do |u| + puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + puts + end + rescue + puts error_msg + end + end + + # Shows last updated status and time for a follower + # Needs a screen name + def follower(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You forgot to enter a screen name.\n\n Usage: twitter follower jnunemaker\n) + exit(0) + end + + screen_name = args.shift + + begin + puts + found = false + Twitter::Base.new(config['email'], config['password']).follower.each do |u| + if u.screen_name == screen_name + puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + found = true + end + end + + unless found + puts "Sorry couldn't find a follower of yours with #{screen_name} as a screen name" + end + puts + rescue + puts error_msg + end + end + + private + # Checks for the config, creates it if not found + def create_or_find_config + begin + config = YAML::load open(ENV['HOME'] + "/.twitter") + rescue + open(ENV["HOME"] + '/.twitter','w').write(@@template) + config = YAML::load open(ENV['HOME'] + "/.twitter") + end + + if config['email'] == nil or config['password'] == nil + puts "Please edit ~/.twitter to include your twitter email and password\nTextmate users: mate ~/.twitter" + exit(0) + end + + config + end + + def error_msg + "\nTwitter what?. Something went wrong and your status could not be updated.\n" + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.1/lib/twitter/easy_class_maker.rb b/pkg/twitter-0.0.1/lib/twitter/easy_class_maker.rb new file mode 100644 index 000000000..1537e3849 --- /dev/null +++ b/pkg/twitter-0.0.1/lib/twitter/easy_class_maker.rb @@ -0,0 +1,43 @@ +# This is pretty much just a macro for creating a class that allows +# using a block to initialize stuff and to define getters and setters +# really quickly. +module Twitter + module EasyClassMaker + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + # creates the attributes class variable and creates each attribute's accessor methods + def attributes(*attrs) + @@attributes = attrs + @@attributes.each { |a| attr_accessor a } + end + + # read method for attributes class variable + def self.attributes; @@attributes end + end + + # allows for any class that includes this to use a block to initialize + # variables instead of assigning each one seperately + # + # Example: + # + # instead of... + # + # s = Status.new + # s.foo = 'thing' + # s.bar = 'another thing' + # + # you can ... + # + # Status.new do |s| + # s.foo = 'thing' + # s.bar = 'another thing' + # end + def initialize + yield self if block_given? + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.1/lib/twitter/status.rb b/pkg/twitter-0.0.1/lib/twitter/status.rb new file mode 100644 index 000000000..4a84a0242 --- /dev/null +++ b/pkg/twitter-0.0.1/lib/twitter/status.rb @@ -0,0 +1,33 @@ +# The attributes are created_at, id, text, relative_created_at, and user (which is a user object) +# new_from_xml expects xml along the lines of: +# +# 1 +# a date +# some text +# about 1 min ago +# +# 1 +# John Nunemaker +# jnunemaker +# +# +module Twitter + class Status + include EasyClassMaker + + attributes :created_at, :id, :text, :relative_created_at, :user + + class << self + # Creates a new status from a piece of xml + def new_from_xml(xml) + Status.new do |s| + s.id = (xml).at('id').innerHTML + s.created_at = (xml).at('created_at').innerHTML + s.text = (xml).at('text').innerHTML + s.relative_created_at = (xml).at('relative_created_at').innerHTML + s.user = User.new_from_xml(xml) if (xml).at('user') + end + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.1/lib/twitter/user.rb b/pkg/twitter-0.0.1/lib/twitter/user.rb new file mode 100644 index 000000000..337a4712e --- /dev/null +++ b/pkg/twitter-0.0.1/lib/twitter/user.rb @@ -0,0 +1,32 @@ +# The attributes for user are id, name, screen_name, status (which is a status object) +# new_from_xml expects xml along the lines of: +# +# 1 +# John Nunemaker +# jnunemaker +# +# 1 +# a date +# some text +# about 1 min ago +# +# +module Twitter + class User + include EasyClassMaker + + attributes :id, :name, :screen_name, :status + + class << self + # Creates a new user from a piece of xml + def new_from_xml(xml) + User.new do |u| + u.id = (xml).at('id').innerHTML + u.name = (xml).at('name').innerHTML + u.screen_name = (xml).at('screen_name').innerHTML + u.status = Status.new_from_xml(xml) if (xml).at('status') + end + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.1/lib/twitter/version.rb b/pkg/twitter-0.0.1/lib/twitter/version.rb new file mode 100644 index 000000000..351cefe00 --- /dev/null +++ b/pkg/twitter-0.0.1/lib/twitter/version.rb @@ -0,0 +1,9 @@ +module Twitter #:nodoc: + module VERSION #:nodoc: + MAJOR = 0 + MINOR = 0 + TINY = 2 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/pkg/twitter-0.0.1/setup.rb b/pkg/twitter-0.0.1/setup.rb new file mode 100644 index 000000000..424a5f37c --- /dev/null +++ b/pkg/twitter-0.0.1/setup.rb @@ -0,0 +1,1585 @@ +# +# setup.rb +# +# Copyright (c) 2000-2005 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +unless Errno.const_defined?(:ENOTEMPTY) # Windows? + module Errno + class ENOTEMPTY + # We do not raise this exception, implementation is not needed. + end + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted Windows' stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class ConfigTable + + include Enumerable + + def initialize(rbconfig) + @rbconfig = rbconfig + @items = [] + @table = {} + # options + @install_prefix = nil + @config_opt = nil + @verbose = true + @no_harm = false + end + + attr_accessor :install_prefix + attr_accessor :config_opt + + attr_writer :verbose + + def verbose? + @verbose + end + + attr_writer :no_harm + + def no_harm? + @no_harm + end + + def [](key) + lookup(key).resolve(self) + end + + def []=(key, val) + lookup(key).set val + end + + def names + @items.map {|i| i.name } + end + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or setup_rb_error "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def load_script(path, inst = nil) + if File.file?(path) + MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path + end + end + + def savefile + '.config' + end + + def load_savefile + begin + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + self[k] = v.strip + end + rescue Errno::ENOENT + setup_rb_error $!.message + "\n#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value? and i.value + end + } + end + + def load_standard_entries + standard_entries(@rbconfig).each do |ent| + add ent + end + end + + def standard_entries(rbconfig) + c = rbconfig + + rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + if c['rubylibdir'] + # V > 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = c['rubylibdir'] + librubyverarch = c['archdir'] + siteruby = c['sitedir'] + siterubyver = c['sitelibdir'] + siterubyverarch = c['sitearchdir'] + elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = c['sitedir'] + siterubyver = "$siteruby/#{version}" + siterubyverarch = "$siterubyver/#{c['arch']}" + else + # V < 1.4.4 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" + siterubyver = siteruby + siterubyverarch = "$siterubyver/#{c['arch']}" + end + parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') + } + + if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] + else + makeprog = 'make' + end + + [ + ExecItem.new('installdirs', 'std/site/home', + 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ + {|val, table| + case val + when 'std' + table['rbdir'] = '$librubyver' + table['sodir'] = '$librubyverarch' + when 'site' + table['rbdir'] = '$siterubyver' + table['sodir'] = '$siterubyverarch' + when 'home' + setup_rb_error '$HOME was not set' unless ENV['HOME'] + table['prefix'] = ENV['HOME'] + table['rbdir'] = '$libdir/ruby' + table['sodir'] = '$libdir/ruby' + end + }, + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', parameterize.call(c['libdir']), + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for system configuration files'), + PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), + 'the directory for local state data'), + PathItem.new('libruby', 'path', libruby, + 'the directory for ruby libraries'), + PathItem.new('librubyver', 'path', librubyver, + 'the directory for standard ruby libraries'), + PathItem.new('librubyverarch', 'path', librubyverarch, + 'the directory for standard ruby extensions'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') + ] + end + private :standard_entries + + def load_multipackage_entries + multipackage_entries().each do |ent| + add ent + end + end + + def multipackage_entries + [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') + ] + end + private :multipackage_entries + + ALIASES = { + 'std-ruby' => 'librubyver', + 'stdruby' => 'librubyver', + 'rubylibdir' => 'librubyver', + 'archdir' => 'librubyverarch', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } + + def fixup + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + @items.freeze + @table.freeze + @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ + end + + def parse_opt(opt) + m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" + m.to_a[1,2] + end + + def dllext + @rbconfig['DLEXT'] + end + + def value_config?(name) + lookup(name).value? + end + + class Item + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value? + true + end + + def value + @value + end + + def resolve(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end + end + + class BoolItem < Item + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + case val + when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' + when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' + else + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + end + end + + class PathItem < Item + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end + end + + class ProgramItem < Item + def config_type + 'program' + end + end + + class SelectItem < Item + def initialize(name, selection, default, desc) + super + @ok = selection.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end + end + + class ExecItem < Item + def initialize(name, selection, desc, &block) + super name, selection, nil, desc + @ok = selection.split('/') + @action = block + end + + def config_type + 'exec' + end + + def value? + false + end + + def resolve(table) + setup_rb_error "$#{name()} wrongly used as option value" + end + + undef set + + def evaluate(val, table) + v = val.strip.downcase + unless @ok.include?(v) + setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" + end + @action.call v, table + end + end + + class PackageSelectionItem < Item + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end + end + + class MetaConfigEnvironment + def initialize(config, installer) + @config = config + @installer = installer + end + + def config_names + @config.names + end + + def config?(name) + @config.key?(name) + end + + def bool_config?(name) + @config.lookup(name).config_type == 'bool' + end + + def path_config?(name) + @config.lookup(name).config_type == 'path' + end + + def value_config?(name) + @config.lookup(name).config_type != 'exec' + end + + def add_config(item) + @config.add item + end + + def add_bool_config(name, default, desc) + @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + @config.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + @config.lookup(name).default = default + end + + def remove_config(name) + @config.remove(name) + end + + # For only multipackage + def packages + raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer + @installer.packages + end + + # For only multipackage + def declare_packages(list) + raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer + @installer.packages = list + end + end + +end # class ConfigTable + + +# This module requires: #verbose?, #no_harm? +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # Does not check '/', it's too abnormal. + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(path) + $stderr.puts "rm -f #{path}" if verbose? + return if no_harm? + force_remove_file path + end + + def rm_rf(path) + $stderr.puts "rm -rf #{path}" if verbose? + return if no_harm? + remove_tree path + end + + def remove_tree(path) + if File.symlink?(path) + remove_file path + elsif File.dir?(path) + remove_tree0 path + else + force_remove_file path + end + end + + def remove_tree0(path) + Dir.foreach(path) do |ent| + next if ent == '.' + next if ent == '..' + entpath = "#{path}/#{ent}" + if File.symlink?(entpath) + remove_file entpath + elsif File.dir?(entpath) + remove_tree0 entpath + else + force_remove_file entpath + end + end + begin + Dir.rmdir path + rescue Errno::ENOTEMPTY + # directory may not be empty + end + end + + def move_file(src, dest) + force_remove_file dest + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| + f.write File.binread(src) + } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def force_remove_file(path) + begin + remove_file path + rescue + end + end + + def remove_file(path) + File.chmod 0777, path + File.unlink path + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(*args) + $stderr.puts args.join(' ') if verbose? + system(*args) or raise RuntimeError, + "system(#{args.map{|a| a.inspect }.join(' ')}) failed" + end + + def ruby(*args) + command config('rubyprog'), *args + end + + def make(task = nil) + command(*[config('makeprog'), task].compact) + end + + def extdir?(dir) + File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") + end + + def files_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.file?("#{dir}/#{ent}") } + } + end + + DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) + + def directories_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT + } + end + +end + + +# This module requires: #srcdir_root, #objdir_root, #relpath +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + # obsolete: use metaconfig to change configuration + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file?(srcfile(path)) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.4.1' + Copyright = 'Copyright (c) 2000-2005 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'test', 'run all tests in test/' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + config = ConfigTable.new(load_rbconfig()) + config.load_standard_entries + config.load_multipackage_entries if multipackage? + config.fixup + klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) + klass.new(File.dirname($0), config).invoke + end + + def ToplevelInstaller.multipackage? + File.dir?(File.dirname($0) + '/packages') + end + + def ToplevelInstaller.load_rbconfig + if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + load File.expand_path(arg.split(/=/, 2)[1]) + $".push 'rbconfig.rb' + else + require 'rbconfig' + end + ::Config::CONFIG + end + + def initialize(ardir_root, config) + @ardir = File.expand_path(ardir_root) + @config = config + # cache + @valid_task_re = nil + end + + def config(key) + @config[key] + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + case task + when 'config', 'test' + ; + when 'clean', 'distclean' + @config.load_savefile if File.exist?(@config.savefile) + else + @config.load_savefile + end + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig" + end + + def init_installers + @installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) + return arg + when '-q', '--quiet' + @config.verbose = false + when '--verbose' + @config.verbose = true + when '--help' + print_usage $stdout + exit 0 + when '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + when '--copyright' + puts Copyright + exit 0 + else + setup_rb_error "unknown global option '#{arg}'" + end + end + nil + end + + def valid_task?(t) + valid_task_re() =~ t + end + + def valid_task_re + @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ + end + + def parsearg_no_options + unless ARGV.empty? + task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) + setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_test parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + evalopt = [] + set = [] + @config.config_opt = [] + while i = ARGV.shift + if /\A--?\z/ =~ i + @config.config_opt = ARGV.dup + break + end + name, value = *@config.parse_opt(i) + if @config.value_config?(name) + @config[name] = value + else + evalopt.push [name, value] + end + set.push name + end + evalopt.each do |name, value| + @config.lookup(name).evaluate value, @config + end + # Check if configuration is valid + set.each do |n| + @config[n] if @config.value_config?(n) + end + end + + def parsearg_install + @config.no_harm = false + @config.install_prefix = '' + while a = ARGV.shift + case a + when '--no-harm' + @config.no_harm = true + when /\A--prefix=/ + path = a.split(/=/, 2)[1] + path = File.expand_path(path) unless path[0,1] == '/' + @config.install_prefix = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, ' --help', 'print this message' + out.printf fmt, ' --version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + @config.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_test + @installer.exec_test + end + + def exec_show + @config.each do |i| + printf "%-20s %s\n", i.name, i.value if i.value? + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end # class ToplevelInstaller + + +class ToplevelInstallerMulti < ToplevelInstaller + + include FileOperations + + def initialize(ardir_root, config) + super + @packages = directories_of("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig", self + @packages.each do |name| + @config.load_script "#{@ardir}/packages/#{name}/metaconfig" + end + end + + attr_reader :packages + + def packages=(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_test + run_hook 'pre-test' + each_selected_installers {|inst| inst.exec_test } + run_hook 'post-test' + end + + def exec_clean + rm_f @config.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f @config.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if verbose? + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def run_hook(id) + @root_installer.run_hook id + end + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + +end # class ToplevelInstallerMulti + + +class Installer + + FILETYPES = %w( bin lib ext data conf man ) + + include FileOperations + include HookScriptAPI + + def initialize(config, srcroot, objroot) + @config = config + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + def noop(rel) + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # Config Access + # + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + + def verbose_off + begin + save, @config.verbose = @config.verbose?, false + yield + ensure + @config.verbose = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + alias config_dir_bin noop + alias config_dir_lib noop + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + alias config_dir_data noop + alias config_dir_conf noop + alias config_dir_man noop + + def extconf + ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + files_of(curr_srcdir()).each do |fname| + update_shebang_line "#{curr_srcdir()}/#{fname}" + end + end + + alias setup_dir_lib noop + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + alias setup_dir_data noop + alias setup_dir_conf noop + alias setup_dir_man noop + + def update_shebang_line(path) + return if no_harm? + return if config('shebang') == 'never' + old = Shebang.load(path) + if old + $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 + new = new_shebang(old) + return if new.to_s == old.to_s + else + return unless config('shebang') == 'all' + new = Shebang.new(config('rubypath')) + end + $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? + open_atomic_writer(path) {|output| + File.open(path, 'rb') {|f| + f.gets if old # discard + output.puts new.to_s + output.print f.read + } + } + end + + def new_shebang(old) + if /\Aruby/ =~ File.basename(old.cmd) + Shebang.new(config('rubypath'), old.args) + elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' + Shebang.new(config('rubypath'), old.args[1..-1]) + else + return old unless config('shebang') == 'all' + Shebang.new(config('rubypath')) + end + end + + def open_atomic_writer(path, &block) + tmpfile = File.basename(path) + '.tmp' + begin + File.open(tmpfile, 'wb', &block) + File.rename tmpfile, File.basename(path) + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + class Shebang + def Shebang.load(path) + line = nil + File.open(path) {|f| + line = f.gets + } + return nil unless /\A#!/ =~ line + parse(line) + end + + def Shebang.parse(line) + cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') + new(cmd, args) + end + + def initialize(cmd, args = []) + @cmd = cmd + @args = args + end + + attr_reader :cmd + attr_reader :args + + def to_s + "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") + end + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files rubyextentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_dir_conf(rel) + # FIXME: should not remove current config files + # (rename previous file to .old/.org) + install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 + end + + def install_dir_man(rel) + install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @config.install_prefix + list.each do |fname| + install fname, dest, mode, @config.install_prefix + end + end + + def libfiles + glob_reject(%w(*.y *.output), targetfiles()) + end + + def rubyextentions(dir) + ents = glob_select("*.#{@config.dllext}", targetfiles()) + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + ents + end + + def targetfiles + mapdir(existfiles() - hookfiles()) + end + + def mapdir(ents) + ents.map {|ent| + if File.exist?(ent) + then ent # objdir + else "#{curr_srcdir()}/#{ent}" # srcdir + end + } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + JUNK_FILES = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + + def existfiles + glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def glob_select(pat, ents) + re = globs2re([pat]) + ents.select {|ent| re =~ ent } + end + + def glob_reject(pats, ents) + re = globs2re(pats) + ents.reject {|ent| re =~ ent } + end + + GLOB2REGEX = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + + def globs2re(pats) + /\A(?:#{ + pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') + })\z/ + end + + # + # TASK test + # + + TESTDIR = 'test' + + def exec_test + unless File.directory?('test') + $stderr.puts 'no test in this package' if verbose? + return + end + $stderr.puts 'Running tests...' if verbose? + begin + require 'test/unit' + rescue LoadError + setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' + end + runner = Test::Unit::AutoRunner.new(true) + runner.to_run << TESTDIR + runner.run + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias clean_dir_bin noop + alias clean_dir_lib noop + alias clean_dir_data noop + alias clean_dir_conf noop + alias clean_dir_man noop + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias distclean_dir_bin noop + alias distclean_dir_lib noop + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + alias distclean_dir_data noop + alias distclean_dir_conf noop + alias distclean_dir_man noop + + # + # Traversing + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if type == 'ext' and config('without-ext') == 'yes' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + directories_of(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + def run_hook(id) + path = [ "#{curr_srcdir()}/#{id}", + "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } + return unless path + begin + instance_eval File.read(path), path, 1 + rescue + raise if $DEBUG + setup_rb_error "hook #{path} failed:\n" + $!.message + end + end + +end # class Installer + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +if $0 == __FILE__ + begin + ToplevelInstaller.invoke + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/pkg/twitter-0.0.2.gem b/pkg/twitter-0.0.2.gem new file mode 100644 index 0000000000000000000000000000000000000000..bf63e20d105d4308b1febb2b988e214ebf24a6af GIT binary patch literal 18432 zcmeI3Rd6Lulb(;5nVFe2VrFLPh?$u+Vrj%QVrFJ$W@ct)<`HWoe`|;BjlJ=W?cIw% zZ>l>ox-0XIitd7ZOpRQP7+sB=87;g3|F4YsALC$S1N>Y2kNt=C2m23>|K+n-7r7!A{zwohZX_wju%|A%vifEfU$ZtcI4Sy@ zCpv~P{P0LT_Q>FV2(`kXNS4H*6(i$9x;2=;g_0wB#2sH(*>axpJzKWifGKs(s5H6$ zsdDvC$rml&zMnIP_nJqg=iF>~5g@6v5bBWw{pB+I&dTs1tnt&R@#eZw5&-r^E0j`c z#sd*@WpIYRtGuk`{In8Jk_#=im;pWog`p$R$ehcoL`#)cDDhzV8!-UP-fpJ&-hr2G z@xRMg9{5N@!h8`Ub!Ja3WezLD6>YuG0^1l3>E;eH*DatM3r4gx4|JAF&=>4Tk!O1I zp}owl^il)TML+f>li+Qve<7Tee3@WMLX#1BCK0(g5^J!9HHNYkDmU#j4N^us z-sdgcbMePEAQ7fmpZK+w_Ur`q*FeX=O}*s%g^=%4pI+QfimA$(TCIE+xN2o^GEUH_uZyLZ`Swhz3F_f zg%f6k1P5;qpoG(5Ne;D7;=Gyn*J5T8S`;-?B(or0sp4S2mIvxvLtZ6BJ5C;=!8@^h zB)!1RfguWs_>jyAF{CC1N|uE;`DRT}2**zJ$M>K@1KnZwniPJ)n!629D zPVxHfciW>+=dJ_D%a54Yc$kDLfiLhKKB%K`-M5v?pj`c%2zljiZ?BB-Z7opUMXjXe z5KR&Re*DvNE=VJ2KXWY)l7;xS42)8%l+_jk+iSkjj);oqBXG|e0JBr~^WgK>Vt2L% z6fcu|z1-QsDOW)M?*ZZ9SFLdpyBo$}(owB6z3|6HZ0x2-dT=<3PeJip)Z#dFVHrw$aISSXyR6!zNsc<%%2!kW8RcfTJFC%Y{+|7v(MefRwR zleaVl;>sy|;0}tF1UYVNn(~E<5V@hqWMJF7!Sf1ZlZ`ptw0d2mtG>~N_$L(VPO-bC z^{A-DKspZ`sF_Cz>RgwsU&`@2!!DPT#(L)b0U|RNk?)2%X@dH)?GD%H?(` zlR?+o4nH!lU}X38_oJ7E!$%k)m;`3XpgI58F6B1#KGVcy^&sO77zf*x>HOxjm&>bs z<&Is{6B)77n<1vrU~_Kki=)qGryvo>9p{ztp_Hy(M2UH;nR9Kce_dt23Or^ z0b!qt?YEB}7Qb?Bt9ElK^ZB-r@V$|VTQgo2! z`?wMezu%~E5kxZ-1)3W?SXd|4-#ZYc z!=|>*DB!%~w$4)n!7`A0@^ur0=P7dcT=}rvfIi|1qGfBk1Fa-6oX7=eHRuefSa8C> z_|7jvO{GChJWvSWy?l{?GGW7zLl@FY#CEO*^7G(4V%Y%YfFJOH6mzUjQKBrtr;`YufzAruQ-f_~l@=cGO%(uOKS_Z}qJ67-|Y$1nMaOQ z`HVdRo4yP=g5FPE3^|gXh-PW(zeChy1!AF;6C_0%j1KJU5Vu#X8%^PlhdJgbsQuvo zc%y=76M;tJ^y7!dW_C++*};fedjIkp_HtzM09fEkaDs_h(xPs|jVA8xGnF>R)1NpzK1GpRF%L&RZy z1##8MN(w|c31jU69ZKSk4iQj`5O_4*OAx!*uQgetF4Jl)v>TfVDalL@u22EgMr_ml zLPm=j69|>+$t8Fxqz_v40=-)JEoTiL#+8wy**^5J>ehPg$YpEX^W*Lu-h=tm>Lv6R z#J#V#7kPZJi-G;(v zjQe&opp0stg*3$d=0CkX{~{Bq1xnafG26l!tem_(BpOg^7@tvJq@4 z1Fee4fV7+77317&d5YJgcgMvOfyOOF=$?U+SFSRRImu5QOJhwogo6)Ad!k+MeK_Z0 z%-EJkwz_R`Maf#)$GN%L1dP9{o!s?>p!!C%{fUDP5Ytly5&*g$G3Ji>B<(-(l>HN{ zbXlzc!U3*K?kn1>uw0T(DOgW$bZP=hu)0#LahnR&XxS`LfT5GF{W#rD6y3bFsu^lP zJ2{O+!IxXlo7)$p_?G5Io0
  • fzKRf#pcnD!#_|1L}?izR}3Q8oSJpyT1H9)Qm5L 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 literal 0 HcmV?d00001 diff --git a/pkg/twitter-0.0.2.tgz b/pkg/twitter-0.0.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..08280c8144bba5457b5b53c074ba72a73e8d989e GIT binary patch literal 15653 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 I0>D832bA&ctpET3 literal 0 HcmV?d00001 diff --git a/pkg/twitter-0.0.2/CHANGELOG b/pkg/twitter-0.0.2/CHANGELOG new file mode 100644 index 000000000..3afcf0cc4 --- /dev/null +++ b/pkg/twitter-0.0.2/CHANGELOG @@ -0,0 +1,2 @@ +0.0.2 - added the command line options i forgot to add (friend and follower); improved some docs +0.0.1 - initial release \ No newline at end of file diff --git a/pkg/twitter-0.0.2/README b/pkg/twitter-0.0.2/README new file mode 100644 index 000000000..eeec08d39 --- /dev/null +++ b/pkg/twitter-0.0.2/README @@ -0,0 +1,46 @@ +addicted to twitter +================== + +... a sweet little diddy that helps you twitter your life away + + +== Command Line Use + +$ twitter + +That will show the commands and each command will either run or show you the options it needs to run + +$ twitter post "releasing my new twitter gem" + +That will post a status update to your twitter + +== Examples + + Twitter::Base.new('your email', 'your password').update('watching veronica mars') + + # or you can use post + Twitter::Base.new('your email', 'your password').post('post works too') + + puts "Public Timeline", "=" * 50 + Twitter::Base.new('your email', 'your password').timeline(:public).each do |s| + puts s.text, s.user.name + puts + end + + puts '', "Friends Timeline", "=" * 50 + Twitter::Base.new('your email', 'your password').timeline.each do |s| + puts s.text, s.user.name + puts + end + + puts '', "Friends", "=" * 50 + Twitter::Base.new('your email', 'your password').friends.each do |u| + puts u.name, u.status.text + puts + end + + puts '', "Followers", "=" * 50 + Twitter::Base.new('your email', 'your password').followers.each do |u| + puts u.name, u.status.text + puts + end \ No newline at end of file diff --git a/pkg/twitter-0.0.2/Rakefile b/pkg/twitter-0.0.2/Rakefile new file mode 100644 index 000000000..0c32f7d3b --- /dev/null +++ b/pkg/twitter-0.0.2/Rakefile @@ -0,0 +1,57 @@ +require 'rubygems' +require 'rake' +require 'rake/clean' +require 'rake/testtask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/rdoctask' +require 'rake/contrib/rubyforgepublisher' +require 'fileutils' +require 'hoe' +include FileUtils +require File.join(File.dirname(__FILE__), 'lib', 'twitter', 'version') + +AUTHOR = "John Nunemaker" # can also be an array of Authors +EMAIL = "nunemaker@gmail.com" +DESCRIPTION = "a command line interface for twitter, also a library which wraps the twitter api" +GEM_NAME = "twitter" # what ppl will type to install your gem +RUBYFORGE_PROJECT = "twitter" # The unix name for your project +HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org" +RELEASE_TYPES = %w( gem ) # can use: gem, tar, zip + + +NAME = "twitter" +REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil +VERS = ENV['VERSION'] || (Twitter::VERSION::STRING + (REV ? ".#{REV}" : "")) +CLEAN.include ['**/.*.sw?', '*.gem', '.config'] +RDOC_OPTS = ['--quiet', '--title', "twitter documentation", + "--opname", "index.html", + "--line-numbers", + "--main", "README", + "--inline-source"] + +# Generate all the Rake tasks +# Run 'rake -T' to see list of generated tasks (from gem root directory) +hoe = Hoe.new(GEM_NAME, VERS) do |p| + p.author = AUTHOR + p.description = DESCRIPTION + p.email = EMAIL + p.summary = DESCRIPTION + p.url = HOMEPATH + p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT + p.test_globs = ["test/**/*_test.rb"] + p.clean_globs = CLEAN #An array of file patterns to delete on clean. + + # == Optional + #p.changes - A description of the release's latest changes. + p.extra_deps = %w( hpricot ) + #p.spec_extras - A hash of extra values to set in the gemspec. +end + + + +desc "Package and Install Gem" +task :package_and_install do + `rake package` + `sudo gem install pkg/#{NAME}-#{VERS}.gem` +end \ No newline at end of file diff --git a/pkg/twitter-0.0.2/bin/twitter b/pkg/twitter-0.0.2/bin/twitter new file mode 100644 index 000000000..cbc7441db --- /dev/null +++ b/pkg/twitter-0.0.2/bin/twitter @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# = addicted to twitter +# +# ... a sweet little diddy that helps you twitter your life away from the command line +# +# == Install +# +# $ sudo gem install twitter +# +# == Command Line Use +# +# $ twitter +# +# Usage: twitter [options] +# +# Available Commands: +# - post +# - timeline +# - friends +# - friend +# - followers +# - follower +# +# That will show the commands and each command will either run or show you the options it needs to run +# +# $ twitter post "releasing my new twitter gem" +# +# Got it! New twitter created at: Mon Nov 27 00:22:27 UTC 2006 +# +# That will post a status update to your twitter +require 'rubygems' +require 'twitter' +require 'twitter/command' + +Twitter::Command.process(ARGV) \ No newline at end of file diff --git a/pkg/twitter-0.0.2/lib/twitter.rb b/pkg/twitter-0.0.2/lib/twitter.rb new file mode 100644 index 000000000..e4127597b --- /dev/null +++ b/pkg/twitter-0.0.2/lib/twitter.rb @@ -0,0 +1,45 @@ +# = addicted to twitter +# +# ... a sweet little diddy that helps you twitter your life away +# +# +# == Install +# +# $ sudo gem install twitter +# +# == Examples +# +# Twitter::Base.new('your email', 'your password').update('watching veronica mars') +# +# # or you can use post +# Twitter::Base.new('your email', 'your password').post('post works too') +# +# puts "Public Timeline", "=" * 50 +# Twitter::Base.new('your email', 'your password').timeline(:public).each do |s| +# puts s.text, s.user.name +# puts +# end +# +# puts '', "Friends Timeline", "=" * 50 +# Twitter::Base.new('your email', 'your password').timeline.each do |s| +# puts s.text, s.user.name +# puts +# end +# +# puts '', "Friends", "=" * 50 +# Twitter::Base.new('your email', 'your password').friends.each do |u| +# puts u.name, u.status.text +# puts +# end +# +# puts '', "Followers", "=" * 50 +# Twitter::Base.new('your email', 'your password').followers.each do |u| +# puts u.name, u.status.text +# puts +# end + +require 'twitter/version' +require 'twitter/easy_class_maker' +require 'twitter/base' +require 'twitter/user' +require 'twitter/status' \ No newline at end of file diff --git a/pkg/twitter-0.0.2/lib/twitter/base.rb b/pkg/twitter-0.0.2/lib/twitter/base.rb new file mode 100644 index 000000000..db3473b81 --- /dev/null +++ b/pkg/twitter-0.0.2/lib/twitter/base.rb @@ -0,0 +1,109 @@ +# This is the base class for the twitter library. It makes all the requests +# to twitter, parses the xml (using hpricot) and returns ruby objects to play with. +# +# The private methods in this one are pretty fun. Be sure to check out users, statuses and call. +require 'uri' +require 'net/http' +require 'rubygems' +require 'hpricot' + +module Twitter + class Untwitterable < StandardError; end + class CantConnect < Untwitterable; end + class BadResponse < Untwitterable; end + class UnknownTimeline < ArgumentError; end + + class Base + + # Twitter's url, duh! + @@api_url = 'twitter.com' + + # Timelines exposed by the twitter api + @@timelines = [:friends, :public] + + def self.timelines + @@timelines + end + + # Initializes the configuration for making requests to twitter + def initialize(email, password) + @config, @config[:email], @config[:password] = {}, email, password + end + + # Returns an array of statuses for a timeline; + # Available timelines are determined from the @@timelines variable + # Defaults to your friends timeline + def timeline(which=:friends) + raise UnknownTimeline unless @@timelines.include?(which) + auth = which.to_s.include?('public') ? false : true + statuses(call("#{which}_timeline", :auth => auth)) + end + + # Returns an array of users who are in your friends list + def friends + users(call(:friends)) + end + + # Returns an array of users who are following you + def followers + users(call(:followers)) + end + + # Updates your twitter with whatever status string is passed in + def post(status) + url = URI.parse("http://#{@@api_url}/statuses/update.xml") + req = Net::HTTP::Post.new(url.path) + + req.basic_auth(@config[:email], @config[:password]) + req.set_form_data({'status' => status}) + + res = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) } + Status.new_from_xml(Hpricot.parse(res.body).at('status')) + end + alias :update :post + + private + # Converts xml to an array of statuses + def statuses(res) + statuses = [] + doc = Hpricot.parse(res) + (doc/:status).each do |status| + statuses << Status.new_from_xml(status) + end + statuses + end + + # Converts xml to an array of users + def users(res) + users = [] + doc = Hpricot.parse(res) + (doc/:user).each do |user| + users << User.new_from_xml(user) + end + users + end + + # Calls whatever api method requested + # + # ie: call(:public_timeline, :auth => false) + def call(method, arg_options={}) + options = { :auth => true }.merge(arg_options) + + path = "/statuses/#{method.to_s}.xml" + headers = { "User-Agent" => @config[:email] } + + begin + response = Net::HTTP.start(@@api_url, 80) do |http| + req = Net::HTTP::Get.new(path, headers) + req.basic_auth(@config[:email], @config[:password]) if options[:auth] + http.request(req) + end + + raise BadResponse unless response.message == 'OK' + response.body + rescue + raise CantConnect + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.2/lib/twitter/command.rb b/pkg/twitter-0.0.2/lib/twitter/command.rb new file mode 100644 index 000000000..81c5a3305 --- /dev/null +++ b/pkg/twitter-0.0.2/lib/twitter/command.rb @@ -0,0 +1,195 @@ +# The command class is used for the command line interface. +# It is only used and included in the bin/twitter file. +require 'yaml' + +module Twitter + class Command + @@commands = [:post, :timeline, :friends, :friend, :followers, :follower] + + @@template = < [options]\n\nAvailable Commands:" + Twitter::Command.commands.each do |com| + puts " - #{com}" + end + end + end + + def commands + @@commands + end + + # Posts an updated status to twitter + def post(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You didn't enter a message to post.\n\n Usage: twitter post "You're fabulous message"\n) + exit(0) + end + + post = args.shift + + begin + status = Twitter::Base.new(config['email'], config['password']).post(post) + puts "\nGot it! New twitter created at: #{status.created_at}\n" + rescue + puts "\nTwitter what?. Something went wrong and your status could not be updated.\n" + end + end + + # Shows status, time and user for the specified timeline + def timeline(args) + config = create_or_find_config + + timeline = :friends + timeline = args.shift.intern if args.size > 0 && Twitter::Base.timelines.include?(args[0].intern) + + begin + puts + Twitter::Base.new(config['email'], config['password']).timeline(timeline).each do |s| + puts "#{s.text}\n-- #{s.relative_created_at} by #{s.user.name}" + puts + end + rescue + puts error_msg + end + end + + def friends + config = create_or_find_config + + begin + puts + Twitter::Base.new(config['email'], config['password']).friends.each do |u| + puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + puts + end + rescue + puts error_msg + end + end + + # Shows last updated status and time for a friend + # Needs a screen name + def friend(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You forgot to enter a screen name.\n\n Usage: twitter friend jnunemaker\n) + exit(0) + end + + screen_name = args.shift + + begin + puts + found = false + Twitter::Base.new(config['email'], config['password']).friends.each do |u| + if u.screen_name == screen_name + puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + found = true + end + end + + unless found + puts "Sorry couldn't find a friend of yours with #{screen_name} as a screen name" + end + puts + rescue + puts error_msg + end + end + + # Shows all followers and their last updated status + def followers + config = create_or_find_config + + begin + puts + Twitter::Base.new(config['email'], config['password']).followers.each do |u| + puts "#{u.name} last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + puts + end + rescue + puts error_msg + end + end + + # Shows last updated status and time for a follower + # Needs a screen name + def follower(args) + config = create_or_find_config + + if args.size == 0 + puts %(\n You forgot to enter a screen name.\n\n Usage: twitter follower jnunemaker\n) + exit(0) + end + + screen_name = args.shift + + begin + puts + found = false + Twitter::Base.new(config['email'], config['password']).follower.each do |u| + if u.screen_name == screen_name + puts "#{u.name} (#{u.screen_name}) last updated #{u.status.relative_created_at}\n-- #{u.status.text}" + found = true + end + end + + unless found + puts "Sorry couldn't find a follower of yours with #{screen_name} as a screen name" + end + puts + rescue + puts error_msg + end + end + + private + # Checks for the config, creates it if not found + def create_or_find_config + begin + config = YAML::load open(ENV['HOME'] + "/.twitter") + rescue + open(ENV["HOME"] + '/.twitter','w').write(@@template) + config = YAML::load open(ENV['HOME'] + "/.twitter") + end + + if config['email'] == nil or config['password'] == nil + puts "Please edit ~/.twitter to include your twitter email and password\nTextmate users: mate ~/.twitter" + exit(0) + end + + config + end + + def error_msg + "\nTwitter what?. Something went wrong and your status could not be updated.\n" + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.2/lib/twitter/easy_class_maker.rb b/pkg/twitter-0.0.2/lib/twitter/easy_class_maker.rb new file mode 100644 index 000000000..1537e3849 --- /dev/null +++ b/pkg/twitter-0.0.2/lib/twitter/easy_class_maker.rb @@ -0,0 +1,43 @@ +# This is pretty much just a macro for creating a class that allows +# using a block to initialize stuff and to define getters and setters +# really quickly. +module Twitter + module EasyClassMaker + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + # creates the attributes class variable and creates each attribute's accessor methods + def attributes(*attrs) + @@attributes = attrs + @@attributes.each { |a| attr_accessor a } + end + + # read method for attributes class variable + def self.attributes; @@attributes end + end + + # allows for any class that includes this to use a block to initialize + # variables instead of assigning each one seperately + # + # Example: + # + # instead of... + # + # s = Status.new + # s.foo = 'thing' + # s.bar = 'another thing' + # + # you can ... + # + # Status.new do |s| + # s.foo = 'thing' + # s.bar = 'another thing' + # end + def initialize + yield self if block_given? + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.2/lib/twitter/status.rb b/pkg/twitter-0.0.2/lib/twitter/status.rb new file mode 100644 index 000000000..4a84a0242 --- /dev/null +++ b/pkg/twitter-0.0.2/lib/twitter/status.rb @@ -0,0 +1,33 @@ +# The attributes are created_at, id, text, relative_created_at, and user (which is a user object) +# new_from_xml expects xml along the lines of: +# +# 1 +# a date +# some text +# about 1 min ago +# +# 1 +# John Nunemaker +# jnunemaker +# +# +module Twitter + class Status + include EasyClassMaker + + attributes :created_at, :id, :text, :relative_created_at, :user + + class << self + # Creates a new status from a piece of xml + def new_from_xml(xml) + Status.new do |s| + s.id = (xml).at('id').innerHTML + s.created_at = (xml).at('created_at').innerHTML + s.text = (xml).at('text').innerHTML + s.relative_created_at = (xml).at('relative_created_at').innerHTML + s.user = User.new_from_xml(xml) if (xml).at('user') + end + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.2/lib/twitter/user.rb b/pkg/twitter-0.0.2/lib/twitter/user.rb new file mode 100644 index 000000000..337a4712e --- /dev/null +++ b/pkg/twitter-0.0.2/lib/twitter/user.rb @@ -0,0 +1,32 @@ +# The attributes for user are id, name, screen_name, status (which is a status object) +# new_from_xml expects xml along the lines of: +# +# 1 +# John Nunemaker +# jnunemaker +# +# 1 +# a date +# some text +# about 1 min ago +# +# +module Twitter + class User + include EasyClassMaker + + attributes :id, :name, :screen_name, :status + + class << self + # Creates a new user from a piece of xml + def new_from_xml(xml) + User.new do |u| + u.id = (xml).at('id').innerHTML + u.name = (xml).at('name').innerHTML + u.screen_name = (xml).at('screen_name').innerHTML + u.status = Status.new_from_xml(xml) if (xml).at('status') + end + end + end + end +end \ No newline at end of file diff --git a/pkg/twitter-0.0.2/lib/twitter/version.rb b/pkg/twitter-0.0.2/lib/twitter/version.rb new file mode 100644 index 000000000..351cefe00 --- /dev/null +++ b/pkg/twitter-0.0.2/lib/twitter/version.rb @@ -0,0 +1,9 @@ +module Twitter #:nodoc: + module VERSION #:nodoc: + MAJOR = 0 + MINOR = 0 + TINY = 2 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/pkg/twitter-0.0.2/setup.rb b/pkg/twitter-0.0.2/setup.rb new file mode 100644 index 000000000..424a5f37c --- /dev/null +++ b/pkg/twitter-0.0.2/setup.rb @@ -0,0 +1,1585 @@ +# +# setup.rb +# +# Copyright (c) 2000-2005 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +unless Errno.const_defined?(:ENOTEMPTY) # Windows? + module Errno + class ENOTEMPTY + # We do not raise this exception, implementation is not needed. + end + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted Windows' stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class ConfigTable + + include Enumerable + + def initialize(rbconfig) + @rbconfig = rbconfig + @items = [] + @table = {} + # options + @install_prefix = nil + @config_opt = nil + @verbose = true + @no_harm = false + end + + attr_accessor :install_prefix + attr_accessor :config_opt + + attr_writer :verbose + + def verbose? + @verbose + end + + attr_writer :no_harm + + def no_harm? + @no_harm + end + + def [](key) + lookup(key).resolve(self) + end + + def []=(key, val) + lookup(key).set val + end + + def names + @items.map {|i| i.name } + end + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or setup_rb_error "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def load_script(path, inst = nil) + if File.file?(path) + MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path + end + end + + def savefile + '.config' + end + + def load_savefile + begin + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + self[k] = v.strip + end + rescue Errno::ENOENT + setup_rb_error $!.message + "\n#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value? and i.value + end + } + end + + def load_standard_entries + standard_entries(@rbconfig).each do |ent| + add ent + end + end + + def standard_entries(rbconfig) + c = rbconfig + + rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + if c['rubylibdir'] + # V > 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = c['rubylibdir'] + librubyverarch = c['archdir'] + siteruby = c['sitedir'] + siterubyver = c['sitelibdir'] + siterubyverarch = c['sitearchdir'] + elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = c['sitedir'] + siterubyver = "$siteruby/#{version}" + siterubyverarch = "$siterubyver/#{c['arch']}" + else + # V < 1.4.4 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" + siterubyver = siteruby + siterubyverarch = "$siterubyver/#{c['arch']}" + end + parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') + } + + if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] + else + makeprog = 'make' + end + + [ + ExecItem.new('installdirs', 'std/site/home', + 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ + {|val, table| + case val + when 'std' + table['rbdir'] = '$librubyver' + table['sodir'] = '$librubyverarch' + when 'site' + table['rbdir'] = '$siterubyver' + table['sodir'] = '$siterubyverarch' + when 'home' + setup_rb_error '$HOME was not set' unless ENV['HOME'] + table['prefix'] = ENV['HOME'] + table['rbdir'] = '$libdir/ruby' + table['sodir'] = '$libdir/ruby' + end + }, + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', parameterize.call(c['libdir']), + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for system configuration files'), + PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), + 'the directory for local state data'), + PathItem.new('libruby', 'path', libruby, + 'the directory for ruby libraries'), + PathItem.new('librubyver', 'path', librubyver, + 'the directory for standard ruby libraries'), + PathItem.new('librubyverarch', 'path', librubyverarch, + 'the directory for standard ruby extensions'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') + ] + end + private :standard_entries + + def load_multipackage_entries + multipackage_entries().each do |ent| + add ent + end + end + + def multipackage_entries + [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') + ] + end + private :multipackage_entries + + ALIASES = { + 'std-ruby' => 'librubyver', + 'stdruby' => 'librubyver', + 'rubylibdir' => 'librubyver', + 'archdir' => 'librubyverarch', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } + + def fixup + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + @items.freeze + @table.freeze + @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ + end + + def parse_opt(opt) + m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" + m.to_a[1,2] + end + + def dllext + @rbconfig['DLEXT'] + end + + def value_config?(name) + lookup(name).value? + end + + class Item + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value? + true + end + + def value + @value + end + + def resolve(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end + end + + class BoolItem < Item + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + case val + when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' + when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' + else + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + end + end + + class PathItem < Item + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end + end + + class ProgramItem < Item + def config_type + 'program' + end + end + + class SelectItem < Item + def initialize(name, selection, default, desc) + super + @ok = selection.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end + end + + class ExecItem < Item + def initialize(name, selection, desc, &block) + super name, selection, nil, desc + @ok = selection.split('/') + @action = block + end + + def config_type + 'exec' + end + + def value? + false + end + + def resolve(table) + setup_rb_error "$#{name()} wrongly used as option value" + end + + undef set + + def evaluate(val, table) + v = val.strip.downcase + unless @ok.include?(v) + setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" + end + @action.call v, table + end + end + + class PackageSelectionItem < Item + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end + end + + class MetaConfigEnvironment + def initialize(config, installer) + @config = config + @installer = installer + end + + def config_names + @config.names + end + + def config?(name) + @config.key?(name) + end + + def bool_config?(name) + @config.lookup(name).config_type == 'bool' + end + + def path_config?(name) + @config.lookup(name).config_type == 'path' + end + + def value_config?(name) + @config.lookup(name).config_type != 'exec' + end + + def add_config(item) + @config.add item + end + + def add_bool_config(name, default, desc) + @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + @config.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + @config.lookup(name).default = default + end + + def remove_config(name) + @config.remove(name) + end + + # For only multipackage + def packages + raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer + @installer.packages + end + + # For only multipackage + def declare_packages(list) + raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer + @installer.packages = list + end + end + +end # class ConfigTable + + +# This module requires: #verbose?, #no_harm? +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # Does not check '/', it's too abnormal. + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(path) + $stderr.puts "rm -f #{path}" if verbose? + return if no_harm? + force_remove_file path + end + + def rm_rf(path) + $stderr.puts "rm -rf #{path}" if verbose? + return if no_harm? + remove_tree path + end + + def remove_tree(path) + if File.symlink?(path) + remove_file path + elsif File.dir?(path) + remove_tree0 path + else + force_remove_file path + end + end + + def remove_tree0(path) + Dir.foreach(path) do |ent| + next if ent == '.' + next if ent == '..' + entpath = "#{path}/#{ent}" + if File.symlink?(entpath) + remove_file entpath + elsif File.dir?(entpath) + remove_tree0 entpath + else + force_remove_file entpath + end + end + begin + Dir.rmdir path + rescue Errno::ENOTEMPTY + # directory may not be empty + end + end + + def move_file(src, dest) + force_remove_file dest + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| + f.write File.binread(src) + } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def force_remove_file(path) + begin + remove_file path + rescue + end + end + + def remove_file(path) + File.chmod 0777, path + File.unlink path + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(*args) + $stderr.puts args.join(' ') if verbose? + system(*args) or raise RuntimeError, + "system(#{args.map{|a| a.inspect }.join(' ')}) failed" + end + + def ruby(*args) + command config('rubyprog'), *args + end + + def make(task = nil) + command(*[config('makeprog'), task].compact) + end + + def extdir?(dir) + File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") + end + + def files_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.file?("#{dir}/#{ent}") } + } + end + + DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) + + def directories_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT + } + end + +end + + +# This module requires: #srcdir_root, #objdir_root, #relpath +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + # obsolete: use metaconfig to change configuration + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file?(srcfile(path)) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.4.1' + Copyright = 'Copyright (c) 2000-2005 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'test', 'run all tests in test/' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + config = ConfigTable.new(load_rbconfig()) + config.load_standard_entries + config.load_multipackage_entries if multipackage? + config.fixup + klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) + klass.new(File.dirname($0), config).invoke + end + + def ToplevelInstaller.multipackage? + File.dir?(File.dirname($0) + '/packages') + end + + def ToplevelInstaller.load_rbconfig + if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + load File.expand_path(arg.split(/=/, 2)[1]) + $".push 'rbconfig.rb' + else + require 'rbconfig' + end + ::Config::CONFIG + end + + def initialize(ardir_root, config) + @ardir = File.expand_path(ardir_root) + @config = config + # cache + @valid_task_re = nil + end + + def config(key) + @config[key] + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + case task + when 'config', 'test' + ; + when 'clean', 'distclean' + @config.load_savefile if File.exist?(@config.savefile) + else + @config.load_savefile + end + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig" + end + + def init_installers + @installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) + return arg + when '-q', '--quiet' + @config.verbose = false + when '--verbose' + @config.verbose = true + when '--help' + print_usage $stdout + exit 0 + when '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + when '--copyright' + puts Copyright + exit 0 + else + setup_rb_error "unknown global option '#{arg}'" + end + end + nil + end + + def valid_task?(t) + valid_task_re() =~ t + end + + def valid_task_re + @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ + end + + def parsearg_no_options + unless ARGV.empty? + task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) + setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_test parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + evalopt = [] + set = [] + @config.config_opt = [] + while i = ARGV.shift + if /\A--?\z/ =~ i + @config.config_opt = ARGV.dup + break + end + name, value = *@config.parse_opt(i) + if @config.value_config?(name) + @config[name] = value + else + evalopt.push [name, value] + end + set.push name + end + evalopt.each do |name, value| + @config.lookup(name).evaluate value, @config + end + # Check if configuration is valid + set.each do |n| + @config[n] if @config.value_config?(n) + end + end + + def parsearg_install + @config.no_harm = false + @config.install_prefix = '' + while a = ARGV.shift + case a + when '--no-harm' + @config.no_harm = true + when /\A--prefix=/ + path = a.split(/=/, 2)[1] + path = File.expand_path(path) unless path[0,1] == '/' + @config.install_prefix = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, ' --help', 'print this message' + out.printf fmt, ' --version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + @config.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_test + @installer.exec_test + end + + def exec_show + @config.each do |i| + printf "%-20s %s\n", i.name, i.value if i.value? + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end # class ToplevelInstaller + + +class ToplevelInstallerMulti < ToplevelInstaller + + include FileOperations + + def initialize(ardir_root, config) + super + @packages = directories_of("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig", self + @packages.each do |name| + @config.load_script "#{@ardir}/packages/#{name}/metaconfig" + end + end + + attr_reader :packages + + def packages=(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_test + run_hook 'pre-test' + each_selected_installers {|inst| inst.exec_test } + run_hook 'post-test' + end + + def exec_clean + rm_f @config.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f @config.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if verbose? + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def run_hook(id) + @root_installer.run_hook id + end + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + +end # class ToplevelInstallerMulti + + +class Installer + + FILETYPES = %w( bin lib ext data conf man ) + + include FileOperations + include HookScriptAPI + + def initialize(config, srcroot, objroot) + @config = config + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + def noop(rel) + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # Config Access + # + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + + def verbose_off + begin + save, @config.verbose = @config.verbose?, false + yield + ensure + @config.verbose = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + alias config_dir_bin noop + alias config_dir_lib noop + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + alias config_dir_data noop + alias config_dir_conf noop + alias config_dir_man noop + + def extconf + ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + files_of(curr_srcdir()).each do |fname| + update_shebang_line "#{curr_srcdir()}/#{fname}" + end + end + + alias setup_dir_lib noop + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + alias setup_dir_data noop + alias setup_dir_conf noop + alias setup_dir_man noop + + def update_shebang_line(path) + return if no_harm? + return if config('shebang') == 'never' + old = Shebang.load(path) + if old + $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 + new = new_shebang(old) + return if new.to_s == old.to_s + else + return unless config('shebang') == 'all' + new = Shebang.new(config('rubypath')) + end + $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? + open_atomic_writer(path) {|output| + File.open(path, 'rb') {|f| + f.gets if old # discard + output.puts new.to_s + output.print f.read + } + } + end + + def new_shebang(old) + if /\Aruby/ =~ File.basename(old.cmd) + Shebang.new(config('rubypath'), old.args) + elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' + Shebang.new(config('rubypath'), old.args[1..-1]) + else + return old unless config('shebang') == 'all' + Shebang.new(config('rubypath')) + end + end + + def open_atomic_writer(path, &block) + tmpfile = File.basename(path) + '.tmp' + begin + File.open(tmpfile, 'wb', &block) + File.rename tmpfile, File.basename(path) + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + class Shebang + def Shebang.load(path) + line = nil + File.open(path) {|f| + line = f.gets + } + return nil unless /\A#!/ =~ line + parse(line) + end + + def Shebang.parse(line) + cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') + new(cmd, args) + end + + def initialize(cmd, args = []) + @cmd = cmd + @args = args + end + + attr_reader :cmd + attr_reader :args + + def to_s + "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") + end + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files rubyextentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_dir_conf(rel) + # FIXME: should not remove current config files + # (rename previous file to .old/.org) + install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 + end + + def install_dir_man(rel) + install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @config.install_prefix + list.each do |fname| + install fname, dest, mode, @config.install_prefix + end + end + + def libfiles + glob_reject(%w(*.y *.output), targetfiles()) + end + + def rubyextentions(dir) + ents = glob_select("*.#{@config.dllext}", targetfiles()) + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + ents + end + + def targetfiles + mapdir(existfiles() - hookfiles()) + end + + def mapdir(ents) + ents.map {|ent| + if File.exist?(ent) + then ent # objdir + else "#{curr_srcdir()}/#{ent}" # srcdir + end + } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + JUNK_FILES = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + + def existfiles + glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def glob_select(pat, ents) + re = globs2re([pat]) + ents.select {|ent| re =~ ent } + end + + def glob_reject(pats, ents) + re = globs2re(pats) + ents.reject {|ent| re =~ ent } + end + + GLOB2REGEX = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + + def globs2re(pats) + /\A(?:#{ + pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') + })\z/ + end + + # + # TASK test + # + + TESTDIR = 'test' + + def exec_test + unless File.directory?('test') + $stderr.puts 'no test in this package' if verbose? + return + end + $stderr.puts 'Running tests...' if verbose? + begin + require 'test/unit' + rescue LoadError + setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' + end + runner = Test::Unit::AutoRunner.new(true) + runner.to_run << TESTDIR + runner.run + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias clean_dir_bin noop + alias clean_dir_lib noop + alias clean_dir_data noop + alias clean_dir_conf noop + alias clean_dir_man noop + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias distclean_dir_bin noop + alias distclean_dir_lib noop + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + alias distclean_dir_data noop + alias distclean_dir_conf noop + alias distclean_dir_man noop + + # + # Traversing + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if type == 'ext' and config('without-ext') == 'yes' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + directories_of(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + def run_hook(id) + path = [ "#{curr_srcdir()}/#{id}", + "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } + return unless path + begin + instance_eval File.read(path), path, 1 + rescue + raise if $DEBUG + setup_rb_error "hook #{path} failed:\n" + $!.message + end + end + +end # class Installer + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +if $0 == __FILE__ + begin + ToplevelInstaller.invoke + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/setup.rb b/setup.rb new file mode 100644 index 000000000..424a5f37c --- /dev/null +++ b/setup.rb @@ -0,0 +1,1585 @@ +# +# setup.rb +# +# Copyright (c) 2000-2005 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +unless Errno.const_defined?(:ENOTEMPTY) # Windows? + module Errno + class ENOTEMPTY + # We do not raise this exception, implementation is not needed. + end + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted Windows' stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class ConfigTable + + include Enumerable + + def initialize(rbconfig) + @rbconfig = rbconfig + @items = [] + @table = {} + # options + @install_prefix = nil + @config_opt = nil + @verbose = true + @no_harm = false + end + + attr_accessor :install_prefix + attr_accessor :config_opt + + attr_writer :verbose + + def verbose? + @verbose + end + + attr_writer :no_harm + + def no_harm? + @no_harm + end + + def [](key) + lookup(key).resolve(self) + end + + def []=(key, val) + lookup(key).set val + end + + def names + @items.map {|i| i.name } + end + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or setup_rb_error "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def load_script(path, inst = nil) + if File.file?(path) + MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path + end + end + + def savefile + '.config' + end + + def load_savefile + begin + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + self[k] = v.strip + end + rescue Errno::ENOENT + setup_rb_error $!.message + "\n#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value? and i.value + end + } + end + + def load_standard_entries + standard_entries(@rbconfig).each do |ent| + add ent + end + end + + def standard_entries(rbconfig) + c = rbconfig + + rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + if c['rubylibdir'] + # V > 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = c['rubylibdir'] + librubyverarch = c['archdir'] + siteruby = c['sitedir'] + siterubyver = c['sitelibdir'] + siterubyverarch = c['sitearchdir'] + elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = c['sitedir'] + siterubyver = "$siteruby/#{version}" + siterubyverarch = "$siterubyver/#{c['arch']}" + else + # V < 1.4.4 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" + siterubyver = siteruby + siterubyverarch = "$siterubyver/#{c['arch']}" + end + parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') + } + + if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] + else + makeprog = 'make' + end + + [ + ExecItem.new('installdirs', 'std/site/home', + 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ + {|val, table| + case val + when 'std' + table['rbdir'] = '$librubyver' + table['sodir'] = '$librubyverarch' + when 'site' + table['rbdir'] = '$siterubyver' + table['sodir'] = '$siterubyverarch' + when 'home' + setup_rb_error '$HOME was not set' unless ENV['HOME'] + table['prefix'] = ENV['HOME'] + table['rbdir'] = '$libdir/ruby' + table['sodir'] = '$libdir/ruby' + end + }, + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', parameterize.call(c['libdir']), + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for system configuration files'), + PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), + 'the directory for local state data'), + PathItem.new('libruby', 'path', libruby, + 'the directory for ruby libraries'), + PathItem.new('librubyver', 'path', librubyver, + 'the directory for standard ruby libraries'), + PathItem.new('librubyverarch', 'path', librubyverarch, + 'the directory for standard ruby extensions'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') + ] + end + private :standard_entries + + def load_multipackage_entries + multipackage_entries().each do |ent| + add ent + end + end + + def multipackage_entries + [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') + ] + end + private :multipackage_entries + + ALIASES = { + 'std-ruby' => 'librubyver', + 'stdruby' => 'librubyver', + 'rubylibdir' => 'librubyver', + 'archdir' => 'librubyverarch', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } + + def fixup + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + @items.freeze + @table.freeze + @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ + end + + def parse_opt(opt) + m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" + m.to_a[1,2] + end + + def dllext + @rbconfig['DLEXT'] + end + + def value_config?(name) + lookup(name).value? + end + + class Item + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value? + true + end + + def value + @value + end + + def resolve(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end + end + + class BoolItem < Item + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + case val + when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' + when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' + else + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + end + end + + class PathItem < Item + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end + end + + class ProgramItem < Item + def config_type + 'program' + end + end + + class SelectItem < Item + def initialize(name, selection, default, desc) + super + @ok = selection.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end + end + + class ExecItem < Item + def initialize(name, selection, desc, &block) + super name, selection, nil, desc + @ok = selection.split('/') + @action = block + end + + def config_type + 'exec' + end + + def value? + false + end + + def resolve(table) + setup_rb_error "$#{name()} wrongly used as option value" + end + + undef set + + def evaluate(val, table) + v = val.strip.downcase + unless @ok.include?(v) + setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" + end + @action.call v, table + end + end + + class PackageSelectionItem < Item + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end + end + + class MetaConfigEnvironment + def initialize(config, installer) + @config = config + @installer = installer + end + + def config_names + @config.names + end + + def config?(name) + @config.key?(name) + end + + def bool_config?(name) + @config.lookup(name).config_type == 'bool' + end + + def path_config?(name) + @config.lookup(name).config_type == 'path' + end + + def value_config?(name) + @config.lookup(name).config_type != 'exec' + end + + def add_config(item) + @config.add item + end + + def add_bool_config(name, default, desc) + @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + @config.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + @config.lookup(name).default = default + end + + def remove_config(name) + @config.remove(name) + end + + # For only multipackage + def packages + raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer + @installer.packages + end + + # For only multipackage + def declare_packages(list) + raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer + @installer.packages = list + end + end + +end # class ConfigTable + + +# This module requires: #verbose?, #no_harm? +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # Does not check '/', it's too abnormal. + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(path) + $stderr.puts "rm -f #{path}" if verbose? + return if no_harm? + force_remove_file path + end + + def rm_rf(path) + $stderr.puts "rm -rf #{path}" if verbose? + return if no_harm? + remove_tree path + end + + def remove_tree(path) + if File.symlink?(path) + remove_file path + elsif File.dir?(path) + remove_tree0 path + else + force_remove_file path + end + end + + def remove_tree0(path) + Dir.foreach(path) do |ent| + next if ent == '.' + next if ent == '..' + entpath = "#{path}/#{ent}" + if File.symlink?(entpath) + remove_file entpath + elsif File.dir?(entpath) + remove_tree0 entpath + else + force_remove_file entpath + end + end + begin + Dir.rmdir path + rescue Errno::ENOTEMPTY + # directory may not be empty + end + end + + def move_file(src, dest) + force_remove_file dest + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| + f.write File.binread(src) + } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def force_remove_file(path) + begin + remove_file path + rescue + end + end + + def remove_file(path) + File.chmod 0777, path + File.unlink path + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(*args) + $stderr.puts args.join(' ') if verbose? + system(*args) or raise RuntimeError, + "system(#{args.map{|a| a.inspect }.join(' ')}) failed" + end + + def ruby(*args) + command config('rubyprog'), *args + end + + def make(task = nil) + command(*[config('makeprog'), task].compact) + end + + def extdir?(dir) + File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") + end + + def files_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.file?("#{dir}/#{ent}") } + } + end + + DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) + + def directories_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT + } + end + +end + + +# This module requires: #srcdir_root, #objdir_root, #relpath +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + # obsolete: use metaconfig to change configuration + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file?(srcfile(path)) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.4.1' + Copyright = 'Copyright (c) 2000-2005 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'test', 'run all tests in test/' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + config = ConfigTable.new(load_rbconfig()) + config.load_standard_entries + config.load_multipackage_entries if multipackage? + config.fixup + klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) + klass.new(File.dirname($0), config).invoke + end + + def ToplevelInstaller.multipackage? + File.dir?(File.dirname($0) + '/packages') + end + + def ToplevelInstaller.load_rbconfig + if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + load File.expand_path(arg.split(/=/, 2)[1]) + $".push 'rbconfig.rb' + else + require 'rbconfig' + end + ::Config::CONFIG + end + + def initialize(ardir_root, config) + @ardir = File.expand_path(ardir_root) + @config = config + # cache + @valid_task_re = nil + end + + def config(key) + @config[key] + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + case task + when 'config', 'test' + ; + when 'clean', 'distclean' + @config.load_savefile if File.exist?(@config.savefile) + else + @config.load_savefile + end + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig" + end + + def init_installers + @installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) + return arg + when '-q', '--quiet' + @config.verbose = false + when '--verbose' + @config.verbose = true + when '--help' + print_usage $stdout + exit 0 + when '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + when '--copyright' + puts Copyright + exit 0 + else + setup_rb_error "unknown global option '#{arg}'" + end + end + nil + end + + def valid_task?(t) + valid_task_re() =~ t + end + + def valid_task_re + @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ + end + + def parsearg_no_options + unless ARGV.empty? + task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) + setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_test parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + evalopt = [] + set = [] + @config.config_opt = [] + while i = ARGV.shift + if /\A--?\z/ =~ i + @config.config_opt = ARGV.dup + break + end + name, value = *@config.parse_opt(i) + if @config.value_config?(name) + @config[name] = value + else + evalopt.push [name, value] + end + set.push name + end + evalopt.each do |name, value| + @config.lookup(name).evaluate value, @config + end + # Check if configuration is valid + set.each do |n| + @config[n] if @config.value_config?(n) + end + end + + def parsearg_install + @config.no_harm = false + @config.install_prefix = '' + while a = ARGV.shift + case a + when '--no-harm' + @config.no_harm = true + when /\A--prefix=/ + path = a.split(/=/, 2)[1] + path = File.expand_path(path) unless path[0,1] == '/' + @config.install_prefix = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, ' --help', 'print this message' + out.printf fmt, ' --version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + @config.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_test + @installer.exec_test + end + + def exec_show + @config.each do |i| + printf "%-20s %s\n", i.name, i.value if i.value? + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end # class ToplevelInstaller + + +class ToplevelInstallerMulti < ToplevelInstaller + + include FileOperations + + def initialize(ardir_root, config) + super + @packages = directories_of("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig", self + @packages.each do |name| + @config.load_script "#{@ardir}/packages/#{name}/metaconfig" + end + end + + attr_reader :packages + + def packages=(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_test + run_hook 'pre-test' + each_selected_installers {|inst| inst.exec_test } + run_hook 'post-test' + end + + def exec_clean + rm_f @config.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f @config.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if verbose? + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def run_hook(id) + @root_installer.run_hook id + end + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + +end # class ToplevelInstallerMulti + + +class Installer + + FILETYPES = %w( bin lib ext data conf man ) + + include FileOperations + include HookScriptAPI + + def initialize(config, srcroot, objroot) + @config = config + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + def noop(rel) + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # Config Access + # + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + + def verbose_off + begin + save, @config.verbose = @config.verbose?, false + yield + ensure + @config.verbose = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + alias config_dir_bin noop + alias config_dir_lib noop + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + alias config_dir_data noop + alias config_dir_conf noop + alias config_dir_man noop + + def extconf + ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + files_of(curr_srcdir()).each do |fname| + update_shebang_line "#{curr_srcdir()}/#{fname}" + end + end + + alias setup_dir_lib noop + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + alias setup_dir_data noop + alias setup_dir_conf noop + alias setup_dir_man noop + + def update_shebang_line(path) + return if no_harm? + return if config('shebang') == 'never' + old = Shebang.load(path) + if old + $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 + new = new_shebang(old) + return if new.to_s == old.to_s + else + return unless config('shebang') == 'all' + new = Shebang.new(config('rubypath')) + end + $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? + open_atomic_writer(path) {|output| + File.open(path, 'rb') {|f| + f.gets if old # discard + output.puts new.to_s + output.print f.read + } + } + end + + def new_shebang(old) + if /\Aruby/ =~ File.basename(old.cmd) + Shebang.new(config('rubypath'), old.args) + elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' + Shebang.new(config('rubypath'), old.args[1..-1]) + else + return old unless config('shebang') == 'all' + Shebang.new(config('rubypath')) + end + end + + def open_atomic_writer(path, &block) + tmpfile = File.basename(path) + '.tmp' + begin + File.open(tmpfile, 'wb', &block) + File.rename tmpfile, File.basename(path) + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + class Shebang + def Shebang.load(path) + line = nil + File.open(path) {|f| + line = f.gets + } + return nil unless /\A#!/ =~ line + parse(line) + end + + def Shebang.parse(line) + cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') + new(cmd, args) + end + + def initialize(cmd, args = []) + @cmd = cmd + @args = args + end + + attr_reader :cmd + attr_reader :args + + def to_s + "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") + end + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files rubyextentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_dir_conf(rel) + # FIXME: should not remove current config files + # (rename previous file to .old/.org) + install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 + end + + def install_dir_man(rel) + install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @config.install_prefix + list.each do |fname| + install fname, dest, mode, @config.install_prefix + end + end + + def libfiles + glob_reject(%w(*.y *.output), targetfiles()) + end + + def rubyextentions(dir) + ents = glob_select("*.#{@config.dllext}", targetfiles()) + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + ents + end + + def targetfiles + mapdir(existfiles() - hookfiles()) + end + + def mapdir(ents) + ents.map {|ent| + if File.exist?(ent) + then ent # objdir + else "#{curr_srcdir()}/#{ent}" # srcdir + end + } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + JUNK_FILES = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + + def existfiles + glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def glob_select(pat, ents) + re = globs2re([pat]) + ents.select {|ent| re =~ ent } + end + + def glob_reject(pats, ents) + re = globs2re(pats) + ents.reject {|ent| re =~ ent } + end + + GLOB2REGEX = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + + def globs2re(pats) + /\A(?:#{ + pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') + })\z/ + end + + # + # TASK test + # + + TESTDIR = 'test' + + def exec_test + unless File.directory?('test') + $stderr.puts 'no test in this package' if verbose? + return + end + $stderr.puts 'Running tests...' if verbose? + begin + require 'test/unit' + rescue LoadError + setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' + end + runner = Test::Unit::AutoRunner.new(true) + runner.to_run << TESTDIR + runner.run + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias clean_dir_bin noop + alias clean_dir_lib noop + alias clean_dir_data noop + alias clean_dir_conf noop + alias clean_dir_man noop + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias distclean_dir_bin noop + alias distclean_dir_lib noop + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + alias distclean_dir_data noop + alias distclean_dir_conf noop + alias distclean_dir_man noop + + # + # Traversing + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if type == 'ext' and config('without-ext') == 'yes' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + directories_of(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + def run_hook(id) + path = [ "#{curr_srcdir()}/#{id}", + "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } + return unless path + begin + instance_eval File.read(path), path, 1 + rescue + raise if $DEBUG + setup_rb_error "hook #{path} failed:\n" + $!.message + end + end + +end # class Installer + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +if $0 == __FILE__ + begin + ToplevelInstaller.invoke + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end