Skip to content
Browse files

first commit

  • Loading branch information...
0 parents commit 0ff13d7f3547fcce760004fc3071612a4866114d @ahoward ahoward committed Jan 7, 2010
Showing with 829 additions and 0 deletions.
  1. +3 −0 LICENSE
  2. +163 −0 README
  3. +31 −0 README.erb
  4. +233 −0 Rakefile
  5. +301 −0 lib/systemu.rb
  6. +11 −0 samples/a.rb
  7. +12 −0 samples/b.rb
  8. +10 −0 samples/c.rb
  9. +11 −0 samples/d.rb
  10. +9 −0 samples/e.rb
  11. +18 −0 samples/f.rb
  12. +27 −0 systemu.gemspec
3 LICENSE
@@ -0,0 +1,3 @@
+same as Ruby's
+
+http://www.ruby-lang.org/en/LICENSE.txt
163 README
@@ -0,0 +1,163 @@
+NAME
+
+ systemu
+
+SYNOPSIS
+
+ univeral capture of stdout and stderr and handling of child process pid for windows, *nix, etc.
+
+URIS
+
+ http://github.com/ahoward/systemu
+ http://rubyforge.org/projects/codeforpeople/
+
+INSTALL
+
+ gem install systemu
+
+HISTORY
+ 1.3.0
+ - move to github
+
+ 1.2.0
+
+ - fixed handling of background thread management - needed
+ Thread.current.abort_on_exception = true
+
+ - fixed reporting of child pid, it was reported as the parent's pid before
+
+SAMPLES
+
+
+ <========< samples/a.rb >========>
+
+ ~ > cat samples/a.rb
+
+ #
+ # systemu can be used on any platform to return status, stdout, and stderr of
+ # any command. unlike other methods like open3/popen4 there is zero danger of
+ # full pipes or threading issues hanging your process or subprocess.
+ #
+ require 'systemu'
+
+ date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " )
+
+ status, stdout, stderr = systemu date
+ p [ status, stdout, stderr ]
+
+ ~ > ruby samples/a.rb
+
+ [#<Process::Status: pid=5138,exited(0)>, "Thu Jan 07 13:19:37 -0700 2010\n", "Thu Jan 07 13:19:37 -0700 2010\n"]
+
+
+ <========< samples/b.rb >========>
+
+ ~ > cat samples/b.rb
+
+ #
+ # quite a few keys can be passed to the command to alter it's behaviour. if
+ # either stdout or stderr is supplied those objects should respond_to? '<<'
+ # and only status will be returned
+ #
+ require 'systemu'
+
+ date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " )
+
+ stdout, stderr = '', ''
+ status = systemu date, 'stdout' => stdout, 'stderr' => stderr
+ p [ status, stdout, stderr ]
+
+ ~ > ruby samples/b.rb
+
+ [#<Process::Status: pid=5143,exited(0)>, "Thu Jan 07 13:19:38 -0700 2010\n", "Thu Jan 07 13:19:38 -0700 2010\n"]
+
+
+ <========< samples/c.rb >========>
+
+ ~ > cat samples/c.rb
+
+ #
+ # of course stdin can be supplied too. synonyms for 'stdin' include '0' and
+ # 0. the other stdio streams have similar shortcuts
+ #
+ require 'systemu'
+
+ cat = %q( ruby -e" ARGF.each{|line| puts line} " )
+
+ status = systemu cat, 0=>'the stdin for cat', 1=>stdout=''
+ puts stdout
+
+ ~ > ruby samples/c.rb
+
+ the stdin for cat
+
+
+ <========< samples/d.rb >========>
+
+ ~ > cat samples/d.rb
+
+ #
+ # the cwd can be supplied
+ #
+ require 'systemu'
+ require 'tmpdir'
+
+ pwd = %q( ruby -e" STDERR.puts Dir.pwd " )
+
+ status = systemu pwd, 2=>(stderr=''), :cwd=>Dir.tmpdir
+ puts stderr
+
+
+ ~ > ruby samples/d.rb
+
+ /private/var/folders/nO/nOjBCb30ELegrm98Bhyvbk+++TM/-Tmp-
+
+
+ <========< samples/e.rb >========>
+
+ ~ > cat samples/e.rb
+
+ #
+ # any environment vars specified are merged into the child's environment
+ #
+ require 'systemu'
+
+ env = %q( ruby -r yaml -e" puts ENV[ 'answer' ] " )
+
+ status = systemu env, 1=>stdout='', 'env'=>{ 'answer' => 0b101010 }
+ puts stdout
+
+ ~ > ruby samples/e.rb
+
+ 42
+
+
+ <========< samples/f.rb >========>
+
+ ~ > cat samples/f.rb
+
+ #
+ # if a block is specified then it is passed the child pid and run in a
+ # background thread. note that this thread will __not__ be blocked during the
+ # execution of the command so it may do useful work such as killing the child
+ # if execution time passes a certain threshold
+ #
+ require 'systemu'
+
+ looper = %q( ruby -e" loop{ STDERR.puts Time.now.to_i; sleep 1 } " )
+
+ status, stdout, stderr =
+ systemu looper do |cid|
+ sleep 3
+ Process.kill 9, cid
+ end
+
+ p status
+ p stderr
+
+ ~ > ruby samples/f.rb
+
+ #<Process::Status: pid=5163,signaled(SIGKILL=9)>
+ "1262895578\n1262895579\n1262895580\n"
+
+
31 README.erb
@@ -0,0 +1,31 @@
+NAME
+
+ systemu
+
+SYNOPSIS
+
+ univeral capture of stdout and stderr and handling of child process pid for windows, *nix, etc.
+
+URIS
+
+ http://github.com/ahoward/systemu
+ http://rubyforge.org/projects/codeforpeople/
+
+INSTALL
+
+ gem install systemu
+
+HISTORY
+ 1.3.0
+ - move to github
+
+ 1.2.0
+
+ - fixed handling of background thread management - needed
+ Thread.current.abort_on_exception = true
+
+ - fixed reporting of child pid, it was reported as the parent's pid before
+
+SAMPLES
+
+<%= samples %>
233 Rakefile
@@ -0,0 +1,233 @@
+
+This.rubyforge_project = 'codeforpeople'
+This.author = "Ara T. Howard"
+This.email = "ara.t.howard@gmail.com"
+This.homepage = "http://github.com/ahoward/#{ This.lib }/tree/master"
+
+
+task :default do
+ puts(Rake::Task.tasks.map{|task| task.name} - ['default'])
+end
+
+
+task :gemspec do
+ ignore_extensions = 'git', 'svn', 'tmp', /sw./, 'bak', 'gem'
+ ignore_directories = 'pkg'
+ ignore_files = 'test/log'
+
+ shiteless =
+ lambda do |list|
+ list.delete_if do |entry|
+ next unless test(?e, entry)
+ extension = File.basename(entry).split(%r/[.]/).last
+ ignore_extensions.any?{|ext| ext === extension}
+ end
+ list.delete_if do |entry|
+ next unless test(?d, entry)
+ dirname = File.expand_path(entry)
+ ignore_directories.any?{|dir| File.expand_path(dir) == dirname}
+ end
+ list.delete_if do |entry|
+ next unless test(?f, entry)
+ filename = File.expand_path(entry)
+ ignore_files.any?{|file| File.expand_path(file) == filename}
+ end
+ end
+
+ lib = This.lib
+ object = This.object
+ version = This.version
+ files = shiteless[Dir::glob("**/**")]
+ executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)}
+ has_rdoc = true #File.exist?('doc')
+ test_files = "test/#{ lib }.rb" if File.file?("test/#{ lib }.rb")
+ summary = object.respond_to?(:summary) ? object.summary : "summary: #{ lib } kicks the ass"
+ description = object.respond_to?(:description) ? object.description : "description: #{ lib } kicks the ass"
+
+ extensions = This.extensions
+ if extensions.nil?
+ %w( Makefile configure extconf.rb ).each do |ext|
+ extensions << ext if File.exists?(ext)
+ end
+ end
+ extensions = [extensions].flatten.compact
+
+ template =
+ if test(?e, 'gemspec.erb')
+ Template{ IO.read('gemspec.erb') }
+ else
+ Template {
+ <<-__
+ ## #{ lib }.gemspec
+ #
+
+ Gem::Specification::new do |spec|
+ spec.name = #{ lib.inspect }
+ spec.version = #{ version.inspect }
+ spec.platform = Gem::Platform::RUBY
+ spec.summary = #{ lib.inspect }
+ spec.description = #{ description.inspect }
+
+ spec.files = #{ files.inspect }
+ spec.executables = #{ executables.inspect }
+
+ spec.require_path = "lib"
+
+ spec.has_rdoc = #{ has_rdoc.inspect }
+ spec.test_files = #{ test_files.inspect }
+ #spec.add_dependency 'lib', '>= version'
+ spec.add_dependency 'fattr'
+
+ spec.extensions.push(*#{ extensions.inspect })
+
+ spec.rubyforge_project = #{ This.rubyforge_project.inspect }
+ spec.author = #{ This.author.inspect }
+ spec.email = #{ This.email.inspect }
+ spec.homepage = #{ This.homepage.inspect }
+ end
+ __
+ }
+ end
+
+ open("#{ lib }.gemspec", "w"){|fd| fd.puts template}
+ This.gemspec = "#{ lib }.gemspec"
+end
+
+task :gem => [:clean, :gemspec] do
+ Fu.mkdir_p This.pkgdir
+ before = Dir['*.gem']
+ cmd = "gem build #{ This.gemspec }"
+ `#{ cmd }`
+ after = Dir['*.gem']
+ gem = ((after - before).first || after.first) or abort('no gem!')
+ Fu.mv gem, This.pkgdir
+ This.gem = File.basename(gem)
+end
+
+task :readme do
+ samples = ''
+ prompt = '~ > '
+ lib = This.lib
+ version = This.version
+
+ Dir['sample*/*'].sort.each do |sample|
+ samples << "\n" << " <========< #{ sample } >========>" << "\n\n"
+
+ cmd = "cat #{ sample }"
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
+ samples << Util.indent(`#{ cmd }`, 4) << "\n"
+
+ cmd = "ruby #{ sample }"
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
+
+ cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'"
+ samples << Util.indent(`#{ cmd } 2>&1`, 4) << "\n"
+ end
+
+ template =
+ if test(?e, 'readme.erb')
+ Template{ IO.read('readme.erb') }
+ else
+ Template {
+ <<-__
+ NAME
+ #{ lib }
+
+ DESCRIPTION
+
+ INSTALL
+ gem install #{ lib }
+
+ SAMPLES
+ #{ samples }
+ __
+ }
+ end
+
+ open("README", "w"){|fd| fd.puts template}
+end
+
+
+task :clean do
+ Dir[File.join(This.pkgdir, '**/**')].each{|entry| Fu.rm_rf(entry)}
+end
+
+
+task :release => [:clean, :gemspec, :gem] do
+ gems = Dir[File.join(This.pkgdir, '*.gem')].flatten
+ raise "which one? : #{ gems.inspect }" if gems.size > 1
+ raise "no gems?" if gems.size < 1
+ cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.pkgdir }/#{ This.gem }"
+ puts cmd
+ system cmd
+end
+
+
+
+
+
+BEGIN {
+ $VERBOSE = nil
+
+ require 'ostruct'
+ require 'erb'
+ require 'fileutils'
+
+ Fu = FileUtils
+
+ This = OpenStruct.new
+
+ This.file = File.expand_path(__FILE__)
+ This.dir = File.dirname(This.file)
+ This.pkgdir = File.join(This.dir, 'pkg')
+
+ lib = ENV['LIB']
+ unless lib
+ lib = File.basename(Dir.pwd)
+ end
+ This.lib = lib
+
+ version = ENV['VERSION']
+ unless version
+ require "./lib/#{ This.lib }"
+ This.name = lib.capitalize
+ This.object = eval(This.name)
+ version = This.object.send(:version)
+ end
+ This.version = version
+
+ abort('no lib') unless This.lib
+ abort('no version') unless This.version
+
+ module Util
+ def indent(s, n = 2)
+ s = unindent(s)
+ ws = ' ' * n
+ s.gsub(%r/^/, ws)
+ end
+
+ def unindent(s)
+ indent = nil
+ s.each do |line|
+ next if line =~ %r/^\s*$/
+ indent = line[%r/^\s*/] and break
+ end
+ indent ? s.gsub(%r/^#{ indent }/, "") : s
+ end
+ extend self
+ end
+
+ class Template
+ def initialize(&block)
+ @block = block
+ @template = block.call.to_s
+ end
+ def expand(b=nil)
+ ERB.new(Util.unindent(@template)).result(b||@block)
+ end
+ alias_method 'to_s', 'expand'
+ end
+ def Template(*args, &block) Template.new(*args, &block) end
+
+ Dir.chdir(This.dir)
+}
301 lib/systemu.rb
@@ -0,0 +1,301 @@
+# vim: ts=2:sw=2:sts=2:et:fdm=marker
+require 'tmpdir'
+require 'socket'
+require 'fileutils'
+require 'rbconfig'
+require 'thread'
+require 'yaml'
+
+class Object
+ def systemu(*a, &b) SystemUniversal.new(*a, &b).systemu end
+end
+
+class SystemUniversal
+#
+# constants
+#
+ SystemUniversal::VERSION = '1.3.0' unless defined? SystemUniversal::VERSION
+ def SystemUniversal.version() SystemUniversal::VERSION end
+ def version() SystemUniversal::VERSION end
+#
+# class methods
+#
+
+ @host = Socket.gethostname
+ @ppid = Process.ppid
+ @pid = Process.pid
+ @turd = ENV['SYSTEMU_TURD']
+
+ c = ::Config::CONFIG
+ ruby = File.join(c['bindir'], c['ruby_install_name']) << c['EXEEXT']
+ @ruby = if system('%s -e 42' % ruby)
+ ruby
+ else
+ system('%s -e 42' % 'ruby') ? 'ruby' : warn('no ruby in PATH/CONFIG')
+ end
+
+ class << self
+ %w( host ppid pid ruby turd ).each{|a| attr_accessor a}
+ end
+
+#
+# instance methods
+#
+
+ def initialize argv, opts = {}, &block
+ getopt = getopts opts
+
+ @argv = argv
+ @block = block
+
+ @stdin = getopt[ ['stdin', 'in', '0', 0] ]
+ @stdout = getopt[ ['stdout', 'out', '1', 1] ]
+ @stderr = getopt[ ['stderr', 'err', '2', 2] ]
+ @env = getopt[ 'env' ]
+ @cwd = getopt[ 'cwd' ]
+
+ @host = getopt[ 'host', self.class.host ]
+ @ppid = getopt[ 'ppid', self.class.ppid ]
+ @pid = getopt[ 'pid', self.class.pid ]
+ @ruby = getopt[ 'ruby', self.class.ruby ]
+ end
+
+ def systemu
+ tmpdir do |tmp|
+ c = child_setup tmp
+ status = nil
+
+ begin
+ thread = nil
+
+ quietly{
+ IO.popen "#{ @ruby } #{ c['program'] }", 'r+' do |pipe|
+ line = pipe.gets
+ case line
+ when %r/^pid: \d+$/
+ cid = Integer line[%r/\d+/]
+ else
+ begin
+ buf = pipe.read
+ buf = "#{ line }#{ buf }"
+ e = Marshal.load buf
+ raise unless Exception === e
+ raise e
+ rescue
+ raise "wtf?\n#{ buf }\n"
+ end
+ end
+ thread = new_thread cid, @block if @block
+ pipe.read rescue nil
+ end
+ }
+ status = $?
+ ensure
+ if thread
+ begin
+ class << status
+ attr 'thread'
+ end
+ status.instance_eval{ @thread = thread }
+ rescue
+ 42
+ end
+ end
+ end
+
+ if @stdout or @stderr
+ open(c['stdout']){|f| relay f => @stdout} if @stdout
+ open(c['stderr']){|f| relay f => @stderr} if @stderr
+ status
+ else
+ [status, IO.read(c['stdout']), IO.read(c['stderr'])]
+ end
+ end
+ end
+
+ def new_thread cid, block
+ q = Queue.new
+ Thread.new(cid) do |cid|
+ current = Thread.current
+ current.abort_on_exception = true
+ q.push current
+ block.call cid
+ end
+ q.pop
+ end
+
+ def child_setup tmp
+ stdin = File.expand_path(File.join(tmp, 'stdin'))
+ stdout = File.expand_path(File.join(tmp, 'stdout'))
+ stderr = File.expand_path(File.join(tmp, 'stderr'))
+ program = File.expand_path(File.join(tmp, 'program'))
+ config = File.expand_path(File.join(tmp, 'config'))
+
+ if @stdin
+ open(stdin, 'w'){|f| relay @stdin => f}
+ else
+ FileUtils.touch stdin
+ end
+ FileUtils.touch stdout
+ FileUtils.touch stderr
+
+ c = {}
+ c['argv'] = @argv
+ c['env'] = @env
+ c['cwd'] = @cwd
+ c['stdin'] = stdin
+ c['stdout'] = stdout
+ c['stderr'] = stderr
+ c['program'] = program
+ open(config, 'w'){|f| YAML.dump c, f}
+
+ open(program, 'w'){|f| f.write child_program(config)}
+
+ c
+ end
+
+ def quietly
+ v = $VERBOSE
+ $VERBOSE = nil
+ yield
+ ensure
+ $VERBOSE = v
+ end
+
+ def child_program config
+ <<-program
+ PIPE = STDOUT.dup
+ begin
+ require 'yaml'
+
+ config = YAML.load(IO.read('#{ config }'))
+
+ argv = config['argv']
+ env = config['env']
+ cwd = config['cwd']
+ stdin = config['stdin']
+ stdout = config['stdout']
+ stderr = config['stderr']
+
+ Dir.chdir cwd if cwd
+ env.each{|k,v| ENV[k.to_s] = v.to_s} if env
+
+ STDIN.reopen stdin
+ STDOUT.reopen stdout
+ STDERR.reopen stderr
+
+ PIPE.puts "pid: \#{ Process.pid }"
+ PIPE.flush ### the process is ready yo!
+ PIPE.close
+
+ exec *argv
+ rescue Exception => e
+ PIPE.write Marshal.dump(e) rescue nil
+ exit 42
+ end
+ program
+ end
+
+ def relay srcdst
+ src, dst, ignored = srcdst.to_a.first
+ if src.respond_to? 'read'
+ while((buf = src.read(8192))); dst << buf; end
+ else
+ src.each{|buf| dst << buf}
+ end
+ end
+
+ def tmpdir d = Dir.tmpdir, max = 42, &b
+ i = -1 and loop{
+ i += 1
+
+ tmp = File.join d, "systemu_#{ @host }_#{ @ppid }_#{ @pid }_#{ rand }_#{ i += 1 }"
+
+ begin
+ Dir.mkdir tmp
+ rescue Errno::EEXIST
+ raise if i >= max
+ next
+ end
+
+ break(
+ if b
+ begin
+ b.call tmp
+ ensure
+ FileUtils.rm_rf tmp unless SystemU.turd
+ end
+ else
+ tmp
+ end
+ )
+ }
+ end
+
+ def getopts opts = {}
+ lambda do |*args|
+ keys, default, ignored = args
+ catch('opt') do
+ [keys].flatten.each do |key|
+ [key, key.to_s, key.to_s.intern].each do |key|
+ throw 'opt', opts[key] if opts.has_key?(key)
+ end
+ end
+ default
+ end
+ end
+ end
+end
+
+SystemU = SystemUniversal unless defined? SystemU
+Systemu = SystemUniversal unless defined? Systemu
+
+
+
+
+
+
+
+
+
+
+
+
+
+if $0 == __FILE__
+#
+# date
+#
+ date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " )
+
+ status, stdout, stderr = systemu date
+ p [status, stdout, stderr]
+
+ status = systemu date, 1=>(stdout = '')
+ p [status, stdout]
+
+ status = systemu date, 2=>(stderr = '')
+ p [status, stderr]
+#
+# sleep
+#
+ sleep = %q( ruby -e" p(sleep(1)) " )
+ status, stdout, stderr = systemu sleep
+ p [status, stdout, stderr]
+
+ sleep = %q( ruby -e" p(sleep(42)) " )
+ status, stdout, stderr = systemu(sleep){|cid| Process.kill 9, cid}
+ p [status, stdout, stderr]
+#
+# env
+#
+ env = %q( ruby -e" p ENV['A'] " )
+ status, stdout, stderr = systemu env, :env => {'A' => 42}
+ p [status, stdout, stderr]
+#
+# cwd
+#
+ env = %q( ruby -e" p Dir.pwd " )
+ status, stdout, stderr = systemu env, :cwd => Dir.tmpdir
+ p [status, stdout, stderr]
+end
11 samples/a.rb
@@ -0,0 +1,11 @@
+#
+# systemu can be used on any platform to return status, stdout, and stderr of
+# any command. unlike other methods like open3/popen4 there is zero danger of
+# full pipes or threading issues hanging your process or subprocess.
+#
+ require 'systemu'
+
+ date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " )
+
+ status, stdout, stderr = systemu date
+ p [ status, stdout, stderr ]
12 samples/b.rb
@@ -0,0 +1,12 @@
+#
+# quite a few keys can be passed to the command to alter it's behaviour. if
+# either stdout or stderr is supplied those objects should respond_to? '<<'
+# and only status will be returned
+#
+ require 'systemu'
+
+ date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " )
+
+ stdout, stderr = '', ''
+ status = systemu date, 'stdout' => stdout, 'stderr' => stderr
+ p [ status, stdout, stderr ]
10 samples/c.rb
@@ -0,0 +1,10 @@
+#
+# of course stdin can be supplied too. synonyms for 'stdin' include '0' and
+# 0. the other stdio streams have similar shortcuts
+#
+ require 'systemu'
+
+ cat = %q( ruby -e" ARGF.each{|line| puts line} " )
+
+ status = systemu cat, 0=>'the stdin for cat', 1=>stdout=''
+ puts stdout
11 samples/d.rb
@@ -0,0 +1,11 @@
+#
+# the cwd can be supplied
+#
+ require 'systemu'
+ require 'tmpdir'
+
+ pwd = %q( ruby -e" STDERR.puts Dir.pwd " )
+
+ status = systemu pwd, 2=>(stderr=''), :cwd=>Dir.tmpdir
+ puts stderr
+
9 samples/e.rb
@@ -0,0 +1,9 @@
+#
+# any environment vars specified are merged into the child's environment
+#
+ require 'systemu'
+
+ env = %q( ruby -r yaml -e" puts ENV[ 'answer' ] " )
+
+ status = systemu env, 1=>stdout='', 'env'=>{ 'answer' => 0b101010 }
+ puts stdout
18 samples/f.rb
@@ -0,0 +1,18 @@
+#
+# if a block is specified then it is passed the child pid and run in a
+# background thread. note that this thread will __not__ be blocked during the
+# execution of the command so it may do useful work such as killing the child
+# if execution time passes a certain threshold
+#
+ require 'systemu'
+
+ looper = %q( ruby -e" loop{ STDERR.puts Time.now.to_i; sleep 1 } " )
+
+ status, stdout, stderr =
+ systemu looper do |cid|
+ sleep 3
+ Process.kill 9, cid
+ end
+
+ p status
+ p stderr
27 systemu.gemspec
@@ -0,0 +1,27 @@
+## systemu.gemspec
+#
+
+Gem::Specification::new do |spec|
+ spec.name = "systemu"
+ spec.version = "1.8.6"
+ spec.platform = Gem::Platform::RUBY
+ spec.summary = "systemu"
+ spec.description = "description: systemu kicks the ass"
+
+ spec.files = ["lib", "lib/systemu.rb", "LICENSE", "Rakefile", "README.erb", "samples", "samples/a.rb", "samples/b.rb", "samples/c.rb", "samples/d.rb", "samples/e.rb", "samples/f.rb", "systemu.gemspec"]
+ spec.executables = []
+
+ spec.require_path = "lib"
+
+ spec.has_rdoc = true
+ spec.test_files = nil
+ #spec.add_dependency 'lib', '>= version'
+ spec.add_dependency 'fattr'
+
+ spec.extensions.push(*[])
+
+ spec.rubyforge_project = "codeforpeople"
+ spec.author = "Ara T. Howard"
+ spec.email = "ara.t.howard@gmail.com"
+ spec.homepage = "http://github.com/ahoward/systemu/tree/master"
+end

0 comments on commit 0ff13d7

Please sign in to comment.
Something went wrong with that request. Please try again.