diff --git a/.gitignore b/.gitignore index aabd6c805c6..669742d1071 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ tool/nailgun/config.log tool/nailgun/config.status tool/nailgun/ng reference.txt +maven/jruby-complete/pom.xml # binaries !bin/gem diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 2b5d7742ec8..305d7fcc7b0 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -1,8 +1,8 @@ - io.tesla.polyglot - tesla-polyglot-ruby - 0.1.2-SNAPSHOT + io.takari.polyglot + polyglot-ruby + 0.1.3 diff --git a/core/pom.xml b/core/pom.xml deleted file mode 100644 index 21c1636df0b..00000000000 --- a/core/pom.xml +++ /dev/null @@ -1,694 +0,0 @@ - - - 4.0.0 - - org.jruby - jruby-parent - 9.0.0.0-SNAPSHOT - - jruby-core - JRuby Core - - 2.2.1 - ${test.dir}/prawn - ${spec.dir}/tags - ${build.dir}/pkg - ${settings.localRepository}/com/headius/unsafe-mock/${unsafe.version}/unsafe-mock-${unsafe.version}.jar - spec - yyyy-MM-dd - 2G - ${jruby.win32ole.gem} - true - git://github.com/sandal/prawn.git - 0 - 2013d - /Applications/install4j 4/bin/install4jc - jay - ${lib.dir} - git://github.com/rails/rails.git - target - true - ${rubyspec.dir}/1.8 - 1024M - 2G - 2.2 - 8.0 - release - lib - ${test.dir}/rails - pom.xml - core/src/main/java/org/jruby/parser - ${basedir}/.. - ${spec.dir}/ruby - 49761 - 3G - ${spec.dir}/mspec - ${maven.build.timestamp} - ${project.parent.basedir} - provided - ${build.dir}/test-results - 0.4.1 - ${build.dir}/mspec.tgz - test/target - ${project.basedir}/target/generated-sources - - test - jflex - jruby-win32ole - ${mspec.dir}/bin/mspec - lib/ruby/gems/shared - org/jruby/runtime/Constants.java - ${test.dir}/target/test-classes - - - - org.ow2.asm - asm - ${asm.version} - - - org.ow2.asm - asm-commons - ${asm.version} - - - org.ow2.asm - asm-analysis - ${asm.version} - - - org.ow2.asm - asm-util - ${asm.version} - - - com.github.jnr - jnr-netdb - 1.1.4 - - - com.github.jnr - jnr-enxio - 0.7 - - - com.github.jnr - jnr-x86asm - 1.0.2 - - - com.github.jnr - jnr-unixsocket - 0.6 - - - com.github.jnr - jnr-posix - 3.0.10 - - - com.github.jnr - jnr-constants - 0.8.6 - - - com.github.jnr - jnr-ffi - 2.0.2 - - - com.github.jnr - jffi - ${jffi.version} - - - com.github.jnr - jffi - ${jffi.version} - native - - - org.jruby.joni - joni - 2.1.5 - - - org.jruby.extras - bytelist - 1.0.12 - - - org.jruby.jcodings - jcodings - 1.0.13-SNAPSHOT - - - org.jruby - dirgra - 0.2 - - - com.headius - invokebinder - 1.5 - - - com.headius - options - 1.1 - - - com.headius - coro-mock - 1.0 - provided - - - com.headius - unsafe-mock - ${unsafe.version} - provided - - - com.headius - jsr292-mock - 1.1 - provided - - - bsf - bsf - 2.4.0 - provided - - - com.jcraft - jzlib - 1.1.3 - - - com.martiansoftware - nailgun-server - 0.9.1 - - - junit - junit - test - - - org.apache.ant - ant - ${ant.version} - provided - - - org.osgi - org.osgi.core - 5.0.0 - provided - - - org.jruby - joda-timezones - ${tzdata.version} - ${tzdata.scope} - - - joda-time - joda-time - ${joda.time.version} - - - - package - - - src/main/ruby - - **/*rb - - - - src/main/resources - - META-INF/**/* - - - - ${project.build.sourceDirectory} - true - ${project.basedir}/src/main/resources - - ${Constants.java} - - - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.codehaus.mojo - properties-maven-plugin - [1.0-alpha-2,) - - read-project-properties - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - [1.8,) - - add-source - - - - - - - - - org.codehaus.mojo - exec-maven-plugin - [1.2.1,) - - exec - - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - [2.8,) - - copy - - - - - - - - - org.apache.maven.plugins - maven-clean-plugin - [2.5,) - - clean - - - - - - - - - - - - - - - org.codehaus.mojo - buildnumber-maven-plugin - 1.2 - - - jruby-revision - generate-sources - - create - - - 7 - jruby.revision - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-populators - process-classes - - add-source - - - - ${anno.sources} - - - - - - - org.codehaus.mojo - exec-maven-plugin - - - invoker-generator - process-classes - - exec - - - - -Djruby.bytecode.version=${base.java.version} - -classpath - - org.jruby.anno.InvokerGenerator - ${anno.sources}/annotated_classes.txt - ${project.build.outputDirectory} - - java - compile - - - - - - maven-compiler-plugin - - - anno - process-resources - - compile - - - - org/jruby/anno/FrameField.java - org/jruby/anno/AnnotationBinder.java - org/jruby/anno/JRubyMethod.java - org/jruby/anno/FrameField.java - org/jruby/CompatVersion.java - org/jruby/runtime/Visibility.java - org/jruby/util/CodegenUtils.java - org/jruby/util/SafePropertyAccessor.java - - - - - default-compile - compile - - compile - - - - org.jruby.anno.AnnotationBinder - - target/generated-sources - - -XDignore.symbol.file=true - -J-Duser.language=en - -J-Dfile.encoding=UTF-8 - -J-Xbootclasspath/p:${unsafe.jar} - -J-Xmx${jruby.compile.memory} - - - - - populators - process-classes - - compile - - - - -XDignore.symbol.file=true - -J-Duser.language=en - -J-Dfile.encoding=UTF-8 - -J-Xbootclasspath/p:${unsafe.jar} - -J-Xmx${jruby.compile.memory} - - - org/jruby/gen/**/*.java - - - - - eclipse-hack - process-classes - - compile - - - true - - **/*.java - - - - - - utf-8 - true - true - true - - -J-Xmx1G - - true - true - ${base.java.version} - 1.7 - ${base.javac.version} - 1.7 - false - - - - maven-clean-plugin - - - default-clean - clean - - clean - - - - - ${project.build.sourceDirectory} - - ${Constants.java} - - - - false - - - - - - maven-surefire-plugin - - 1 - false - - 1.9 - ${basedir}/.. - - -Xmx${jruby.test.memory} -XX:MaxPermSize=${jruby.test.memory.permgen} -Dfile.encoding=UTF-8 -Djava.awt.headless=true - - org/jruby/test/MainTestSuite.java - org/jruby/embed/**/*Test*.java - org/jruby/util/**/*Test*.java - - - ${basedir}/src/test/ruby - - - - - maven-shade-plugin - - - create lib/jruby.jar - package - - shade - - - - - org.objectweb - org.jruby.org.objectweb - - - ${jruby.basedir}/lib/jruby.jar - - - org.jruby.Main - - - - - - shade the asm classes - verify - - shade - - - - - com.github.jnr:jnr-ffi - org.ow2.asm:* - - - - - org.objectweb - org.jruby.org.objectweb - - - - - - - - - - - jruby.bash - - - ../bin/jruby - - - - - - maven-antrun-plugin - - - copy - initialize - - run - - - - - - - - - - - - - - - - - native - - - ../lib/jni - - - - - - maven-dependency-plugin - - - unzip native - process-classes - - unpack - - - META-INF,META-INF/* - - - com.github.jnr - jffi - ${jffi.version} - jar - native - false - ${jruby.basedir}/lib - - - - - - - - - - - test - - false - - - - build.properties - - - - - - - org.codehaus.mojo - properties-maven-plugin - 1.0-alpha-2 - - - properties - initialize - - read-project-properties - - - - ${jruby.basedir}/build.properties - - true - - - - - - - - - tzdata - - - tzdata.version - - - - ${tzdata.version} - runtime - - - - diff --git a/lib/pom.xml b/lib/pom.xml deleted file mode 100644 index a2d04312ec6..00000000000 --- a/lib/pom.xml +++ /dev/null @@ -1,316 +0,0 @@ - - - 4.0.0 - - org.jruby - jruby-parent - 9.0.0.0-SNAPSHOT - - jruby-stdlib - JRuby Lib Setup - - true - ${jruby.complete.home}/lib/ruby/gems/shared - ${basedir}/ruby/gems/shared - 1.0.5 - pom.xml - ${project.build.outputDirectory}/META-INF/jruby.home - 0.1.1 - - - - org.jruby - jruby-core - 9.0.0.0-SNAPSHOT - test - - - rubygems - jruby-openssl - 0.9.6 - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - jruby-readline - 1.0 - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - rake - ${rake.version} - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - rdoc - ${rdoc.version} - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - json - ${json.version} - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - jar-dependencies - 0.1.8 - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - minitest - ${minitest.version} - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - test-unit - ${test-unit.version} - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - power_assert - ${power_assert.version} - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - psych - 2.0.9-SNAPSHOT - gem - provided - - - jar-dependencies - rubygems - - - - - rubygems - ruby-maven - 3.1.1.0.8 - gem - provided - - - - - rubygems-releases - http://rubygems-proxy.torquebox.org/releases - - - - - - ${jruby.complete.gems} - ${gem.home} - - gems/rake-${rake.version}/bin/r* - gems/rdoc-${rdoc.version}/bin/r* - specifications/default/*.gemspec - - - - ${jruby.complete.home} - ${basedir}/.. - - bin/ast* - bin/gem* - bin/irb* - bin/jgem* - bin/jirb* - bin/jruby* - bin/rake* - bin/ri* - bin/rdoc* - bin/testrb* - lib/ruby/stdlib/** - lib/ruby/truffle/** - - - bin/jruby - bin/jruby*_* - bin/jruby*-* - **/.* - lib/ruby/stdlib/rubygems/defaults/jruby_native.rb - - - - - - maven-clean-plugin - - - - ${basedir}/ruby/gems/shared/specifications/default - - * - - - - ${basedir}/ruby/stdlib - - **/bouncycastle/**/*.jar - - - - - - - maven-dependency-plugin - - - package - - copy-dependencies - - - - - true - ruby/stdlib - rubygems - provided - - - - maven-source-plugin - - true - - - - org.codehaus.mojo - build-helper-maven-plugin - - - io.tesla.polyglot - tesla-polyglot-maven-plugin - ${tesla.version} - - - install_gems - initialize - - execute - - - install_gems - pom.rb - - - - fix shebang on gem bin files and add *.bat files - prepare-resources - - execute - - - fix shebang on gem bin files and add *.bat files - pom.rb - - - - copy bin/jruby.bash to bin/jruby - process-resources - - execute - - - copy bin/jruby.bash to bin/jruby - pom.rb - - - - jrubydir - prepare-package - - execute - - - jrubydir - pom.rb - - - - - - io.tesla.polyglot - tesla-polyglot-ruby - ${tesla.version} - - - - - - diff --git a/lib/ruby/shared/gauntlet_rdoc.rb b/lib/ruby/shared/gauntlet_rdoc.rb new file mode 100644 index 00000000000..16665da7708 --- /dev/null +++ b/lib/ruby/shared/gauntlet_rdoc.rb @@ -0,0 +1,84 @@ +require 'rubygems' +Gem.load_yaml +require 'rdoc' +require 'gauntlet' +require 'fileutils' + +## +# Allows for testing of RDoc against every gem + +class RDoc::Gauntlet < Gauntlet + + def initialize # :nodoc: + super + + @args = nil + @type = nil + end + + ## + # Runs an RDoc generator for gem +name+ + + def run name + return if self.data.key? name + + dir = File.expand_path "~/.gauntlet/data/#{@type}/#{name}" + FileUtils.rm_rf dir if File.exist? dir + + yaml = File.read 'gemspec' + begin + spec = Gem::Specification.from_yaml yaml + rescue Psych::SyntaxError + puts "bad spec #{name}" + self.data[name] = false + return + end + + args = @args.dup + args << '--op' << dir + args.concat spec.rdoc_options + args << spec.require_paths + args << spec.extra_rdoc_files + args = args.flatten.map { |a| a.to_s } + args.delete '--quiet' + + puts "#{name} - rdoc #{args.join ' '}" + + self.dirty = true + r = RDoc::RDoc.new + + begin + r.document args + self.data[name] = true + puts 'passed' + FileUtils.rm_rf dir + rescue Interrupt, StandardError, RDoc::Error, SystemStackError => e + puts "failed - (#{e.class}) #{e.message}" + self.data[name] = false + end + rescue Gem::Exception + puts "bad gem #{name}" + ensure + puts + end + + ## + # Runs the gauntlet with the given +type+ (rdoc or ri) and +filter+ for + # which gems to run + + def run_the_gauntlet type = 'rdoc', filter = nil + @type = type || 'rdoc' + @args = type == 'rdoc' ? [] : %w[--ri] + @data_file = "#{DATADIR}/#{@type}-data.yml" + + super filter + end + +end + +type = ARGV.shift +filter = ARGV.shift +filter = /#{filter}/ if filter + +RDoc::Gauntlet.new.run_the_gauntlet type, filter + diff --git a/lib/ruby/shared/jar-dependencies.rb b/lib/ruby/shared/jar-dependencies.rb new file mode 100644 index 00000000000..b298894283e --- /dev/null +++ b/lib/ruby/shared/jar-dependencies.rb @@ -0,0 +1,22 @@ +# +# Copyright (C) 2014 Christian Meier +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +require 'jar_dependencies' diff --git a/lib/ruby/shared/jar_dependencies.rb b/lib/ruby/shared/jar_dependencies.rb new file mode 100644 index 00000000000..0016fed42f5 --- /dev/null +++ b/lib/ruby/shared/jar_dependencies.rb @@ -0,0 +1,217 @@ +# +# Copyright (C) 2014 Christian Meier +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +module Jars + HOME = 'JARS_HOME'.freeze + MAVEN_SETTINGS = 'JARS_MAVEN_SETTINGS'.freeze + SKIP = 'JARS_SKIP'.freeze + NO_REQUIRE = 'JARS_NO_REQUIRE'.freeze + QUIET = 'JARS_QUIET'.freeze + VERBOSE = 'JARS_VERBOSE'.freeze + DEBUG = 'JARS_DEBUG'.freeze + VENDOR = 'JARS_VENDOR'.freeze + + class << self + + if defined? JRUBY_VERSION + def to_prop( key ) + java.lang.System.getProperty( key.downcase.gsub( /_/, '.' ) ) || + ENV[ key.upcase.gsub( /[.]/, '_' ) ] + end + else + def to_prop( key ) + ENV[ key.upcase.gsub( /[.]/, '_' ) ] + end + end + + def to_boolean( key ) + prop = to_prop( key ) + # prop == nil => false + # prop == 'false' => false + # anything else => true + prop == '' or prop == 'true' + end + + def skip? + to_boolean( SKIP ) + end + + def no_require? + to_boolean( NO_REQUIRE ) + end + + def quiet? + to_boolean( QUIET ) + end + + def verbose? + to_boolean( VERBOSE ) + end + + def debug? + to_boolean( DEBUG ) + end + + def vendor? + to_boolean( VENDOR ) + end + + def freeze_loading + ENV[ NO_REQUIRE ] = 'true' + end + + def reset + instance_variables.each { |var| instance_variable_set(var, nil) } + ( @@jars ||= {} ).clear + end + + def maven_user_settings + if @_jars_maven_user_settings_.nil? + if settings = absolute( to_prop( MAVEN_SETTINGS ) ) + settings = File.expand_path(settings) + unless File.exists?(settings) + warn "configured ENV['#{MAVEN_SETTINGS}'] = '#{settings}' not found" unless quiet? + settings = false + end + else # use maven default (user) settings + settings = File.join( user_home, '.m2', 'settings.xml' ) + settings = false unless File.exists?(settings) + end + @_jars_maven_user_settings_ = settings + end + @_jars_maven_user_settings_ || nil + end + alias maven_settings maven_user_settings + + def maven_global_settings + if @_jars_maven_global_settings_.nil? + if mvn_home = ENV[ 'M2_HOME' ] || ENV[ 'MAVEN_HOME' ] + settings = File.join( mvn_home, 'conf/settings.xml' ) + settings = false unless File.exists?(settings) + else + settings = false + end + @_jars_maven_global_settings_ = settings + end + @_jars_maven_global_settings_ || nil + end + + def home + if @_jars_home_.nil? + unless @_jars_home_ = absolute( to_prop( HOME ) ) + begin + if user_settings = maven_user_settings + @_jars_home_ = detect_local_repository(user_settings) + end + if ! @_jars_home_ && global_settings = maven_global_settings + @_jars_home_ = detect_local_repository(global_settings) + end + rescue # ignore + end + end + # use maven default repository + @_jars_home_ ||= File.join( user_home, '.m2', 'repository' ) + end + @_jars_home_ + end + + def require_jar( group_id, artifact_id, *classifier_version ) + version = classifier_version[ -1 ] + classifier = classifier_version[ -2 ] + + @@jars ||= {} + coordinate = "#{group_id}:#{artifact_id}" + coordinate += ":#{classifier}" if classifier + if @@jars.key? coordinate + if @@jars[ coordinate ] == version + false + else + # version of already registered jar + @@jars[ coordinate ] + end + else + do_require( group_id, artifact_id, version, classifier ) + @@jars[ coordinate ] = version + return true + end + end + + private + + def absolute( file ) + File.expand_path( file ) if file + end + + def user_home + ENV[ 'HOME' ] || begin + user_home = Dir.home if Dir.respond_to?(:home) + unless user_home + user_home = ENV_JAVA[ 'user.home' ] if Object.const_defined?(:ENV_JAVA) + end + user_home + end + end + + def detect_local_repository(settings); require 'rexml/document' + doc = REXML::Document.new( File.read( settings ) ) + if local_repo = doc.root.elements['localRepository'] + if ( local_repo = local_repo.first ) + local_repo = local_repo.value + local_repo = nil if local_repo.empty? + end + end + local_repo + end + + def to_jar( group_id, artifact_id, version, classifier ) + file = "#{group_id.gsub( /\./, '/' )}/#{artifact_id}/#{version}/#{artifact_id}-#{version}" + file << "-#{classifier}" if classifier + file << '.jar' + file + end + + def do_require( *args ) + jar = to_jar( *args ) + file = File.join( home, jar ) + # use jar from local repository if exists + if File.exists?( file ) + require file + else + # otherwise try to find it on the load path + require jar + end + rescue LoadError => e + raise "\n\n\tyou might need to reinstall the gem which depends on the missing jar\n\n" + e.message + " (LoadError)" + end + + end # class << self + +end + +def require_jar( *args ) + return false if Jars.no_require? + result = Jars.require_jar( *args ) + if result.is_a? String + warn "jar coordinate #{args[0..-2].join( ':' )} already loaded with version #{result}" unless Jars.quiet? + return false + end + result +end diff --git a/lib/ruby/shared/jar_install_post_install_hook.rb b/lib/ruby/shared/jar_install_post_install_hook.rb new file mode 100644 index 00000000000..ee1b1da4395 --- /dev/null +++ b/lib/ruby/shared/jar_install_post_install_hook.rb @@ -0,0 +1,31 @@ +# +# Copyright (C) 2014 Christian Meier +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +if defined?( JRUBY_VERSION ) && Gem.post_install_hooks.empty? + Gem.post_install do |gem_installer| + unless (ENV['JARS_SKIP'] || ENV_JAVA['jars.skip']) == 'true' + require 'jar_installer' + jars = Jars::JarInstaller.new( gem_installer.spec ) + jars.ruby_maven_install_options = gem_installer.options || {} + jars.vendor_jars + end + end +end diff --git a/lib/ruby/shared/jar_installer.rb b/lib/ruby/shared/jar_installer.rb new file mode 100644 index 00000000000..169a674c6b4 --- /dev/null +++ b/lib/ruby/shared/jar_installer.rb @@ -0,0 +1,258 @@ +require 'jar_dependencies' +module Jars + class JarInstaller + + class Dependency + + attr_reader :path, :file, :gav, :scope, :type, :coord + + def self.new( line ) + if line.match /:jar:|:pom:/ + super + end + end + + def setup_type( line ) + if line.match /:pom:/ + @type = :pom + elsif line.match /:jar:/ + @type = :jar + end + end + private :setup_type + + def setup_scope( line ) + @scope = + case line + when /:provided:/ + :provided + when /:test:/ + :test + else + :runtime + end + end + private :setup_scope + + def initialize( line ) + setup_type( line ) + + line.sub!( /^\s+/, '' ) + @coord = line.sub( /:[^:]+:[^:]+$/, '' ) + first, second = line.sub( /:[^:]+:[^:]+$/, '' ).split( /:#{type}:/ ) + group_id, artifact_id = first.split( /:/ ) + parts = group_id.split( '.' ) + parts << artifact_id + parts << second.split( /:/ )[ -1 ] + parts << File.basename( line.sub /.:/, '' ) + @path = File.join( parts ).strip + + setup_scope( line ) + + line.gsub!( /:jar:|:pom:|:test:|:compile:|:runtime:|:provided:/, ':' ) + @file = line.sub( /^.*:/, '' ).strip + @gav = line.sub( /:[^:]+$/, '' ) + end + end + + def self.install_jars( write_require_file = false ) + new.install_jars( write_require_file ) + end + + def self.vendor_jars( write_require_file = false ) + new.vendor_jars( write_require_file ) + end + + def self.load_from_maven( file ) + result = [] + File.read( file ).each_line do |line| + dep = Dependency.new( line ) + result << dep if dep + end + result + end + + def self.write_require_file( require_filename ) + FileUtils.mkdir_p( File.dirname( require_filename ) ) + comment = '# this is a generated file, to avoid over-writing it just delete this comment' + if ! File.exists?( require_filename ) || File.read( require_filename ).match( comment ) + f = File.open( require_filename, 'w' ) + f.puts comment + f.puts "require 'jar_dependencies'" + f.puts + f + end + end + + def self.vendor_file( dir, dep ) + vendored = File.join( dir, dep.path ) + FileUtils.mkdir_p( File.dirname( vendored ) ) + FileUtils.cp( dep.file, vendored ) + end + + def self.install_deps( deps, dir, require_filename, vendor ) + f = write_require_file( require_filename ) if require_filename + deps.each do |dep| + next if dep.type != :jar || dep.scope != :runtime + args = dep.gav.gsub( /:/, "', '" ) + vendor_file( dir, dep ) if vendor + f.puts( "require_jar( '#{args}' )" ) if f + end + yield f if block_given? + ensure + f.close if f + end + + def find_spec( allow_no_file ) + specs = Dir[ '*.gemspec' ] + case specs.size + when 0 + raise 'no gemspec found' unless allow_no_file + when 1 + specs.first + else + raise 'more then one gemspec found. please specify a specfile' unless allow_no_file + end + end + private :find_spec + + def initialize( spec = nil ) + setup( spec ) + end + + def setup( spec = nil, allow_no_file = false ) + spec ||= find_spec( allow_no_file ) + + case spec + when String + @specfile = File.expand_path( spec ) + @basedir = File.dirname( @specfile ) + spec = eval( File.read( spec ) ) + when Gem::Specification + if File.exists?( spec.spec_file ) + @basedir = spec.gem_dir + @specfile = spec.spec_file + else + # this happens with bundle and local gems + # there the spec_file is "not installed" but inside + # the gem_dir directory + Dir.chdir( spec.gem_dir ) do + setup( nil, true ) + end + end + when NilClass + else + raise 'spec must be either String or Gem::Specification' + end + + @spec = spec + rescue + # for all those strange gemspec we skip looking for jar-dependencies + end + + def ruby_maven_install_options=( options ) + @options = options.dup + end + + def vendor_jars( write_require_file = true ) + return unless has_jars? + # do not vendor only if set explicitly via ENV/system-properties + do_install( Jars.to_prop( Jars::VENDOR ) != 'false', write_require_file ) + end + + def install_jars( write_require_file = true ) + return unless has_jars? + do_install( false, write_require_file ) + end + + private + + def has_jars? + # first look if there are any requirements in the spec + # and then if gem depends on jar-dependencies + # only then install the jars declared in the requirements + ! @spec.requirements.empty? && @spec.dependencies.detect { |d| d.name == 'jar-dependencies' && d.type == :runtime } + end + + def do_install( vendor, write_require_file ) + vendor_dir = File.join( @basedir, @spec.require_path ) + jars_file = File.join( vendor_dir, "#{@spec.name}_jars.rb" ) + + # write out new jars_file it write_require_file is true or + # check timestamps: + # do not generate file if specfile is older then the generated file + if ! write_require_file && + File.exists?( jars_file ) && + File.mtime( @specfile ) < File.mtime( jars_file ) + # leave jars_file as is + jars_file = nil + end + self.class.install_deps( install_dependencies, vendor_dir, + jars_file, vendor ) + end + + def setup_arguments( deps ) + args = [ 'dependency:list', "-DoutputFile=#{deps}", '-DincludeScope=runtime', '-DoutputAbsoluteArtifactFilename=true', '-DincludeTypes=jar', '-DoutputScope=true', '-f', @specfile ] + + if Jars.debug? + args << '-X' + elsif not Jars.verbose? + args << '--quiet' + end + + if defined? JRUBY_VERSION + args << "-Dmaven.repo.local=#{java.io.File.new( Jars.home ).absolute_path}" + else + args << "-Dmaven.repo.local=#{File.expand_path( Jars.home )}" + end + + args + end + + def lazy_load_maven + require 'maven/ruby/maven' + rescue LoadError + install_ruby_maven + require 'maven/ruby/maven' + end + + def install_ruby_maven + require 'rubygems/dependency_installer' + jars = Gem.loaded_specs[ 'jar-dependencies' ] + dep = jars.dependencies.detect { |d| d.name == 'ruby-maven' } + req = dep.nil? ? Gem::Requirement.create( '>0' ) : dep.requirement + inst = Gem::DependencyInstaller.new( @options || {} ) + inst.install 'ruby-maven', req + rescue => e + warn e.backtrace.join( "\n" ) if Jars.verbose? + raise "there was an error installing 'ruby-maven'. please install it manually: #{e.inspect}" + end + + def monkey_path_gem_dependencies + # monkey patch to NOT include gem dependencies + require 'maven/tools/gemspec_dependencies' + eval <= '2.1.0' + load('jopenssl21/openssl.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl.rb') +else + load('jopenssl18/openssl.rb') +end + +require 'openssl/pkcs12' diff --git a/lib/ruby/shared/jopenssl/version.rb b/lib/ruby/shared/jopenssl/version.rb new file mode 100644 index 00000000000..5d2b69bc77f --- /dev/null +++ b/lib/ruby/shared/jopenssl/version.rb @@ -0,0 +1,6 @@ +module Jopenssl + module Version + VERSION = '0.9.6' + BOUNCY_CASTLE_VERSION = '1.49' + end +end diff --git a/lib/ruby/shared/jopenssl18/openssl.rb b/lib/ruby/shared/jopenssl18/openssl.rb new file mode 100644 index 00000000000..246963476f9 --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl.rb @@ -0,0 +1,23 @@ +=begin += $RCSfile$ -- Loader for all OpenSSL C-space and Ruby-space definitions + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2002 Michal Rokos + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id: openssl.rb 12496 2007-06-08 15:02:04Z technorama $ +=end + +require 'openssl/bn' +require 'openssl/cipher' +require 'openssl/config' +require 'openssl/digest' +require 'openssl/pkcs7' +require 'openssl/ssl-internal' +require 'openssl/x509-internal' diff --git a/lib/ruby/shared/jopenssl18/openssl/bn.rb b/lib/ruby/shared/jopenssl18/openssl/bn.rb new file mode 100644 index 00000000000..2c38224fd3d --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/bn.rb @@ -0,0 +1,25 @@ +=begin += $RCSfile$ -- Ruby-space definitions that completes C-space funcs for BN + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2002 Michal Rokos + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +## +# Add double dispatch to Integer +# +class Integer + def to_bn + OpenSSL::BN::new(self.to_s(16), 16) + end +end # Integer + diff --git a/lib/ruby/shared/jopenssl18/openssl/buffering.rb b/lib/ruby/shared/jopenssl18/openssl/buffering.rb new file mode 100644 index 00000000000..c83b92b79e3 --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/buffering.rb @@ -0,0 +1,241 @@ +=begin += $RCSfile$ -- Buffering mix-in module. + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU YUUZOU + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +module OpenSSL +module Buffering + include Enumerable + attr_accessor :sync + BLOCK_SIZE = 1024*16 + + def initialize(*args) + @eof = false + @rbuffer = "" + @sync = @io.sync + end + + # + # for reading. + # + private + + def fill_rbuff + begin + @rbuffer << self.sysread(BLOCK_SIZE) + rescue Errno::EAGAIN + retry + rescue EOFError + @eof = true + end + end + + def consume_rbuff(size=nil) + if @rbuffer.empty? + nil + else + size = @rbuffer.size unless size + ret = @rbuffer[0, size] + @rbuffer[0, size] = "" + ret + end + end + + public + + def read(size=nil, buf=nil) + if size == 0 + if buf + buf.clear + else + buf = "" + end + return @eof ? nil : buf + end + until @eof + break if size && size <= @rbuffer.size + fill_rbuff + end + ret = consume_rbuff(size) || "" + if buf + buf.replace(ret) + ret = buf + end + (size && ret.empty?) ? nil : ret + end + + def readpartial(maxlen, buf=nil) + if maxlen == 0 + if buf + buf.clear + else + buf = "" + end + return @eof ? nil : buf + end + if @rbuffer.empty? + begin + return sysread(maxlen, buf) + rescue Errno::EAGAIN + retry + end + end + ret = consume_rbuff(maxlen) + if buf + buf.replace(ret) + ret = buf + end + raise EOFError if ret.empty? + ret + end + + def gets(eol=$/) + idx = @rbuffer.index(eol) + until @eof + break if idx + fill_rbuff + idx = @rbuffer.index(eol) + end + if eol.is_a?(Regexp) + size = idx ? idx+$&.size : nil + else + size = idx ? idx+eol.size : nil + end + consume_rbuff(size) + end + + def each(eol=$/) + while line = self.gets(eol) + yield line + end + end + alias each_line each + + def readlines(eol=$/) + ary = [] + while line = self.gets(eol) + ary << line + end + ary + end + + def readline(eol=$/) + raise EOFError if eof? + gets(eol) + end + + def getc + c = read(1) + c ? c[0] : nil + end + + def each_byte + while c = getc + yield(c) + end + end + + def readchar + raise EOFError if eof? + getc + end + + def ungetc(c) + @rbuffer[0,0] = c.chr + end + + def eof? + fill_rbuff if !@eof && @rbuffer.empty? + @eof && @rbuffer.empty? + end + alias eof eof? + + # + # for writing. + # + private + + def do_write(s) + @wbuffer = "" unless defined? @wbuffer + @wbuffer << s + @sync ||= false + if @sync or @wbuffer.size > BLOCK_SIZE or idx = @wbuffer.rindex($/) + remain = idx ? idx + $/.size : @wbuffer.length + nwritten = 0 + while remain > 0 + str = @wbuffer[nwritten,remain] + begin + nwrote = syswrite(str) + rescue Errno::EAGAIN + retry + end + remain -= nwrote + nwritten += nwrote + end + @wbuffer[0,nwritten] = "" + end + end + + public + + def write(s) + do_write(s) + s.length + end + + def << (s) + do_write(s) + self + end + + def puts(*args) + s = "" + if args.empty? + s << "\n" + end + args.each{|arg| + s << arg.to_s + if $/ && /\n\z/ !~ s + s << "\n" + end + } + do_write(s) + nil + end + + def print(*args) + s = "" + args.each{ |arg| s << arg.to_s } + do_write(s) + nil + end + + def printf(s, *args) + do_write(s % args) + nil + end + + def flush + osync = @sync + @sync = true + do_write "" + @sync = osync + end + + def close + flush rescue nil + sysclose + end +end +end diff --git a/lib/ruby/shared/jopenssl18/openssl/cipher.rb b/lib/ruby/shared/jopenssl18/openssl/cipher.rb new file mode 100644 index 00000000000..3e8307f2b9f --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/cipher.rb @@ -0,0 +1,28 @@ +=begin += $RCSfile$ -- Ruby-space predefined Cipher subclasses + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2002 Michal Rokos + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +## +# Should we care what if somebody require this file directly? +#require 'openssl' + +module OpenSSL + class Cipher + # This class is only provided for backwards compatibility. Use OpenSSL::Digest in the future. + class Cipher < Cipher + # add warning + end + end # Cipher +end # OpenSSL \ No newline at end of file diff --git a/lib/ruby/shared/jopenssl18/openssl/config.rb b/lib/ruby/shared/jopenssl18/openssl/config.rb new file mode 100644 index 00000000000..21c98b7b183 --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/config.rb @@ -0,0 +1,316 @@ +=begin += Ruby-space definitions that completes C-space funcs for Config + += Info + Copyright (C) 2010 Hiroshi Nakamura + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + +=end + +## +# Should we care what if somebody require this file directly? +#require 'openssl' +require 'stringio' + +module OpenSSL + class Config + include Enumerable + + class << self + def parse(str) + c = new() + parse_config(StringIO.new(str)).each do |section, hash| + c[section] = hash + end + c + end + + alias load new + + def parse_config(io) + begin + parse_config_lines(io) + rescue ConfigError => e + e.message.replace("error in line #{io.lineno}: " + e.message) + raise + end + end + + def get_key_string(data, section, key) # :nodoc: + if v = data[section] && data[section][key] + return v + elsif section == 'ENV' + if v = ENV[key] + return v + end + end + if v = data['default'] && data['default'][key] + return v + end + end + + private + + def parse_config_lines(io) + section = 'default' + data = {section => {}} + while definition = get_definition(io) + definition = clear_comments(definition) + next if definition.empty? + if definition[0] == ?[ + if /\[([^\]]*)\]/ =~ definition + section = $1.strip + data[section] ||= {} + else + raise ConfigError, "missing close square bracket" + end + else + if /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ =~ definition + if $2 + section = $1 + key = $2 + else + key = $1 + end + value = unescape_value(data, section, $3) + (data[section] ||= {})[key] = value.strip + else + raise ConfigError, "missing equal sign" + end + end + end + data + end + + # escape with backslash + QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/ + # escape with backslash and doubled dq + QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/ + # escaped char map + ESCAPE_MAP = { + "r" => "\r", + "n" => "\n", + "b" => "\b", + "t" => "\t", + } + + def unescape_value(data, section, value) + scanned = [] + while m = value.match(/['"\\$]/) + scanned << m.pre_match + c = m[0] + value = m.post_match + case c + when "'" + if m = value.match(QUOTE_REGEXP_SQ) + scanned << m[1].gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when '"' + if m = value.match(QUOTE_REGEXP_DQ) + scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when "\\" + c = value.slice!(0, 1) + scanned << (ESCAPE_MAP[c] || c) + when "$" + ref, value = extract_reference(value) + refsec = section + if ref.index('::') + refsec, ref = ref.split('::', 2) + end + if v = get_key_string(data, refsec, ref) + scanned << v + else + raise ConfigError, "variable has no value" + end + else + raise 'must not reaced' + end + end + scanned << value + scanned.join + end + + def extract_reference(value) + rest = '' + if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/) + value = m[1] || m[2] + rest = m.post_match + elsif [?(, ?{].include?(value[0]) + raise ConfigError, "no close brace" + end + if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/) + return m[0], m.post_match + rest + else + raise + end + end + + def clear_comments(line) + # FCOMMENT + if m = line.match(/\A([\t\n\f ]*);.*\z/) + return m[1] + end + # COMMENT + scanned = [] + while m = line.match(/[#'"\\]/) + scanned << m.pre_match + c = m[0] + line = m.post_match + case c + when '#' + line = nil + break + when "'", '"' + regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ + scanned << c + if m = line.match(regexp) + scanned << m[0] + line = m.post_match + else + scanned << line + line = nil + break + end + when "\\" + scanned << c + scanned << line.slice!(0, 1) + else + raise 'must not reaced' + end + end + scanned << line + scanned.join + end + + def get_definition(io) + if line = get_line(io) + while /[^\\]\\\z/ =~ line + if extra = get_line(io) + line += extra + else + break + end + end + return line.strip + end + end + + def get_line(io) + if line = io.gets + line.gsub(/[\r\n]*/, '') + end + end + end + + def initialize(filename = nil) + @data = {} + if filename + File.open(filename.to_s) do |file| + Config.parse_config(file).each do |section, hash| + self[section] = hash + end + end + end + end + + def get_value(section, key) + if section.nil? + raise TypeError.new('nil not allowed') + end + section = 'default' if section.empty? + get_key_string(section, key) + end + + def value(arg1, arg2 = nil) + warn('Config#value is deprecated; use Config#get_value') + if arg2.nil? + section, key = 'default', arg1 + else + section, key = arg1, arg2 + end + section ||= 'default' + section = 'default' if section.empty? + get_key_string(section, key) + end + + def add_value(section, key, value) + check_modify + (@data[section] ||= {})[key] = value + end + + def [](section) + @data[section] || {} + end + + def section(name) + warn('Config#section is deprecated; use Config#[]') + @data[name] || {} + end + + def []=(section, pairs) + check_modify + @data[section] ||= {} + pairs.each do |key, value| + self.add_value(section, key, value) + end + end + + def sections + @data.keys + end + + def to_s + ary = [] + @data.keys.sort.each do |section| + ary << "[ #{section} ]\n" + @data[section].keys.each do |key| + ary << "#{key}=#{@data[section][key]}\n" + end + ary << "\n" + end + ary.join + end + + def each + @data.each do |section, hash| + hash.each do |key, value| + yield(section, key, value) + end + end + end + + def inspect + "#<#{self.class.name} sections=#{sections.inspect}>" + end + + protected + + def data + @data + end + + private + + def initialize_copy(other) + @data = other.data.dup + end + + def check_modify + raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen? + end + + def get_key_string(section, key) + Config.get_key_string(@data, section, key) + end + end +end diff --git a/lib/ruby/shared/jopenssl18/openssl/digest.rb b/lib/ruby/shared/jopenssl18/openssl/digest.rb new file mode 100644 index 00000000000..122900866e7 --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/digest.rb @@ -0,0 +1,32 @@ +=begin += $RCSfile$ -- Ruby-space predefined Digest subclasses + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2002 Michal Rokos + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +## +# Should we care what if somebody require this file directly? +#require 'openssl' + +module OpenSSL + class Digest + # This class is only provided for backwards compatibility. Use OpenSSL::Digest in the future. + class Digest < Digest + def initialize(*args) + # add warning + super(*args) + end + end + end # Digest +end # OpenSSL + diff --git a/lib/ruby/shared/jopenssl18/openssl/pkcs7.rb b/lib/ruby/shared/jopenssl18/openssl/pkcs7.rb new file mode 100644 index 00000000000..1f88c1de5ec --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/pkcs7.rb @@ -0,0 +1,25 @@ +=begin += $RCSfile$ -- PKCS7 + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id: digest.rb 12148 2007-04-05 05:59:22Z technorama $ +=end + +module OpenSSL + class PKCS7 + # This class is only provided for backwards compatibility. Use OpenSSL::PKCS7 in the future. + class PKCS7 < PKCS7 + def initialize(*args) + super(*args) + + warn("Warning: OpenSSL::PKCS7::PKCS7 is deprecated after Ruby 1.9; use OpenSSL::PKCS7 instead") + end + end + + end # PKCS7 +end # OpenSSL + diff --git a/lib/ruby/shared/jopenssl18/openssl/ssl-internal.rb b/lib/ruby/shared/jopenssl18/openssl/ssl-internal.rb new file mode 100644 index 00000000000..95d8b5ddcb4 --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/ssl-internal.rb @@ -0,0 +1,155 @@ +=begin += $RCSfile$ -- Ruby-space definitions that completes C-space funcs for SSL + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU YUUZOU + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +require "openssl/buffering" +require "fcntl" + +module OpenSSL + module SSL + module SocketForwarder + def addr + to_io.addr + end + + def peeraddr + to_io.peeraddr + end + + def setsockopt(level, optname, optval) + to_io.setsockopt(level, optname, optval) + end + + def getsockopt(level, optname) + to_io.getsockopt(level, optname) + end + + def fcntl(*args) + to_io.fcntl(*args) + end + + def closed? + to_io.closed? + end + + def do_not_reverse_lookup=(flag) + to_io.do_not_reverse_lookup = flag + end + end + + module Nonblock + def initialize(*args) + flag = File::NONBLOCK + flag |= @io.fcntl(Fcntl::F_GETFL) if defined?(Fcntl::F_GETFL) + @io.fcntl(Fcntl::F_SETFL, flag) + super + end + end + + def verify_certificate_identity(cert, hostname) + should_verify_common_name = true + cert.extensions.each{|ext| + next if ext.oid != "subjectAltName" + ext.value.split(/,\s+/).each{|general_name| + if /\ADNS:(.*)/ =~ general_name + should_verify_common_name = false + reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + # NOTE: somehow we need the IP: canonical form + # seems there were failures elsewhere when not + # not sure how that's possible possible to-do! + elsif /\AIP(?: Address)?:(.*)/ =~ general_name + #elsif /\AIP Address:(.*)/ =~ general_name + should_verify_common_name = false + return true if $1 == hostname + end + } + } + if should_verify_common_name + cert.subject.to_a.each{|oid, value| + if oid == "CN" + reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + end + } + end + return false + end + module_function :verify_certificate_identity + + class SSLSocket + include Buffering + include SocketForwarder + include Nonblock + + def post_connection_check(hostname) + unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname) + raise SSLError, "hostname was not match with the server certificate" + end + return true + end + + def session + SSL::Session.new(self) + rescue SSL::Session::SessionError + nil + end + end + + class SSLServer + include SocketForwarder + attr_accessor :start_immediately + + def initialize(svr, ctx) + @svr = svr + @ctx = ctx + unless ctx.session_id_context + session_id = OpenSSL::Digest::MD5.hexdigest($0) + @ctx.session_id_context = session_id + end + @start_immediately = true + end + + def to_io + @svr + end + + def listen(backlog=5) + @svr.listen(backlog) + end + + def shutdown(how=Socket::SHUT_RDWR) + @svr.shutdown(how) + end + + def accept + sock = @svr.accept + begin + ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx) + ssl.sync_close = true + ssl.accept if @start_immediately + ssl + rescue SSLError => ex + sock.close + raise ex + end + end + + def close + @svr.close + end + end + end +end diff --git a/lib/ruby/shared/jopenssl18/openssl/ssl.rb b/lib/ruby/shared/jopenssl18/openssl/ssl.rb new file mode 100644 index 00000000000..3f17f5aa29b --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/ssl.rb @@ -0,0 +1 @@ +require 'openssl' diff --git a/lib/ruby/shared/jopenssl18/openssl/x509-internal.rb b/lib/ruby/shared/jopenssl18/openssl/x509-internal.rb new file mode 100644 index 00000000000..d1c30e8e36d --- /dev/null +++ b/lib/ruby/shared/jopenssl18/openssl/x509-internal.rb @@ -0,0 +1,110 @@ +=begin += $RCSfile$ -- Ruby-space definitions that completes C-space funcs for X509 and subclasses + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2002 Michal Rokos + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +module OpenSSL + module X509 + class Name + module RFC2253DN + Special = ',=+<>#;' + HexChar = /[0-9a-fA-F]/ + HexPair = /#{HexChar}#{HexChar}/ + HexString = /#{HexPair}+/ + Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/ + StringChar = /[^#{Special}\\"]/ + QuoteChar = /[^\\"]/ + AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/ + AttributeValue = / + (?!["#])((?:#{StringChar}|#{Pair})*)| + \#(#{HexString})| + "((?:#{QuoteChar}|#{Pair})*)" + /x + TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/ + + module_function + + def expand_pair(str) + return nil unless str + return str.gsub(Pair){ + pair = $& + case pair.size + when 2 then pair[1,1] + when 3 then Integer("0x#{pair[1,2]}").chr + else raise OpenSSL::X509::NameError, "invalid pair: #{str}" + end + } + end + + def expand_hexstring(str) + return nil unless str + der = str.gsub(HexPair){$&.to_i(16).chr } + a1 = OpenSSL::ASN1.decode(der) + return a1.value, a1.tag + end + + def expand_value(str1, str2, str3) + value = expand_pair(str1) + value, tag = expand_hexstring(str2) unless value + value = expand_pair(str3) unless value + return value, tag + end + + def scan(dn) + str = dn + ary = [] + while true + if md = TypeAndValue.match(str) + matched = md.to_s + remain = md.post_match + type = md[1] + value, tag = expand_value(md[2], md[3], md[4]) rescue nil + if value + type_and_value = [type, value] + type_and_value.push(tag) if tag + ary.unshift(type_and_value) + if remain.length > 2 && remain[0] == ?, + str = remain[1..-1] + next + elsif remain.length > 2 && remain[0] == ?+ + raise OpenSSL::X509::NameError, + "multi-valued RDN is not supported: #{dn}" + elsif remain.empty? + break + end + end + end + msg_dn = dn[0, dn.length - str.length] + " =>" + str + raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}" + end + return ary + end + end + + class <= 0 + size = [size, limit].min + end + consume_rbuff(size) + end + + ## + # Executes the block for every line in the stream where lines are separated + # by +eol+. + # + # See also #gets + + def each(eol=$/) + while line = self.gets(eol) + yield line + end + end + alias each_line each + + ## + # Reads lines from the stream which are separated by +eol+. + # + # See also #gets + + def readlines(eol=$/) + ary = [] + while line = self.gets(eol) + ary << line + end + ary + end + + ## + # Reads a line from the stream which is separated by +eol+. + # + # Raises EOFError if at end of file. + + def readline(eol=$/) + raise EOFError if eof? + gets(eol) + end + + ## + # Reads one character from the stream. Returns nil if called at end of + # file. + + def getc + read(1) + end + + ## + # Calls the given block once for each byte in the stream. + + def each_byte # :yields: byte + while c = getc + yield(c.ord) + end + end + + ## + # Reads a one-character string from the stream. Raises an EOFError at end + # of file. + + def readchar + raise EOFError if eof? + getc + end + + ## + # Pushes character +c+ back onto the stream such that a subsequent buffered + # character read will return it. + # + # Unlike IO#getc multiple bytes may be pushed back onto the stream. + # + # Has no effect on unbuffered reads (such as #sysread). + + def ungetc(c) + @rbuffer[0,0] = c.chr + end + + ## + # Returns true if the stream is at file which means there is no more data to + # be read. + + def eof? + fill_rbuff if !@eof && @rbuffer.empty? + @eof && @rbuffer.empty? + end + alias eof eof? + + # + # for writing. + # + private + + ## + # Writes +s+ to the buffer. When the buffer is full or #sync is true the + # buffer is flushed to the underlying socket. + + def do_write(s) + @wbuffer = "" unless defined? @wbuffer + @wbuffer << s + @wbuffer.force_encoding(Encoding::BINARY) + @sync ||= false + if @sync or @wbuffer.size > BLOCK_SIZE or idx = @wbuffer.rindex($/) + remain = idx ? idx + $/.size : @wbuffer.length + nwritten = 0 + while remain > 0 + str = @wbuffer[nwritten,remain] + begin + nwrote = syswrite(str) + rescue Errno::EAGAIN + retry + end + remain -= nwrote + nwritten += nwrote + end + @wbuffer[0,nwritten] = "" + end + end + + public + + ## + # Writes +s+ to the stream. If the argument is not a string it will be + # converted using String#to_s. Returns the number of bytes written. + + def write(s) + do_write(s) + s.bytesize + end + + ## + # Writes +str+ in the non-blocking manner. + # + # If there is buffered data, it is flushed first. This may block. + # + # write_nonblock returns number of bytes written to the SSL connection. + # + # When no data can be written without blocking it raises + # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable. + # + # IO::WaitReadable means SSL needs to read internally so write_nonblock + # should be called again after the underlying IO is readable. + # + # IO::WaitWritable means SSL needs to write internally so write_nonblock + # should be called again after underlying IO is writable. + # + # So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows. + # + # # emulates blocking write. + # begin + # result = ssl.write_nonblock(str) + # rescue IO::WaitReadable + # IO.select([io]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [io]) + # retry + # end + # + # Note that one reason that write_nonblock reads from the underlying IO + # is when the peer requests a new TLS/SSL handshake. See the openssl FAQ + # for more details. http://www.openssl.org/support/faq.html + + def write_nonblock(s) + flush + syswrite_nonblock(s) + end + + ## + # Writes +s+ to the stream. +s+ will be converted to a String using + # String#to_s. + + def << (s) + do_write(s) + self + end + + ## + # Writes +args+ to the stream along with a record separator. + # + # See IO#puts for full details. + + def puts(*args) + s = "" + if args.empty? + s << "\n" + end + args.each{|arg| + s << arg.to_s + if $/ && /\n\z/ !~ s + s << "\n" + end + } + do_write(s) + nil + end + + ## + # Writes +args+ to the stream. + # + # See IO#print for full details. + + def print(*args) + s = "" + args.each{ |arg| s << arg.to_s } + do_write(s) + nil + end + + ## + # Formats and writes to the stream converting parameters under control of + # the format string. + # + # See Kernel#sprintf for format string details. + + def printf(s, *args) + do_write(s % args) + nil + end + + ## + # Flushes buffered data to the SSLSocket. + + def flush + osync = @sync + @sync = true + do_write "" + return self + ensure + @sync = osync + end + + ## + # Closes the SSLSocket and flushes any unwritten data. + + def close + flush rescue nil + sysclose + end +end diff --git a/lib/ruby/shared/jopenssl19/openssl/cipher.rb b/lib/ruby/shared/jopenssl19/openssl/cipher.rb new file mode 100644 index 00000000000..b254a238c22 --- /dev/null +++ b/lib/ruby/shared/jopenssl19/openssl/cipher.rb @@ -0,0 +1,28 @@ +#-- +# +# $RCSfile$ +# +# = Ruby-space predefined Cipher subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licenced under the same licence as Ruby. +# (See the file 'LICENCE'.) +# +# = Version +# $Id$ +# +#++ + +module OpenSSL + class Cipher + # This class is only provided for backwards compatibility. Use OpenSSL::Cipher in the future. + class Cipher < Cipher + # add warning + end + end # Cipher +end # OpenSSL \ No newline at end of file diff --git a/lib/ruby/shared/jopenssl19/openssl/config.rb b/lib/ruby/shared/jopenssl19/openssl/config.rb new file mode 100644 index 00000000000..24a54c91eca --- /dev/null +++ b/lib/ruby/shared/jopenssl19/openssl/config.rb @@ -0,0 +1,313 @@ +=begin += Ruby-space definitions that completes C-space funcs for Config + += Info + Copyright (C) 2010 Hiroshi Nakamura + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + +=end + +require 'stringio' + +module OpenSSL + class Config + include Enumerable + + class << self + def parse(str) + c = new() + parse_config(StringIO.new(str)).each do |section, hash| + c[section] = hash + end + c + end + + alias load new + + def parse_config(io) + begin + parse_config_lines(io) + rescue ConfigError => e + e.message.replace("error in line #{io.lineno}: " + e.message) + raise + end + end + + def get_key_string(data, section, key) # :nodoc: + if v = data[section] && data[section][key] + return v + elsif section == 'ENV' + if v = ENV[key] + return v + end + end + if v = data['default'] && data['default'][key] + return v + end + end + + private + + def parse_config_lines(io) + section = 'default' + data = {section => {}} + while definition = get_definition(io) + definition = clear_comments(definition) + next if definition.empty? + if definition[0] == ?[ + if /\[([^\]]*)\]/ =~ definition + section = $1.strip + data[section] ||= {} + else + raise ConfigError, "missing close square bracket" + end + else + if /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ =~ definition + if $2 + section = $1 + key = $2 + else + key = $1 + end + value = unescape_value(data, section, $3) + (data[section] ||= {})[key] = value.strip + else + raise ConfigError, "missing equal sign" + end + end + end + data + end + + # escape with backslash + QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/ + # escape with backslash and doubled dq + QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/ + # escaped char map + ESCAPE_MAP = { + "r" => "\r", + "n" => "\n", + "b" => "\b", + "t" => "\t", + } + + def unescape_value(data, section, value) + scanned = [] + while m = value.match(/['"\\$]/) + scanned << m.pre_match + c = m[0] + value = m.post_match + case c + when "'" + if m = value.match(QUOTE_REGEXP_SQ) + scanned << m[1].gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when '"' + if m = value.match(QUOTE_REGEXP_DQ) + scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when "\\" + c = value.slice!(0, 1) + scanned << (ESCAPE_MAP[c] || c) + when "$" + ref, value = extract_reference(value) + refsec = section + if ref.index('::') + refsec, ref = ref.split('::', 2) + end + if v = get_key_string(data, refsec, ref) + scanned << v + else + raise ConfigError, "variable has no value" + end + else + raise 'must not reaced' + end + end + scanned << value + scanned.join + end + + def extract_reference(value) + rest = '' + if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/) + value = m[1] || m[2] + rest = m.post_match + elsif [?(, ?{].include?(value[0]) + raise ConfigError, "no close brace" + end + if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/) + return m[0], m.post_match + rest + else + raise + end + end + + def clear_comments(line) + # FCOMMENT + if m = line.match(/\A([\t\n\f ]*);.*\z/) + return m[1] + end + # COMMENT + scanned = [] + while m = line.match(/[#'"\\]/) + scanned << m.pre_match + c = m[0] + line = m.post_match + case c + when '#' + line = nil + break + when "'", '"' + regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ + scanned << c + if m = line.match(regexp) + scanned << m[0] + line = m.post_match + else + scanned << line + line = nil + break + end + when "\\" + scanned << c + scanned << line.slice!(0, 1) + else + raise 'must not reaced' + end + end + scanned << line + scanned.join + end + + def get_definition(io) + if line = get_line(io) + while /[^\\]\\\z/ =~ line + if extra = get_line(io) + line += extra + else + break + end + end + return line.strip + end + end + + def get_line(io) + if line = io.gets + line.gsub(/[\r\n]*/, '') + end + end + end + + def initialize(filename = nil) + @data = {} + if filename + File.open(filename.to_s) do |file| + Config.parse_config(file).each do |section, hash| + self[section] = hash + end + end + end + end + + def get_value(section, key) + if section.nil? + raise TypeError.new('nil not allowed') + end + section = 'default' if section.empty? + get_key_string(section, key) + end + + def value(arg1, arg2 = nil) + warn('Config#value is deprecated; use Config#get_value') + if arg2.nil? + section, key = 'default', arg1 + else + section, key = arg1, arg2 + end + section ||= 'default' + section = 'default' if section.empty? + get_key_string(section, key) + end + + def add_value(section, key, value) + check_modify + (@data[section] ||= {})[key] = value + end + + def [](section) + @data[section] || {} + end + + def section(name) + warn('Config#section is deprecated; use Config#[]') + @data[name] || {} + end + + def []=(section, pairs) + check_modify + @data[section] ||= {} + pairs.each do |key, value| + self.add_value(section, key, value) + end + end + + def sections + @data.keys + end + + def to_s + ary = [] + @data.keys.sort.each do |section| + ary << "[ #{section} ]\n" + @data[section].keys.each do |key| + ary << "#{key}=#{@data[section][key]}\n" + end + ary << "\n" + end + ary.join + end + + def each + @data.each do |section, hash| + hash.each do |key, value| + yield [section, key, value] + end + end + end + + def inspect + "#<#{self.class.name} sections=#{sections.inspect}>" + end + + protected + + def data + @data + end + + private + + def initialize_copy(other) + @data = other.data.dup + end + + def check_modify + raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen? + end + + def get_key_string(section, key) + Config.get_key_string(@data, section, key) + end + end +end diff --git a/lib/ruby/shared/jopenssl19/openssl/digest.rb b/lib/ruby/shared/jopenssl19/openssl/digest.rb new file mode 100644 index 00000000000..d62993b285e --- /dev/null +++ b/lib/ruby/shared/jopenssl19/openssl/digest.rb @@ -0,0 +1,32 @@ +#-- +# +# $RCSfile$ +# +# = Ruby-space predefined Digest subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licenced under the same licence as Ruby. +# (See the file 'LICENCE'.) +# +# = Version +# $Id$ +# +#++ + +module OpenSSL + class Digest + # This class is only provided for backwards compatibility. Use OpenSSL::Digest in the future. + class Digest < Digest + def initialize(*args) + # add warning + super(*args) + end + end + end # Digest +end # OpenSSL + diff --git a/lib/ruby/shared/jopenssl19/openssl/ssl-internal.rb b/lib/ruby/shared/jopenssl19/openssl/ssl-internal.rb new file mode 100644 index 00000000000..6fb8eaed052 --- /dev/null +++ b/lib/ruby/shared/jopenssl19/openssl/ssl-internal.rb @@ -0,0 +1,155 @@ +=begin += $RCSfile$ -- Ruby-space definitions that completes C-space funcs for SSL + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU YUUZOU + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +require "openssl/buffering" +require "fcntl" + +module OpenSSL + module SSL + module SocketForwarder + def addr + to_io.addr + end + + def peeraddr + to_io.peeraddr + end + + def setsockopt(level, optname, optval) + to_io.setsockopt(level, optname, optval) + end + + def getsockopt(level, optname) + to_io.getsockopt(level, optname) + end + + def fcntl(*args) + to_io.fcntl(*args) + end + + def closed? + to_io.closed? + end + + def do_not_reverse_lookup=(flag) + to_io.do_not_reverse_lookup = flag + end + end + + module Nonblock + def initialize(*args) + flag = File::NONBLOCK + flag |= @io.fcntl(Fcntl::F_GETFL) if defined?(Fcntl::F_GETFL) + @io.fcntl(Fcntl::F_SETFL, flag) + super + end + end + + def verify_certificate_identity(cert, hostname) + should_verify_common_name = true + cert.extensions.each{|ext| + next if ext.oid != "subjectAltName" + ext.value.split(/,\s+/).each{|general_name| + if /\ADNS:(.*)/ =~ general_name + should_verify_common_name = false + reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + # NOTE: somehow we need the IP: canonical form + # seems there were failures elsewhere when not + # not sure how that's possible possible to-do! + elsif /\AIP(?: Address)?:(.*)/ =~ general_name + #elsif /\AIP Address:(.*)/ =~ general_name + should_verify_common_name = false + return true if $1 == hostname + end + } + } + if should_verify_common_name + cert.subject.to_a.each{|oid, value| + if oid == "CN" + reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + end + } + end + return false + end + module_function :verify_certificate_identity + + class SSLSocket + include Buffering + include SocketForwarder + include Nonblock + + def post_connection_check(hostname) + unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname) + raise SSLError, "hostname does not match the server certificate" + end + return true + end + + def session + SSL::Session.new(self) + rescue SSL::Session::SessionError + nil + end + end + + class SSLServer + include SocketForwarder + attr_accessor :start_immediately + + def initialize(svr, ctx) + @svr = svr + @ctx = ctx + unless ctx.session_id_context + session_id = OpenSSL::Digest::MD5.hexdigest($0) + @ctx.session_id_context = session_id + end + @start_immediately = true + end + + def to_io + @svr + end + + def listen(backlog=5) + @svr.listen(backlog) + end + + def shutdown(how=Socket::SHUT_RDWR) + @svr.shutdown(how) + end + + def accept + sock = @svr.accept + begin + ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx) + ssl.sync_close = true + ssl.accept if @start_immediately + ssl + rescue SSLError => ex + sock.close + raise ex + end + end + + def close + @svr.close + end + end + end +end diff --git a/lib/ruby/shared/jopenssl19/openssl/ssl.rb b/lib/ruby/shared/jopenssl19/openssl/ssl.rb new file mode 100644 index 00000000000..15f42d60918 --- /dev/null +++ b/lib/ruby/shared/jopenssl19/openssl/ssl.rb @@ -0,0 +1,2 @@ +warn 'deprecated openssl/ssl use: require "openssl" instead of "openssl/ssl"' +require 'openssl' diff --git a/lib/ruby/shared/jopenssl19/openssl/x509-internal.rb b/lib/ruby/shared/jopenssl19/openssl/x509-internal.rb new file mode 100644 index 00000000000..24eeff7d875 --- /dev/null +++ b/lib/ruby/shared/jopenssl19/openssl/x509-internal.rb @@ -0,0 +1,115 @@ +=begin += $RCSfile$ -- Ruby-space definitions that completes C-space funcs for X509 and subclasses + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2002 Michal Rokos + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +module OpenSSL + module X509 + class Name + module RFC2253DN + Special = ',=+<>#;' + HexChar = /[0-9a-fA-F]/ + HexPair = /#{HexChar}#{HexChar}/ + HexString = /#{HexPair}+/ + Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/ + StringChar = /[^#{Special}\\"]/ + QuoteChar = /[^\\"]/ + AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/ + AttributeValue = / + (?!["#])((?:#{StringChar}|#{Pair})*)| + \#(#{HexString})| + "((?:#{QuoteChar}|#{Pair})*)" + /x + TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/ + + module_function + + def expand_pair(str) + return nil unless str + return str.gsub(Pair){ + pair = $& + case pair.size + when 2 then pair[1,1] + when 3 then Integer("0x#{pair[1,2]}").chr + else raise OpenSSL::X509::NameError, "invalid pair: #{str}" + end + } + end + + def expand_hexstring(str) + return nil unless str + der = str.gsub(HexPair){$&.to_i(16).chr } + a1 = OpenSSL::ASN1.decode(der) + return a1.value, a1.tag + end + + def expand_value(str1, str2, str3) + value = expand_pair(str1) + value, tag = expand_hexstring(str2) unless value + value = expand_pair(str3) unless value + return value, tag + end + + def scan(dn) + str = dn + ary = [] + while true + if md = TypeAndValue.match(str) + remain = md.post_match + type = md[1] + value, tag = expand_value(md[2], md[3], md[4]) rescue nil + if value + type_and_value = [type, value] + type_and_value.push(tag) if tag + ary.unshift(type_and_value) + if remain.length > 2 && remain[0] == ?, + str = remain[1..-1] + next + elsif remain.length > 2 && remain[0] == ?+ + raise OpenSSL::X509::NameError, + "multi-valued RDN is not supported: #{dn}" + elsif remain.empty? + break + end + end + end + msg_dn = dn[0, dn.length - str.length] + " =>" + str + raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}" + end + return ary + end + end + + class << self + def parse_rfc2253(str, template=OBJECT_TYPE_TEMPLATE) + ary = OpenSSL::X509::Name::RFC2253DN.scan(str) + self.new(ary, template) + end + + def parse_openssl(str, template=OBJECT_TYPE_TEMPLATE) + ary = str.scan(/\s*([^\/,]+)\s*/).collect{|i| i[0].split("=", 2) } + self.new(ary, template) + end + + alias parse parse_openssl + end + end + + class StoreContext + def cleanup + warn "(#{caller.first}) OpenSSL::X509::StoreContext#cleanup is deprecated with no replacement" if $VERBOSE + end + end + end +end diff --git a/lib/ruby/shared/jopenssl19/openssl/x509.rb b/lib/ruby/shared/jopenssl19/openssl/x509.rb new file mode 100644 index 00000000000..f1777cdf061 --- /dev/null +++ b/lib/ruby/shared/jopenssl19/openssl/x509.rb @@ -0,0 +1,2 @@ +warn 'deprecated openssl/x509 use: require "openssl" instead of "openssl/x509"' +require 'openssl' diff --git a/lib/ruby/shared/jopenssl21/openssl.rb b/lib/ruby/shared/jopenssl21/openssl.rb new file mode 100644 index 00000000000..b1d7406c4ca --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl.rb @@ -0,0 +1,22 @@ +=begin += $RCSfile$ -- Loader for all OpenSSL C-space and Ruby-space definitions + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2002 Michal Rokos + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +require 'openssl/bn' +require 'openssl/cipher' +require 'openssl/config' +require 'openssl/digest' +require 'openssl/x509' +require 'openssl/ssl' diff --git a/lib/ruby/shared/jopenssl21/openssl/bn.rb b/lib/ruby/shared/jopenssl21/openssl/bn.rb new file mode 100644 index 00000000000..3933ee3cedc --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl/bn.rb @@ -0,0 +1,29 @@ +#-- +# +# $RCSfile$ +# +# = Ruby-space definitions that completes C-space funcs for BN +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licenced under the same licence as Ruby. +# (See the file 'LICENCE'.) +# +# = Version +# $Id$ +# +#++ + +## +# Add double dispatch to Integer +# +class Integer + def to_bn + OpenSSL::BN::new(self) + end +end # Integer + diff --git a/lib/ruby/shared/jopenssl21/openssl/buffering.rb b/lib/ruby/shared/jopenssl21/openssl/buffering.rb new file mode 100644 index 00000000000..e40dfee667b --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl/buffering.rb @@ -0,0 +1,449 @@ +=begin += $RCSfile$ -- Buffering mix-in module. + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU YUUZOU + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +## +# OpenSSL IO buffering mix-in module. +# +# This module allows an OpenSSL::SSL::SSLSocket to behave like an IO. + +module OpenSSL::Buffering + include Enumerable + + ## + # The "sync mode" of the SSLSocket. + # + # See IO#sync for full details. + + attr_accessor :sync + + ## + # Default size to read from or write to the SSLSocket for buffer operations. + + BLOCK_SIZE = 1024*16 + + def initialize(*args) + @eof = false + @rbuffer = "" + @sync = @io.sync + end + + # + # for reading. + # + private + + ## + # Fills the buffer from the underlying SSLSocket + + def fill_rbuff + begin + @rbuffer << self.sysread(BLOCK_SIZE) + rescue Errno::EAGAIN + retry + rescue EOFError + @eof = true + end + end + + ## + # Consumes +size+ bytes from the buffer + + def consume_rbuff(size=nil) + if @rbuffer.empty? + nil + else + size = @rbuffer.size unless size + ret = @rbuffer[0, size] + @rbuffer[0, size] = "" + ret + end + end + + public + + ## + # Reads +size+ bytes from the stream. If +buf+ is provided it must + # reference a string which will receive the data. + # + # See IO#read for full details. + + def read(size=nil, buf=nil) + if size == 0 + if buf + buf.clear + return buf + else + return "" + end + end + until @eof + break if size && size <= @rbuffer.size + fill_rbuff + end + ret = consume_rbuff(size) || "" + if buf + buf.replace(ret) + ret = buf + end + (size && ret.empty?) ? nil : ret + end + + ## + # Reads at most +maxlen+ bytes from the stream. If +buf+ is provided it + # must reference a string which will receive the data. + # + # See IO#readpartial for full details. + + def readpartial(maxlen, buf=nil) + if maxlen == 0 + if buf + buf.clear + return buf + else + return "" + end + end + if @rbuffer.empty? + begin + return sysread(maxlen, buf) + rescue Errno::EAGAIN + retry + end + end + ret = consume_rbuff(maxlen) + if buf + buf.replace(ret) + ret = buf + end + raise EOFError if ret.empty? + ret + end + + ## + # Reads at most +maxlen+ bytes in the non-blocking manner. + # + # When no data can be read without blocking it raises + # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable. + # + # IO::WaitReadable means SSL needs to read internally so read_nonblock + # should be called again when the underlying IO is readable. + # + # IO::WaitWritable means SSL needs to write internally so read_nonblock + # should be called again after the underlying IO is writable. + # + # OpenSSL::Buffering#read_nonblock needs two rescue clause as follows: + # + # # emulates blocking read (readpartial). + # begin + # result = ssl.read_nonblock(maxlen) + # rescue IO::WaitReadable + # IO.select([io]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [io]) + # retry + # end + # + # Note that one reason that read_nonblock writes to the underlying IO is + # when the peer requests a new TLS/SSL handshake. See openssl the FAQ for + # more details. http://www.openssl.org/support/faq.html + + def read_nonblock(maxlen, buf=nil, exception: true) + if maxlen == 0 + if buf + buf.clear + return buf + else + return "" + end + end + if @rbuffer.empty? + return sysread_nonblock(maxlen, buf, exception: exception) + end + ret = consume_rbuff(maxlen) + if buf + buf.replace(ret) + ret = buf + end + raise EOFError if ret.empty? + ret + end + + ## + # Reads the next "line+ from the stream. Lines are separated by +eol+. If + # +limit+ is provided the result will not be longer than the given number of + # bytes. + # + # +eol+ may be a String or Regexp. + # + # Unlike IO#gets the line read will not be assigned to +$_+. + # + # Unlike IO#gets the separator must be provided if a limit is provided. + + def gets(eol=$/, limit=nil) + idx = @rbuffer.index(eol) + until @eof + break if idx + fill_rbuff + idx = @rbuffer.index(eol) + end + if eol.is_a?(Regexp) + size = idx ? idx+$&.size : nil + else + size = idx ? idx+eol.size : nil + end + if limit and limit >= 0 + size = [size, limit].min + end + consume_rbuff(size) + end + + ## + # Executes the block for every line in the stream where lines are separated + # by +eol+. + # + # See also #gets + + def each(eol=$/) + while line = self.gets(eol) + yield line + end + end + alias each_line each + + ## + # Reads lines from the stream which are separated by +eol+. + # + # See also #gets + + def readlines(eol=$/) + ary = [] + while line = self.gets(eol) + ary << line + end + ary + end + + ## + # Reads a line from the stream which is separated by +eol+. + # + # Raises EOFError if at end of file. + + def readline(eol=$/) + raise EOFError if eof? + gets(eol) + end + + ## + # Reads one character from the stream. Returns nil if called at end of + # file. + + def getc + read(1) + end + + ## + # Calls the given block once for each byte in the stream. + + def each_byte # :yields: byte + while c = getc + yield(c.ord) + end + end + + ## + # Reads a one-character string from the stream. Raises an EOFError at end + # of file. + + def readchar + raise EOFError if eof? + getc + end + + ## + # Pushes character +c+ back onto the stream such that a subsequent buffered + # character read will return it. + # + # Unlike IO#getc multiple bytes may be pushed back onto the stream. + # + # Has no effect on unbuffered reads (such as #sysread). + + def ungetc(c) + @rbuffer[0,0] = c.chr + end + + ## + # Returns true if the stream is at file which means there is no more data to + # be read. + + def eof? + fill_rbuff if !@eof && @rbuffer.empty? + @eof && @rbuffer.empty? + end + alias eof eof? + + # + # for writing. + # + private + + ## + # Writes +s+ to the buffer. When the buffer is full or #sync is true the + # buffer is flushed to the underlying socket. + + def do_write(s) + @wbuffer = "" unless defined? @wbuffer + @wbuffer << s + @wbuffer.force_encoding(Encoding::BINARY) + @sync ||= false + if @sync or @wbuffer.size > BLOCK_SIZE or idx = @wbuffer.rindex($/) + remain = idx ? idx + $/.size : @wbuffer.length + nwritten = 0 + while remain > 0 + str = @wbuffer[nwritten,remain] + begin + nwrote = syswrite(str) + rescue Errno::EAGAIN + retry + end + remain -= nwrote + nwritten += nwrote + end + @wbuffer[0,nwritten] = "" + end + end + + public + + ## + # Writes +s+ to the stream. If the argument is not a string it will be + # converted using String#to_s. Returns the number of bytes written. + + def write(s) + do_write(s) + s.bytesize + end + + ## + # Writes +str+ in the non-blocking manner. + # + # If there is buffered data, it is flushed first. This may block. + # + # write_nonblock returns number of bytes written to the SSL connection. + # + # When no data can be written without blocking it raises + # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable. + # + # IO::WaitReadable means SSL needs to read internally so write_nonblock + # should be called again after the underlying IO is readable. + # + # IO::WaitWritable means SSL needs to write internally so write_nonblock + # should be called again after underlying IO is writable. + # + # So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows. + # + # # emulates blocking write. + # begin + # result = ssl.write_nonblock(str) + # rescue IO::WaitReadable + # IO.select([io]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [io]) + # retry + # end + # + # Note that one reason that write_nonblock reads from the underlying IO + # is when the peer requests a new TLS/SSL handshake. See the openssl FAQ + # for more details. http://www.openssl.org/support/faq.html + + def write_nonblock(s, exception: true) + flush + syswrite_nonblock(s, exception: exception) + end + + ## + # Writes +s+ to the stream. +s+ will be converted to a String using + # String#to_s. + + def << (s) + do_write(s) + self + end + + ## + # Writes +args+ to the stream along with a record separator. + # + # See IO#puts for full details. + + def puts(*args) + s = "" + if args.empty? + s << "\n" + end + args.each{|arg| + s << arg.to_s + if $/ && /\n\z/ !~ s + s << "\n" + end + } + do_write(s) + nil + end + + ## + # Writes +args+ to the stream. + # + # See IO#print for full details. + + def print(*args) + s = "" + args.each{ |arg| s << arg.to_s } + do_write(s) + nil + end + + ## + # Formats and writes to the stream converting parameters under control of + # the format string. + # + # See Kernel#sprintf for format string details. + + def printf(s, *args) + do_write(s % args) + nil + end + + ## + # Flushes buffered data to the SSLSocket. + + def flush + osync = @sync + @sync = true + do_write "" + return self + ensure + @sync = osync + end + + ## + # Closes the SSLSocket and flushes any unwritten data. + + def close + flush rescue nil + sysclose + end +end diff --git a/lib/ruby/shared/jopenssl21/openssl/cipher.rb b/lib/ruby/shared/jopenssl21/openssl/cipher.rb new file mode 100644 index 00000000000..b254a238c22 --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl/cipher.rb @@ -0,0 +1,28 @@ +#-- +# +# $RCSfile$ +# +# = Ruby-space predefined Cipher subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licenced under the same licence as Ruby. +# (See the file 'LICENCE'.) +# +# = Version +# $Id$ +# +#++ + +module OpenSSL + class Cipher + # This class is only provided for backwards compatibility. Use OpenSSL::Cipher in the future. + class Cipher < Cipher + # add warning + end + end # Cipher +end # OpenSSL \ No newline at end of file diff --git a/lib/ruby/shared/jopenssl21/openssl/config.rb b/lib/ruby/shared/jopenssl21/openssl/config.rb new file mode 100644 index 00000000000..24a54c91eca --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl/config.rb @@ -0,0 +1,313 @@ +=begin += Ruby-space definitions that completes C-space funcs for Config + += Info + Copyright (C) 2010 Hiroshi Nakamura + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + +=end + +require 'stringio' + +module OpenSSL + class Config + include Enumerable + + class << self + def parse(str) + c = new() + parse_config(StringIO.new(str)).each do |section, hash| + c[section] = hash + end + c + end + + alias load new + + def parse_config(io) + begin + parse_config_lines(io) + rescue ConfigError => e + e.message.replace("error in line #{io.lineno}: " + e.message) + raise + end + end + + def get_key_string(data, section, key) # :nodoc: + if v = data[section] && data[section][key] + return v + elsif section == 'ENV' + if v = ENV[key] + return v + end + end + if v = data['default'] && data['default'][key] + return v + end + end + + private + + def parse_config_lines(io) + section = 'default' + data = {section => {}} + while definition = get_definition(io) + definition = clear_comments(definition) + next if definition.empty? + if definition[0] == ?[ + if /\[([^\]]*)\]/ =~ definition + section = $1.strip + data[section] ||= {} + else + raise ConfigError, "missing close square bracket" + end + else + if /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ =~ definition + if $2 + section = $1 + key = $2 + else + key = $1 + end + value = unescape_value(data, section, $3) + (data[section] ||= {})[key] = value.strip + else + raise ConfigError, "missing equal sign" + end + end + end + data + end + + # escape with backslash + QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/ + # escape with backslash and doubled dq + QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/ + # escaped char map + ESCAPE_MAP = { + "r" => "\r", + "n" => "\n", + "b" => "\b", + "t" => "\t", + } + + def unescape_value(data, section, value) + scanned = [] + while m = value.match(/['"\\$]/) + scanned << m.pre_match + c = m[0] + value = m.post_match + case c + when "'" + if m = value.match(QUOTE_REGEXP_SQ) + scanned << m[1].gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when '"' + if m = value.match(QUOTE_REGEXP_DQ) + scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when "\\" + c = value.slice!(0, 1) + scanned << (ESCAPE_MAP[c] || c) + when "$" + ref, value = extract_reference(value) + refsec = section + if ref.index('::') + refsec, ref = ref.split('::', 2) + end + if v = get_key_string(data, refsec, ref) + scanned << v + else + raise ConfigError, "variable has no value" + end + else + raise 'must not reaced' + end + end + scanned << value + scanned.join + end + + def extract_reference(value) + rest = '' + if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/) + value = m[1] || m[2] + rest = m.post_match + elsif [?(, ?{].include?(value[0]) + raise ConfigError, "no close brace" + end + if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/) + return m[0], m.post_match + rest + else + raise + end + end + + def clear_comments(line) + # FCOMMENT + if m = line.match(/\A([\t\n\f ]*);.*\z/) + return m[1] + end + # COMMENT + scanned = [] + while m = line.match(/[#'"\\]/) + scanned << m.pre_match + c = m[0] + line = m.post_match + case c + when '#' + line = nil + break + when "'", '"' + regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ + scanned << c + if m = line.match(regexp) + scanned << m[0] + line = m.post_match + else + scanned << line + line = nil + break + end + when "\\" + scanned << c + scanned << line.slice!(0, 1) + else + raise 'must not reaced' + end + end + scanned << line + scanned.join + end + + def get_definition(io) + if line = get_line(io) + while /[^\\]\\\z/ =~ line + if extra = get_line(io) + line += extra + else + break + end + end + return line.strip + end + end + + def get_line(io) + if line = io.gets + line.gsub(/[\r\n]*/, '') + end + end + end + + def initialize(filename = nil) + @data = {} + if filename + File.open(filename.to_s) do |file| + Config.parse_config(file).each do |section, hash| + self[section] = hash + end + end + end + end + + def get_value(section, key) + if section.nil? + raise TypeError.new('nil not allowed') + end + section = 'default' if section.empty? + get_key_string(section, key) + end + + def value(arg1, arg2 = nil) + warn('Config#value is deprecated; use Config#get_value') + if arg2.nil? + section, key = 'default', arg1 + else + section, key = arg1, arg2 + end + section ||= 'default' + section = 'default' if section.empty? + get_key_string(section, key) + end + + def add_value(section, key, value) + check_modify + (@data[section] ||= {})[key] = value + end + + def [](section) + @data[section] || {} + end + + def section(name) + warn('Config#section is deprecated; use Config#[]') + @data[name] || {} + end + + def []=(section, pairs) + check_modify + @data[section] ||= {} + pairs.each do |key, value| + self.add_value(section, key, value) + end + end + + def sections + @data.keys + end + + def to_s + ary = [] + @data.keys.sort.each do |section| + ary << "[ #{section} ]\n" + @data[section].keys.each do |key| + ary << "#{key}=#{@data[section][key]}\n" + end + ary << "\n" + end + ary.join + end + + def each + @data.each do |section, hash| + hash.each do |key, value| + yield [section, key, value] + end + end + end + + def inspect + "#<#{self.class.name} sections=#{sections.inspect}>" + end + + protected + + def data + @data + end + + private + + def initialize_copy(other) + @data = other.data.dup + end + + def check_modify + raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen? + end + + def get_key_string(section, key) + Config.get_key_string(@data, section, key) + end + end +end diff --git a/lib/ruby/shared/jopenssl21/openssl/digest.rb b/lib/ruby/shared/jopenssl21/openssl/digest.rb new file mode 100644 index 00000000000..615aa798276 --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl/digest.rb @@ -0,0 +1,49 @@ +#-- +# +# $RCSfile$ +# +# = Ruby-space predefined Digest subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licenced under the same licence as Ruby. +# (See the file 'LICENCE'.) +# +# = Version +# $Id$ +# +#++ + +module OpenSSL + class Digest + # This class is only provided for backwards compatibility. Use OpenSSL::Digest in the future. + class Digest < Digest + def initialize(*args) + # add warning + super(*args) + end + end + end # Digest + + # Returns a Digest subclass by +name+. + # + # require 'openssl' + # + # OpenSSL::Digest("MD5") + # # => OpenSSL::Digest::MD5 + # + # Digest("Foo") + # # => NameError: wrong constant name Foo + + def Digest(name) + OpenSSL::Digest.const_get(name) + end + + module_function :Digest + +end # OpenSSL + diff --git a/lib/ruby/shared/jopenssl21/openssl/ssl.rb b/lib/ruby/shared/jopenssl21/openssl/ssl.rb new file mode 100644 index 00000000000..456f426044d --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl/ssl.rb @@ -0,0 +1,205 @@ +=begin += $RCSfile$ -- Ruby-space definitions that completes C-space funcs for SSL + += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU YUUZOU + All rights reserved. + += Licence + This program is licenced under the same licence as Ruby. + (See the file 'LICENCE'.) + += Version + $Id$ +=end + +require "openssl/buffering" +require "fcntl" + +module OpenSSL + module SSL + module SocketForwarder + def addr + to_io.addr + end + + def peeraddr + to_io.peeraddr + end + + def setsockopt(level, optname, optval) + to_io.setsockopt(level, optname, optval) + end + + def getsockopt(level, optname) + to_io.getsockopt(level, optname) + end + + def fcntl(*args) + to_io.fcntl(*args) + end + + def closed? + to_io.closed? + end + + def do_not_reverse_lookup=(flag) + to_io.do_not_reverse_lookup = flag + end + end + + module Nonblock + def initialize(*args) + flag = File::NONBLOCK + flag |= @io.fcntl(Fcntl::F_GETFL) if defined?(Fcntl::F_GETFL) + @io.fcntl(Fcntl::F_SETFL, flag) + super + end + end + + # FIXME: Using the old non-ASN1 logic here because our ASN1 appears to + # return the wrong types for some decoded objects. See #1102 + def verify_certificate_identity(cert, hostname) + should_verify_common_name = true + cert.extensions.each{|ext| + next if ext.oid != "subjectAltName" + ext.value.split(/,\s+/).each{|general_name| + if /\ADNS:(.*)/ =~ general_name + should_verify_common_name = false + reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + # NOTE: somehow we need the IP: canonical form + # seems there were failures elsewhere when not + # not sure how that's possible possible to-do! + elsif /\AIP(?: Address)?:(.*)/ =~ general_name + #elsif /\AIP Address:(.*)/ =~ general_name + should_verify_common_name = false + return true if $1 == hostname + end + } + } + if should_verify_common_name + cert.subject.to_a.each{|oid, value| + if oid == "CN" + reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + end + } + end + return false + end +=begin + def verify_certificate_identity(cert, hostname) + should_verify_common_name = true + cert.extensions.each{|ext| + next if ext.oid != "subjectAltName" + ostr = OpenSSL::ASN1.decode(ext.to_der).value.last + sequence = OpenSSL::ASN1.decode(ostr.value) + sequence.value.each{|san| + case san.tag + when 2 # dNSName in GeneralName (RFC5280) + should_verify_common_name = false + reg = Regexp.escape(san.value).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + when 7 # iPAddress in GeneralName (RFC5280) + should_verify_common_name = false + # follows GENERAL_NAME_print() in x509v3/v3_alt.c + if san.value.size == 4 + return true if san.value.unpack('C*').join('.') == hostname + elsif san.value.size == 16 + return true if san.value.unpack('n*').map { |e| sprintf("%X", e) }.join(':') == hostname + end + end + } + } + if should_verify_common_name + cert.subject.to_a.each{|oid, value| + if oid == "CN" + reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + end + } + end + return false + end +=end + module_function :verify_certificate_identity + + class SSLSocket + include Buffering + include SocketForwarder + include Nonblock + + def post_connection_check(hostname) + unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname) + raise SSLError, "hostname \"#{hostname}\" does not match the server certificate" + end + return true + end + + def session + SSL::Session.new(self) + rescue SSL::Session::SessionError + nil + end + end + + ## + # SSLServer represents a TCP/IP server socket with Secure Sockets Layer. + class SSLServer + include SocketForwarder + # When true then #accept works exactly the same as TCPServer#accept + attr_accessor :start_immediately + + # Creates a new instance of SSLServer. + # * +srv+ is an instance of TCPServer. + # * +ctx+ is an instance of OpenSSL::SSL::SSLContext. + def initialize(svr, ctx) + @svr = svr + @ctx = ctx + unless ctx.session_id_context + # see #6137 - session id may not exceed 32 bytes + prng = ::Random.new($0.hash) + session_id = prng.bytes(16).unpack('H*')[0] + @ctx.session_id_context = session_id + end + @start_immediately = true + end + + # Returns the TCPServer passed to the SSLServer when initialized. + def to_io + @svr + end + + # See TCPServer#listen for details. + def listen(backlog=5) + @svr.listen(backlog) + end + + # See BasicSocket#shutdown for details. + def shutdown(how=Socket::SHUT_RDWR) + @svr.shutdown(how) + end + + # Works similar to TCPServer#accept. + def accept + sock = @svr.accept + begin + ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx) + ssl.sync_close = true + ssl.accept if @start_immediately + ssl + rescue SSLError => ex + sock.close + raise ex + end + end + + # See IO#close for details. + def close + @svr.close + end + end + end +end diff --git a/lib/ruby/shared/jopenssl21/openssl/x509.rb b/lib/ruby/shared/jopenssl21/openssl/x509.rb new file mode 100644 index 00000000000..eabd676eba2 --- /dev/null +++ b/lib/ruby/shared/jopenssl21/openssl/x509.rb @@ -0,0 +1,119 @@ +#-- +# +# $RCSfile$ +# +# = Ruby-space definitions that completes C-space funcs for X509 and subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licenced under the same licence as Ruby. +# (See the file 'LICENCE'.) +# +# = Version +# $Id$ +# +#++ + +module OpenSSL + module X509 + class Name + module RFC2253DN + Special = ',=+<>#;' + HexChar = /[0-9a-fA-F]/ + HexPair = /#{HexChar}#{HexChar}/ + HexString = /#{HexPair}+/ + Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/ + StringChar = /[^#{Special}\\"]/ + QuoteChar = /[^\\"]/ + AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/ + AttributeValue = / + (?!["#])((?:#{StringChar}|#{Pair})*)| + \#(#{HexString})| + "((?:#{QuoteChar}|#{Pair})*)" + /x + TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/ + + module_function + + def expand_pair(str) + return nil unless str + return str.gsub(Pair){ + pair = $& + case pair.size + when 2 then pair[1,1] + when 3 then Integer("0x#{pair[1,2]}").chr + else raise OpenSSL::X509::NameError, "invalid pair: #{str}" + end + } + end + + def expand_hexstring(str) + return nil unless str + der = str.gsub(HexPair){$&.to_i(16).chr } + a1 = OpenSSL::ASN1.decode(der) + return a1.value, a1.tag + end + + def expand_value(str1, str2, str3) + value = expand_pair(str1) + value, tag = expand_hexstring(str2) unless value + value = expand_pair(str3) unless value + return value, tag + end + + def scan(dn) + str = dn + ary = [] + while true + if md = TypeAndValue.match(str) + remain = md.post_match + type = md[1] + value, tag = expand_value(md[2], md[3], md[4]) rescue nil + if value + type_and_value = [type, value] + type_and_value.push(tag) if tag + ary.unshift(type_and_value) + if remain.length > 2 && remain[0] == ?, + str = remain[1..-1] + next + elsif remain.length > 2 && remain[0] == ?+ + raise OpenSSL::X509::NameError, + "multi-valued RDN is not supported: #{dn}" + elsif remain.empty? + break + end + end + end + msg_dn = dn[0, dn.length - str.length] + " =>" + str + raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}" + end + return ary + end + end + + class << self + def parse_rfc2253(str, template=OBJECT_TYPE_TEMPLATE) + ary = OpenSSL::X509::Name::RFC2253DN.scan(str) + self.new(ary, template) + end + + def parse_openssl(str, template=OBJECT_TYPE_TEMPLATE) + ary = str.scan(/\s*([^\/,]+)\s*/).collect{|i| i[0].split("=", 2) } + self.new(ary, template) + end + + alias parse parse_openssl + end + end + + class StoreContext + def cleanup + warn "(#{caller.first}) OpenSSL::X509::StoreContext#cleanup is deprecated with no replacement" if $VERBOSE + end + end + end +end diff --git a/lib/ruby/shared/json.rb b/lib/ruby/shared/json.rb new file mode 100644 index 00000000000..24aa385c91a --- /dev/null +++ b/lib/ruby/shared/json.rb @@ -0,0 +1,62 @@ +require 'json/common' + +## +# = JavaScript Object Notation (JSON) +# +# JSON is a lightweight data-interchange format. It is easy for us +# humans to read and write. Plus, equally simple for machines to generate or parse. +# JSON is completely language agnostic, making it the ideal interchange format. +# +# Built on two universally available structures: +# 1. A collection of name/value pairs. Often referred to as an _object_, hash table, record, struct, keyed list, or associative array. +# 2. An ordered list of values. More commonly called an _array_, vector, sequence or list. +# +# To read more about JSON visit: http://json.org +# +# == Parsing JSON +# +# To parse a JSON string received by another application or generated within +# your existing application: +# +# require 'json' +# +# my_hash = JSON.parse('{"hello": "goodbye"}') +# puts my_hash["hello"] => "goodbye" +# +# Notice the extra quotes '' around the hash notation. Ruby expects +# the argument to be a string and can't convert objects like a hash or array. +# +# Ruby converts your string into a hash +# +# == Generating JSON +# +# Creating a JSON string for communication or serialization is +# just as simple. +# +# require 'json' +# +# my_hash = {:hello => "goodbye"} +# puts JSON.generate(my_hash) => "{\"hello\":\"goodbye\"}" +# +# Or an alternative way: +# +# require 'json' +# puts {:hello => "goodbye"}.to_json => "{\"hello\":\"goodbye\"}" +# +# JSON.generate only allows objects or arrays to be converted +# to JSON syntax. to_json, however, accepts many Ruby classes +# even though it acts only as a method for serialization: +# +# require 'json' +# +# 1.to_json => "1" +# +module JSON + require 'json/version' + + begin + require 'json/ext' + rescue LoadError + require 'json/pure' + end +end diff --git a/lib/ruby/shared/json/add/bigdecimal.rb b/lib/ruby/shared/json/add/bigdecimal.rb new file mode 100644 index 00000000000..0ef69f12e07 --- /dev/null +++ b/lib/ruby/shared/json/add/bigdecimal.rb @@ -0,0 +1,28 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end +defined?(::BigDecimal) or require 'bigdecimal' + +class BigDecimal + # Import a JSON Marshalled object. + # + # method used for JSON marshalling support. + def self.json_create(object) + BigDecimal._load object['b'] + end + + # Marshal the object to JSON. + # + # method used for JSON marshalling support. + def as_json(*) + { + JSON.create_id => self.class.name, + 'b' => _dump, + } + end + + # return the JSON value + def to_json(*) + as_json.to_json + end +end diff --git a/lib/ruby/shared/json/add/complex.rb b/lib/ruby/shared/json/add/complex.rb new file mode 100644 index 00000000000..d7ebebf5f7e --- /dev/null +++ b/lib/ruby/shared/json/add/complex.rb @@ -0,0 +1,22 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end +defined?(::Complex) or require 'complex' + +class Complex + def self.json_create(object) + Complex(object['r'], object['i']) + end + + def as_json(*) + { + JSON.create_id => self.class.name, + 'r' => real, + 'i' => imag, + } + end + + def to_json(*) + as_json.to_json + end +end diff --git a/lib/ruby/shared/json/add/core.rb b/lib/ruby/shared/json/add/core.rb new file mode 100644 index 00000000000..77d9dc0b201 --- /dev/null +++ b/lib/ruby/shared/json/add/core.rb @@ -0,0 +1,11 @@ +# This file requires the implementations of ruby core's custom objects for +# serialisation/deserialisation. + +require 'json/add/date' +require 'json/add/date_time' +require 'json/add/exception' +require 'json/add/range' +require 'json/add/regexp' +require 'json/add/struct' +require 'json/add/symbol' +require 'json/add/time' diff --git a/lib/ruby/shared/json/add/date.rb b/lib/ruby/shared/json/add/date.rb new file mode 100644 index 00000000000..4288237db1f --- /dev/null +++ b/lib/ruby/shared/json/add/date.rb @@ -0,0 +1,34 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end +require 'date' + +# Date serialization/deserialization +class Date + + # Deserializes JSON string by converting Julian year y, month + # m, day d and Day of Calendar Reform sg to Date. + def self.json_create(object) + civil(*object.values_at('y', 'm', 'd', 'sg')) + end + + alias start sg unless method_defined?(:start) + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + { + JSON.create_id => self.class.name, + 'y' => year, + 'm' => month, + 'd' => day, + 'sg' => start, + } + end + + # Stores class name (Date) with Julian year y, month m, day + # d and Day of Calendar Reform sg as JSON string + def to_json(*args) + as_json.to_json(*args) + end +end diff --git a/lib/ruby/shared/json/add/date_time.rb b/lib/ruby/shared/json/add/date_time.rb new file mode 100644 index 00000000000..5ea42ea6565 --- /dev/null +++ b/lib/ruby/shared/json/add/date_time.rb @@ -0,0 +1,50 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end +require 'date' + +# DateTime serialization/deserialization +class DateTime + + # Deserializes JSON string by converting year y, month m, + # day d, hour H, minute M, second S, + # offset of and Day of Calendar Reform sg to DateTime. + def self.json_create(object) + args = object.values_at('y', 'm', 'd', 'H', 'M', 'S') + of_a, of_b = object['of'].split('/') + if of_b and of_b != '0' + args << Rational(of_a.to_i, of_b.to_i) + else + args << of_a + end + args << object['sg'] + civil(*args) + end + + alias start sg unless method_defined?(:start) + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + { + JSON.create_id => self.class.name, + 'y' => year, + 'm' => month, + 'd' => day, + 'H' => hour, + 'M' => min, + 'S' => sec, + 'of' => offset.to_s, + 'sg' => start, + } + end + + # Stores class name (DateTime) with Julian year y, month m, + # day d, hour H, minute M, second S, + # offset of and Day of Calendar Reform sg as JSON string + def to_json(*args) + as_json.to_json(*args) + end +end + + diff --git a/lib/ruby/shared/json/add/exception.rb b/lib/ruby/shared/json/add/exception.rb new file mode 100644 index 00000000000..e6ad257abf9 --- /dev/null +++ b/lib/ruby/shared/json/add/exception.rb @@ -0,0 +1,31 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end + +# Exception serialization/deserialization +class Exception + + # Deserializes JSON string by constructing new Exception object with message + # m and backtrace b serialized with to_json + def self.json_create(object) + result = new(object['m']) + result.set_backtrace object['b'] + result + end + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + { + JSON.create_id => self.class.name, + 'm' => message, + 'b' => backtrace, + } + end + + # Stores class name (Exception) with message m and backtrace array + # b as JSON string + def to_json(*args) + as_json.to_json(*args) + end +end diff --git a/lib/ruby/shared/json/add/ostruct.rb b/lib/ruby/shared/json/add/ostruct.rb new file mode 100644 index 00000000000..da81e107a7d --- /dev/null +++ b/lib/ruby/shared/json/add/ostruct.rb @@ -0,0 +1,31 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end +require 'ostruct' + +# OpenStruct serialization/deserialization +class OpenStruct + + # Deserializes JSON string by constructing new Struct object with values + # v serialized by to_json. + def self.json_create(object) + new(object['t'] || object[:t]) + end + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + klass = self.class.name + klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!" + { + JSON.create_id => klass, + 't' => table, + } + end + + # Stores class name (OpenStruct) with this struct's values v as a + # JSON string. + def to_json(*args) + as_json.to_json(*args) + end +end diff --git a/lib/ruby/shared/json/add/range.rb b/lib/ruby/shared/json/add/range.rb new file mode 100644 index 00000000000..e61e553cdba --- /dev/null +++ b/lib/ruby/shared/json/add/range.rb @@ -0,0 +1,29 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end + +# Range serialization/deserialization +class Range + + # Deserializes JSON string by constructing new Range object with arguments + # a serialized by to_json. + def self.json_create(object) + new(*object['a']) + end + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + { + JSON.create_id => self.class.name, + 'a' => [ first, last, exclude_end? ] + } + end + + # Stores class name (Range) with JSON array of arguments a which + # include first (integer), last (integer), and + # exclude_end? (boolean) as JSON string. + def to_json(*args) + as_json.to_json(*args) + end +end diff --git a/lib/ruby/shared/json/add/rational.rb b/lib/ruby/shared/json/add/rational.rb new file mode 100644 index 00000000000..867cd92f059 --- /dev/null +++ b/lib/ruby/shared/json/add/rational.rb @@ -0,0 +1,22 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end +defined?(::Rational) or require 'rational' + +class Rational + def self.json_create(object) + Rational(object['n'], object['d']) + end + + def as_json(*) + { + JSON.create_id => self.class.name, + 'n' => numerator, + 'd' => denominator, + } + end + + def to_json(*) + as_json.to_json + end +end diff --git a/lib/ruby/shared/json/add/regexp.rb b/lib/ruby/shared/json/add/regexp.rb new file mode 100644 index 00000000000..2fcbb6fb14d --- /dev/null +++ b/lib/ruby/shared/json/add/regexp.rb @@ -0,0 +1,30 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end + +# Regexp serialization/deserialization +class Regexp + + # Deserializes JSON string by constructing new Regexp object with source + # s (Regexp or String) and options o serialized by + # to_json + def self.json_create(object) + new(object['s'], object['o']) + end + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + { + JSON.create_id => self.class.name, + 'o' => options, + 's' => source, + } + end + + # Stores class name (Regexp) with options o and source s + # (Regexp or String) as JSON string + def to_json(*) + as_json.to_json + end +end diff --git a/lib/ruby/shared/json/add/struct.rb b/lib/ruby/shared/json/add/struct.rb new file mode 100644 index 00000000000..6847cde99b1 --- /dev/null +++ b/lib/ruby/shared/json/add/struct.rb @@ -0,0 +1,30 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end + +# Struct serialization/deserialization +class Struct + + # Deserializes JSON string by constructing new Struct object with values + # v serialized by to_json. + def self.json_create(object) + new(*object['v']) + end + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + klass = self.class.name + klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!" + { + JSON.create_id => klass, + 'v' => values, + } + end + + # Stores class name (Struct) with Struct values v as a JSON string. + # Only named structs are supported. + def to_json(*args) + as_json.to_json(*args) + end +end diff --git a/lib/ruby/shared/json/add/symbol.rb b/lib/ruby/shared/json/add/symbol.rb new file mode 100644 index 00000000000..03dc9a56a55 --- /dev/null +++ b/lib/ruby/shared/json/add/symbol.rb @@ -0,0 +1,25 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end + +# Symbol serialization/deserialization +class Symbol + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + { + JSON.create_id => self.class.name, + 's' => to_s, + } + end + + # Stores class name (Symbol) with String representation of Symbol as a JSON string. + def to_json(*a) + as_json.to_json(*a) + end + + # Deserializes JSON string by converting the string value stored in the object to a Symbol + def self.json_create(o) + o['s'].to_sym + end +end diff --git a/lib/ruby/shared/json/add/time.rb b/lib/ruby/shared/json/add/time.rb new file mode 100644 index 00000000000..338209d8999 --- /dev/null +++ b/lib/ruby/shared/json/add/time.rb @@ -0,0 +1,38 @@ +unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED + require 'json' +end + +# Time serialization/deserialization +class Time + + # Deserializes JSON string by converting time since epoch to Time + def self.json_create(object) + if usec = object.delete('u') # used to be tv_usec -> tv_nsec + object['n'] = usec * 1000 + end + if instance_methods.include?(:tv_nsec) + at(object['s'], Rational(object['n'], 1000)) + else + at(object['s'], object['n'] / 1000) + end + end + + # Returns a hash, that will be turned into a JSON object and represent this + # object. + def as_json(*) + nanoseconds = [ tv_usec * 1000 ] + respond_to?(:tv_nsec) and nanoseconds << tv_nsec + nanoseconds = nanoseconds.max + { + JSON.create_id => self.class.name, + 's' => tv_sec, + 'n' => nanoseconds, + } + end + + # Stores class name (Time) with number of seconds since epoch and number of + # microseconds for Time as JSON string + def to_json(*args) + as_json.to_json(*args) + end +end diff --git a/lib/ruby/shared/json/common.rb b/lib/ruby/shared/json/common.rb new file mode 100644 index 00000000000..65a74a1aa42 --- /dev/null +++ b/lib/ruby/shared/json/common.rb @@ -0,0 +1,487 @@ +require 'json/version' +require 'json/generic_object' + +module JSON + class << self + # If _object_ is string-like, parse the string and return the parsed result + # as a Ruby data structure. Otherwise generate a JSON text from the Ruby + # data structure object and return it. + # + # The _opts_ argument is passed through to generate/parse respectively. See + # generate and parse for their documentation. + def [](object, opts = {}) + if object.respond_to? :to_str + JSON.parse(object.to_str, opts) + else + JSON.generate(object, opts) + end + end + + # Returns the JSON parser class that is used by JSON. This is either + # JSON::Ext::Parser or JSON::Pure::Parser. + attr_reader :parser + + # Set the JSON parser class _parser_ to be used by JSON. + def parser=(parser) # :nodoc: + @parser = parser + remove_const :Parser if JSON.const_defined_in?(self, :Parser) + const_set :Parser, parser + end + + # Return the constant located at _path_. The format of _path_ has to be + # either ::A::B::C or A::B::C. In any case, A has to be located at the top + # level (absolute namespace path?). If there doesn't exist a constant at + # the given path, an ArgumentError is raised. + def deep_const_get(path) # :nodoc: + path.to_s.split(/::/).inject(Object) do |p, c| + case + when c.empty? then p + when JSON.const_defined_in?(p, c) then p.const_get(c) + else + begin + p.const_missing(c) + rescue NameError => e + raise ArgumentError, "can't get const #{path}: #{e}" + end + end + end + end + + # Set the module _generator_ to be used by JSON. + def generator=(generator) # :nodoc: + old, $VERBOSE = $VERBOSE, nil + @generator = generator + generator_methods = generator::GeneratorMethods + for const in generator_methods.constants + klass = deep_const_get(const) + modul = generator_methods.const_get(const) + klass.class_eval do + instance_methods(false).each do |m| + m.to_s == 'to_json' and remove_method m + end + include modul + end + end + self.state = generator::State + const_set :State, self.state + const_set :SAFE_STATE_PROTOTYPE, State.new + const_set :FAST_STATE_PROTOTYPE, State.new( + :indent => '', + :space => '', + :object_nl => "", + :array_nl => "", + :max_nesting => false + ) + const_set :PRETTY_STATE_PROTOTYPE, State.new( + :indent => ' ', + :space => ' ', + :object_nl => "\n", + :array_nl => "\n" + ) + ensure + $VERBOSE = old + end + + # Returns the JSON generator module that is used by JSON. This is + # either JSON::Ext::Generator or JSON::Pure::Generator. + attr_reader :generator + + # Returns the JSON generator state class that is used by JSON. This is + # either JSON::Ext::Generator::State or JSON::Pure::Generator::State. + attr_accessor :state + + # This is create identifier, which is used to decide if the _json_create_ + # hook of a class should be called. It defaults to 'json_class'. + attr_accessor :create_id + end + self.create_id = 'json_class' + + NaN = 0.0/0 + + Infinity = 1.0/0 + + MinusInfinity = -Infinity + + # The base exception for JSON errors. + class JSONError < StandardError + def self.wrap(exception) + obj = new("Wrapped(#{exception.class}): #{exception.message.inspect}") + obj.set_backtrace exception.backtrace + obj + end + end + + # This exception is raised if a parser error occurs. + class ParserError < JSONError; end + + # This exception is raised if the nesting of parsed data structures is too + # deep. + class NestingError < ParserError; end + + # :stopdoc: + class CircularDatastructure < NestingError; end + # :startdoc: + + # This exception is raised if a generator or unparser error occurs. + class GeneratorError < JSONError; end + # For backwards compatibility + UnparserError = GeneratorError + + # This exception is raised if the required unicode support is missing on the + # system. Usually this means that the iconv library is not installed. + class MissingUnicodeSupport < JSONError; end + + module_function + + # Parse the JSON document _source_ into a Ruby data structure and return it. + # + # _opts_ can have the following + # keys: + # * *max_nesting*: The maximum depth of nesting allowed in the parsed data + # structures. Disable depth checking with :max_nesting => false. It defaults + # to 100. + # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in + # defiance of RFC 4627 to be parsed by the Parser. This option defaults + # to false. + # * *symbolize_names*: If set to true, returns symbols for the names + # (keys) in a JSON object. Otherwise strings are returned. Strings are + # the default. + # * *create_additions*: If set to false, the Parser doesn't create + # additions even if a matching class and create_id was found. This option + # defaults to true. + # * *object_class*: Defaults to Hash + # * *array_class*: Defaults to Array + def parse(source, opts = {}) + Parser.new(source, opts).parse + end + + # Parse the JSON document _source_ into a Ruby data structure and return it. + # The bang version of the parse method defaults to the more dangerous values + # for the _opts_ hash, so be sure only to parse trusted _source_ documents. + # + # _opts_ can have the following keys: + # * *max_nesting*: The maximum depth of nesting allowed in the parsed data + # structures. Enable depth checking with :max_nesting => anInteger. The parse! + # methods defaults to not doing max depth checking: This can be dangerous + # if someone wants to fill up your stack. + # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in + # defiance of RFC 4627 to be parsed by the Parser. This option defaults + # to true. + # * *create_additions*: If set to false, the Parser doesn't create + # additions even if a matching class and create_id was found. This option + # defaults to true. + def parse!(source, opts = {}) + opts = { + :max_nesting => false, + :allow_nan => true + }.update(opts) + Parser.new(source, opts).parse + end + + # Generate a JSON document from the Ruby data structure _obj_ and return + # it. _state_ is * a JSON::State object, + # * or a Hash like object (responding to to_hash), + # * an object convertible into a hash by a to_h method, + # that is used as or to configure a State object. + # + # It defaults to a state object, that creates the shortest possible JSON text + # in one line, checks for circular data structures and doesn't allow NaN, + # Infinity, and -Infinity. + # + # A _state_ hash can have the following keys: + # * *indent*: a string used to indent levels (default: ''), + # * *space*: a string that is put after, a : or , delimiter (default: ''), + # * *space_before*: a string that is put before a : pair delimiter (default: ''), + # * *object_nl*: a string that is put at the end of a JSON object (default: ''), + # * *array_nl*: a string that is put at the end of a JSON array (default: ''), + # * *allow_nan*: true if NaN, Infinity, and -Infinity should be + # generated, otherwise an exception is thrown if these values are + # encountered. This options defaults to false. + # * *max_nesting*: The maximum depth of nesting allowed in the data + # structures from which JSON is to be generated. Disable depth checking + # with :max_nesting => false, it defaults to 100. + # + # See also the fast_generate for the fastest creation method with the least + # amount of sanity checks, and the pretty_generate method for some + # defaults for pretty output. + def generate(obj, opts = nil) + if State === opts + state, opts = opts, nil + else + state = SAFE_STATE_PROTOTYPE.dup + end + if opts + if opts.respond_to? :to_hash + opts = opts.to_hash + elsif opts.respond_to? :to_h + opts = opts.to_h + else + raise TypeError, "can't convert #{opts.class} into Hash" + end + state = state.configure(opts) + end + state.generate(obj) + end + + # :stopdoc: + # I want to deprecate these later, so I'll first be silent about them, and + # later delete them. + alias unparse generate + module_function :unparse + # :startdoc: + + # Generate a JSON document from the Ruby data structure _obj_ and return it. + # This method disables the checks for circles in Ruby objects. + # + # *WARNING*: Be careful not to pass any Ruby data structures with circles as + # _obj_ argument because this will cause JSON to go into an infinite loop. + def fast_generate(obj, opts = nil) + if State === opts + state, opts = opts, nil + else + state = FAST_STATE_PROTOTYPE.dup + end + if opts + if opts.respond_to? :to_hash + opts = opts.to_hash + elsif opts.respond_to? :to_h + opts = opts.to_h + else + raise TypeError, "can't convert #{opts.class} into Hash" + end + state.configure(opts) + end + state.generate(obj) + end + + # :stopdoc: + # I want to deprecate these later, so I'll first be silent about them, and later delete them. + alias fast_unparse fast_generate + module_function :fast_unparse + # :startdoc: + + # Generate a JSON document from the Ruby data structure _obj_ and return it. + # The returned document is a prettier form of the document returned by + # #unparse. + # + # The _opts_ argument can be used to configure the generator. See the + # generate method for a more detailed explanation. + def pretty_generate(obj, opts = nil) + if State === opts + state, opts = opts, nil + else + state = PRETTY_STATE_PROTOTYPE.dup + end + if opts + if opts.respond_to? :to_hash + opts = opts.to_hash + elsif opts.respond_to? :to_h + opts = opts.to_h + else + raise TypeError, "can't convert #{opts.class} into Hash" + end + state.configure(opts) + end + state.generate(obj) + end + + # :stopdoc: + # I want to deprecate these later, so I'll first be silent about them, and later delete them. + alias pretty_unparse pretty_generate + module_function :pretty_unparse + # :startdoc: + + class << self + # The global default options for the JSON.load method: + # :max_nesting: false + # :allow_nan: true + # :quirks_mode: true + attr_accessor :load_default_options + end + self.load_default_options = { + :max_nesting => false, + :allow_nan => true, + :quirks_mode => true, + :create_additions => true, + } + + # Load a ruby data structure from a JSON _source_ and return it. A source can + # either be a string-like object, an IO-like object, or an object responding + # to the read method. If _proc_ was given, it will be called with any nested + # Ruby object as an argument recursively in depth first order. To modify the + # default options pass in the optional _options_ argument as well. + # + # BEWARE: This method is meant to serialise data from trusted user input, + # like from your own database server or clients under your control, it could + # be dangerous to allow untrusted users to pass JSON sources into it. The + # default options for the parser can be changed via the load_default_options + # method. + # + # This method is part of the implementation of the load/dump interface of + # Marshal and YAML. + def load(source, proc = nil, options = {}) + opts = load_default_options.merge options + if source.respond_to? :to_str + source = source.to_str + elsif source.respond_to? :to_io + source = source.to_io.read + elsif source.respond_to?(:read) + source = source.read + end + if opts[:quirks_mode] && (source.nil? || source.empty?) + source = 'null' + end + result = parse(source, opts) + recurse_proc(result, &proc) if proc + result + end + + # Recursively calls passed _Proc_ if the parsed data structure is an _Array_ or _Hash_ + def recurse_proc(result, &proc) + case result + when Array + result.each { |x| recurse_proc x, &proc } + proc.call result + when Hash + result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc } + proc.call result + else + proc.call result + end + end + + alias restore load + module_function :restore + + class << self + # The global default options for the JSON.dump method: + # :max_nesting: false + # :allow_nan: true + # :quirks_mode: true + attr_accessor :dump_default_options + end + self.dump_default_options = { + :max_nesting => false, + :allow_nan => true, + :quirks_mode => true, + } + + # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns + # the result. + # + # If anIO (an IO-like object or an object that responds to the write method) + # was given, the resulting JSON is written to it. + # + # If the number of nested arrays or objects exceeds _limit_, an ArgumentError + # exception is raised. This argument is similar (but not exactly the + # same!) to the _limit_ argument in Marshal.dump. + # + # The default options for the generator can be changed via the + # dump_default_options method. + # + # This method is part of the implementation of the load/dump interface of + # Marshal and YAML. + def dump(obj, anIO = nil, limit = nil) + if anIO and limit.nil? + anIO = anIO.to_io if anIO.respond_to?(:to_io) + unless anIO.respond_to?(:write) + limit = anIO + anIO = nil + end + end + opts = JSON.dump_default_options + limit and opts.update(:max_nesting => limit) + result = generate(obj, opts) + if anIO + anIO.write result + anIO + else + result + end + rescue JSON::NestingError + raise ArgumentError, "exceed depth limit" + end + + # Swap consecutive bytes of _string_ in place. + def self.swap!(string) # :nodoc: + 0.upto(string.size / 2) do |i| + break unless string[2 * i + 1] + string[2 * i], string[2 * i + 1] = string[2 * i + 1], string[2 * i] + end + string + end + + # Shortuct for iconv. + if ::String.method_defined?(:encode) && + # XXX Rubinius doesn't support ruby 1.9 encoding yet + defined?(RUBY_ENGINE) && RUBY_ENGINE != 'rbx' + then + # Encodes string using Ruby's _String.encode_ + def self.iconv(to, from, string) + string.encode(to, from) + end + else + require 'iconv' + # Encodes string using _iconv_ library + def self.iconv(to, from, string) + Iconv.conv(to, from, string) + end + end + + if ::Object.method(:const_defined?).arity == 1 + def self.const_defined_in?(modul, constant) + modul.const_defined?(constant) + end + else + def self.const_defined_in?(modul, constant) + modul.const_defined?(constant, false) + end + end +end + +module ::Kernel + private + + # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in + # one line. + def j(*objs) + objs.each do |obj| + puts JSON::generate(obj, :allow_nan => true, :max_nesting => false) + end + nil + end + + # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with + # indentation and over many lines. + def jj(*objs) + objs.each do |obj| + puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false) + end + nil + end + + # If _object_ is string-like, parse the string and return the parsed result as + # a Ruby data structure. Otherwise, generate a JSON text from the Ruby data + # structure object and return it. + # + # The _opts_ argument is passed through to generate/parse respectively. See + # generate and parse for their documentation. + def JSON(object, *args) + if object.respond_to? :to_str + JSON.parse(object.to_str, args.first) + else + JSON.generate(object, args.first) + end + end +end + +# Extends any Class to include _json_creatable?_ method. +class ::Class + # Returns true if this class can be used to create an instance + # from a serialised JSON string. The class has to implement a class + # method _json_create_ that expects a hash as first parameter. The hash + # should include the required data. + def json_creatable? + respond_to?(:json_create) + end +end diff --git a/lib/ruby/shared/json/ext.rb b/lib/ruby/shared/json/ext.rb new file mode 100644 index 00000000000..c5f813181d4 --- /dev/null +++ b/lib/ruby/shared/json/ext.rb @@ -0,0 +1,21 @@ +if ENV['SIMPLECOV_COVERAGE'].to_i == 1 + require 'simplecov' + SimpleCov.start do + add_filter "/tests/" + end +end +require 'json/common' + +module JSON + # This module holds all the modules/classes that implement JSON's + # functionality as C extensions. + module Ext + require 'json/ext/parser' + require 'json/ext/generator' + $DEBUG and warn "Using Ext extension for JSON." + JSON.parser = Parser + JSON.generator = Generator + end + + JSON_LOADED = true unless defined?(::JSON::JSON_LOADED) +end diff --git a/lib/ruby/shared/json/ext/generator.jar b/lib/ruby/shared/json/ext/generator.jar new file mode 100644 index 00000000000..4109442e49f Binary files /dev/null and b/lib/ruby/shared/json/ext/generator.jar differ diff --git a/lib/ruby/shared/json/ext/parser.jar b/lib/ruby/shared/json/ext/parser.jar new file mode 100644 index 00000000000..d954fd8d663 Binary files /dev/null and b/lib/ruby/shared/json/ext/parser.jar differ diff --git a/lib/ruby/shared/json/generic_object.rb b/lib/ruby/shared/json/generic_object.rb new file mode 100644 index 00000000000..8b8fd53bef0 --- /dev/null +++ b/lib/ruby/shared/json/generic_object.rb @@ -0,0 +1,70 @@ +require 'ostruct' + +module JSON + class GenericObject < OpenStruct + class << self + alias [] new + + def json_creatable? + @json_creatable + end + + attr_writer :json_creatable + + def json_create(data) + data = data.dup + data.delete JSON.create_id + self[data] + end + + def from_hash(object) + case + when object.respond_to?(:to_hash) + result = new + object.to_hash.each do |key, value| + result[key] = from_hash(value) + end + result + when object.respond_to?(:to_ary) + object.to_ary.map { |a| from_hash(a) } + else + object + end + end + + def load(source, proc = nil, opts = {}) + result = ::JSON.load(source, proc, opts.merge(:object_class => self)) + result.nil? ? new : result + end + + def dump(obj, *args) + ::JSON.dump(obj, *args) + end + end + self.json_creatable = false + + def to_hash + table + end + + def [](name) + table[name.to_sym] + end + + def []=(name, value) + __send__ "#{name}=", value + end + + def |(other) + self.class[other.to_hash.merge(to_hash)] + end + + def as_json(*) + { JSON.create_id => self.class.name }.merge to_hash + end + + def to_json(*a) + as_json.to_json(*a) + end + end +end diff --git a/lib/ruby/shared/json/pure.rb b/lib/ruby/shared/json/pure.rb new file mode 100644 index 00000000000..b68668b2cbf --- /dev/null +++ b/lib/ruby/shared/json/pure.rb @@ -0,0 +1,21 @@ +if ENV['SIMPLECOV_COVERAGE'].to_i == 1 + require 'simplecov' + SimpleCov.start do + add_filter "/tests/" + end +end +require 'json/common' +require 'json/pure/parser' +require 'json/pure/generator' + +module JSON + # This module holds all the modules/classes that implement JSON's + # functionality in pure ruby. + module Pure + $DEBUG and warn "Using Pure library for JSON." + JSON.parser = Parser + JSON.generator = Generator + end + + JSON_LOADED = true unless defined?(::JSON::JSON_LOADED) +end diff --git a/lib/ruby/shared/json/pure/generator.rb b/lib/ruby/shared/json/pure/generator.rb new file mode 100644 index 00000000000..9056a5d7f61 --- /dev/null +++ b/lib/ruby/shared/json/pure/generator.rb @@ -0,0 +1,522 @@ +module JSON + MAP = { + "\x0" => '\u0000', + "\x1" => '\u0001', + "\x2" => '\u0002', + "\x3" => '\u0003', + "\x4" => '\u0004', + "\x5" => '\u0005', + "\x6" => '\u0006', + "\x7" => '\u0007', + "\b" => '\b', + "\t" => '\t', + "\n" => '\n', + "\xb" => '\u000b', + "\f" => '\f', + "\r" => '\r', + "\xe" => '\u000e', + "\xf" => '\u000f', + "\x10" => '\u0010', + "\x11" => '\u0011', + "\x12" => '\u0012', + "\x13" => '\u0013', + "\x14" => '\u0014', + "\x15" => '\u0015', + "\x16" => '\u0016', + "\x17" => '\u0017', + "\x18" => '\u0018', + "\x19" => '\u0019', + "\x1a" => '\u001a', + "\x1b" => '\u001b', + "\x1c" => '\u001c', + "\x1d" => '\u001d', + "\x1e" => '\u001e', + "\x1f" => '\u001f', + '"' => '\"', + '\\' => '\\\\', + } # :nodoc: + + # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with + # UTF16 big endian characters as \u????, and return it. + if defined?(::Encoding) + def utf8_to_json(string) # :nodoc: + string = string.dup + string.force_encoding(::Encoding::ASCII_8BIT) + string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] } + string.force_encoding(::Encoding::UTF_8) + string + end + + def utf8_to_json_ascii(string) # :nodoc: + string = string.dup + string.force_encoding(::Encoding::ASCII_8BIT) + string.gsub!(/["\\\x0-\x1f]/n) { MAP[$&] } + string.gsub!(/( + (?: + [\xc2-\xdf][\x80-\xbf] | + [\xe0-\xef][\x80-\xbf]{2} | + [\xf0-\xf4][\x80-\xbf]{3} + )+ | + [\x80-\xc1\xf5-\xff] # invalid + )/nx) { |c| + c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" + s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0] + s.force_encoding(::Encoding::ASCII_8BIT) + s.gsub!(/.{4}/n, '\\\\u\&') + s.force_encoding(::Encoding::UTF_8) + } + string.force_encoding(::Encoding::UTF_8) + string + rescue => e + raise GeneratorError.wrap(e) + end + + def valid_utf8?(string) + encoding = string.encoding + (encoding == Encoding::UTF_8 || encoding == Encoding::ASCII) && + string.valid_encoding? + end + module_function :valid_utf8? + else + def utf8_to_json(string) # :nodoc: + string.gsub(/["\\\x0-\x1f]/n) { MAP[$&] } + end + + def utf8_to_json_ascii(string) # :nodoc: + string = string.gsub(/["\\\x0-\x1f]/) { MAP[$&] } + string.gsub!(/( + (?: + [\xc2-\xdf][\x80-\xbf] | + [\xe0-\xef][\x80-\xbf]{2} | + [\xf0-\xf4][\x80-\xbf]{3} + )+ | + [\x80-\xc1\xf5-\xff] # invalid + )/nx) { |c| + c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" + s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0] + s.gsub!(/.{4}/n, '\\\\u\&') + } + string + rescue => e + raise GeneratorError.wrap(e) + end + + def valid_utf8?(string) + string =~ + /\A( [\x09\x0a\x0d\x20-\x7e] # ASCII + | [\xc2-\xdf][\x80-\xbf] # non-overlong 2-byte + | \xe0[\xa0-\xbf][\x80-\xbf] # excluding overlongs + | [\xe1-\xec\xee\xef][\x80-\xbf]{2} # straight 3-byte + | \xed[\x80-\x9f][\x80-\xbf] # excluding surrogates + | \xf0[\x90-\xbf][\x80-\xbf]{2} # planes 1-3 + | [\xf1-\xf3][\x80-\xbf]{3} # planes 4-15 + | \xf4[\x80-\x8f][\x80-\xbf]{2} # plane 16 + )*\z/nx + end + end + module_function :utf8_to_json, :utf8_to_json_ascii, :valid_utf8? + + + module Pure + module Generator + # This class is used to create State instances, that are use to hold data + # while generating a JSON text from a Ruby data structure. + class State + # Creates a State object from _opts_, which ought to be Hash to create + # a new State instance configured by _opts_, something else to create + # an unconfigured instance. If _opts_ is a State object, it is just + # returned. + def self.from_state(opts) + case + when self === opts + opts + when opts.respond_to?(:to_hash) + new(opts.to_hash) + when opts.respond_to?(:to_h) + new(opts.to_h) + else + SAFE_STATE_PROTOTYPE.dup + end + end + + # Instantiates a new State object, configured by _opts_. + # + # _opts_ can have the following keys: + # + # * *indent*: a string used to indent levels (default: ''), + # * *space*: a string that is put after, a : or , delimiter (default: ''), + # * *space_before*: a string that is put before a : pair delimiter (default: ''), + # * *object_nl*: a string that is put at the end of a JSON object (default: ''), + # * *array_nl*: a string that is put at the end of a JSON array (default: ''), + # * *check_circular*: is deprecated now, use the :max_nesting option instead, + # * *max_nesting*: sets the maximum level of data structure nesting in + # the generated JSON, max_nesting = 0 if no maximum should be checked. + # * *allow_nan*: true if NaN, Infinity, and -Infinity should be + # generated, otherwise an exception is thrown, if these values are + # encountered. This options defaults to false. + # * *quirks_mode*: Enables quirks_mode for parser, that is for example + # generating single JSON values instead of documents is possible. + def initialize(opts = {}) + @indent = '' + @space = '' + @space_before = '' + @object_nl = '' + @array_nl = '' + @allow_nan = false + @ascii_only = false + @quirks_mode = false + @buffer_initial_length = 1024 + configure opts + end + + # This string is used to indent levels in the JSON text. + attr_accessor :indent + + # This string is used to insert a space between the tokens in a JSON + # string. + attr_accessor :space + + # This string is used to insert a space before the ':' in JSON objects. + attr_accessor :space_before + + # This string is put at the end of a line that holds a JSON object (or + # Hash). + attr_accessor :object_nl + + # This string is put at the end of a line that holds a JSON array. + attr_accessor :array_nl + + # This integer returns the maximum level of data structure nesting in + # the generated JSON, max_nesting = 0 if no maximum is checked. + attr_accessor :max_nesting + + # If this attribute is set to true, quirks mode is enabled, otherwise + # it's disabled. + attr_accessor :quirks_mode + + # :stopdoc: + attr_reader :buffer_initial_length + + def buffer_initial_length=(length) + if length > 0 + @buffer_initial_length = length + end + end + # :startdoc: + + # This integer returns the current depth data structure nesting in the + # generated JSON. + attr_accessor :depth + + def check_max_nesting # :nodoc: + return if @max_nesting.zero? + current_nesting = depth + 1 + current_nesting > @max_nesting and + raise NestingError, "nesting of #{current_nesting} is too deep" + end + + # Returns true, if circular data structures are checked, + # otherwise returns false. + def check_circular? + !@max_nesting.zero? + end + + # Returns true if NaN, Infinity, and -Infinity should be considered as + # valid JSON and output. + def allow_nan? + @allow_nan + end + + # Returns true, if only ASCII characters should be generated. Otherwise + # returns false. + def ascii_only? + @ascii_only + end + + # Returns true, if quirks mode is enabled. Otherwise returns false. + def quirks_mode? + @quirks_mode + end + + # Configure this State instance with the Hash _opts_, and return + # itself. + def configure(opts) + if opts.respond_to?(:to_hash) + opts = opts.to_hash + elsif opts.respond_to?(:to_h) + opts = opts.to_h + else + raise TypeError, "can't convert #{opts.class} into Hash" + end + for key, value in opts + instance_variable_set "@#{key}", value + end + @indent = opts[:indent] if opts.key?(:indent) + @space = opts[:space] if opts.key?(:space) + @space_before = opts[:space_before] if opts.key?(:space_before) + @object_nl = opts[:object_nl] if opts.key?(:object_nl) + @array_nl = opts[:array_nl] if opts.key?(:array_nl) + @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) + @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) + @depth = opts[:depth] || 0 + @quirks_mode = opts[:quirks_mode] if opts.key?(:quirks_mode) + @buffer_initial_length ||= opts[:buffer_initial_length] + + if !opts.key?(:max_nesting) # defaults to 100 + @max_nesting = 100 + elsif opts[:max_nesting] + @max_nesting = opts[:max_nesting] + else + @max_nesting = 0 + end + self + end + alias merge configure + + # Returns the configuration instance variables as a hash, that can be + # passed to the configure method. + def to_h + result = {} + for iv in instance_variables + iv = iv.to_s[1..-1] + result[iv.to_sym] = self[iv] + end + result + end + + alias to_hash to_h + + # Generates a valid JSON document from object +obj+ and returns the + # result. If no valid JSON document can be created this method raises a + # GeneratorError exception. + def generate(obj) + result = obj.to_json(self) + JSON.valid_utf8?(result) or raise GeneratorError, + "source sequence #{result.inspect} is illegal/malformed utf-8" + unless @quirks_mode + unless result =~ /\A\s*\[/ && result =~ /\]\s*\Z/ || + result =~ /\A\s*\{/ && result =~ /\}\s*\Z/ + then + raise GeneratorError, "only generation of JSON objects or arrays allowed" + end + end + result + end + + # Return the value returned by method +name+. + def [](name) + if respond_to?(name) + __send__(name) + else + instance_variable_get("@#{name}") + end + end + + def []=(name, value) + if respond_to?(name_writer = "#{name}=") + __send__ name_writer, value + else + instance_variable_set "@#{name}", value + end + end + end + + module GeneratorMethods + module Object + # Converts this object to a string (calling #to_s), converts + # it to a JSON string, and returns the result. This is a fallback, if no + # special method #to_json was defined for some object. + def to_json(*) to_s.to_json end + end + + module Hash + # Returns a JSON string containing a JSON object, that is unparsed from + # this Hash instance. + # _state_ is a JSON::State object, that can also be used to configure the + # produced JSON string output further. + # _depth_ is used to find out nesting depth, to indent accordingly. + def to_json(state = nil, *) + state = State.from_state(state) + state.check_max_nesting + json_transform(state) + end + + private + + def json_shift(state) + state.object_nl.empty? or return '' + state.indent * state.depth + end + + def json_transform(state) + delim = ',' + delim << state.object_nl + result = '{' + result << state.object_nl + depth = state.depth += 1 + first = true + indent = !state.object_nl.empty? + each { |key,value| + result << delim unless first + result << state.indent * depth if indent + result << key.to_s.to_json(state) + result << state.space_before + result << ':' + result << state.space + result << value.to_json(state) + first = false + } + depth = state.depth -= 1 + result << state.object_nl + result << state.indent * depth if indent + result << '}' + result + end + end + + module Array + # Returns a JSON string containing a JSON array, that is unparsed from + # this Array instance. + # _state_ is a JSON::State object, that can also be used to configure the + # produced JSON string output further. + def to_json(state = nil, *) + state = State.from_state(state) + state.check_max_nesting + json_transform(state) + end + + private + + def json_transform(state) + delim = ',' + delim << state.array_nl + result = '[' + result << state.array_nl + depth = state.depth += 1 + first = true + indent = !state.array_nl.empty? + each { |value| + result << delim unless first + result << state.indent * depth if indent + result << value.to_json(state) + first = false + } + depth = state.depth -= 1 + result << state.array_nl + result << state.indent * depth if indent + result << ']' + end + end + + module Integer + # Returns a JSON string representation for this Integer number. + def to_json(*) to_s end + end + + module Float + # Returns a JSON string representation for this Float number. + def to_json(state = nil, *) + state = State.from_state(state) + case + when infinite? + if state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + when nan? + if state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + else + to_s + end + end + end + + module String + if defined?(::Encoding) + # This string should be encoded with UTF-8 A call to this method + # returns a JSON string encoded with UTF16 big endian characters as + # \u????. + def to_json(state = nil, *args) + state = State.from_state(state) + if encoding == ::Encoding::UTF_8 + string = self + else + string = encode(::Encoding::UTF_8) + end + if state.ascii_only? + '"' << JSON.utf8_to_json_ascii(string) << '"' + else + '"' << JSON.utf8_to_json(string) << '"' + end + end + else + # This string should be encoded with UTF-8 A call to this method + # returns a JSON string encoded with UTF16 big endian characters as + # \u????. + def to_json(state = nil, *args) + state = State.from_state(state) + if state.ascii_only? + '"' << JSON.utf8_to_json_ascii(self) << '"' + else + '"' << JSON.utf8_to_json(self) << '"' + end + end + end + + # Module that holds the extinding methods if, the String module is + # included. + module Extend + # Raw Strings are JSON Objects (the raw bytes are stored in an + # array for the key "raw"). The Ruby String can be created by this + # module method. + def json_create(o) + o['raw'].pack('C*') + end + end + + # Extends _modul_ with the String::Extend module. + def self.included(modul) + modul.extend Extend + end + + # This method creates a raw object hash, that can be nested into + # other data structures and will be unparsed as a raw string. This + # method should be used, if you want to convert raw strings to JSON + # instead of UTF-8 strings, e. g. binary data. + def to_json_raw_object + { + JSON.create_id => self.class.name, + 'raw' => self.unpack('C*'), + } + end + + # This method creates a JSON text from the result of + # a call to to_json_raw_object of this String. + def to_json_raw(*args) + to_json_raw_object.to_json(*args) + end + end + + module TrueClass + # Returns a JSON string for true: 'true'. + def to_json(*) 'true' end + end + + module FalseClass + # Returns a JSON string for false: 'false'. + def to_json(*) 'false' end + end + + module NilClass + # Returns a JSON string for nil: 'null'. + def to_json(*) 'null' end + end + end + end + end +end diff --git a/lib/ruby/shared/json/pure/parser.rb b/lib/ruby/shared/json/pure/parser.rb new file mode 100644 index 00000000000..a41d1eeb1c6 --- /dev/null +++ b/lib/ruby/shared/json/pure/parser.rb @@ -0,0 +1,359 @@ +require 'strscan' + +module JSON + module Pure + # This class implements the JSON parser that is used to parse a JSON string + # into a Ruby data structure. + class Parser < StringScanner + STRING = /" ((?:[^\x0-\x1f"\\] | + # escaped special characters: + \\["\\\/bfnrt] | + \\u[0-9a-fA-F]{4} | + # match all but escaped special characters: + \\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*) + "/nx + INTEGER = /(-?0|-?[1-9]\d*)/ + FLOAT = /(-? + (?:0|[1-9]\d*) + (?: + \.\d+(?i:e[+-]?\d+) | + \.\d+ | + (?i:e[+-]?\d+) + ) + )/x + NAN = /NaN/ + INFINITY = /Infinity/ + MINUS_INFINITY = /-Infinity/ + OBJECT_OPEN = /\{/ + OBJECT_CLOSE = /\}/ + ARRAY_OPEN = /\[/ + ARRAY_CLOSE = /\]/ + PAIR_DELIMITER = /:/ + COLLECTION_DELIMITER = /,/ + TRUE = /true/ + FALSE = /false/ + NULL = /null/ + IGNORE = %r( + (?: + //[^\n\r]*[\n\r]| # line comments + /\* # c-style comments + (?: + [^*/]| # normal chars + /[^*]| # slashes that do not start a nested comment + \*[^/]| # asterisks that do not end this comment + /(?=\*/) # single slash before this comment's end + )* + \*/ # the End of this comment + |[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr + )+ + )mx + + UNPARSED = Object.new + + # Creates a new JSON::Pure::Parser instance for the string _source_. + # + # It will be configured by the _opts_ hash. _opts_ can have the following + # keys: + # * *max_nesting*: The maximum depth of nesting allowed in the parsed data + # structures. Disable depth checking with :max_nesting => false|nil|0, + # it defaults to 100. + # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in + # defiance of RFC 4627 to be parsed by the Parser. This option defaults + # to false. + # * *symbolize_names*: If set to true, returns symbols for the names + # (keys) in a JSON object. Otherwise strings are returned, which is also + # the default. + # * *create_additions*: If set to true, the Parser creates + # additions when if a matching class and create_id was found. This + # option defaults to false. + # * *object_class*: Defaults to Hash + # * *array_class*: Defaults to Array + # * *quirks_mode*: Enables quirks_mode for parser, that is for example + # parsing single JSON values instead of documents is possible. + def initialize(source, opts = {}) + opts ||= {} + unless @quirks_mode = opts[:quirks_mode] + source = convert_encoding source + end + super source + if !opts.key?(:max_nesting) # defaults to 100 + @max_nesting = 100 + elsif opts[:max_nesting] + @max_nesting = opts[:max_nesting] + else + @max_nesting = 0 + end + @allow_nan = !!opts[:allow_nan] + @symbolize_names = !!opts[:symbolize_names] + if opts.key?(:create_additions) + @create_additions = !!opts[:create_additions] + else + @create_additions = false + end + @create_id = @create_additions ? JSON.create_id : nil + @object_class = opts[:object_class] || Hash + @array_class = opts[:array_class] || Array + @match_string = opts[:match_string] + end + + alias source string + + def quirks_mode? + !!@quirks_mode + end + + def reset + super + @current_nesting = 0 + end + + # Parses the current JSON string _source_ and returns the complete data + # structure as a result. + def parse + reset + obj = nil + if @quirks_mode + while !eos? && skip(IGNORE) + end + if eos? + raise ParserError, "source did not contain any JSON!" + else + obj = parse_value + obj == UNPARSED and raise ParserError, "source did not contain any JSON!" + end + else + until eos? + case + when scan(OBJECT_OPEN) + obj and raise ParserError, "source '#{peek(20)}' not in JSON!" + @current_nesting = 1 + obj = parse_object + when scan(ARRAY_OPEN) + obj and raise ParserError, "source '#{peek(20)}' not in JSON!" + @current_nesting = 1 + obj = parse_array + when skip(IGNORE) + ; + else + raise ParserError, "source '#{peek(20)}' not in JSON!" + end + end + obj or raise ParserError, "source did not contain any JSON!" + end + obj + end + + private + + def convert_encoding(source) + if source.respond_to?(:to_str) + source = source.to_str + else + raise TypeError, "#{source.inspect} is not like a string" + end + if defined?(::Encoding) + if source.encoding == ::Encoding::ASCII_8BIT + b = source[0, 4].bytes.to_a + source = + case + when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0 + source.dup.force_encoding(::Encoding::UTF_32BE).encode!(::Encoding::UTF_8) + when b.size >= 4 && b[0] == 0 && b[2] == 0 + source.dup.force_encoding(::Encoding::UTF_16BE).encode!(::Encoding::UTF_8) + when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0 + source.dup.force_encoding(::Encoding::UTF_32LE).encode!(::Encoding::UTF_8) + when b.size >= 4 && b[1] == 0 && b[3] == 0 + source.dup.force_encoding(::Encoding::UTF_16LE).encode!(::Encoding::UTF_8) + else + source.dup + end + else + source = source.encode(::Encoding::UTF_8) + end + source.force_encoding(::Encoding::ASCII_8BIT) + else + b = source + source = + case + when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0 + JSON.iconv('utf-8', 'utf-32be', b) + when b.size >= 4 && b[0] == 0 && b[2] == 0 + JSON.iconv('utf-8', 'utf-16be', b) + when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0 + JSON.iconv('utf-8', 'utf-32le', b) + when b.size >= 4 && b[1] == 0 && b[3] == 0 + JSON.iconv('utf-8', 'utf-16le', b) + else + b + end + end + source + end + + # Unescape characters in strings. + UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr } + UNESCAPE_MAP.update({ + ?" => '"', + ?\\ => '\\', + ?/ => '/', + ?b => "\b", + ?f => "\f", + ?n => "\n", + ?r => "\r", + ?t => "\t", + ?u => nil, + }) + + EMPTY_8BIT_STRING = '' + if ::String.method_defined?(:encode) + EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT + end + + def parse_string + if scan(STRING) + return '' if self[1].empty? + string = self[1].gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c| + if u = UNESCAPE_MAP[$&[1]] + u + else # \uXXXX + bytes = EMPTY_8BIT_STRING.dup + i = 0 + while c[6 * i] == ?\\ && c[6 * i + 1] == ?u + bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16) + i += 1 + end + JSON.iconv('utf-8', 'utf-16be', bytes) + end + end + if string.respond_to?(:force_encoding) + string.force_encoding(::Encoding::UTF_8) + end + if @create_additions and @match_string + for (regexp, klass) in @match_string + klass.json_creatable? or next + string =~ regexp and return klass.json_create(string) + end + end + string + else + UNPARSED + end + rescue => e + raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}" + end + + def parse_value + case + when scan(FLOAT) + Float(self[1]) + when scan(INTEGER) + Integer(self[1]) + when scan(TRUE) + true + when scan(FALSE) + false + when scan(NULL) + nil + when (string = parse_string) != UNPARSED + string + when scan(ARRAY_OPEN) + @current_nesting += 1 + ary = parse_array + @current_nesting -= 1 + ary + when scan(OBJECT_OPEN) + @current_nesting += 1 + obj = parse_object + @current_nesting -= 1 + obj + when @allow_nan && scan(NAN) + NaN + when @allow_nan && scan(INFINITY) + Infinity + when @allow_nan && scan(MINUS_INFINITY) + MinusInfinity + else + UNPARSED + end + end + + def parse_array + raise NestingError, "nesting of #@current_nesting is too deep" if + @max_nesting.nonzero? && @current_nesting > @max_nesting + result = @array_class.new + delim = false + until eos? + case + when (value = parse_value) != UNPARSED + delim = false + result << value + skip(IGNORE) + if scan(COLLECTION_DELIMITER) + delim = true + elsif match?(ARRAY_CLOSE) + ; + else + raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!" + end + when scan(ARRAY_CLOSE) + if delim + raise ParserError, "expected next element in array at '#{peek(20)}'!" + end + break + when skip(IGNORE) + ; + else + raise ParserError, "unexpected token in array at '#{peek(20)}'!" + end + end + result + end + + def parse_object + raise NestingError, "nesting of #@current_nesting is too deep" if + @max_nesting.nonzero? && @current_nesting > @max_nesting + result = @object_class.new + delim = false + until eos? + case + when (string = parse_string) != UNPARSED + skip(IGNORE) + unless scan(PAIR_DELIMITER) + raise ParserError, "expected ':' in object at '#{peek(20)}'!" + end + skip(IGNORE) + unless (value = parse_value).equal? UNPARSED + result[@symbolize_names ? string.to_sym : string] = value + delim = false + skip(IGNORE) + if scan(COLLECTION_DELIMITER) + delim = true + elsif match?(OBJECT_CLOSE) + ; + else + raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!" + end + else + raise ParserError, "expected value in object at '#{peek(20)}'!" + end + when scan(OBJECT_CLOSE) + if delim + raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!" + end + if @create_additions and klassname = result[@create_id] + klass = JSON.deep_const_get klassname + break unless klass and klass.json_creatable? + result = klass.json_create(result) + end + break + when skip(IGNORE) + ; + else + raise ParserError, "unexpected token in object at '#{peek(20)}'!" + end + end + result + end + end + end +end diff --git a/lib/ruby/shared/json/version.rb b/lib/ruby/shared/json/version.rb new file mode 100644 index 00000000000..935484524bd --- /dev/null +++ b/lib/ruby/shared/json/version.rb @@ -0,0 +1,8 @@ +module JSON + # JSON version + VERSION = '1.8.0' + VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc: + VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc: + VERSION_MINOR = VERSION_ARRAY[1] # :nodoc: + VERSION_BUILD = VERSION_ARRAY[2] # :nodoc: +end diff --git a/lib/ruby/shared/openssl/bn.rb b/lib/ruby/shared/openssl/bn.rb new file mode 100644 index 00000000000..a13ff4fb284 --- /dev/null +++ b/lib/ruby/shared/openssl/bn.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + load('jopenssl21/openssl/bn.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/bn.rb') +else + load('jopenssl18/openssl/bn.rb') +end diff --git a/lib/ruby/shared/openssl/buffering.rb b/lib/ruby/shared/openssl/buffering.rb new file mode 100644 index 00000000000..e785c359b57 --- /dev/null +++ b/lib/ruby/shared/openssl/buffering.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + load('jopenssl21/openssl/buffering.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/buffering.rb') +else + load('jopenssl18/openssl/buffering.rb') +end diff --git a/lib/ruby/shared/openssl/cipher.rb b/lib/ruby/shared/openssl/cipher.rb new file mode 100644 index 00000000000..1a7d40a110a --- /dev/null +++ b/lib/ruby/shared/openssl/cipher.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + load('jopenssl21/openssl/cipher.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/cipher.rb') +else + load('jopenssl18/openssl/cipher.rb') +end diff --git a/lib/ruby/shared/openssl/config.rb b/lib/ruby/shared/openssl/config.rb new file mode 100644 index 00000000000..c06e8d79df7 --- /dev/null +++ b/lib/ruby/shared/openssl/config.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + load('jopenssl21/openssl/config.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/config.rb') +else + load('jopenssl18/openssl/config.rb') +end diff --git a/lib/ruby/shared/openssl/digest.rb b/lib/ruby/shared/openssl/digest.rb new file mode 100644 index 00000000000..6e24ccf66bb --- /dev/null +++ b/lib/ruby/shared/openssl/digest.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + load('jopenssl21/openssl/digest.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/digest.rb') +else + load('jopenssl18/openssl/digest.rb') +end diff --git a/lib/ruby/shared/openssl/pkcs12.rb b/lib/ruby/shared/openssl/pkcs12.rb new file mode 100644 index 00000000000..e1884515b5f --- /dev/null +++ b/lib/ruby/shared/openssl/pkcs12.rb @@ -0,0 +1,106 @@ +require 'java' + +module OpenSSL + class PKCS12 + class PKCS12Error < OpenSSLError + end + + java_import java.io.StringReader + java_import java.io.StringBufferInputStream + java_import java.io.ByteArrayOutputStream + java_import 'org.jruby.ext.openssl.PEMUtils' + java_import 'org.jruby.ext.openssl.SecurityHelper' + + def self.create(pass, name, key, cert, ca = nil) + pkcs12 = self.new + pkcs12.generate(pass, name, key, cert, ca) + pkcs12 + end + + attr_reader :key, :certificate, :ca_certs + + def initialize(str = nil, password = '') + return @der = nil unless str + + if str.is_a?(File) + file = File.open(str.path, "rb") + @der = file.read + file.close + else + str.force_encoding(Encoding::ASCII_8BIT) + @der = str + end + + p12_input_stream = StringBufferInputStream.new(@der) + + store = SecurityHelper.getKeyStore("PKCS12") + store.load(p12_input_stream, password.to_java.to_char_array) + + aliases = store.aliases + aliases.each do |alias_name| + if store.is_key_entry(alias_name) + if java_certificate = store.get_certificate(alias_name) + der = String.from_java_bytes(java_certificate.get_encoded) + @certificate = OpenSSL::X509::Certificate.new(der) + end + + java_key = store.get_key(alias_name, password.to_java.to_char_array) + if java_key + der = String.from_java_bytes(java_key.get_encoded) + algorithm = java_key.get_algorithm + if algorithm == "RSA" + @key = OpenSSL::PKey::RSA.new(der) + elsif algorithm == "DSA" + @key = OpenSSL::PKey::DSA.new(der) + elsif algorithm == "DH" + @key = OpenSSL::PKey::DH.new(der) + elsif algorithm == "EC" + @key = OpenSSL::PKey::EC.new(der) + else + raise PKCS12Error, "Unknown key algorithm #{algorithm}" + end + end + + @ca_certs = Array.new + java_ca_certs = store.get_certificate_chain(alias_name) + if java_ca_certs + java_ca_certs.each do |java_ca_cert| + der = String.from_java_bytes(java_ca_cert.get_encoded) + ruby_cert = OpenSSL::X509::Certificate.new(der) + if (ruby_cert.to_pem != @certificate.to_pem) + @ca_certs << ruby_cert + end + end + end + end + break + end + rescue java.lang.Exception => e + raise PKCS12Error, "Exception: #{e}" + end + + def generate(pass, alias_name, key, cert, ca = nil) + @key, @certificate, @ca_certs = key, cert, ca + + certificates = cert.to_pem + ca.each { |ca_cert| certificates << ca_cert.to_pem } if ca + + begin + der_bytes = PEMUtils.generatePKCS12( + StringReader.new(key.to_pem), certificates.to_java_bytes, + alias_name, ( pass.nil? ? "" : pass ).to_java.to_char_array + ) + rescue java.security.KeyStoreException, java.security.cert.CertificateException => e + raise PKCS12Error, e.message + rescue java.security.GeneralSecurityException, java.io.IOException => e + raise PKCS12Error, "Exception: #{e}" + end + + @der = String.from_java_bytes(der_bytes) + end + + def to_der + @der + end + end +end diff --git a/lib/ruby/shared/openssl/pkcs7.rb b/lib/ruby/shared/openssl/pkcs7.rb new file mode 100644 index 00000000000..1e347f78a7f --- /dev/null +++ b/lib/ruby/shared/openssl/pkcs7.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + raise LoadError, "no such library in 2.1: openssl/pkcs7" +elsif RUBY_VERSION >= '1.9.0' + raise LoadError, "no such library in 1.9: openssl/pkcs7" +else + load('jopenssl18/openssl/pkcs7.rb') +end diff --git a/lib/ruby/shared/openssl/ssl-internal.rb b/lib/ruby/shared/openssl/ssl-internal.rb new file mode 100644 index 00000000000..0809a9c31ca --- /dev/null +++ b/lib/ruby/shared/openssl/ssl-internal.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + raise LoadError, "no such library in 2.1: openssl/ssl-internal.rb" +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/ssl-internal.rb') +else + load('jopenssl18/openssl/ssl-internal.rb') +end diff --git a/lib/ruby/shared/openssl/ssl.rb b/lib/ruby/shared/openssl/ssl.rb new file mode 100644 index 00000000000..37315622814 --- /dev/null +++ b/lib/ruby/shared/openssl/ssl.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + load('jopenssl21/openssl/ssl.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/ssl.rb') +else + load('jopenssl18/openssl/ssl.rb') +end diff --git a/lib/ruby/shared/openssl/x509-internal.rb b/lib/ruby/shared/openssl/x509-internal.rb new file mode 100644 index 00000000000..889f83e2b6d --- /dev/null +++ b/lib/ruby/shared/openssl/x509-internal.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + raise LoadError, "no such library in 2.1: openssl/x509-internal.rb" +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/x509-internal.rb') +else + load('jopenssl18/openssl/x509-internal.rb') +end diff --git a/lib/ruby/shared/openssl/x509.rb b/lib/ruby/shared/openssl/x509.rb new file mode 100644 index 00000000000..7ea96165022 --- /dev/null +++ b/lib/ruby/shared/openssl/x509.rb @@ -0,0 +1,7 @@ +if RUBY_VERSION >= '2.1.0' + load('jopenssl21/openssl/x509.rb') +elsif RUBY_VERSION >= '1.9.0' + load('jopenssl19/openssl/x509.rb') +else + load('jopenssl18/openssl/x509.rb') +end diff --git a/lib/ruby/shared/org/bouncycastle/bcpkix-jdk15on/1.49/bcpkix-jdk15on-1.49.jar b/lib/ruby/shared/org/bouncycastle/bcpkix-jdk15on/1.49/bcpkix-jdk15on-1.49.jar new file mode 100644 index 00000000000..96d1985565c Binary files /dev/null and b/lib/ruby/shared/org/bouncycastle/bcpkix-jdk15on/1.49/bcpkix-jdk15on-1.49.jar differ diff --git a/lib/ruby/shared/org/bouncycastle/bcprov-jdk15on/1.49/bcprov-jdk15on-1.49.jar b/lib/ruby/shared/org/bouncycastle/bcprov-jdk15on/1.49/bcprov-jdk15on-1.49.jar new file mode 100644 index 00000000000..e1d4bb31ce1 Binary files /dev/null and b/lib/ruby/shared/org/bouncycastle/bcprov-jdk15on/1.49/bcprov-jdk15on-1.49.jar differ diff --git a/lib/ruby/shared/rake.rb b/lib/ruby/shared/rake.rb new file mode 100644 index 00000000000..531cfc82b83 --- /dev/null +++ b/lib/ruby/shared/rake.rb @@ -0,0 +1,73 @@ +#-- + +# Copyright 2003-2010 by Jim Weirich (jim.weirich@gmail.com) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +#++ + +require 'rake/version' + +# :stopdoc: +RAKEVERSION = Rake::VERSION +# :startdoc: + +require 'rbconfig' +require 'fileutils' +require 'singleton' +require 'monitor' +require 'optparse' +require 'ostruct' + +require 'rake/ext/module' +require 'rake/ext/string' +require 'rake/ext/time' + +require 'rake/win32' + +require 'rake/linked_list' +require 'rake/scope' +require 'rake/task_argument_error' +require 'rake/rule_recursion_overflow_error' +require 'rake/rake_module' +require 'rake/trace_output' +require 'rake/pseudo_status' +require 'rake/task_arguments' +require 'rake/invocation_chain' +require 'rake/task' +require 'rake/file_task' +require 'rake/file_creation_task' +require 'rake/multi_task' +require 'rake/dsl_definition' +require 'rake/file_utils_ext' +require 'rake/file_list' +require 'rake/default_loader' +require 'rake/early_time' +require 'rake/name_space' +require 'rake/task_manager' +require 'rake/application' +require 'rake/backtrace' + +$trace = false + +# :stopdoc: +# +# Some top level Constants. + +FileList = Rake::FileList +RakeFileUtils = Rake::FileUtilsExt diff --git a/lib/ruby/shared/rake/alt_system.rb b/lib/ruby/shared/rake/alt_system.rb new file mode 100644 index 00000000000..a42597bf090 --- /dev/null +++ b/lib/ruby/shared/rake/alt_system.rb @@ -0,0 +1,108 @@ +# +# Copyright (c) 2008 James M. Lawrence +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +require 'rbconfig' + +# +# Alternate implementations of system() and backticks `` on Windows +# for ruby-1.8 and earlier. +# +module Rake::AltSystem + WINDOWS = RbConfig::CONFIG["host_os"] =~ + %r!(msdos|mswin|djgpp|mingw|[Ww]indows)! + + class << self + def define_module_function(name, &block) + define_method(name, &block) + module_function(name) + end + end + + if WINDOWS && RUBY_VERSION < "1.9.0" + RUNNABLE_EXTS = %w[com exe bat cmd] + RUNNABLE_PATTERN = %r!\.(#{RUNNABLE_EXTS.join('|')})\Z!i + + define_module_function :kernel_system, &Kernel.method(:system) + define_module_function :kernel_backticks, &Kernel.method(:'`') + + module_function + + def repair_command(cmd) + "call " + ( + if cmd =~ %r!\A\s*\".*?\"! + # already quoted + cmd + elsif match = cmd.match(%r!\A\s*(\S+)!) + if match[1] =~ %r!/! + # avoid x/y.bat interpretation as x with option /y + %Q!"#{match[1]}"! + match.post_match + else + # a shell command will fail if quoted + cmd + end + else + # empty or whitespace + cmd + end + ) + end + + def find_runnable(file) + if file =~ RUNNABLE_PATTERN + file + else + RUNNABLE_EXTS.each { |ext| + test = "#{file}.#{ext}" + return test if File.exist?(test) + } + nil + end + end + + def system(cmd, *args) + repaired = ( + if args.empty? + [repair_command(cmd)] + elsif runnable = find_runnable(cmd) + [File.expand_path(runnable), *args] + else + # non-existent file + [cmd, *args] + end + ) + kernel_system(*repaired) + end + + def backticks(cmd) + kernel_backticks(repair_command(cmd)) + end + + define_module_function :'`', &method(:backticks) + else + # Non-Windows or ruby-1.9+: same as Kernel versions + define_module_function :system, &Kernel.method(:system) + define_module_function :backticks, &Kernel.method(:'`') + define_module_function :'`', &Kernel.method(:'`') + end +end diff --git a/lib/ruby/shared/rake/application.rb b/lib/ruby/shared/rake/application.rb new file mode 100644 index 00000000000..b76244b7a38 --- /dev/null +++ b/lib/ruby/shared/rake/application.rb @@ -0,0 +1,728 @@ +require 'shellwords' +require 'optparse' + +require 'rake/task_manager' +require 'rake/file_list' +require 'rake/thread_pool' +require 'rake/thread_history_display' +require 'rake/trace_output' +require 'rake/win32' + +module Rake + + CommandLineOptionError = Class.new(StandardError) + + ###################################################################### + # Rake main application object. When invoking +rake+ from the + # command line, a Rake::Application object is created and run. + # + class Application + include TaskManager + include TraceOutput + + # The name of the application (typically 'rake') + attr_reader :name + + # The original directory where rake was invoked. + attr_reader :original_dir + + # Name of the actual rakefile used. + attr_reader :rakefile + + # Number of columns on the terminal + attr_accessor :terminal_columns + + # List of the top level task names (task names from the command line). + attr_reader :top_level_tasks + + DEFAULT_RAKEFILES = [ + 'rakefile', + 'Rakefile', + 'rakefile.rb', + 'Rakefile.rb' + ].freeze + + # Initialize a Rake::Application object. + def initialize + super + @name = 'rake' + @rakefiles = DEFAULT_RAKEFILES.dup + @rakefile = nil + @pending_imports = [] + @imported = [] + @loaders = {} + @default_loader = Rake::DefaultLoader.new + @original_dir = Dir.pwd + @top_level_tasks = [] + add_loader('rb', DefaultLoader.new) + add_loader('rf', DefaultLoader.new) + add_loader('rake', DefaultLoader.new) + @tty_output = STDOUT.tty? + @terminal_columns = ENV['RAKE_COLUMNS'].to_i + end + + # Run the Rake application. The run method performs the following + # three steps: + # + # * Initialize the command line options (+init+). + # * Define the tasks (+load_rakefile+). + # * Run the top level tasks (+top_level+). + # + # If you wish to build a custom rake command, you should call + # +init+ on your application. Then define any tasks. Finally, + # call +top_level+ to run your top level tasks. + def run + standard_exception_handling do + init + load_rakefile + top_level + end + end + + # Initialize the command line parameters and app name. + def init(app_name='rake') + standard_exception_handling do + @name = app_name + handle_options + collect_tasks + end + end + + # Find the rakefile and then load it and any pending imports. + def load_rakefile + standard_exception_handling do + raw_load_rakefile + end + end + + # Run the top level tasks of a Rake application. + def top_level + run_with_threads do + if options.show_tasks + display_tasks_and_comments + elsif options.show_prereqs + display_prerequisites + else + top_level_tasks.each { |task_name| invoke_task(task_name) } + end + end + end + + # Run the given block with the thread startup and shutdown. + def run_with_threads + thread_pool.gather_history if options.job_stats == :history + + yield + + thread_pool.join + if options.job_stats + stats = thread_pool.statistics + puts "Maximum active threads: #{stats[:max_active_threads]}" + puts "Total threads in play: #{stats[:total_threads_in_play]}" + end + ThreadHistoryDisplay.new(thread_pool.history).show if + options.job_stats == :history + end + + # Add a loader to handle imported files ending in the extension + # +ext+. + def add_loader(ext, loader) + ext = ".#{ext}" unless ext =~ /^\./ + @loaders[ext] = loader + end + + # Application options from the command line + def options + @options ||= OpenStruct.new + end + + # Return the thread pool used for multithreaded processing. + def thread_pool # :nodoc: + @thread_pool ||= ThreadPool.new(options.thread_pool_size || FIXNUM_MAX) + end + + # private ---------------------------------------------------------------- + + def invoke_task(task_string) + name, args = parse_task_string(task_string) + t = self[name] + t.invoke(*args) + end + + def parse_task_string(string) + if string =~ /^([^\[]+)(\[(.*)\])$/ + name = $1 + args = $3.split(/\s*,\s*/) + else + name = string + args = [] + end + [name, args] + end + + # Provide standard exception handling for the given block. + def standard_exception_handling + yield + rescue SystemExit + # Exit silently with current status + raise + rescue OptionParser::InvalidOption => ex + $stderr.puts ex.message + exit(false) + rescue Exception => ex + # Exit with error message + display_error_message(ex) + exit_because_of_exception(ex) + end + + # Exit the program because of an unhandle exception. + # (may be overridden by subclasses) + def exit_because_of_exception(ex) + exit(false) + end + + # Display the error message that caused the exception. + def display_error_message(ex) + trace "#{name} aborted!" + trace ex.message + if options.backtrace + trace ex.backtrace.join("\n") + else + trace Backtrace.collapse(ex.backtrace).join("\n") + end + trace "Tasks: #{ex.chain}" if has_chain?(ex) + trace "(See full trace by running task with --trace)" unless + options.backtrace + end + + # Warn about deprecated usage. + # + # Example: + # Rake.application.deprecate("import", "Rake.import", caller.first) + # + def deprecate(old_usage, new_usage, call_site) + unless options.ignore_deprecate + $stderr.puts "WARNING: '#{old_usage}' is deprecated. " + + "Please use '#{new_usage}' instead.\n" + + " at #{call_site}" + end + end + + # Does the exception have a task invocation chain? + def has_chain?(exception) + exception.respond_to?(:chain) && exception.chain + end + private :has_chain? + + # True if one of the files in RAKEFILES is in the current directory. + # If a match is found, it is copied into @rakefile. + def have_rakefile + @rakefiles.each do |fn| + if File.exist?(fn) + others = FileList.glob(fn, File::FNM_CASEFOLD) + return others.size == 1 ? others.first : fn + elsif fn == '' + return fn + end + end + return nil + end + + # True if we are outputting to TTY, false otherwise + def tty_output? + @tty_output + end + + # Override the detected TTY output state (mostly for testing) + def tty_output=(tty_output_state) + @tty_output = tty_output_state + end + + # We will truncate output if we are outputting to a TTY or if we've been + # given an explicit column width to honor + def truncate_output? + tty_output? || @terminal_columns.nonzero? + end + + # Display the tasks and comments. + def display_tasks_and_comments + displayable_tasks = tasks.select { |t| + (options.show_all_tasks || t.comment) && + t.name =~ options.show_task_pattern + } + case options.show_tasks + when :tasks + width = displayable_tasks.map { |t| t.name_with_args.length }.max || 10 + if truncate_output? + max_column = terminal_width - name.size - width - 7 + else + max_column = nil + end + + displayable_tasks.each do |t| + printf("#{name} %-#{width}s # %s\n", + t.name_with_args, + max_column ? truncate(t.comment, max_column) : t.comment) + end + when :describe + displayable_tasks.each do |t| + puts "#{name} #{t.name_with_args}" + comment = t.full_comment || "" + comment.split("\n").each do |line| + puts " #{line}" + end + puts + end + when :lines + displayable_tasks.each do |t| + t.locations.each do |loc| + printf "#{name} %-30s %s\n", t.name_with_args, loc + end + end + else + fail "Unknown show task mode: '#{options.show_tasks}'" + end + end + + def terminal_width + if @terminal_columns.nonzero? + result = @terminal_columns + else + result = unix? ? dynamic_width : 80 + end + (result < 10) ? 80 : result + rescue + 80 + end + + # Calculate the dynamic width of the + def dynamic_width + @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) + end + + def dynamic_width_stty + %x{stty size 2>/dev/null}.split[1].to_i + end + + def dynamic_width_tput + %x{tput cols 2>/dev/null}.to_i + end + + def unix? + RbConfig::CONFIG['host_os'] =~ + /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i + end + + def windows? + Win32.windows? + end + + def truncate(string, width) + if string.nil? + "" + elsif string.length <= width + string + else + (string[0, width - 3] || "") + "..." + end + end + + # Display the tasks and prerequisites + def display_prerequisites + tasks.each do |t| + puts "#{name} #{t.name}" + t.prerequisites.each { |pre| puts " #{pre}" } + end + end + + def trace(*strings) + options.trace_output ||= $stderr + trace_on(options.trace_output, *strings) + end + + def sort_options(options) + options.sort_by { |opt| + opt.select { |o| o =~ /^-/ }.map { |o| o.downcase }.sort.reverse + } + end + private :sort_options + + # A list of all the standard options used in rake, suitable for + # passing to OptionParser. + def standard_rake_options + sort_options( + [ + ['--all', '-A', + "Show all tasks, even uncommented ones", + lambda { |value| + options.show_all_tasks = value + } + ], + ['--backtrace=[OUT]', + "Enable full backtrace. OUT can be stderr (default) or stdout.", + lambda { |value| + options.backtrace = true + select_trace_output(options, 'backtrace', value) + } + ], + ['--comments', + "Show commented tasks only", + lambda { |value| + options.show_all_tasks = !value + } + ], + ['--describe', '-D [PATTERN]', + "Describe the tasks (matching optional PATTERN), then exit.", + lambda { |value| + select_tasks_to_show(options, :describe, value) + } + ], + ['--dry-run', '-n', + "Do a dry run without executing actions.", + lambda { |value| + Rake.verbose(true) + Rake.nowrite(true) + options.dryrun = true + options.trace = true + } + ], + ['--execute', '-e CODE', + "Execute some Ruby code and exit.", + lambda { |value| + eval(value) + exit + } + ], + ['--execute-print', '-p CODE', + "Execute some Ruby code, print the result, then exit.", + lambda { |value| + puts eval(value) + exit + } + ], + ['--execute-continue', '-E CODE', + "Execute some Ruby code, " + + "then continue with normal task processing.", + lambda { |value| eval(value) } + ], + ['--jobs', '-j [NUMBER]', + "Specifies the maximum number of tasks to execute in parallel. " + + "(default is 2)", + lambda { |value| + options.thread_pool_size = [(value || 2).to_i, 2].max + } + ], + ['--job-stats [LEVEL]', + "Display job statistics. " + + "LEVEL=history displays a complete job list", + lambda { |value| + if value =~ /^history/i + options.job_stats = :history + else + options.job_stats = true + end + } + ], + ['--libdir', '-I LIBDIR', + "Include LIBDIR in the search path for required modules.", + lambda { |value| $:.push(value) } + ], + ['--multitask', '-m', + "Treat all tasks as multitasks.", + lambda { |value| options.always_multitask = true } + ], + ['--no-search', '--nosearch', + '-N', "Do not search parent directories for the Rakefile.", + lambda { |value| options.nosearch = true } + ], + ['--prereqs', '-P', + "Display the tasks and dependencies, then exit.", + lambda { |value| options.show_prereqs = true } + ], + ['--quiet', '-q', + "Do not log messages to standard output.", + lambda { |value| Rake.verbose(false) } + ], + ['--rakefile', '-f [FILE]', + "Use FILE as the rakefile.", + lambda { |value| + value ||= '' + @rakefiles.clear + @rakefiles << value + } + ], + ['--rakelibdir', '--rakelib', '-R RAKELIBDIR', + "Auto-import any .rake files in RAKELIBDIR. " + + "(default is 'rakelib')", + lambda { |value| + options.rakelib = value.split(File::PATH_SEPARATOR) + } + ], + ['--require', '-r MODULE', + "Require MODULE before executing rakefile.", + lambda { |value| + begin + require value + rescue LoadError => ex + begin + rake_require value + rescue LoadError + raise ex + end + end + } + ], + ['--rules', + "Trace the rules resolution.", + lambda { |value| options.trace_rules = true } + ], + ['--silent', '-s', + "Like --quiet, but also suppresses the " + + "'in directory' announcement.", + lambda { |value| + Rake.verbose(false) + options.silent = true + } + ], + ['--suppress-backtrace PATTERN', + "Suppress backtrace lines matching regexp PATTERN. " + + "Ignored if --trace is on.", + lambda { |value| + options.suppress_backtrace_pattern = Regexp.new(value) + } + ], + ['--system', '-g', + "Using system wide (global) rakefiles " + + "(usually '~/.rake/*.rake').", + lambda { |value| options.load_system = true } + ], + ['--no-system', '--nosystem', '-G', + "Use standard project Rakefile search paths, " + + "ignore system wide rakefiles.", + lambda { |value| options.ignore_system = true } + ], + ['--tasks', '-T [PATTERN]', + "Display the tasks (matching optional PATTERN) " + + "with descriptions, then exit.", + lambda { |value| + select_tasks_to_show(options, :tasks, value) + } + ], + ['--trace=[OUT]', '-t', + "Turn on invoke/execute tracing, enable full backtrace. " + + "OUT can be stderr (default) or stdout.", + lambda { |value| + options.trace = true + options.backtrace = true + select_trace_output(options, 'trace', value) + Rake.verbose(true) + } + ], + ['--verbose', '-v', + "Log message to standard output.", + lambda { |value| Rake.verbose(true) } + ], + ['--version', '-V', + "Display the program version.", + lambda { |value| + puts "rake, version #{RAKEVERSION}" + exit + } + ], + ['--where', '-W [PATTERN]', + "Describe the tasks (matching optional PATTERN), then exit.", + lambda { |value| + select_tasks_to_show(options, :lines, value) + options.show_all_tasks = true + } + ], + ['--no-deprecation-warnings', '-X', + "Disable the deprecation warnings.", + lambda { |value| + options.ignore_deprecate = true + } + ], + ]) + end + + def select_tasks_to_show(options, show_tasks, value) + options.show_tasks = show_tasks + options.show_task_pattern = Regexp.new(value || '') + Rake::TaskManager.record_task_metadata = true + end + private :select_tasks_to_show + + def select_trace_output(options, trace_option, value) + value = value.strip unless value.nil? + case value + when 'stdout' + options.trace_output = $stdout + when 'stderr', nil + options.trace_output = $stderr + else + fail CommandLineOptionError, + "Unrecognized --#{trace_option} option '#{value}'" + end + end + private :select_trace_output + + # Read and handle the command line options. + def handle_options + options.rakelib = ['rakelib'] + options.trace_output = $stderr + + OptionParser.new do |opts| + opts.banner = "#{Rake.application.name} [-f rakefile] {options} targets..." + opts.separator "" + opts.separator "Options are ..." + + opts.on_tail("-h", "--help", "-H", "Display this help message.") do + puts opts + exit + end + + standard_rake_options.each { |args| opts.on(*args) } + opts.environment('RAKEOPT') + end.parse! + end + + # Similar to the regular Ruby +require+ command, but will check + # for *.rake files in addition to *.rb files. + def rake_require(file_name, paths=$LOAD_PATH, loaded=$") + fn = file_name + ".rake" + return false if loaded.include?(fn) + paths.each do |path| + full_path = File.join(path, fn) + if File.exist?(full_path) + Rake.load_rakefile(full_path) + loaded << fn + return true + end + end + fail LoadError, "Can't find #{file_name}" + end + + def find_rakefile_location + here = Dir.pwd + until (fn = have_rakefile) + Dir.chdir("..") + return nil if Dir.pwd == here || options.nosearch + here = Dir.pwd + end + [fn, here] + ensure + Dir.chdir(Rake.original_dir) + end + + def print_rakefile_directory(location) + $stderr.puts "(in #{Dir.pwd})" unless + options.silent or original_dir == location + end + + def raw_load_rakefile # :nodoc: + rakefile, location = find_rakefile_location + if (! options.ignore_system) && + (options.load_system || rakefile.nil?) && + system_dir && File.directory?(system_dir) + print_rakefile_directory(location) + glob("#{system_dir}/*.rake") do |name| + add_import name + end + else + fail "No Rakefile found (looking for: #{@rakefiles.join(', ')})" if + rakefile.nil? + @rakefile = rakefile + Dir.chdir(location) + print_rakefile_directory(location) + Rake.load_rakefile(File.expand_path(@rakefile)) if + @rakefile && @rakefile != '' + options.rakelib.each do |rlib| + glob("#{rlib}/*.rake") do |name| + add_import name + end + end + end + load_imports + end + + def glob(path, &block) + FileList.glob(path.gsub("\\", '/')).each(&block) + end + private :glob + + # The directory path containing the system wide rakefiles. + def system_dir + @system_dir ||= + begin + if ENV['RAKE_SYSTEM'] + ENV['RAKE_SYSTEM'] + else + standard_system_dir + end + end + end + + # The standard directory containing system wide rake files. + if Win32.windows? + def standard_system_dir #:nodoc: + Win32.win32_system_dir + end + else + def standard_system_dir #:nodoc: + File.join(File.expand_path('~'), '.rake') + end + end + private :standard_system_dir + + # Collect the list of tasks on the command line. If no tasks are + # given, return a list containing only the default task. + # Environmental assignments are processed at this time as well. + def collect_tasks + @top_level_tasks = [] + ARGV.each do |arg| + if arg =~ /^(\w+)=(.*)$/m + ENV[$1] = $2 + else + @top_level_tasks << arg unless arg =~ /^-/ + end + end + @top_level_tasks.push(default_task_name) if @top_level_tasks.empty? + end + + # Default task name ("default"). + # (May be overridden by subclasses) + def default_task_name + "default" + end + + # Add a file to the list of files to be imported. + def add_import(fn) + @pending_imports << fn + end + + # Load the pending list of imported files. + def load_imports + while fn = @pending_imports.shift + next if @imported.member?(fn) + fn_task = lookup(fn) and fn_task.invoke + ext = File.extname(fn) + loader = @loaders[ext] || @default_loader + loader.load(fn) + @imported << fn + end + end + + def rakefile_location(backtrace=caller) + backtrace.map { |t| t[/([^:]+):/, 1] } + + re = /^#{@rakefile}$/ + re = /#{re.source}/i if windows? + + backtrace.find { |str| str =~ re } || '' + end + + private + FIXNUM_MAX = (2**(0.size * 8 - 2) - 1) # :nodoc: + + end +end diff --git a/lib/ruby/shared/rake/backtrace.rb b/lib/ruby/shared/rake/backtrace.rb new file mode 100644 index 00000000000..9b2ba6157fb --- /dev/null +++ b/lib/ruby/shared/rake/backtrace.rb @@ -0,0 +1,20 @@ +module Rake + module Backtrace + SYS_KEYS = RbConfig::CONFIG.keys.grep(/(prefix|libdir)/) + SYS_PATHS = RbConfig::CONFIG.values_at(*SYS_KEYS).uniq + + [ File.join(File.dirname(__FILE__), "..") ] + + SUPPRESSED_PATHS = SYS_PATHS. + map { |s| s.gsub("\\", "/") }. + map { |f| File.expand_path(f) }. + reject { |s| s.nil? || s =~ /^ *$/ } + SUPPRESSED_PATHS_RE = SUPPRESSED_PATHS.map { |f| Regexp.quote(f) }.join("|") + SUPPRESS_PATTERN = %r!(\A(#{SUPPRESSED_PATHS_RE})|bin/rake:\d+)!i + + def self.collapse(backtrace) + pattern = Rake.application.options.suppress_backtrace_pattern || + SUPPRESS_PATTERN + backtrace.reject { |elem| elem =~ pattern } + end + end +end diff --git a/lib/ruby/shared/rake/clean.rb b/lib/ruby/shared/rake/clean.rb new file mode 100644 index 00000000000..8001ce569af --- /dev/null +++ b/lib/ruby/shared/rake/clean.rb @@ -0,0 +1,55 @@ +# The 'rake/clean' file defines two file lists (CLEAN and CLOBBER) and +# two rake tasks (:clean and :clobber). +# +# [:clean] Clean up the project by deleting scratch files and backup +# files. Add files to the CLEAN file list to have the :clean +# target handle them. +# +# [:clobber] Clobber all generated and non-source files in a project. +# The task depends on :clean, so all the clean files will +# be deleted as well as files in the CLOBBER file list. +# The intent of this task is to return a project to its +# pristine, just unpacked state. + +require 'rake' + +# :stopdoc: + +module Rake + module Cleaner + extend FileUtils + + module_function + + def cleanup_files(file_names) + file_names.each do |file_name| + cleanup(file_name) + end + end + + def cleanup(file_name, opts={}) + begin + rm_r file_name, opts + rescue StandardError => ex + puts "Failed to remove #{file_name}: #{ex}" + end + end + end +end + +CLEAN = ::Rake::FileList["**/*~", "**/*.bak", "**/core"] +CLEAN.clear_exclude.exclude { |fn| + fn.pathmap("%f").downcase == 'core' && File.directory?(fn) +} + +desc "Remove any temporary products." +task :clean do + Rake::Cleaner.cleanup_files(CLEAN) +end + +CLOBBER = ::Rake::FileList.new + +desc "Remove any generated file." +task :clobber => [:clean] do + Rake::Cleaner.cleanup_files(CLOBBER) +end diff --git a/lib/ruby/shared/rake/cloneable.rb b/lib/ruby/shared/rake/cloneable.rb new file mode 100644 index 00000000000..ac67471232f --- /dev/null +++ b/lib/ruby/shared/rake/cloneable.rb @@ -0,0 +1,16 @@ +module Rake + # ########################################################################## + # Mixin for creating easily cloned objects. + # + module Cloneable + # The hook that invoked by 'clone' and 'dup' methods. + def initialize_copy(source) + super + source.instance_variables.each do |var| + src_value = source.instance_variable_get(var) + value = src_value.clone rescue src_value + instance_variable_set(var, value) + end + end + end +end diff --git a/lib/ruby/shared/rake/contrib/compositepublisher.rb b/lib/ruby/shared/rake/contrib/compositepublisher.rb new file mode 100644 index 00000000000..69952a08081 --- /dev/null +++ b/lib/ruby/shared/rake/contrib/compositepublisher.rb @@ -0,0 +1,21 @@ +module Rake + + # Manage several publishers as a single entity. + class CompositePublisher + def initialize + @publishers = [] + end + + # Add a publisher to the composite. + def add(pub) + @publishers << pub + end + + # Upload all the individual publishers. + def upload + @publishers.each { |p| p.upload } + end + end + +end + diff --git a/lib/ruby/shared/rake/contrib/ftptools.rb b/lib/ruby/shared/rake/contrib/ftptools.rb new file mode 100644 index 00000000000..0dd50fdc8d4 --- /dev/null +++ b/lib/ruby/shared/rake/contrib/ftptools.rb @@ -0,0 +1,139 @@ +# = Tools for FTP uploading. +# +# This file is still under development and is not released for general +# use. + +require 'date' +require 'net/ftp' +require 'rake/file_list' + +module Rake # :nodoc: + + #################################################################### + # Note: Not released for general use. + class FtpFile + attr_reader :name, :size, :owner, :group, :time + + def self.date + @date_class ||= Date + end + + def self.time + @time_class ||= Time + end + + def initialize(path, entry) + @path = path + @mode, _, @owner, @group, size, d1, d2, d3, @name = entry.split(' ') + @size = size.to_i + @time = determine_time(d1, d2, d3) + end + + def path + File.join(@path, @name) + end + + def directory? + @mode[0] == ?d + end + + def mode + parse_mode(@mode) + end + + def symlink? + @mode[0] == ?l + end + + private # -------------------------------------------------------- + + def parse_mode(m) + result = 0 + (1..9).each do |i| + result = 2 * result + ((m[i] == ?-) ? 0 : 1) + end + result + end + + def determine_time(d1, d2, d3) + now = self.class.time.now + if /:/ !~ d3 + result = Time.parse("#{d1} #{d2} #{d3}") + else + result = Time.parse("#{d1} #{d2} #{now.year} #{d3}") + result = Time.parse("#{d1} #{d2} #{now.year - 1} #{d3}") if + result > now + end + result + end + end + + #################################################################### + # Manage the uploading of files to an FTP account. + class FtpUploader + + # Log uploads to standard output when true. + attr_accessor :verbose + + class << FtpUploader + # Create an uploader and pass it to the given block as +up+. + # When the block is complete, close the uploader. + def connect(path, host, account, password) + up = self.new(path, host, account, password) + begin + yield(up) + ensure + up.close + end + end + end + + # Create an FTP uploader targeting the directory +path+ on +host+ + # using the given account and password. +path+ will be the root + # path of the uploader. + def initialize(path, host, account, password) + @created = Hash.new + @path = path + @ftp = Net::FTP.new(host, account, password) + makedirs(@path) + @ftp.chdir(@path) + end + + # Create the directory +path+ in the uploader root path. + def makedirs(path) + route = [] + File.split(path).each do |dir| + route << dir + current_dir = File.join(route) + if @created[current_dir].nil? + @created[current_dir] = true + $stderr.puts "Creating Directory #{current_dir}" if @verbose + @ftp.mkdir(current_dir) rescue nil + end + end + end + + # Upload all files matching +wildcard+ to the uploader's root + # path. + def upload_files(wildcard) + FileList.glob(wildcard).each do |fn| + upload(fn) + end + end + + # Close the uploader. + def close + @ftp.close + end + + private # -------------------------------------------------------- + + # Upload a single file to the uploader's root path. + def upload(file) + $stderr.puts "Uploading #{file}" if @verbose + dir = File.dirname(file) + makedirs(dir) + @ftp.putbinaryfile(file, file) unless File.directory?(file) + end + end +end diff --git a/lib/ruby/shared/rake/contrib/publisher.rb b/lib/ruby/shared/rake/contrib/publisher.rb new file mode 100644 index 00000000000..8b11edb59c4 --- /dev/null +++ b/lib/ruby/shared/rake/contrib/publisher.rb @@ -0,0 +1,73 @@ +# Copyright 2003-2010 by Jim Weirich (jim.weirich@gmail.com) +# All rights reserved. + +# :stopdoc: + +# Configuration information about an upload host system. +# name :: Name of host system. +# webdir :: Base directory for the web information for the +# application. The application name (APP) is appended to +# this directory before using. +# pkgdir :: Directory on the host system where packages can be +# placed. +HostInfo = Struct.new(:name, :webdir, :pkgdir) + +# :startdoc: + +# Manage several publishers as a single entity. +class CompositePublisher + def initialize + @publishers = [] + end + + # Add a publisher to the composite. + def add(pub) + @publishers << pub + end + + # Upload all the individual publishers. + def upload + @publishers.each { |p| p.upload } + end +end + +# Publish an entire directory to an existing remote directory using +# SSH. +class SshDirPublisher + def initialize(host, remote_dir, local_dir) + @host = host + @remote_dir = remote_dir + @local_dir = local_dir + end + + def upload + run %{scp -rq #{@local_dir}/* #{@host}:#{@remote_dir}} + end +end + +# Publish an entire directory to a fresh remote directory using SSH. +class SshFreshDirPublisher < SshDirPublisher + def upload + run %{ssh #{@host} rm -rf #{@remote_dir}} rescue nil + run %{ssh #{@host} mkdir #{@remote_dir}} + super + end +end + +# Publish a list of files to an existing remote directory. +class SshFilePublisher + # Create a publisher using the give host information. + def initialize(host, remote_dir, local_dir, *files) + @host = host + @remote_dir = remote_dir + @local_dir = local_dir + @files = files + end + + # Upload the local directory to the remote directory. + def upload + @files.each do |fn| + run %{scp -q #{@local_dir}/#{fn} #{@host}:#{@remote_dir}} + end + end +end diff --git a/lib/ruby/shared/rake/contrib/rubyforgepublisher.rb b/lib/ruby/shared/rake/contrib/rubyforgepublisher.rb new file mode 100644 index 00000000000..a4b96936c8d --- /dev/null +++ b/lib/ruby/shared/rake/contrib/rubyforgepublisher.rb @@ -0,0 +1,16 @@ +require 'rake/contrib/sshpublisher' + +module Rake + + class RubyForgePublisher < SshDirPublisher + attr_reader :project, :proj_id, :user + + def initialize(projname, user) + super( + "#{user}@rubyforge.org", + "/var/www/gforge-projects/#{projname}", + "html") + end + end + +end diff --git a/lib/ruby/shared/rake/contrib/sshpublisher.rb b/lib/ruby/shared/rake/contrib/sshpublisher.rb new file mode 100644 index 00000000000..bd6adc127e3 --- /dev/null +++ b/lib/ruby/shared/rake/contrib/sshpublisher.rb @@ -0,0 +1,50 @@ +require 'rake/dsl_definition' +require 'rake/contrib/compositepublisher' + +module Rake + + # Publish an entire directory to an existing remote directory using + # SSH. + class SshDirPublisher + include Rake::DSL + + def initialize(host, remote_dir, local_dir) + @host = host + @remote_dir = remote_dir + @local_dir = local_dir + end + + def upload + sh %{scp -rq #{@local_dir}/* #{@host}:#{@remote_dir}} + end + end + + # Publish an entire directory to a fresh remote directory using SSH. + class SshFreshDirPublisher < SshDirPublisher + def upload + sh %{ssh #{@host} rm -rf #{@remote_dir}} rescue nil + sh %{ssh #{@host} mkdir #{@remote_dir}} + super + end + end + + # Publish a list of files to an existing remote directory. + class SshFilePublisher + include Rake::DSL + + # Create a publisher using the give host information. + def initialize(host, remote_dir, local_dir, *files) + @host = host + @remote_dir = remote_dir + @local_dir = local_dir + @files = files + end + + # Upload the local directory to the remote directory. + def upload + @files.each do |fn| + sh %{scp -q #{@local_dir}/#{fn} #{@host}:#{@remote_dir}} + end + end + end +end diff --git a/lib/ruby/shared/rake/contrib/sys.rb b/lib/ruby/shared/rake/contrib/sys.rb new file mode 100644 index 00000000000..a3a9f69e25a --- /dev/null +++ b/lib/ruby/shared/rake/contrib/sys.rb @@ -0,0 +1,2 @@ +fail "ERROR: 'rake/contrib/sys' is obsolete and no longer supported. " + + "Use 'FileUtils' instead." diff --git a/lib/ruby/shared/rake/default_loader.rb b/lib/ruby/shared/rake/default_loader.rb new file mode 100644 index 00000000000..5dd3c05617b --- /dev/null +++ b/lib/ruby/shared/rake/default_loader.rb @@ -0,0 +1,10 @@ +module Rake + + # Default Rakefile loader used by +import+. + class DefaultLoader + def load(fn) + Rake.load_rakefile(File.expand_path(fn)) + end + end + +end diff --git a/lib/ruby/shared/rake/dsl_definition.rb b/lib/ruby/shared/rake/dsl_definition.rb new file mode 100644 index 00000000000..b24a821386e --- /dev/null +++ b/lib/ruby/shared/rake/dsl_definition.rb @@ -0,0 +1,157 @@ +# Rake DSL functions. +require 'rake/file_utils_ext' + +module Rake + + ## + # DSL is a module that provides #task, #desc, #namespace, etc. Use this + # when you'd like to use rake outside the top level scope. + + module DSL + + #-- + # Include the FileUtils file manipulation functions in the top + # level module, but mark them private so that they don't + # unintentionally define methods on other objects. + #++ + + include FileUtilsExt + private(*FileUtils.instance_methods(false)) + private(*FileUtilsExt.instance_methods(false)) + + private + + # Declare a basic task. + # + # Example: + # task :clobber => [:clean] do + # rm_rf "html" + # end + # + def task(*args, &block) + Rake::Task.define_task(*args, &block) + end + + # Declare a file task. + # + # Example: + # file "config.cfg" => ["config.template"] do + # open("config.cfg", "w") do |outfile| + # open("config.template") do |infile| + # while line = infile.gets + # outfile.puts line + # end + # end + # end + # end + # + def file(*args, &block) + Rake::FileTask.define_task(*args, &block) + end + + # Declare a file creation task. + # (Mainly used for the directory command). + def file_create(*args, &block) + Rake::FileCreationTask.define_task(*args, &block) + end + + # Declare a set of files tasks to create the given directories on + # demand. + # + # Example: + # directory "testdata/doc" + # + def directory(*args, &block) + result = file_create(*args, &block) + dir, _ = *Rake.application.resolve_args(args) + Rake.each_dir_parent(dir) do |d| + file_create d do |t| + mkdir_p t.name unless File.exist?(t.name) + end + end + result + end + + # Declare a task that performs its prerequisites in + # parallel. Multitasks does *not* guarantee that its prerequisites + # will execute in any given order (which is obvious when you think + # about it) + # + # Example: + # multitask :deploy => [:deploy_gem, :deploy_rdoc] + # + def multitask(*args, &block) + Rake::MultiTask.define_task(*args, &block) + end + + # Create a new rake namespace and use it for evaluating the given + # block. Returns a NameSpace object that can be used to lookup + # tasks defined in the namespace. + # + # E.g. + # + # ns = namespace "nested" do + # task :run + # end + # task_run = ns[:run] # find :run in the given namespace. + # + def namespace(name=nil, &block) + name = name.to_s if name.kind_of?(Symbol) + name = name.to_str if name.respond_to?(:to_str) + unless name.kind_of?(String) || name.nil? + raise ArgumentError, "Expected a String or Symbol for a namespace name" + end + Rake.application.in_namespace(name, &block) + end + + # Declare a rule for auto-tasks. + # + # Example: + # rule '.o' => '.c' do |t| + # sh %{cc -o #{t.name} #{t.source}} + # end + # + def rule(*args, &block) + Rake::Task.create_rule(*args, &block) + end + + # Describe the next rake task. + # Duplicate descriptions are discarded. + # + # Example: + # desc "Run the Unit Tests" + # task :test => [:build] + # runtests + # end + # + def desc(description) + Rake.application.last_description = description + end + + # Import the partial Rakefiles +fn+. Imported files are loaded + # _after_ the current file is completely loaded. This allows the + # import statement to appear anywhere in the importing file, and yet + # allowing the imported files to depend on objects defined in the + # importing file. + # + # A common use of the import statement is to include files + # containing dependency declarations. + # + # See also the --rakelibdir command line option. + # + # Example: + # import ".depend", "my_rules" + # + def import(*fns) + fns.each do |fn| + Rake.application.add_import(fn) + end + end + end + extend FileUtilsExt +end + +# Extend the main object with the DSL commands. This allows top-level +# calls to task, etc. to work from a Rakefile without polluting the +# object inheritance tree. +self.extend Rake::DSL diff --git a/lib/ruby/shared/rake/early_time.rb b/lib/ruby/shared/rake/early_time.rb new file mode 100644 index 00000000000..8c0e7d33398 --- /dev/null +++ b/lib/ruby/shared/rake/early_time.rb @@ -0,0 +1,18 @@ +module Rake + + # EarlyTime is a fake timestamp that occurs _before_ any other time value. + class EarlyTime + include Comparable + include Singleton + + def <=>(other) + -1 + end + + def to_s + "" + end + end + + EARLY = EarlyTime.instance +end diff --git a/lib/ruby/shared/rake/ext/core.rb b/lib/ruby/shared/rake/ext/core.rb new file mode 100644 index 00000000000..c924c7eaba5 --- /dev/null +++ b/lib/ruby/shared/rake/ext/core.rb @@ -0,0 +1,28 @@ +###################################################################### +# Core extension library +# +class Module + # Check for an existing method in the current class before extending. IF + # the method already exists, then a warning is printed and the extension is + # not added. Otherwise the block is yielded and any definitions in the + # block will take effect. + # + # Usage: + # + # class String + # rake_extension("xyz") do + # def xyz + # ... + # end + # end + # end + # + def rake_extension(method) + if method_defined?(method) + $stderr.puts "WARNING: Possible conflict with Rake extension: " + + "#{self}##{method} already exists" + else + yield + end + end +end diff --git a/lib/ruby/shared/rake/ext/module.rb b/lib/ruby/shared/rake/ext/module.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ruby/shared/rake/ext/string.rb b/lib/ruby/shared/rake/ext/string.rb new file mode 100644 index 00000000000..07ef167f825 --- /dev/null +++ b/lib/ruby/shared/rake/ext/string.rb @@ -0,0 +1,166 @@ +require 'rake/ext/core' + +###################################################################### +# Rake extension methods for String. +# +class String + + rake_extension("ext") do + # Replace the file extension with +newext+. If there is no extension on + # the string, append the new extension to the end. If the new extension + # is not given, or is the empty string, remove any existing extension. + # + # +ext+ is a user added method for the String class. + def ext(newext='') + return self.dup if ['.', '..'].include? self + newext = (newext =~ /^\./) ? newext : ("." + newext) if newext != '' + self.chomp(File.extname(self)) << newext + end + end + + rake_extension("pathmap") do + # Explode a path into individual components. Used by +pathmap+. + def pathmap_explode + head, tail = File.split(self) + return [self] if head == self + return [tail] if head == '.' || tail == '/' + return [head, tail] if head == '/' + return head.pathmap_explode + [tail] + end + protected :pathmap_explode + + # Extract a partial path from the path. Include +n+ directories from the + # front end (left hand side) if +n+ is positive. Include |+n+| + # directories from the back end (right hand side) if +n+ is negative. + def pathmap_partial(n) + dirs = File.dirname(self).pathmap_explode + partial_dirs = + if n > 0 + dirs[0...n] + elsif n < 0 + dirs.reverse[0...-n].reverse + else + "." + end + File.join(partial_dirs) + end + protected :pathmap_partial + + # Preform the pathmap replacement operations on the given path. The + # patterns take the form 'pat1,rep1;pat2,rep2...'. + def pathmap_replace(patterns, &block) + result = self + patterns.split(';').each do |pair| + pattern, replacement = pair.split(',') + pattern = Regexp.new(pattern) + if replacement == '*' && block_given? + result = result.sub(pattern, &block) + elsif replacement + result = result.sub(pattern, replacement) + else + result = result.sub(pattern, '') + end + end + result + end + protected :pathmap_replace + + # Map the path according to the given specification. The specification + # controls the details of the mapping. The following special patterns are + # recognized: + # + # * %p -- The complete path. + # * %f -- The base file name of the path, with its file extension, + # but without any directories. + # * %n -- The file name of the path without its file extension. + # * %d -- The directory list of the path. + # * %x -- The file extension of the path. An empty string if there + # is no extension. + # * %X -- Everything *but* the file extension. + # * %s -- The alternate file separator if defined, otherwise use + # the standard file separator. + # * %% -- A percent sign. + # + # The %d specifier can also have a numeric prefix (e.g. '%2d'). If the + # number is positive, only return (up to) +n+ directories in the path, + # starting from the left hand side. If +n+ is negative, return (up to) + # |+n+| directories from the right hand side of the path. + # + # Examples: + # + # 'a/b/c/d/file.txt'.pathmap("%2d") => 'a/b' + # 'a/b/c/d/file.txt'.pathmap("%-2d") => 'c/d' + # + # Also the %d, %p, %f, %n, %x, and %X operators can take a + # pattern/replacement argument to perform simple string substitutions on a + # particular part of the path. The pattern and replacement are separated + # by a comma and are enclosed by curly braces. The replacement spec comes + # after the % character but before the operator letter. (e.g. + # "%{old,new}d"). Multiple replacement specs should be separated by + # semi-colons (e.g. "%{old,new;src,bin}d"). + # + # Regular expressions may be used for the pattern, and back refs may be + # used in the replacement text. Curly braces, commas and semi-colons are + # excluded from both the pattern and replacement text (let's keep parsing + # reasonable). + # + # For example: + # + # "src/org/onestepback/proj/A.java".pathmap("%{^src,bin}X.class") + # + # returns: + # + # "bin/org/onestepback/proj/A.class" + # + # If the replacement text is '*', then a block may be provided to perform + # some arbitrary calculation for the replacement. + # + # For example: + # + # "/path/to/file.TXT".pathmap("%X%{.*,*}x") { |ext| + # ext.downcase + # } + # + # Returns: + # + # "/path/to/file.txt" + # + def pathmap(spec=nil, &block) + return self if spec.nil? + result = '' + spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag| + case frag + when '%f' + result << File.basename(self) + when '%n' + result << File.basename(self).ext + when '%d' + result << File.dirname(self) + when '%x' + result << File.extname(self) + when '%X' + result << self.ext + when '%p' + result << self + when '%s' + result << (File::ALT_SEPARATOR || File::SEPARATOR) + when '%-' + # do nothing + when '%%' + result << "%" + when /%(-?\d+)d/ + result << pathmap_partial($1.to_i) + when /^%\{([^}]*)\}(\d*[dpfnxX])/ + patterns, operator = $1, $2 + result << pathmap('%' + operator).pathmap_replace(patterns, &block) + when /^%/ + fail ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'" + else + result << frag + end + end + result + end + end + +end diff --git a/lib/ruby/shared/rake/ext/time.rb b/lib/ruby/shared/rake/ext/time.rb new file mode 100644 index 00000000000..ea8b037e393 --- /dev/null +++ b/lib/ruby/shared/rake/ext/time.rb @@ -0,0 +1,15 @@ +#-- +# Extensions to time to allow comparisons with an early time class. + +require 'rake/early_time' + +class Time + alias rake_original_time_compare :<=> + def <=>(other) + if Rake::EarlyTime === other + - other.<=>(self) + else + rake_original_time_compare(other) + end + end +end diff --git a/lib/ruby/shared/rake/file_creation_task.rb b/lib/ruby/shared/rake/file_creation_task.rb new file mode 100644 index 00000000000..c87e2192bb4 --- /dev/null +++ b/lib/ruby/shared/rake/file_creation_task.rb @@ -0,0 +1,24 @@ +require 'rake/file_task' +require 'rake/early_time' + +module Rake + + # A FileCreationTask is a file task that when used as a dependency will be + # needed if and only if the file has not been created. Once created, it is + # not re-triggered if any of its dependencies are newer, nor does trigger + # any rebuilds of tasks that depend on it whenever it is updated. + # + class FileCreationTask < FileTask + # Is this file task needed? Yes if it doesn't exist. + def needed? + ! File.exist?(name) + end + + # Time stamp for file creation task. This time stamp is earlier + # than any other time stamp. + def timestamp + Rake::EARLY + end + end + +end diff --git a/lib/ruby/shared/rake/file_list.rb b/lib/ruby/shared/rake/file_list.rb new file mode 100644 index 00000000000..0b60925f090 --- /dev/null +++ b/lib/ruby/shared/rake/file_list.rb @@ -0,0 +1,416 @@ +require 'rake/cloneable' +require 'rake/file_utils_ext' +require 'rake/pathmap' + +###################################################################### +module Rake + + # ######################################################################### + # A FileList is essentially an array with a few helper methods defined to + # make file manipulation a bit easier. + # + # FileLists are lazy. When given a list of glob patterns for possible files + # to be included in the file list, instead of searching the file structures + # to find the files, a FileList holds the pattern for latter use. + # + # This allows us to define a number of FileList to match any number of + # files, but only search out the actual files when then FileList itself is + # actually used. The key is that the first time an element of the + # FileList/Array is requested, the pending patterns are resolved into a real + # list of file names. + # + class FileList + + include Cloneable + + # == Method Delegation + # + # The lazy evaluation magic of FileLists happens by implementing all the + # array specific methods to call +resolve+ before delegating the heavy + # lifting to an embedded array object (@items). + # + # In addition, there are two kinds of delegation calls. The regular kind + # delegates to the @items array and returns the result directly. Well, + # almost directly. It checks if the returned value is the @items object + # itself, and if so will return the FileList object instead. + # + # The second kind of delegation call is used in methods that normally + # return a new Array object. We want to capture the return value of these + # methods and wrap them in a new FileList object. We enumerate these + # methods in the +SPECIAL_RETURN+ list below. + + # List of array methods (that are not in +Object+) that need to be + # delegated. + ARRAY_METHODS = (Array.instance_methods - Object.instance_methods). + map { |n| n.to_s } + + # List of additional methods that must be delegated. + MUST_DEFINE = %w[inspect <=>] + + # List of methods that should not be delegated here (we define special + # versions of them explicitly below). + MUST_NOT_DEFINE = %w[to_a to_ary partition *] + + # List of delegated methods that return new array values which need + # wrapping. + SPECIAL_RETURN = %w[ + map collect sort sort_by select find_all reject grep + compact flatten uniq values_at + + - & | + ] + + DELEGATING_METHODS = (ARRAY_METHODS + MUST_DEFINE - MUST_NOT_DEFINE). + map { |s| s.to_s }.sort.uniq + + # Now do the delegation. + DELEGATING_METHODS.each do |sym| + if SPECIAL_RETURN.include?(sym) + ln = __LINE__ + 1 + class_eval %{ + def #{sym}(*args, &block) + resolve + result = @items.send(:#{sym}, *args, &block) + FileList.new.import(result) + end + }, __FILE__, ln + else + ln = __LINE__ + 1 + class_eval %{ + def #{sym}(*args, &block) + resolve + result = @items.send(:#{sym}, *args, &block) + result.object_id == @items.object_id ? self : result + end + }, __FILE__, ln + end + end + + # Create a file list from the globbable patterns given. If you wish to + # perform multiple includes or excludes at object build time, use the + # "yield self" pattern. + # + # Example: + # file_list = FileList.new('lib/**/*.rb', 'test/test*.rb') + # + # pkg_files = FileList.new('lib/**/*') do |fl| + # fl.exclude(/\bCVS\b/) + # end + # + def initialize(*patterns) + @pending_add = [] + @pending = false + @exclude_patterns = DEFAULT_IGNORE_PATTERNS.dup + @exclude_procs = DEFAULT_IGNORE_PROCS.dup + @items = [] + patterns.each { |pattern| include(pattern) } + yield self if block_given? + end + + # Add file names defined by glob patterns to the file list. If an array + # is given, add each element of the array. + # + # Example: + # file_list.include("*.java", "*.cfg") + # file_list.include %w( math.c lib.h *.o ) + # + def include(*filenames) + # TODO: check for pending + filenames.each do |fn| + if fn.respond_to? :to_ary + include(*fn.to_ary) + else + @pending_add << fn + end + end + @pending = true + self + end + alias :add :include + + # Register a list of file name patterns that should be excluded from the + # list. Patterns may be regular expressions, glob patterns or regular + # strings. In addition, a block given to exclude will remove entries that + # return true when given to the block. + # + # Note that glob patterns are expanded against the file system. If a file + # is explicitly added to a file list, but does not exist in the file + # system, then an glob pattern in the exclude list will not exclude the + # file. + # + # Examples: + # FileList['a.c', 'b.c'].exclude("a.c") => ['b.c'] + # FileList['a.c', 'b.c'].exclude(/^a/) => ['b.c'] + # + # If "a.c" is a file, then ... + # FileList['a.c', 'b.c'].exclude("a.*") => ['b.c'] + # + # If "a.c" is not a file, then ... + # FileList['a.c', 'b.c'].exclude("a.*") => ['a.c', 'b.c'] + # + def exclude(*patterns, &block) + patterns.each do |pat| + @exclude_patterns << pat + end + @exclude_procs << block if block_given? + resolve_exclude unless @pending + self + end + + + # Clear all the exclude patterns so that we exclude nothing. + def clear_exclude + @exclude_patterns = [] + @exclude_procs = [] + self + end + + # Define equality. + def ==(array) + to_ary == array + end + + # Return the internal array object. + def to_a + resolve + @items + end + + # Return the internal array object. + def to_ary + to_a + end + + # Lie about our class. + def is_a?(klass) + klass == Array || super(klass) + end + alias kind_of? is_a? + + # Redefine * to return either a string or a new file list. + def *(other) + result = @items * other + case result + when Array + FileList.new.import(result) + else + result + end + end + + # Resolve all the pending adds now. + def resolve + if @pending + @pending = false + @pending_add.each do |fn| resolve_add(fn) end + @pending_add = [] + resolve_exclude + end + self + end + + def resolve_add(fn) + case fn + when %r{[*?\[\{]} + add_matching(fn) + else + self << fn + end + end + private :resolve_add + + def resolve_exclude + reject! { |fn| excluded_from_list?(fn) } + self + end + private :resolve_exclude + + # Return a new FileList with the results of running +sub+ against each + # element of the original list. + # + # Example: + # FileList['a.c', 'b.c'].sub(/\.c$/, '.o') => ['a.o', 'b.o'] + # + def sub(pat, rep) + inject(FileList.new) { |res, fn| res << fn.sub(pat, rep) } + end + + # Return a new FileList with the results of running +gsub+ against each + # element of the original list. + # + # Example: + # FileList['lib/test/file', 'x/y'].gsub(/\//, "\\") + # => ['lib\\test\\file', 'x\\y'] + # + def gsub(pat, rep) + inject(FileList.new) { |res, fn| res << fn.gsub(pat, rep) } + end + + # Same as +sub+ except that the original file list is modified. + def sub!(pat, rep) + each_with_index { |fn, i| self[i] = fn.sub(pat, rep) } + self + end + + # Same as +gsub+ except that the original file list is modified. + def gsub!(pat, rep) + each_with_index { |fn, i| self[i] = fn.gsub(pat, rep) } + self + end + + # Apply the pathmap spec to each of the included file names, returning a + # new file list with the modified paths. (See String#pathmap for + # details.) + def pathmap(spec=nil) + collect { |fn| fn.pathmap(spec) } + end + + # Return a new FileList with String#ext method applied to + # each member of the array. + # + # This method is a shortcut for: + # + # array.collect { |item| item.ext(newext) } + # + # +ext+ is a user added method for the Array class. + def ext(newext='') + collect { |fn| fn.ext(newext) } + end + + + # Grep each of the files in the filelist using the given pattern. If a + # block is given, call the block on each matching line, passing the file + # name, line number, and the matching line of text. If no block is given, + # a standard emacs style file:linenumber:line message will be printed to + # standard out. Returns the number of matched items. + def egrep(pattern, *options) + matched = 0 + each do |fn| + begin + open(fn, "r", *options) do |inf| + count = 0 + inf.each do |line| + count += 1 + if pattern.match(line) + matched += 1 + if block_given? + yield fn, count, line + else + puts "#{fn}:#{count}:#{line}" + end + end + end + end + rescue StandardError => ex + $stderr.puts "Error while processing '#{fn}': #{ex}" + end + end + matched + end + + # Return a new file list that only contains file names from the current + # file list that exist on the file system. + def existing + select { |fn| File.exist?(fn) } + end + + # Modify the current file list so that it contains only file name that + # exist on the file system. + def existing! + resolve + @items = @items.select { |fn| File.exist?(fn) } + self + end + + # FileList version of partition. Needed because the nested arrays should + # be FileLists in this version. + def partition(&block) # :nodoc: + resolve + result = @items.partition(&block) + [ + FileList.new.import(result[0]), + FileList.new.import(result[1]), + ] + end + + # Convert a FileList to a string by joining all elements with a space. + def to_s + resolve + self.join(' ') + end + + # Add matching glob patterns. + def add_matching(pattern) + FileList.glob(pattern).each do |fn| + self << fn unless excluded_from_list?(fn) + end + end + private :add_matching + + # Should the given file name be excluded from the list? + # + # NOTE: This method was formally named "exclude?", but Rails + # introduced an exclude? method as an array method and setup a + # conflict with file list. We renamed the method to avoid + # confusion. If you were using "FileList#exclude?" in your user + # code, you will need to update. + def excluded_from_list?(fn) + return true if @exclude_patterns.any? do |pat| + case pat + when Regexp + fn =~ pat + when /[*?]/ + File.fnmatch?(pat, fn, File::FNM_PATHNAME) + else + fn == pat + end + end + @exclude_procs.any? { |p| p.call(fn) } + end + + DEFAULT_IGNORE_PATTERNS = [ + /(^|[\/\\])CVS([\/\\]|$)/, + /(^|[\/\\])\.svn([\/\\]|$)/, + /\.bak$/, + /~$/ + ] + DEFAULT_IGNORE_PROCS = [ + proc { |fn| fn =~ /(^|[\/\\])core$/ && ! File.directory?(fn) } + ] + + def import(array) + @items = array + self + end + + class << self + # Create a new file list including the files listed. Similar to: + # + # FileList.new(*args) + def [](*args) + new(*args) + end + + # Get a sorted list of files matching the pattern. This method + # should be prefered to Dir[pattern] and Dir.glob(pattern) because + # the files returned are guaranteed to be sorted. + def glob(pattern, *args) + Dir.glob(pattern, *args).sort + end + end + end +end + +module Rake + class << self + + # Yield each file or directory component. + def each_dir_parent(dir) # :nodoc: + old_length = nil + while dir != '.' && dir.length != old_length + yield(dir) + old_length = dir.length + dir = File.dirname(dir) + end + end + end +end # module Rake diff --git a/lib/ruby/shared/rake/file_task.rb b/lib/ruby/shared/rake/file_task.rb new file mode 100644 index 00000000000..3e717c24b76 --- /dev/null +++ b/lib/ruby/shared/rake/file_task.rb @@ -0,0 +1,46 @@ +require 'rake/task.rb' +require 'rake/early_time' + +module Rake + # ######################################################################### + # A FileTask is a task that includes time based dependencies. If any of a + # FileTask's prerequisites have a timestamp that is later than the file + # represented by this task, then the file must be rebuilt (using the + # supplied actions). + # + class FileTask < Task + + # Is this file task needed? Yes if it doesn't exist, or if its time stamp + # is out of date. + def needed? + ! File.exist?(name) || out_of_date?(timestamp) + end + + # Time stamp for file task. + def timestamp + if File.exist?(name) + File.mtime(name.to_s) + else + Rake::EARLY + end + end + + private + + # Are there any prerequisites with a later time than the given time stamp? + def out_of_date?(stamp) + @prerequisites.any? { |n| application[n, @scope].timestamp > stamp } + end + + # ---------------------------------------------------------------- + # Task class methods. + # + class << self + # Apply the scope to the task name according to the rules for this kind + # of task. File based tasks ignore the scope when creating the name. + def scope_name(scope, task_name) + task_name + end + end + end +end diff --git a/lib/ruby/shared/rake/file_utils.rb b/lib/ruby/shared/rake/file_utils.rb new file mode 100644 index 00000000000..0f7f459d87c --- /dev/null +++ b/lib/ruby/shared/rake/file_utils.rb @@ -0,0 +1,116 @@ +require 'rbconfig' +require 'fileutils' + +#-- +# This a FileUtils extension that defines several additional commands to be +# added to the FileUtils utility functions. +module FileUtils + # Path to the currently running Ruby program + RUBY = ENV['RUBY'] || File.join( + RbConfig::CONFIG['bindir'], + RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']). + sub(/.*\s.*/m, '"\&"') + + OPT_TABLE['sh'] = %w(noop verbose) + OPT_TABLE['ruby'] = %w(noop verbose) + + # Run the system command +cmd+. If multiple arguments are given the command + # is not run with the shell (same semantics as Kernel::exec and + # Kernel::system). + # + # Example: + # sh %{ls -ltr} + # + # sh 'ls', 'file with spaces' + # + # # check exit status after command runs + # sh %{grep pattern file} do |ok, res| + # if ! ok + # puts "pattern not found (status = #{res.exitstatus})" + # end + # end + # + def sh(*cmd, &block) + options = (Hash === cmd.last) ? cmd.pop : {} + shell_runner = block_given? ? block : create_shell_runner(cmd) + set_verbose_option(options) + options[:noop] ||= Rake::FileUtilsExt.nowrite_flag + Rake.rake_check_options options, :noop, :verbose + Rake.rake_output_message cmd.join(" ") if options[:verbose] + + unless options[:noop] + res = rake_system(*cmd) + status = $? + status = Rake::PseudoStatus.new(1) if !res && status.nil? + shell_runner.call(res, status) + end + end + + def create_shell_runner(cmd) # :nodoc: + show_command = cmd.join(" ") + show_command = show_command[0, 42] + "..." unless $trace + lambda do |ok, status| + ok or + fail "Command failed with status (#{status.exitstatus}): " + + "[#{show_command}]" + end + end + private :create_shell_runner + + def set_verbose_option(options) # :nodoc: + unless options.key? :verbose + options[:verbose] = + (Rake::FileUtilsExt.verbose_flag == Rake::FileUtilsExt::DEFAULT) || + Rake::FileUtilsExt.verbose_flag + end + end + private :set_verbose_option + + def rake_system(*cmd) # :nodoc: + Rake::AltSystem.system(*cmd) + end + private :rake_system + + # Run a Ruby interpreter with the given arguments. + # + # Example: + # ruby %{-pe '$_.upcase!' 1 + sh(*([RUBY] + args + [options]), &block) + else + sh("#{RUBY} #{args.first}", options, &block) + end + end + + LN_SUPPORTED = [true] + + # Attempt to do a normal file link, but fall back to a copy if the link + # fails. + def safe_ln(*args) + if ! LN_SUPPORTED[0] + cp(*args) + else + begin + ln(*args) + rescue StandardError, NotImplementedError + LN_SUPPORTED[0] = false + cp(*args) + end + end + end + + # Split a file path into individual directory names. + # + # Example: + # split_all("a/b/c") => ['a', 'b', 'c'] + # + def split_all(path) + head, tail = File.split(path) + return [tail] if head == '.' || tail == '/' + return [head, tail] if head == '/' + return split_all(head) + [tail] + end +end diff --git a/lib/ruby/shared/rake/file_utils_ext.rb b/lib/ruby/shared/rake/file_utils_ext.rb new file mode 100644 index 00000000000..309159aec12 --- /dev/null +++ b/lib/ruby/shared/rake/file_utils_ext.rb @@ -0,0 +1,144 @@ +require 'rake/file_utils' + +module Rake + # + # FileUtilsExt provides a custom version of the FileUtils methods + # that respond to the verbose and nowrite + # commands. + # + module FileUtilsExt + include FileUtils + + class << self + attr_accessor :verbose_flag, :nowrite_flag + end + + DEFAULT = Object.new + + FileUtilsExt.verbose_flag = DEFAULT + FileUtilsExt.nowrite_flag = false + + FileUtils.commands.each do |name| + opts = FileUtils.options_of name + default_options = [] + if opts.include?("verbose") + default_options << ':verbose => FileUtilsExt.verbose_flag' + end + if opts.include?("noop") + default_options << ':noop => FileUtilsExt.nowrite_flag' + end + + next if default_options.empty? + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}( *args, &block ) + super( + *rake_merge_option(args, + #{default_options.join(', ')} + ), &block) + end + EOS + end + + # Get/set the verbose flag controlling output from the FileUtils + # utilities. If verbose is true, then the utility method is + # echoed to standard output. + # + # Examples: + # verbose # return the current value of the + # # verbose flag + # verbose(v) # set the verbose flag to _v_. + # verbose(v) { code } # Execute code with the verbose flag set + # # temporarily to _v_. Return to the + # # original value when code is done. + def verbose(value=nil) + oldvalue = FileUtilsExt.verbose_flag + FileUtilsExt.verbose_flag = value unless value.nil? + if block_given? + begin + yield + ensure + FileUtilsExt.verbose_flag = oldvalue + end + end + FileUtilsExt.verbose_flag + end + + # Get/set the nowrite flag controlling output from the FileUtils + # utilities. If verbose is true, then the utility method is + # echoed to standard output. + # + # Examples: + # nowrite # return the current value of the + # # nowrite flag + # nowrite(v) # set the nowrite flag to _v_. + # nowrite(v) { code } # Execute code with the nowrite flag set + # # temporarily to _v_. Return to the + # # original value when code is done. + def nowrite(value=nil) + oldvalue = FileUtilsExt.nowrite_flag + FileUtilsExt.nowrite_flag = value unless value.nil? + if block_given? + begin + yield + ensure + FileUtilsExt.nowrite_flag = oldvalue + end + end + oldvalue + end + + # Use this function to prevent potentially destructive ruby code + # from running when the :nowrite flag is set. + # + # Example: + # + # when_writing("Building Project") do + # project.build + # end + # + # The following code will build the project under normal + # conditions. If the nowrite(true) flag is set, then the example + # will print: + # + # DRYRUN: Building Project + # + # instead of actually building the project. + # + def when_writing(msg=nil) + if FileUtilsExt.nowrite_flag + $stderr.puts "DRYRUN: #{msg}" if msg + else + yield + end + end + + # Merge the given options with the default values. + def rake_merge_option(args, defaults) + if Hash === args.last + defaults.update(args.last) + args.pop + end + args.push defaults + args + end + + # Send the message to the default rake output (which is $stderr). + def rake_output_message(message) + $stderr.puts(message) + end + + # Check that the options do not contain options not listed in + # +optdecl+. An ArgumentError exception is thrown if non-declared + # options are found. + def rake_check_options(options, *optdecl) + h = options.dup + optdecl.each do |name| + h.delete name + end + raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless + h.empty? + end + + extend self + end +end diff --git a/lib/ruby/shared/rake/gempackagetask.rb b/lib/ruby/shared/rake/gempackagetask.rb new file mode 100644 index 00000000000..4ace0a6f0e9 --- /dev/null +++ b/lib/ruby/shared/rake/gempackagetask.rb @@ -0,0 +1,2 @@ +fail "ERROR: 'rake/gempackagetask' is obsolete and no longer supported. " + + "Use 'rubygems/packagetask' instead." diff --git a/lib/ruby/shared/rake/invocation_chain.rb b/lib/ruby/shared/rake/invocation_chain.rb new file mode 100644 index 00000000000..dae9a35915c --- /dev/null +++ b/lib/ruby/shared/rake/invocation_chain.rb @@ -0,0 +1,57 @@ +module Rake + + #################################################################### + # InvocationChain tracks the chain of task invocations to detect + # circular dependencies. + class InvocationChain < LinkedList + + # Is the invocation already in the chain? + def member?(invocation) + head == invocation || tail.member?(invocation) + end + + # Append an invocation to the chain of invocations. It is an error + # if the invocation already listed. + def append(invocation) + if member?(invocation) + fail RuntimeError, "Circular dependency detected: #{to_s} => #{invocation}" + end + conj(invocation) + end + + # Convert to string, ie: TOP => invocation => invocation + def to_s + "#{prefix}#{head}" + end + + # Class level append. + def self.append(invocation, chain) + chain.append(invocation) + end + + private + + def prefix + "#{tail.to_s} => " + end + + # Null object for an empty chain. + class EmptyInvocationChain < LinkedList::EmptyLinkedList + @parent = InvocationChain + + def member?(obj) + false + end + + def append(invocation) + conj(invocation) + end + + def to_s + "TOP" + end + end + + EMPTY = EmptyInvocationChain.new + end +end diff --git a/lib/ruby/shared/rake/invocation_exception_mixin.rb b/lib/ruby/shared/rake/invocation_exception_mixin.rb new file mode 100644 index 00000000000..84ff3353ba9 --- /dev/null +++ b/lib/ruby/shared/rake/invocation_exception_mixin.rb @@ -0,0 +1,16 @@ +module Rake + module InvocationExceptionMixin + # Return the invocation chain (list of Rake tasks) that were in + # effect when this exception was detected by rake. May be null if + # no tasks were active. + def chain + @rake_invocation_chain ||= nil + end + + # Set the invocation chain in effect when this exception was + # detected. + def chain=(value) + @rake_invocation_chain = value + end + end +end diff --git a/lib/ruby/shared/rake/linked_list.rb b/lib/ruby/shared/rake/linked_list.rb new file mode 100644 index 00000000000..26483703f42 --- /dev/null +++ b/lib/ruby/shared/rake/linked_list.rb @@ -0,0 +1,103 @@ +module Rake + + # Polylithic linked list structure used to implement several data + # structures in Rake. + class LinkedList + include Enumerable + + attr_reader :head, :tail + + def initialize(head, tail=EMPTY) + @head = head + @tail = tail + end + + # Polymorphically add a new element to the head of a list. The + # type of head node will be the same list type has the tail. + def conj(item) + self.class.cons(item, self) + end + + # Is the list empty? + def empty? + false + end + + # Lists are structurally equivalent. + def ==(other) + current = self + while ! current.empty? && ! other.empty? + return false if current.head != other.head + current = current.tail + other = other.tail + end + current.empty? && other.empty? + end + + # Convert to string: LL(item, item...) + def to_s + items = map { |item| item.to_s }.join(", ") + "LL(#{items})" + end + + # Same as +to_s+, but with inspected items. + def inspect + items = map { |item| item.inspect }.join(", ") + "LL(#{items})" + end + + # For each item in the list. + def each + current = self + while ! current.empty? + yield(current.head) + current = current.tail + end + self + end + + # Make a list out of the given arguments. This method is + # polymorphic + def self.make(*args) + result = empty + args.reverse_each do |item| + result = cons(item, result) + end + result + end + + # Cons a new head onto the tail list. + def self.cons(head, tail) + new(head, tail) + end + + # The standard empty list class for the given LinkedList class. + def self.empty + self::EMPTY + end + + # Represent an empty list, using the Null Object Pattern. + # + # When inheriting from the LinkedList class, you should implement + # a type specific Empty class as well. Make sure you set the class + # instance variable @parent to the assocated list class (this + # allows conj, cons and make to work polymorphically). + class EmptyLinkedList < LinkedList + @parent = LinkedList + + def initialize + end + + def empty? + true + end + + def self.cons(head, tail) + @parent.cons(head, tail) + end + end + + EMPTY = EmptyLinkedList.new + end + +end diff --git a/lib/ruby/shared/rake/loaders/makefile.rb b/lib/ruby/shared/rake/loaders/makefile.rb new file mode 100644 index 00000000000..4ece4323af8 --- /dev/null +++ b/lib/ruby/shared/rake/loaders/makefile.rb @@ -0,0 +1,40 @@ +module Rake + + # Makefile loader to be used with the import file loader. + class MakefileLoader + include Rake::DSL + + SPACE_MARK = "\0" + + # Load the makefile dependencies in +fn+. + def load(fn) + lines = File.read fn + lines.gsub!(/\\ /, SPACE_MARK) + lines.gsub!(/#[^\n]*\n/m, "") + lines.gsub!(/\\\n/, ' ') + lines.each_line do |line| + process_line(line) + end + end + + private + + # Process one logical line of makefile data. + def process_line(line) + file_tasks, args = line.split(':', 2) + return if args.nil? + dependents = args.split.map { |d| respace(d) } + file_tasks.scan(/\S+/) do |file_task| + file_task = respace(file_task) + file file_task => dependents + end + end + + def respace(str) + str.tr SPACE_MARK, ' ' + end + end + + # Install the handler + Rake.application.add_loader('mf', MakefileLoader.new) +end diff --git a/lib/ruby/shared/rake/multi_task.rb b/lib/ruby/shared/rake/multi_task.rb new file mode 100644 index 00000000000..5418a7a7b0e --- /dev/null +++ b/lib/ruby/shared/rake/multi_task.rb @@ -0,0 +1,13 @@ +module Rake + + # Same as a regular task, but the immediate prerequisites are done in + # parallel using Ruby threads. + # + class MultiTask < Task + private + def invoke_prerequisites(task_args, invocation_chain) # :nodoc: + invoke_prerequisites_concurrently(task_args, invocation_chain) + end + end + +end diff --git a/lib/ruby/shared/rake/name_space.rb b/lib/ruby/shared/rake/name_space.rb new file mode 100644 index 00000000000..e1cc0940b8a --- /dev/null +++ b/lib/ruby/shared/rake/name_space.rb @@ -0,0 +1,25 @@ +module Rake + + # The NameSpace class will lookup task names in the the scope + # defined by a +namespace+ command. + # + class NameSpace + + # Create a namespace lookup object using the given task manager + # and the list of scopes. + def initialize(task_manager, scope_list) + @task_manager = task_manager + @scope = scope_list.dup + end + + # Lookup a task named +name+ in the namespace. + def [](name) + @task_manager.lookup(name, @scope) + end + + # Return the list of tasks defined in this and nested namespaces. + def tasks + @task_manager.tasks_in_scope(@scope) + end + end +end diff --git a/lib/ruby/shared/rake/packagetask.rb b/lib/ruby/shared/rake/packagetask.rb new file mode 100644 index 00000000000..029caa6d49c --- /dev/null +++ b/lib/ruby/shared/rake/packagetask.rb @@ -0,0 +1,190 @@ +# Define a package task library to aid in the definition of +# redistributable package files. + +require 'rake' +require 'rake/tasklib' + +module Rake + + # Create a packaging task that will package the project into + # distributable files (e.g zip archive or tar files). + # + # The PackageTask will create the following targets: + # + # [:package] + # Create all the requested package files. + # + # [:clobber_package] + # Delete all the package files. This target is automatically + # added to the main clobber target. + # + # [:repackage] + # Rebuild the package files from scratch, even if they are not out + # of date. + # + # ["package_dir/name-version.tgz"] + # Create a gzipped tar package (if need_tar is true). + # + # ["package_dir/name-version.tar.gz"] + # Create a gzipped tar package (if need_tar_gz is true). + # + # ["package_dir/name-version.tar.bz2"] + # Create a bzip2'd tar package (if need_tar_bz2 is true). + # + # ["package_dir/name-version.zip"] + # Create a zip package archive (if need_zip is true). + # + # Example: + # + # Rake::PackageTask.new("rake", "1.2.3") do |p| + # p.need_tar = true + # p.package_files.include("lib/**/*.rb") + # end + # + class PackageTask < TaskLib + # Name of the package (from the GEM Spec). + attr_accessor :name + + # Version of the package (e.g. '1.3.2'). + attr_accessor :version + + # Directory used to store the package files (default is 'pkg'). + attr_accessor :package_dir + + # True if a gzipped tar file (tgz) should be produced (default is + # false). + attr_accessor :need_tar + + # True if a gzipped tar file (tar.gz) should be produced (default + # is false). + attr_accessor :need_tar_gz + + # True if a bzip2'd tar file (tar.bz2) should be produced (default + # is false). + attr_accessor :need_tar_bz2 + + # True if a zip file should be produced (default is false) + attr_accessor :need_zip + + # List of files to be included in the package. + attr_accessor :package_files + + # Tar command for gzipped or bzip2ed archives. The default is 'tar'. + attr_accessor :tar_command + + # Zip command for zipped archives. The default is 'zip'. + attr_accessor :zip_command + + # Create a Package Task with the given name and version. Use +:noversion+ + # as the version to build a package without a version or to provide a + # fully-versioned package name. + + def initialize(name=nil, version=nil) + init(name, version) + yield self if block_given? + define unless name.nil? + end + + # Initialization that bypasses the "yield self" and "define" step. + def init(name, version) + @name = name + @version = version + @package_files = Rake::FileList.new + @package_dir = 'pkg' + @need_tar = false + @need_tar_gz = false + @need_tar_bz2 = false + @need_zip = false + @tar_command = 'tar' + @zip_command = 'zip' + end + + # Create the tasks defined by this task library. + def define + fail "Version required (or :noversion)" if @version.nil? + @version = nil if :noversion == @version + + desc "Build all the packages" + task :package + + desc "Force a rebuild of the package files" + task :repackage => [:clobber_package, :package] + + desc "Remove package products" + task :clobber_package do + rm_r package_dir rescue nil + end + + task :clobber => [:clobber_package] + + [ + [need_tar, tgz_file, "z"], + [need_tar_gz, tar_gz_file, "z"], + [need_tar_bz2, tar_bz2_file, "j"] + ].each do |(need, file, flag)| + if need + task :package => ["#{package_dir}/#{file}"] + file "#{package_dir}/#{file}" => + [package_dir_path] + package_files do + chdir(package_dir) do + sh %{#{@tar_command} #{flag}cvf #{file} #{package_name}} + end + end + end + end + + if need_zip + task :package => ["#{package_dir}/#{zip_file}"] + file "#{package_dir}/#{zip_file}" => + [package_dir_path] + package_files do + chdir(package_dir) do + sh %{#{@zip_command} -r #{zip_file} #{package_name}} + end + end + end + + directory package_dir + + file package_dir_path => @package_files do + mkdir_p package_dir rescue nil + @package_files.each do |fn| + f = File.join(package_dir_path, fn) + fdir = File.dirname(f) + mkdir_p(fdir) unless File.exist?(fdir) + if File.directory?(fn) + mkdir_p(f) + else + rm_f f + safe_ln(fn, f) + end + end + end + self + end + + def package_name + @version ? "#{@name}-#{@version}" : @name + end + + def package_dir_path + "#{package_dir}/#{package_name}" + end + + def tgz_file + "#{package_name}.tgz" + end + + def tar_gz_file + "#{package_name}.tar.gz" + end + + def tar_bz2_file + "#{package_name}.tar.bz2" + end + + def zip_file + "#{package_name}.zip" + end + end + +end diff --git a/lib/ruby/shared/rake/pathmap.rb b/lib/ruby/shared/rake/pathmap.rb new file mode 100644 index 00000000000..22757243413 --- /dev/null +++ b/lib/ruby/shared/rake/pathmap.rb @@ -0,0 +1 @@ +require 'rake/ext/string' diff --git a/lib/ruby/shared/rake/phony.rb b/lib/ruby/shared/rake/phony.rb new file mode 100644 index 00000000000..29633ae0660 --- /dev/null +++ b/lib/ruby/shared/rake/phony.rb @@ -0,0 +1,15 @@ +# Defines a :phony task that you can use as a dependency. This allows +# file-based tasks to use non-file-based tasks as prerequisites +# without forcing them to rebuild. +# +# See FileTask#out_of_date? and Task#timestamp for more info. + +require 'rake' + +task :phony + +Rake::Task[:phony].tap do |task| + def task.timestamp # :nodoc: + Time.at 0 + end +end diff --git a/lib/ruby/shared/rake/private_reader.rb b/lib/ruby/shared/rake/private_reader.rb new file mode 100644 index 00000000000..1620978576d --- /dev/null +++ b/lib/ruby/shared/rake/private_reader.rb @@ -0,0 +1,20 @@ +module Rake + + # Include PrivateReader to use +private_reader+. + module PrivateReader # :nodoc: all + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + + # Declare a list of private accessors + def private_reader(*names) + attr_reader(*names) + private(*names) + end + end + + end +end diff --git a/lib/ruby/shared/rake/promise.rb b/lib/ruby/shared/rake/promise.rb new file mode 100644 index 00000000000..31c4563476f --- /dev/null +++ b/lib/ruby/shared/rake/promise.rb @@ -0,0 +1,99 @@ +module Rake + + # A Promise object represents a promise to do work (a chore) in the + # future. The promise is created with a block and a list of + # arguments for the block. Calling value will return the value of + # the promised chore. + # + # Used by ThreadPool. + # + class Promise # :nodoc: all + NOT_SET = Object.new.freeze # :nodoc: + + attr_accessor :recorder + + # Create a promise to do the chore specified by the block. + def initialize(args, &block) + @mutex = Mutex.new + @result = NOT_SET + @error = NOT_SET + @args = args + @block = block + end + + # Return the value of this promise. + # + # If the promised chore is not yet complete, then do the work + # synchronously. We will wait. + def value + unless complete? + stat :sleeping_on, :item_id => object_id + @mutex.synchronize do + stat :has_lock_on, :item_id => object_id + chore + stat :releasing_lock_on, :item_id => object_id + end + end + error? ? raise(@error) : @result + end + + # If no one else is working this promise, go ahead and do the chore. + def work + stat :attempting_lock_on, :item_id => object_id + if @mutex.try_lock + stat :has_lock_on, :item_id => object_id + chore + stat :releasing_lock_on, :item_id => object_id + @mutex.unlock + else + stat :bailed_on, :item_id => object_id + end + end + + private + + # Perform the chore promised + def chore + if complete? + stat :found_completed, :item_id => object_id + return + end + stat :will_execute, :item_id => object_id + begin + @result = @block.call(*@args) + rescue Exception => e + @error = e + end + stat :did_execute, :item_id => object_id + discard + end + + # Do we have a result for the promise + def result? + ! @result.equal?(NOT_SET) + end + + # Did the promise throw an error + def error? + ! @error.equal?(NOT_SET) + end + + # Are we done with the promise + def complete? + result? || error? + end + + # free up these items for the GC + def discard + @args = nil + @block = nil + end + + # Record execution statistics if there is a recorder + def stat(*args) + @recorder.call(*args) if @recorder + end + + end + +end diff --git a/lib/ruby/shared/rake/pseudo_status.rb b/lib/ruby/shared/rake/pseudo_status.rb new file mode 100644 index 00000000000..09d5c88c7e0 --- /dev/null +++ b/lib/ruby/shared/rake/pseudo_status.rb @@ -0,0 +1,29 @@ +module Rake + + #################################################################### + # Exit status class for times the system just gives us a nil. + class PseudoStatus + attr_reader :exitstatus + + def initialize(code=0) + @exitstatus = code + end + + def to_i + @exitstatus << 8 + end + + def >>(n) + to_i >> n + end + + def stopped? + false + end + + def exited? + true + end + end + +end diff --git a/lib/ruby/shared/rake/rake_module.rb b/lib/ruby/shared/rake/rake_module.rb new file mode 100644 index 00000000000..fcf5800064b --- /dev/null +++ b/lib/ruby/shared/rake/rake_module.rb @@ -0,0 +1,37 @@ +require 'rake/application' + +module Rake + + # Rake module singleton methods. + # + class << self + # Current Rake Application + def application + @application ||= Rake::Application.new + end + + # Set the current Rake application object. + def application=(app) + @application = app + end + + # Return the original directory where the Rake application was started. + def original_dir + application.original_dir + end + + # Load a rakefile. + def load_rakefile(path) + load(path) + end + + # Add files to the rakelib list + def add_rakelib(*files) + application.options.rakelib ||= [] + files.each do |file| + application.options.rakelib << file + end + end + end + +end diff --git a/lib/ruby/shared/rake/rake_test_loader.rb b/lib/ruby/shared/rake/rake_test_loader.rb new file mode 100644 index 00000000000..7e3a6b3f352 --- /dev/null +++ b/lib/ruby/shared/rake/rake_test_loader.rb @@ -0,0 +1,22 @@ +require 'rake' + +# Load the test files from the command line. +argv = ARGV.select do |argument| + case argument + when /^-/ then + argument + when /\*/ then + FileList[argument].to_a.each do |file| + require File.expand_path file + end + + false + else + require File.expand_path argument + + false + end +end + +ARGV.replace argv + diff --git a/lib/ruby/shared/rake/rdoctask.rb b/lib/ruby/shared/rake/rdoctask.rb new file mode 100644 index 00000000000..50b7e269d50 --- /dev/null +++ b/lib/ruby/shared/rake/rdoctask.rb @@ -0,0 +1,2 @@ +fail "ERROR: 'rake/rdoctask' is obsolete and no longer supported. " + + "Use 'rdoc/task' (available in RDoc 2.4.2+) instead." diff --git a/lib/ruby/shared/rake/ruby182_test_unit_fix.rb b/lib/ruby/shared/rake/ruby182_test_unit_fix.rb new file mode 100644 index 00000000000..e47feeb09c6 --- /dev/null +++ b/lib/ruby/shared/rake/ruby182_test_unit_fix.rb @@ -0,0 +1,27 @@ +# Local Rake override to fix bug in Ruby 0.8.2 +module Test # :nodoc: + # Local Rake override to fix bug in Ruby 0.8.2 + module Unit # :nodoc: + # Local Rake override to fix bug in Ruby 0.8.2 + module Collector # :nodoc: + # Local Rake override to fix bug in Ruby 0.8.2 + class Dir # :nodoc: + undef collect_file + def collect_file(name, suites, already_gathered) # :nodoc: + dir = File.dirname(File.expand_path(name)) + $:.unshift(dir) unless $:.first == dir + if @req + @req.require(name) + else + require(name) + end + find_test_cases(already_gathered).each do |t| + add_suite(suites, t.suite) + end + ensure + $:.delete_at $:.rindex(dir) + end + end + end + end +end diff --git a/lib/ruby/shared/rake/rule_recursion_overflow_error.rb b/lib/ruby/shared/rake/rule_recursion_overflow_error.rb new file mode 100644 index 00000000000..da4318da9dc --- /dev/null +++ b/lib/ruby/shared/rake/rule_recursion_overflow_error.rb @@ -0,0 +1,20 @@ + +module Rake + + # Error indicating a recursion overflow error in task selection. + class RuleRecursionOverflowError < StandardError + def initialize(*args) + super + @targets = [] + end + + def add_target(target) + @targets << target + end + + def message + super + ": [" + @targets.reverse.join(' => ') + "]" + end + end + +end diff --git a/lib/ruby/shared/rake/runtest.rb b/lib/ruby/shared/rake/runtest.rb new file mode 100644 index 00000000000..3f01b28cadb --- /dev/null +++ b/lib/ruby/shared/rake/runtest.rb @@ -0,0 +1,22 @@ +require 'test/unit' +require 'test/unit/assertions' +require 'rake/file_list' + +module Rake + include Test::Unit::Assertions + + def run_tests(pattern='test/test*.rb', log_enabled=false) + FileList.glob(pattern).each do |fn| + $stderr.puts fn if log_enabled + begin + require fn + rescue Exception => ex + $stderr.puts "Error in #{fn}: #{ex.message}" + $stderr.puts ex.backtrace + assert false + end + end + end + + extend self +end diff --git a/lib/ruby/shared/rake/scope.rb b/lib/ruby/shared/rake/scope.rb new file mode 100644 index 00000000000..33e1c08e7b9 --- /dev/null +++ b/lib/ruby/shared/rake/scope.rb @@ -0,0 +1,42 @@ +module Rake + class Scope < LinkedList + + # Path for the scope. + def path + map { |item| item.to_s }.reverse.join(":") + end + + # Path for the scope + the named path. + def path_with_task_name(task_name) + "#{path}:#{task_name}" + end + + # Trim +n+ innermost scope levels from the scope. In no case will + # this trim beyond the toplevel scope. + def trim(n) + result = self + while n > 0 && ! result.empty? + result = result.tail + n -= 1 + end + result + end + + # Scope lists always end with an EmptyScope object. See Null + # Object Pattern) + class EmptyScope < EmptyLinkedList + @parent = Scope + + def path + "" + end + + def path_with_task_name(task_name) + task_name + end + end + + # Singleton null object for an empty scope. + EMPTY = EmptyScope.new + end +end diff --git a/lib/ruby/shared/rake/task.rb b/lib/ruby/shared/rake/task.rb new file mode 100644 index 00000000000..5e4dd64d4e9 --- /dev/null +++ b/lib/ruby/shared/rake/task.rb @@ -0,0 +1,378 @@ +require 'rake/invocation_exception_mixin' + +module Rake + + # ######################################################################### + # A Task is the basic unit of work in a Rakefile. Tasks have associated + # actions (possibly more than one) and a list of prerequisites. When + # invoked, a task will first ensure that all of its prerequisites have an + # opportunity to run and then it will execute its own actions. + # + # Tasks are not usually created directly using the new method, but rather + # use the +file+ and +task+ convenience methods. + # + class Task + # List of prerequisites for a task. + attr_reader :prerequisites + + # List of actions attached to a task. + attr_reader :actions + + # Application owning this task. + attr_accessor :application + + # Array of nested namespaces names used for task lookup by this task. + attr_reader :scope + + # File/Line locations of each of the task definitions for this + # task (only valid if the task was defined with the detect + # location option set). + attr_reader :locations + + # Return task name + def to_s + name + end + + def inspect + "<#{self.class} #{name} => [#{prerequisites.join(', ')}]>" + end + + # List of sources for task. + attr_writer :sources + def sources + @sources ||= [] + end + + # List of prerequisite tasks + def prerequisite_tasks + prerequisites.map { |pre| lookup_prerequisite(pre) } + end + + def lookup_prerequisite(prerequisite_name) + application[prerequisite_name, @scope] + end + private :lookup_prerequisite + + # List of all unique prerequisite tasks including prerequisite tasks' + # prerequisites. + # Includes self when cyclic dependencies are found. + def all_prerequisite_tasks + seen = {} + collect_prerequisites(seen) + seen.values + end + + def collect_prerequisites(seen) + prerequisite_tasks.each do |pre| + next if seen[pre.name] + seen[pre.name] = pre + pre.collect_prerequisites(seen) + end + end + protected :collect_prerequisites + + # First source from a rule (nil if no sources) + def source + @sources.first if defined?(@sources) + end + + # Create a task named +task_name+ with no actions or prerequisites. Use + # +enhance+ to add actions and prerequisites. + def initialize(task_name, app) + @name = task_name.to_s + @prerequisites = [] + @actions = [] + @already_invoked = false + @comments = [] + @lock = Monitor.new + @application = app + @scope = app.current_scope + @arg_names = nil + @locations = [] + end + + # Enhance a task with prerequisites or actions. Returns self. + def enhance(deps=nil, &block) + @prerequisites |= deps if deps + @actions << block if block_given? + self + end + + # Name of the task, including any namespace qualifiers. + def name + @name.to_s + end + + # Name of task with argument list description. + def name_with_args # :nodoc: + if arg_description + "#{name}#{arg_description}" + else + name + end + end + + # Argument description (nil if none). + def arg_description # :nodoc: + @arg_names ? "[#{arg_names.join(',')}]" : nil + end + + # Name of arguments for this task. + def arg_names + @arg_names || [] + end + + # Reenable the task, allowing its tasks to be executed if the task + # is invoked again. + def reenable + @already_invoked = false + end + + # Clear the existing prerequisites and actions of a rake task. + def clear + clear_prerequisites + clear_actions + clear_comments + self + end + + # Clear the existing prerequisites of a rake task. + def clear_prerequisites + prerequisites.clear + self + end + + # Clear the existing actions on a rake task. + def clear_actions + actions.clear + self + end + + # Clear the existing comments on a rake task. + def clear_comments + @comments = [] + self + end + + # Invoke the task if it is needed. Prerequisites are invoked first. + def invoke(*args) + task_args = TaskArguments.new(arg_names, args) + invoke_with_call_chain(task_args, InvocationChain::EMPTY) + end + + # Same as invoke, but explicitly pass a call chain to detect + # circular dependencies. + def invoke_with_call_chain(task_args, invocation_chain) # :nodoc: + new_chain = InvocationChain.append(self, invocation_chain) + @lock.synchronize do + if application.options.trace + application.trace "** Invoke #{name} #{format_trace_flags}" + end + return if @already_invoked + @already_invoked = true + invoke_prerequisites(task_args, new_chain) + execute(task_args) if needed? + end + rescue Exception => ex + add_chain_to(ex, new_chain) + raise ex + end + protected :invoke_with_call_chain + + def add_chain_to(exception, new_chain) + exception.extend(InvocationExceptionMixin) unless + exception.respond_to?(:chain) + exception.chain = new_chain if exception.chain.nil? + end + private :add_chain_to + + # Invoke all the prerequisites of a task. + def invoke_prerequisites(task_args, invocation_chain) # :nodoc: + if application.options.always_multitask + invoke_prerequisites_concurrently(task_args, invocation_chain) + else + prerequisite_tasks.each { |p| + prereq_args = task_args.new_scope(p.arg_names) + p.invoke_with_call_chain(prereq_args, invocation_chain) + } + end + end + + # Invoke all the prerequisites of a task in parallel. + def invoke_prerequisites_concurrently(task_args, invocation_chain)# :nodoc: + futures = prerequisite_tasks.map do |p| + prereq_args = task_args.new_scope(p.arg_names) + application.thread_pool.future(p) do |r| + r.invoke_with_call_chain(prereq_args, invocation_chain) + end + end + futures.each { |f| f.value } + end + + # Format the trace flags for display. + def format_trace_flags + flags = [] + flags << "first_time" unless @already_invoked + flags << "not_needed" unless needed? + flags.empty? ? "" : "(" + flags.join(", ") + ")" + end + private :format_trace_flags + + # Execute the actions associated with this task. + def execute(args=nil) + args ||= EMPTY_TASK_ARGS + if application.options.dryrun + application.trace "** Execute (dry run) #{name}" + return + end + application.trace "** Execute #{name}" if application.options.trace + application.enhance_with_matching_rule(name) if @actions.empty? + @actions.each do |act| + case act.arity + when 1 + act.call(self) + else + act.call(self, args) + end + end + end + + # Is this task needed? + def needed? + true + end + + # Timestamp for this task. Basic tasks return the current time for their + # time stamp. Other tasks can be more sophisticated. + def timestamp + Time.now + end + + # Add a description to the task. The description can consist of an option + # argument list (enclosed brackets) and an optional comment. + def add_description(description) + return unless description + comment = description.strip + add_comment(comment) if comment && ! comment.empty? + end + + def comment=(comment) + add_comment(comment) + end + + def add_comment(comment) + @comments << comment unless @comments.include?(comment) + end + private :add_comment + + # Full collection of comments. Multiple comments are separated by + # newlines. + def full_comment + transform_comments("\n") + end + + # First line (or sentence) of all comments. Multiple comments are + # separated by a "/". + def comment + transform_comments(" / ") { |c| first_sentence(c) } + end + + # Transform the list of comments as specified by the block and + # join with the separator. + def transform_comments(separator, &block) + if @comments.empty? + nil + else + block ||= lambda { |c| c } + @comments.map(&block).join(separator) + end + end + private :transform_comments + + # Get the first sentence in a string. The sentence is terminated + # by the first period or the end of the line. Decimal points do + # not count as periods. + def first_sentence(string) + string.split(/\.[ \t]|\.$|\n/).first + end + private :first_sentence + + # Set the names of the arguments for this task. +args+ should be + # an array of symbols, one for each argument name. + def set_arg_names(args) + @arg_names = args.map { |a| a.to_sym } + end + + # Return a string describing the internal state of a task. Useful for + # debugging. + def investigation + result = "------------------------------\n" + result << "Investigating #{name}\n" + result << "class: #{self.class}\n" + result << "task needed: #{needed?}\n" + result << "timestamp: #{timestamp}\n" + result << "pre-requisites: \n" + prereqs = prerequisite_tasks + prereqs.sort! { |a, b| a.timestamp <=> b.timestamp } + prereqs.each do |p| + result << "--#{p.name} (#{p.timestamp})\n" + end + latest_prereq = prerequisite_tasks.map { |pre| pre.timestamp }.max + result << "latest-prerequisite time: #{latest_prereq}\n" + result << "................................\n\n" + return result + end + + # ---------------------------------------------------------------- + # Rake Module Methods + # + class << self + + # Clear the task list. This cause rake to immediately forget all the + # tasks that have been assigned. (Normally used in the unit tests.) + def clear + Rake.application.clear + end + + # List of all defined tasks. + def tasks + Rake.application.tasks + end + + # Return a task with the given name. If the task is not currently + # known, try to synthesize one from the defined rules. If no rules are + # found, but an existing file matches the task name, assume it is a file + # task with no dependencies or actions. + def [](task_name) + Rake.application[task_name] + end + + # TRUE if the task name is already defined. + def task_defined?(task_name) + Rake.application.lookup(task_name) != nil + end + + # Define a task given +args+ and an option block. If a rule with the + # given name already exists, the prerequisites and actions are added to + # the existing task. Returns the defined task. + def define_task(*args, &block) + Rake.application.define_task(self, *args, &block) + end + + # Define a rule for synthesizing tasks. + def create_rule(*args, &block) + Rake.application.create_rule(*args, &block) + end + + # Apply the scope to the task name according to the rules for + # this kind of task. Generic tasks will accept the scope as + # part of the name. + def scope_name(scope, task_name) +# (scope + [task_name]).join(':') + scope.path_with_task_name(task_name) + end + + end # class << Rake::Task + end # class Rake::Task +end diff --git a/lib/ruby/shared/rake/task_argument_error.rb b/lib/ruby/shared/rake/task_argument_error.rb new file mode 100644 index 00000000000..3e1dda64dba --- /dev/null +++ b/lib/ruby/shared/rake/task_argument_error.rb @@ -0,0 +1,7 @@ +module Rake + + # Error indicating an ill-formed task declaration. + class TaskArgumentError < ArgumentError + end + +end diff --git a/lib/ruby/shared/rake/task_arguments.rb b/lib/ruby/shared/rake/task_arguments.rb new file mode 100644 index 00000000000..00946825793 --- /dev/null +++ b/lib/ruby/shared/rake/task_arguments.rb @@ -0,0 +1,89 @@ +module Rake + + #################################################################### + # TaskArguments manage the arguments passed to a task. + # + class TaskArguments + include Enumerable + + attr_reader :names + + # Create a TaskArgument object with a list of named arguments + # (given by :names) and a set of associated values (given by + # :values). :parent is the parent argument object. + def initialize(names, values, parent=nil) + @names = names + @parent = parent + @hash = {} + @values = values + names.each_with_index { |name, i| + @hash[name.to_sym] = values[i] unless values[i].nil? + } + end + + # Retrive the complete array of sequential values + def to_a + @values.dup + end + + # Retrive the list of values not associated with named arguments + def extras + @values[@names.length..-1] || [] + end + + # Create a new argument scope using the prerequisite argument + # names. + def new_scope(names) + values = names.map { |n| self[n] } + self.class.new(names, values + extras, self) + end + + # Find an argument value by name or index. + def [](index) + lookup(index.to_sym) + end + + # Specify a hash of default values for task arguments. Use the + # defaults only if there is no specific value for the given + # argument. + def with_defaults(defaults) + @hash = defaults.merge(@hash) + end + + def each(&block) + @hash.each(&block) + end + + def values_at(*keys) + keys.map { |k| lookup(k) } + end + + def method_missing(sym, *args) + lookup(sym.to_sym) + end + + def to_hash + @hash + end + + def to_s + @hash.inspect + end + + def inspect + to_s + end + + protected + + def lookup(name) + if @hash.has_key?(name) + @hash[name] + elsif @parent + @parent.lookup(name) + end + end + end + + EMPTY_TASK_ARGS = TaskArguments.new([], []) +end diff --git a/lib/ruby/shared/rake/task_manager.rb b/lib/ruby/shared/rake/task_manager.rb new file mode 100644 index 00000000000..06c243a7b20 --- /dev/null +++ b/lib/ruby/shared/rake/task_manager.rb @@ -0,0 +1,297 @@ +module Rake + + # The TaskManager module is a mixin for managing tasks. + module TaskManager + # Track the last comment made in the Rakefile. + attr_accessor :last_description + alias :last_comment :last_description # Backwards compatibility + + def initialize + super + @tasks = Hash.new + @rules = Array.new + @scope = Scope.make + @last_description = nil + end + + def create_rule(*args, &block) + pattern, args, deps = resolve_args(args) + pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern + @rules << [pattern, args, deps, block] + end + + def define_task(task_class, *args, &block) + task_name, arg_names, deps = resolve_args(args) + task_name = task_class.scope_name(@scope, task_name) + deps = [deps] unless deps.respond_to?(:to_ary) + deps = deps.map { |d| d.to_s } + task = intern(task_class, task_name) + task.set_arg_names(arg_names) unless arg_names.empty? + if Rake::TaskManager.record_task_metadata + add_location(task) + task.add_description(get_description(task)) + end + task.enhance(deps, &block) + end + + # Lookup a task. Return an existing task if found, otherwise + # create a task of the current type. + def intern(task_class, task_name) + @tasks[task_name.to_s] ||= task_class.new(task_name, self) + end + + # Find a matching task for +task_name+. + def [](task_name, scopes=nil) + task_name = task_name.to_s + self.lookup(task_name, scopes) or + enhance_with_matching_rule(task_name) or + synthesize_file_task(task_name) or + fail "Don't know how to build task '#{task_name}'" + end + + def synthesize_file_task(task_name) + return nil unless File.exist?(task_name) + define_task(Rake::FileTask, task_name) + end + + # Resolve the arguments for a task/rule. Returns a triplet of + # [task_name, arg_name_list, prerequisites]. + def resolve_args(args) + if args.last.is_a?(Hash) + deps = args.pop + resolve_args_with_dependencies(args, deps) + else + resolve_args_without_dependencies(args) + end + end + + # Resolve task arguments for a task or rule when there are no + # dependencies declared. + # + # The patterns recognized by this argument resolving function are: + # + # task :t + # task :t, [:a] + # + def resolve_args_without_dependencies(args) + task_name = args.shift + if args.size == 1 && args.first.respond_to?(:to_ary) + arg_names = args.first.to_ary + else + arg_names = args + end + [task_name, arg_names, []] + end + private :resolve_args_without_dependencies + + # Resolve task arguments for a task or rule when there are + # dependencies declared. + # + # The patterns recognized by this argument resolving function are: + # + # task :t => [:d] + # task :t, [a] => [:d] + # + def resolve_args_with_dependencies(args, hash) # :nodoc: + fail "Task Argument Error" if hash.size != 1 + key, value = hash.map { |k, v| [k, v] }.first + if args.empty? + task_name = key + arg_names = [] + deps = value + else + task_name = args.shift + arg_names = key + deps = value + end + deps = [deps] unless deps.respond_to?(:to_ary) + [task_name, arg_names, deps] + end + private :resolve_args_with_dependencies + + # If a rule can be found that matches the task name, enhance the + # task with the prerequisites and actions from the rule. Set the + # source attribute of the task appropriately for the rule. Return + # the enhanced task or nil of no rule was found. + def enhance_with_matching_rule(task_name, level=0) + fail Rake::RuleRecursionOverflowError, + "Rule Recursion Too Deep" if level >= 16 + @rules.each do |pattern, args, extensions, block| + if pattern.match(task_name) + task = attempt_rule(task_name, args, extensions, block, level) + return task if task + end + end + nil + rescue Rake::RuleRecursionOverflowError => ex + ex.add_target(task_name) + fail ex + end + + # List of all defined tasks in this application. + def tasks + @tasks.values.sort_by { |t| t.name } + end + + # List of all the tasks defined in the given scope (and its + # sub-scopes). + def tasks_in_scope(scope) + prefix = scope.path + tasks.select { |t| + /^#{prefix}:/ =~ t.name + } + end + + # Clear all tasks in this application. + def clear + @tasks.clear + @rules.clear + end + + # Lookup a task, using scope and the scope hints in the task name. + # This method performs straight lookups without trying to + # synthesize file tasks or rules. Special scope names (e.g. '^') + # are recognized. If no scope argument is supplied, use the + # current scope. Return nil if the task cannot be found. + def lookup(task_name, initial_scope=nil) + initial_scope ||= @scope + task_name = task_name.to_s + if task_name =~ /^rake:/ + scopes = Scope.make + task_name = task_name.sub(/^rake:/, '') + elsif task_name =~ /^(\^+)/ + scopes = initial_scope.trim($1.size) + task_name = task_name.sub(/^(\^+)/, '') + else + scopes = initial_scope + end + lookup_in_scope(task_name, scopes) + end + + # Lookup the task name + def lookup_in_scope(name, scope) + loop do + tn = scope.path_with_task_name(name) + task = @tasks[tn] + return task if task + break if scope.empty? + scope = scope.tail + end + nil + end + private :lookup_in_scope + + # Return the list of scope names currently active in the task + # manager. + def current_scope + @scope + end + + # Evaluate the block in a nested namespace named +name+. Create + # an anonymous namespace if +name+ is nil. + def in_namespace(name) + name ||= generate_name + @scope = Scope.new(name, @scope) + ns = NameSpace.new(self, @scope) + yield(ns) + ns + ensure + @scope = @scope.tail + end + + private + + # Add a location to the locations field of the given task. + def add_location(task) + loc = find_location + task.locations << loc if loc + task + end + + # Find the location that called into the dsl layer. + def find_location + locations = caller + i = 0 + while locations[i] + return locations[i + 1] if locations[i] =~ /rake\/dsl_definition.rb/ + i += 1 + end + nil + end + + # Generate an anonymous namespace name. + def generate_name + @seed ||= 0 + @seed += 1 + "_anon_#{@seed}" + end + + def trace_rule(level, message) + options.trace_output.puts "#{" " * level}#{message}" if + Rake.application.options.trace_rules + end + + # Attempt to create a rule given the list of prerequisites. + def attempt_rule(task_name, args, extensions, block, level) + sources = make_sources(task_name, extensions) + prereqs = sources.map { |source| + trace_rule level, "Attempting Rule #{task_name} => #{source}" + if File.exist?(source) || Rake::Task.task_defined?(source) + trace_rule level, "(#{task_name} => #{source} ... EXIST)" + source + elsif parent = enhance_with_matching_rule(source, level + 1) + trace_rule level, "(#{task_name} => #{source} ... ENHANCE)" + parent.name + else + trace_rule level, "(#{task_name} => #{source} ... FAIL)" + return nil + end + } + task = FileTask.define_task(task_name, {args => prereqs}, &block) + task.sources = prereqs + task + end + + # Make a list of sources from the list of file name extensions / + # translation procs. + def make_sources(task_name, extensions) + result = extensions.map { |ext| + case ext + when /%/ + task_name.pathmap(ext) + when %r{/} + ext + when /^\./ + task_name.ext(ext) + when String + ext + when Proc + if ext.arity == 1 + ext.call(task_name) + else + ext.call + end + else + fail "Don't know how to handle rule dependent: #{ext.inspect}" + end + } + result.flatten + end + + + private + + # Return the current description, clearing it in the process. + def get_description(task) + desc = @last_description + @last_description = nil + desc + end + + class << self + attr_accessor :record_task_metadata + TaskManager.record_task_metadata = false + end + end + +end diff --git a/lib/ruby/shared/rake/tasklib.rb b/lib/ruby/shared/rake/tasklib.rb new file mode 100644 index 00000000000..48d27df9ed0 --- /dev/null +++ b/lib/ruby/shared/rake/tasklib.rb @@ -0,0 +1,22 @@ +require 'rake' + +module Rake + + # Base class for Task Libraries. + class TaskLib + include Cloneable + include Rake::DSL + + # Make a symbol by pasting two strings together. + # + # NOTE: DEPRECATED! This method is kinda stupid. I don't know why + # I didn't just use string interpolation. But now other task + # libraries depend on this so I can't remove it without breaking + # other people's code. So for now it stays for backwards + # compatibility. BUT DON'T USE IT. + def paste(a, b) # :nodoc: + (a.to_s + b.to_s).intern + end + end + +end diff --git a/lib/ruby/shared/rake/testtask.rb b/lib/ruby/shared/rake/testtask.rb new file mode 100644 index 00000000000..c693dd26262 --- /dev/null +++ b/lib/ruby/shared/rake/testtask.rb @@ -0,0 +1,201 @@ +# Define a task library for running unit tests. + +require 'rake' +require 'rake/tasklib' + +module Rake + + # Create a task that runs a set of tests. + # + # Example: + # + # Rake::TestTask.new do |t| + # t.libs << "test" + # t.test_files = FileList['test/test*.rb'] + # t.verbose = true + # end + # + # If rake is invoked with a "TEST=filename" command line option, + # then the list of test files will be overridden to include only the + # filename specified on the command line. This provides an easy way + # to run just one test. + # + # If rake is invoked with a "TESTOPTS=options" command line option, + # then the given options are passed to the test process after a + # '--'. This allows Test::Unit options to be passed to the test + # suite. + # + # Examples: + # + # rake test # run tests normally + # rake test TEST=just_one_file.rb # run just one test file. + # rake test TESTOPTS="-v" # run in verbose mode + # rake test TESTOPTS="--runner=fox" # use the fox test runner + # + class TestTask < TaskLib + + # Name of test task. (default is :test) + attr_accessor :name + + # List of directories to added to $LOAD_PATH before running the + # tests. (default is 'lib') + attr_accessor :libs + + # True if verbose test output desired. (default is false) + attr_accessor :verbose + + # Test options passed to the test suite. An explicit + # TESTOPTS=opts on the command line will override this. (default + # is NONE) + attr_accessor :options + + # Request that the tests be run with the warning flag set. + # E.g. warning=true implies "ruby -w" used to run the tests. + attr_accessor :warning + + # Glob pattern to match test files. (default is 'test/test*.rb') + attr_accessor :pattern + + # Style of test loader to use. Options are: + # + # * :rake -- Rake provided test loading script (default). + # * :testrb -- Ruby provided test loading script. + # * :direct -- Load tests using command line loader. + # + attr_accessor :loader + + # Array of commandline options to pass to ruby when running test loader. + attr_accessor :ruby_opts + + # Explicitly define the list of test files to be included in a + # test. +list+ is expected to be an array of file names (a + # FileList is acceptable). If both +pattern+ and +test_files+ are + # used, then the list of test files is the union of the two. + def test_files=(list) + @test_files = list + end + + # Create a testing task. + def initialize(name=:test) + @name = name + @libs = ["lib"] + @pattern = nil + @options = nil + @test_files = nil + @verbose = false + @warning = false + @loader = :rake + @ruby_opts = [] + yield self if block_given? + @pattern = 'test/test*.rb' if @pattern.nil? && @test_files.nil? + define + end + + # Create the tasks defined by this task lib. + def define + desc "Run tests" + (@name == :test ? "" : " for #{@name}") + task @name do + FileUtilsExt.verbose(@verbose) do + args = + "#{ruby_opts_string} #{run_code} " + + "#{file_list_string} #{option_list}" + ruby args do |ok, status| + if !ok && status.respond_to?(:signaled?) && status.signaled? + raise SignalException.new(status.termsig) + elsif !ok + fail "Command failed with status (#{status.exitstatus}): " + + "[ruby #{args}]" + end + end + end + end + self + end + + def option_list # :nodoc: + (ENV['TESTOPTS'] || + ENV['TESTOPT'] || + ENV['TEST_OPTS'] || + ENV['TEST_OPT'] || + @options || + "") + end + + def ruby_opts_string + opts = @ruby_opts.dup + opts.unshift("-I\"#{lib_path}\"") unless @libs.empty? + opts.unshift("-w") if @warning + opts.join(" ") + end + + def lib_path + @libs.join(File::PATH_SEPARATOR) + end + + def file_list_string + file_list.map { |fn| "\"#{fn}\"" }.join(' ') + end + + def file_list # :nodoc: + if ENV['TEST'] + FileList[ENV['TEST']] + else + result = [] + result += @test_files.to_a if @test_files + result << @pattern if @pattern + result + end + end + + def fix # :nodoc: + case ruby_version + when '1.8.2' + "\"#{find_file 'rake/ruby182_test_unit_fix'}\"" + else + nil + end || '' + end + + def ruby_version + RUBY_VERSION + end + + def run_code + case @loader + when :direct + "-e \"ARGV.each{|f| require f}\"" + when :testrb + "-S testrb #{fix}" + when :rake + "-I\"#{rake_lib_dir}\" \"#{rake_loader}\"" + end + end + + def rake_loader # :nodoc: + find_file('rake/rake_test_loader') or + fail "unable to find rake test loader" + end + + def find_file(fn) # :nodoc: + $LOAD_PATH.each do |path| + file_path = File.join(path, "#{fn}.rb") + return file_path if File.exist? file_path + end + nil + end + + def rake_lib_dir # :nodoc: + find_dir('rake') or + fail "unable to find rake lib" + end + + def find_dir(fn) # :nodoc: + $LOAD_PATH.each do |path| + file_path = File.join(path, "#{fn}.rb") + return path if File.exist? file_path + end + nil + end + + end +end diff --git a/lib/ruby/shared/rake/thread_history_display.rb b/lib/ruby/shared/rake/thread_history_display.rb new file mode 100644 index 00000000000..c2af9ecef53 --- /dev/null +++ b/lib/ruby/shared/rake/thread_history_display.rb @@ -0,0 +1,48 @@ +require 'rake/private_reader' + +module Rake + + class ThreadHistoryDisplay # :nodoc: all + include Rake::PrivateReader + + private_reader :stats, :items, :threads + + def initialize(stats) + @stats = stats + @items = { :_seq_ => 1 } + @threads = { :_seq_ => "A" } + end + + def show + puts "Job History:" + stats.each do |stat| + stat[:data] ||= {} + rename(stat, :thread, threads) + rename(stat[:data], :item_id, items) + rename(stat[:data], :new_thread, threads) + rename(stat[:data], :deleted_thread, threads) + printf("%8d %2s %-20s %s\n", + (stat[:time] * 1_000_000).round, + stat[:thread], + stat[:event], + stat[:data].map do |k, v| "#{k}:#{v}" end.join(" ")) + end + end + + private + + def rename(hash, key, renames) + if hash && hash[key] + original = hash[key] + value = renames[original] + unless value + value = renames[:_seq_] + renames[:_seq_] = renames[:_seq_].succ + renames[original] = value + end + hash[key] = value + end + end + end + +end diff --git a/lib/ruby/shared/rake/thread_pool.rb b/lib/ruby/shared/rake/thread_pool.rb new file mode 100644 index 00000000000..44bc7483e4e --- /dev/null +++ b/lib/ruby/shared/rake/thread_pool.rb @@ -0,0 +1,161 @@ +require 'thread' +require 'set' + +require 'rake/promise' + +module Rake + + class ThreadPool # :nodoc: all + + # Creates a ThreadPool object. + # The parameter is the size of the pool. + def initialize(thread_count) + @max_active_threads = [thread_count, 0].max + @threads = Set.new + @threads_mon = Monitor.new + @queue = Queue.new + @join_cond = @threads_mon.new_cond + + @history_start_time = nil + @history = [] + @history_mon = Monitor.new + @total_threads_in_play = 0 + end + + # Creates a future executed by the +ThreadPool+. + # + # The args are passed to the block when executing (similarly to + # Thread#new) The return value is an object representing + # a future which has been created and added to the queue in the + # pool. Sending #value to the object will sleep the + # current thread until the future is finished and will return the + # result (or raise an exception thrown from the future) + def future(*args, &block) + promise = Promise.new(args, &block) + promise.recorder = lambda { |*stats| stat(*stats) } + + @queue.enq promise + stat :queued, :item_id => promise.object_id + start_thread + promise + end + + # Waits until the queue of futures is empty and all threads have exited. + def join + @threads_mon.synchronize do + begin + stat :joining + @join_cond.wait unless @threads.empty? + stat :joined + rescue Exception => e + stat :joined + $stderr.puts e + $stderr.print "Queue contains #{@queue.size} items. " + + "Thread pool contains #{@threads.count} threads\n" + $stderr.print "Current Thread #{Thread.current} status = " + + "#{Thread.current.status}\n" + $stderr.puts e.backtrace.join("\n") + @threads.each do |t| + $stderr.print "Thread #{t} status = #{t.status}\n" + # 1.8 doesn't support Thread#backtrace + $stderr.puts t.backtrace.join("\n") if t.respond_to? :backtrace + end + raise e + end + end + end + + # Enable the gathering of history events. + def gather_history #:nodoc: + @history_start_time = Time.now if @history_start_time.nil? + end + + # Return a array of history events for the thread pool. + # + # History gathering must be enabled to be able to see the events + # (see #gather_history). Best to call this when the job is + # complete (i.e. after ThreadPool#join is called). + def history # :nodoc: + @history_mon.synchronize { @history.dup }. + sort_by { |i| i[:time] }. + each { |i| i[:time] -= @history_start_time } + end + + # Return a hash of always collected statistics for the thread pool. + def statistics # :nodoc: + { + :total_threads_in_play => @total_threads_in_play, + :max_active_threads => @max_active_threads, + } + end + + private + + # processes one item on the queue. Returns true if there was an + # item to process, false if there was no item + def process_queue_item #:nodoc: + return false if @queue.empty? + + # Even though we just asked if the queue was empty, it + # still could have had an item which by this statement + # is now gone. For this reason we pass true to Queue#deq + # because we will sleep indefinitely if it is empty. + promise = @queue.deq(true) + stat :dequeued, :item_id => promise.object_id + promise.work + return true + + rescue ThreadError # this means the queue is empty + false + end + + def start_thread # :nodoc: + @threads_mon.synchronize do + next unless @threads.count < @max_active_threads + + t = Thread.new do + begin + while @threads.count <= @max_active_threads + break unless process_queue_item + end + ensure + @threads_mon.synchronize do + @threads.delete Thread.current + stat :ended, :thread_count => @threads.count + @join_cond.broadcast if @threads.empty? + end + end + end + @threads << t + stat( + :spawned, + :new_thread => t.object_id, + :thread_count => @threads.count) + @total_threads_in_play = @threads.count if + @threads.count > @total_threads_in_play + end + end + + def stat(event, data=nil) # :nodoc: + return if @history_start_time.nil? + info = { + :event => event, + :data => data, + :time => Time.now, + :thread => Thread.current.object_id, + } + @history_mon.synchronize { @history << info } + end + + # for testing only + + def __queue__ # :nodoc: + @queue + end + + def __threads__ # :nodoc: + @threads.dup + end + end + +end diff --git a/lib/ruby/shared/rake/trace_output.rb b/lib/ruby/shared/rake/trace_output.rb new file mode 100644 index 00000000000..1cd19451ca2 --- /dev/null +++ b/lib/ruby/shared/rake/trace_output.rb @@ -0,0 +1,22 @@ +module Rake + module TraceOutput + + # Write trace output to output stream +out+. + # + # The write is done as a single IO call (to print) to lessen the + # chance that the trace output is interrupted by other tasks also + # producing output. + def trace_on(out, *strings) + sep = $\ || "\n" + if strings.empty? + output = sep + else + output = strings.map { |s| + next if s.nil? + s =~ /#{sep}$/ ? s : s + sep + }.join + end + out.print(output) + end + end +end diff --git a/lib/ruby/shared/rake/version.rb b/lib/ruby/shared/rake/version.rb new file mode 100644 index 00000000000..05c934d7854 --- /dev/null +++ b/lib/ruby/shared/rake/version.rb @@ -0,0 +1,9 @@ +module Rake + VERSION = '10.1.0' + + module Version # :nodoc: all + MAJOR, MINOR, BUILD, *OTHER = Rake::VERSION.split '.' + + NUMBERS = [MAJOR, MINOR, BUILD, *OTHER] + end +end diff --git a/lib/ruby/shared/rake/win32.rb b/lib/ruby/shared/rake/win32.rb new file mode 100644 index 00000000000..edb33938b4a --- /dev/null +++ b/lib/ruby/shared/rake/win32.rb @@ -0,0 +1,56 @@ + +module Rake + require 'rake/alt_system' + + # Win 32 interface methods for Rake. Windows specific functionality + # will be placed here to collect that knowledge in one spot. + module Win32 + + # Error indicating a problem in locating the home directory on a + # Win32 system. + class Win32HomeError < RuntimeError + end + + class << self + # True if running on a windows system. + def windows? + AltSystem::WINDOWS + end + + # Run a command line on windows. + def rake_system(*cmd) + AltSystem.system(*cmd) + end + + # The standard directory containing system wide rake files on + # Win 32 systems. Try the following environment variables (in + # order): + # + # * HOME + # * HOMEDRIVE + HOMEPATH + # * APPDATA + # * USERPROFILE + # + # If the above are not defined, the return nil. + def win32_system_dir #:nodoc: + win32_shared_path = ENV['HOME'] + if win32_shared_path.nil? && ENV['HOMEDRIVE'] && ENV['HOMEPATH'] + win32_shared_path = ENV['HOMEDRIVE'] + ENV['HOMEPATH'] + end + + win32_shared_path ||= ENV['APPDATA'] + win32_shared_path ||= ENV['USERPROFILE'] + raise Win32HomeError, + "Unable to determine home path environment variable." if + win32_shared_path.nil? or win32_shared_path.empty? + normalize(File.join(win32_shared_path, 'Rake')) + end + + # Normalize a win32 path so that the slashes are all forward slashes. + def normalize(path) + path.gsub(/\\/, '/') + end + + end + end +end diff --git a/lib/ruby/shared/rdoc.rb b/lib/ruby/shared/rdoc.rb new file mode 100644 index 00000000000..5a05da3324a --- /dev/null +++ b/lib/ruby/shared/rdoc.rb @@ -0,0 +1,183 @@ +$DEBUG_RDOC = nil + +# :main: README.rdoc + +## +# RDoc produces documentation for Ruby source files by parsing the source and +# extracting the definition for classes, modules, methods, includes and +# requires. It associates these with optional documentation contained in an +# immediately preceding comment block then renders the result using an output +# formatter. +# +# For a simple introduction to writing or generating documentation using RDoc +# see the README. +# +# == Roadmap +# +# If you think you found a bug in RDoc see CONTRIBUTING@Bugs +# +# If you want to use RDoc to create documentation for your Ruby source files, +# see RDoc::Markup and refer to rdoc --help for command line usage. +# +# If you want to set the default markup format see +# RDoc::Markup@Supported+Formats +# +# If you want to store rdoc configuration in your gem (such as the default +# markup format) see RDoc::Options@Saved+Options +# +# If you want to write documentation for Ruby files see RDoc::Parser::Ruby +# +# If you want to write documentation for extensions written in C see +# RDoc::Parser::C +# +# If you want to generate documentation using rake see RDoc::Task. +# +# If you want to drive RDoc programmatically, see RDoc::RDoc. +# +# If you want to use the library to format text blocks into HTML or other +# formats, look at RDoc::Markup. +# +# If you want to make an RDoc plugin such as a generator or directive handler +# see RDoc::RDoc. +# +# If you want to write your own output generator see RDoc::Generator. +# +# If you want an overview of how RDoc works see CONTRIBUTING +# +# == Credits +# +# RDoc is currently being maintained by Eric Hodel . +# +# Dave Thomas is the original author of RDoc. +# +# * The Ruby parser in rdoc/parse.rb is based heavily on the outstanding +# work of Keiju ISHITSUKA of Nippon Rational Inc, who produced the Ruby +# parser for irb and the rtags package. + +module RDoc + + ## + # Exception thrown by any rdoc error. + + class Error < RuntimeError; end + + ## + # RDoc version you are using + + VERSION = '4.1.2' + + ## + # Method visibilities + + VISIBILITIES = [:public, :protected, :private] + + ## + # Name of the dotfile that contains the description of files to be processed + # in the current directory + + DOT_DOC_FILENAME = ".document" + + ## + # General RDoc modifiers + + GENERAL_MODIFIERS = %w[nodoc].freeze + + ## + # RDoc modifiers for classes + + CLASS_MODIFIERS = GENERAL_MODIFIERS + + ## + # RDoc modifiers for attributes + + ATTR_MODIFIERS = GENERAL_MODIFIERS + + ## + # RDoc modifiers for constants + + CONSTANT_MODIFIERS = GENERAL_MODIFIERS + + ## + # RDoc modifiers for methods + + METHOD_MODIFIERS = GENERAL_MODIFIERS + + %w[arg args yield yields notnew not-new not_new doc] + + ## + # Loads the best available YAML library. + + def self.load_yaml + begin + gem 'psych' + rescue Gem::LoadError + end + + begin + require 'psych' + rescue ::LoadError + ensure + require 'yaml' + end + end + + autoload :RDoc, 'rdoc/rdoc' + + autoload :TestCase, 'rdoc/test_case' + + autoload :CrossReference, 'rdoc/cross_reference' + autoload :ERBIO, 'rdoc/erbio' + autoload :ERBPartial, 'rdoc/erb_partial' + autoload :Encoding, 'rdoc/encoding' + autoload :Generator, 'rdoc/generator' + autoload :Options, 'rdoc/options' + autoload :Parser, 'rdoc/parser' + autoload :Servlet, 'rdoc/servlet' + autoload :RI, 'rdoc/ri' + autoload :Stats, 'rdoc/stats' + autoload :Store, 'rdoc/store' + autoload :Task, 'rdoc/task' + autoload :Text, 'rdoc/text' + + autoload :Markdown, 'rdoc/markdown' + autoload :Markup, 'rdoc/markup' + autoload :RD, 'rdoc/rd' + autoload :TomDoc, 'rdoc/tom_doc' + + autoload :KNOWN_CLASSES, 'rdoc/known_classes' + + autoload :RubyLex, 'rdoc/ruby_lex' + autoload :RubyToken, 'rdoc/ruby_token' + autoload :TokenStream, 'rdoc/token_stream' + + autoload :Comment, 'rdoc/comment' + + # code objects + # + # We represent the various high-level code constructs that appear in Ruby + # programs: classes, modules, methods, and so on. + autoload :CodeObject, 'rdoc/code_object' + + autoload :Context, 'rdoc/context' + autoload :TopLevel, 'rdoc/top_level' + + autoload :AnonClass, 'rdoc/anon_class' + autoload :ClassModule, 'rdoc/class_module' + autoload :NormalClass, 'rdoc/normal_class' + autoload :NormalModule, 'rdoc/normal_module' + autoload :SingleClass, 'rdoc/single_class' + + autoload :Alias, 'rdoc/alias' + autoload :AnyMethod, 'rdoc/any_method' + autoload :MethodAttr, 'rdoc/method_attr' + autoload :GhostMethod, 'rdoc/ghost_method' + autoload :MetaMethod, 'rdoc/meta_method' + autoload :Attr, 'rdoc/attr' + + autoload :Constant, 'rdoc/constant' + autoload :Mixin, 'rdoc/mixin' + autoload :Include, 'rdoc/include' + autoload :Extend, 'rdoc/extend' + autoload :Require, 'rdoc/require' + +end + diff --git a/lib/ruby/shared/rdoc/alias.rb b/lib/ruby/shared/rdoc/alias.rb new file mode 100644 index 00000000000..39d2694817e --- /dev/null +++ b/lib/ruby/shared/rdoc/alias.rb @@ -0,0 +1,111 @@ +## +# Represent an alias, which is an old_name/new_name pair associated with a +# particular context +#-- +# TODO implement Alias as a proxy to a method/attribute, inheriting from +# MethodAttr + +class RDoc::Alias < RDoc::CodeObject + + ## + # Aliased method's name + + attr_reader :new_name + + alias name new_name + + ## + # Aliasee method's name + + attr_reader :old_name + + ## + # Is this an alias declared in a singleton context? + + attr_accessor :singleton + + ## + # Source file token stream + + attr_reader :text + + ## + # Creates a new Alias with a token stream of +text+ that aliases +old_name+ + # to +new_name+, has +comment+ and is a +singleton+ context. + + def initialize(text, old_name, new_name, comment, singleton = false) + super() + + @text = text + @singleton = singleton + @old_name = old_name + @new_name = new_name + self.comment = comment + end + + ## + # Order by #singleton then #new_name + + def <=>(other) + [@singleton ? 0 : 1, new_name] <=> [other.singleton ? 0 : 1, other.new_name] + end + + ## + # HTML fragment reference for this alias + + def aref + type = singleton ? 'c' : 'i' + "#alias-#{type}-#{html_name}" + end + + ## + # Full old name including namespace + + def full_old_name + @full_name || "#{parent.name}#{pretty_old_name}" + end + + ## + # HTML id-friendly version of +#new_name+. + + def html_name + CGI.escape(@new_name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '') + end + + def inspect # :nodoc: + parent_name = parent ? parent.name : '(unknown)' + "#<%s:0x%x %s.alias_method %s, %s>" % [ + self.class, object_id, + parent_name, @old_name, @new_name, + ] + end + + ## + # '::' for the alias of a singleton method/attribute, '#' for instance-level. + + def name_prefix + singleton ? '::' : '#' + end + + ## + # Old name with prefix '::' or '#'. + + def pretty_old_name + "#{singleton ? '::' : '#'}#{@old_name}" + end + + ## + # New name with prefix '::' or '#'. + + def pretty_new_name + "#{singleton ? '::' : '#'}#{@new_name}" + end + + alias pretty_name pretty_new_name + + def to_s # :nodoc: + "alias: #{self.new_name} -> #{self.pretty_old_name} in: #{parent}" + end + +end + diff --git a/lib/ruby/shared/rdoc/anon_class.rb b/lib/ruby/shared/rdoc/anon_class.rb new file mode 100644 index 00000000000..c23d8e5d966 --- /dev/null +++ b/lib/ruby/shared/rdoc/anon_class.rb @@ -0,0 +1,10 @@ +## +# An anonymous class like: +# +# c = Class.new do end +# +# AnonClass is currently not used. + +class RDoc::AnonClass < RDoc::ClassModule +end + diff --git a/lib/ruby/shared/rdoc/any_method.rb b/lib/ruby/shared/rdoc/any_method.rb new file mode 100644 index 00000000000..ae022d72f82 --- /dev/null +++ b/lib/ruby/shared/rdoc/any_method.rb @@ -0,0 +1,316 @@ +## +# AnyMethod is the base class for objects representing methods + +class RDoc::AnyMethod < RDoc::MethodAttr + + ## + # 2:: + # RDoc 4 + # Added calls_super + # Added parent name and class + # Added section title + # 3:: + # RDoc 4.1 + # Added is_alias_for + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Don't rename \#initialize to \::new + + attr_accessor :dont_rename_initialize + + ## + # The C function that implements this method (if it was defined in a C file) + + attr_accessor :c_function + + ## + # Different ways to call this method + + attr_reader :call_seq + + ## + # Parameters for this method + + attr_accessor :params + + ## + # If true this method uses +super+ to call a superclass version + + attr_accessor :calls_super + + include RDoc::TokenStream + + ## + # Creates a new AnyMethod with a token stream +text+ and +name+ + + def initialize text, name + super + + @c_function = nil + @dont_rename_initialize = false + @token_stream = nil + @calls_super = false + @superclass_method = nil + end + + ## + # Adds +an_alias+ as an alias for this method in +context+. + + def add_alias an_alias, context = nil + method = self.class.new an_alias.text, an_alias.new_name + + method.record_location an_alias.file + method.singleton = self.singleton + method.params = self.params + method.visibility = self.visibility + method.comment = an_alias.comment + method.is_alias_for = self + @aliases << method + context.add_method method if context + method + end + + ## + # Prefix for +aref+ is 'method'. + + def aref_prefix + 'method' + end + + ## + # The call_seq or the param_seq with method name, if there is no call_seq. + # + # Use this for displaying a method's argument lists. + + def arglists + if @call_seq then + @call_seq + elsif @params then + "#{name}#{param_seq}" + end + end + + ## + # Sets the different ways you can call this method. If an empty +call_seq+ + # is given nil is assumed. + # + # See also #param_seq + + def call_seq= call_seq + return if call_seq.empty? + + @call_seq = call_seq + end + + ## + # Loads is_alias_for from the internal name. Returns nil if the alias + # cannot be found. + + def is_alias_for # :nodoc: + case @is_alias_for + when RDoc::MethodAttr then + @is_alias_for + when Array then + return nil unless @store + + klass_name, singleton, method_name = @is_alias_for + + return nil unless klass = @store.find_class_or_module(klass_name) + + @is_alias_for = klass.find_method method_name, singleton + end + end + + ## + # Dumps this AnyMethod for use by ri. See also #marshal_load + + def marshal_dump + aliases = @aliases.map do |a| + [a.name, parse(a.comment)] + end + + is_alias_for = [ + @is_alias_for.parent.full_name, + @is_alias_for.singleton, + @is_alias_for.name + ] if @is_alias_for + + [ MARSHAL_VERSION, + @name, + full_name, + @singleton, + @visibility, + parse(@comment), + @call_seq, + @block_params, + aliases, + @params, + @file.relative_name, + @calls_super, + @parent.name, + @parent.class, + @section.title, + is_alias_for, + ] + end + + ## + # Loads this AnyMethod from +array+. For a loaded AnyMethod the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize_visibility + + @dont_rename_initialize = nil + @token_stream = nil + @aliases = [] + @parent = nil + @parent_name = nil + @parent_class = nil + @section = nil + @file = nil + + version = array[0] + @name = array[1] + @full_name = array[2] + @singleton = array[3] + @visibility = array[4] + @comment = array[5] + @call_seq = array[6] + @block_params = array[7] + # 8 handled below + @params = array[9] + # 10 handled below + @calls_super = array[11] + @parent_name = array[12] + @parent_title = array[13] + @section_title = array[14] + @is_alias_for = array[15] + + array[8].each do |new_name, comment| + add_alias RDoc::Alias.new(nil, @name, new_name, comment, @singleton) + end + + @parent_name ||= if @full_name =~ /#/ then + $` + else + name = @full_name.split('::') + name.pop + name.join '::' + end + + @file = RDoc::TopLevel.new array[10] if version > 0 + end + + ## + # Method name + # + # If the method has no assigned name, it extracts it from #call_seq. + + def name + return @name if @name + + @name = + @call_seq[/^.*?\.(\w+)/, 1] || + @call_seq[/^.*?(\w+)/, 1] || + @call_seq if @call_seq + end + + ## + # A list of this method's method and yield parameters. +call-seq+ params + # are preferred over parsed method and block params. + + def param_list + if @call_seq then + params = @call_seq.split("\n").last + params = params.sub(/.*?\((.*)\)/, '\1') + params = params.sub(/(\{|do)\s*\|([^|]*)\|.*/, ',\2') + elsif @params then + params = @params.sub(/\((.*)\)/, '\1') + + params << ",#{@block_params}" if @block_params + elsif @block_params then + params = @block_params + else + return [] + end + + if @block_params then + # If this method has explicit block parameters, remove any explicit + # &block + params.sub!(/,?\s*&\w+/, '') + else + params.sub!(/\&(\w+)/, '\1') + end + + params = params.gsub(/\s+/, '').split(',').reject(&:empty?) + + params.map { |param| param.sub(/=.*/, '') } + end + + ## + # Pretty parameter list for this method. If the method's parameters were + # given by +call-seq+ it is preferred over the parsed values. + + def param_seq + if @call_seq then + params = @call_seq.split("\n").last + params = params.sub(/[^( ]+/, '') + params = params.sub(/(\|[^|]+\|)\s*\.\.\.\s*(end|\})/, '\1 \2') + elsif @params then + params = @params.gsub(/\s*\#.*/, '') + params = params.tr("\n", " ").squeeze(" ") + params = "(#{params})" unless params[0] == ?( + else + params = '' + end + + if @block_params then + # If this method has explicit block parameters, remove any explicit + # &block + params.sub!(/,?\s*&\w+/, '') + + block = @block_params.gsub(/\s*\#.*/, '') + block = block.tr("\n", " ").squeeze(" ") + if block[0] == ?( + block.sub!(/^\(/, '').sub!(/\)/, '') + end + params << " { |#{block}| ... }" + end + + params + end + + ## + # Sets the store for this method and its referenced code objects. + + def store= store + super + + @file = @store.add_file @file.full_name if @file + end + + ## + # For methods that +super+, find the superclass method that would be called. + + def superclass_method + return unless @calls_super + return @superclass_method if @superclass_method + + parent.each_ancestor do |ancestor| + if method = ancestor.method_list.find { |m| m.name == @name } then + @superclass_method = method + break + end + end + + @superclass_method + end + +end + diff --git a/lib/ruby/shared/rdoc/attr.rb b/lib/ruby/shared/rdoc/attr.rb new file mode 100644 index 00000000000..960e1d1107f --- /dev/null +++ b/lib/ruby/shared/rdoc/attr.rb @@ -0,0 +1,175 @@ +## +# An attribute created by \#attr, \#attr_reader, \#attr_writer or +# \#attr_accessor + +class RDoc::Attr < RDoc::MethodAttr + + ## + # 3:: + # RDoc 4 + # Added parent name and class + # Added section title + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Is the attribute readable ('R'), writable ('W') or both ('RW')? + + attr_accessor :rw + + ## + # Creates a new Attr with body +text+, +name+, read/write status +rw+ and + # +comment+. +singleton+ marks this as a class attribute. + + def initialize(text, name, rw, comment, singleton = false) + super text, name + + @rw = rw + @singleton = singleton + self.comment = comment + end + + ## + # Attributes are equal when their names, singleton and rw are identical + + def == other + self.class == other.class and + self.name == other.name and + self.rw == other.rw and + self.singleton == other.singleton + end + + ## + # Add +an_alias+ as an attribute in +context+. + + def add_alias(an_alias, context) + new_attr = self.class.new(self.text, an_alias.new_name, self.rw, + self.comment, self.singleton) + + new_attr.record_location an_alias.file + new_attr.visibility = self.visibility + new_attr.is_alias_for = self + @aliases << new_attr + context.add_attribute new_attr + new_attr + end + + ## + # The #aref prefix for attributes + + def aref_prefix + 'attribute' + end + + ## + # Attributes never call super. See RDoc::AnyMethod#calls_super + # + # An RDoc::Attr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def calls_super # :nodoc: + false + end + + ## + # Returns attr_reader, attr_writer or attr_accessor as appropriate. + + def definition + case @rw + when 'RW' then 'attr_accessor' + when 'R' then 'attr_reader' + when 'W' then 'attr_writer' + end + end + + def inspect # :nodoc: + alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil + visibility = self.visibility + visibility = "forced #{visibility}" if force_documentation + "#<%s:0x%x %s %s (%s)%s>" % [ + self.class, object_id, + full_name, + rw, + visibility, + alias_for, + ] + end + + ## + # Dumps this Attr for use by ri. See also #marshal_load + + def marshal_dump + [ MARSHAL_VERSION, + @name, + full_name, + @rw, + @visibility, + parse(@comment), + singleton, + @file.relative_name, + @parent.full_name, + @parent.class, + @section.title + ] + end + + ## + # Loads this Attr from +array+. For a loaded Attr the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize_visibility + + @aliases = [] + @parent = nil + @parent_name = nil + @parent_class = nil + @section = nil + @file = nil + + version = array[0] + @name = array[1] + @full_name = array[2] + @rw = array[3] + @visibility = array[4] + @comment = array[5] + @singleton = array[6] || false # MARSHAL_VERSION == 0 + # 7 handled below + @parent_name = array[8] + @parent_class = array[9] + @section_title = array[10] + + @file = RDoc::TopLevel.new array[7] if version > 1 + + @parent_name ||= @full_name.split('#', 2).first + end + + def pretty_print q # :nodoc: + q.group 2, "[#{self.class.name} #{full_name} #{rw} #{visibility}", "]" do + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + def to_s # :nodoc: + "#{definition} #{name} in: #{parent}" + end + + ## + # Attributes do not have token streams. + # + # An RDoc::Attr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def token_stream # :nodoc: + end + +end + diff --git a/lib/ruby/shared/rdoc/class_module.rb b/lib/ruby/shared/rdoc/class_module.rb new file mode 100644 index 00000000000..71566f050a0 --- /dev/null +++ b/lib/ruby/shared/rdoc/class_module.rb @@ -0,0 +1,799 @@ +## +# ClassModule is the base class for objects representing either a class or a +# module. + +class RDoc::ClassModule < RDoc::Context + + ## + # 1:: + # RDoc 3.7 + # * Added visibility, singleton and file to attributes + # * Added file to constants + # * Added file to includes + # * Added file to methods + # 2:: + # RDoc 3.13 + # * Added extends + # 3:: + # RDoc 4.0 + # * Added sections + # * Added in_files + # * Added parent name + # * Complete Constant dump + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Constants that are aliases for this class or module + + attr_accessor :constant_aliases + + ## + # Comment and the location it came from. Use #add_comment to add comments + + attr_accessor :comment_location + + attr_accessor :diagram # :nodoc: + + ## + # Class or module this constant is an alias for + + attr_accessor :is_alias_for + + ## + # Return a RDoc::ClassModule of class +class_type+ that is a copy + # of module +module+. Used to promote modules to classes. + #-- + # TODO move to RDoc::NormalClass (I think) + + def self.from_module class_type, mod + klass = class_type.new mod.name + + mod.comment_location.each do |comment, location| + klass.add_comment comment, location + end + + klass.parent = mod.parent + klass.section = mod.section + klass.viewer = mod.viewer + + klass.attributes.concat mod.attributes + klass.method_list.concat mod.method_list + klass.aliases.concat mod.aliases + klass.external_aliases.concat mod.external_aliases + klass.constants.concat mod.constants + klass.includes.concat mod.includes + klass.extends.concat mod.extends + + klass.methods_hash.update mod.methods_hash + klass.constants_hash.update mod.constants_hash + + klass.current_section = mod.current_section + klass.in_files.concat mod.in_files + klass.sections.concat mod.sections + klass.unmatched_alias_lists = mod.unmatched_alias_lists + klass.current_section = mod.current_section + klass.visibility = mod.visibility + + klass.classes_hash.update mod.classes_hash + klass.modules_hash.update mod.modules_hash + klass.metadata.update mod.metadata + + klass.document_self = mod.received_nodoc ? nil : mod.document_self + klass.document_children = mod.document_children + klass.force_documentation = mod.force_documentation + klass.done_documenting = mod.done_documenting + + # update the parent of all children + + (klass.attributes + + klass.method_list + + klass.aliases + + klass.external_aliases + + klass.constants + + klass.includes + + klass.extends + + klass.classes + + klass.modules).each do |obj| + obj.parent = klass + obj.full_name = nil + end + + klass + end + + ## + # Creates a new ClassModule with +name+ with optional +superclass+ + # + # This is a constructor for subclasses, and must never be called directly. + + def initialize(name, superclass = nil) + @constant_aliases = [] + @diagram = nil + @is_alias_for = nil + @name = name + @superclass = superclass + @comment_location = [] # [[comment, location]] + + super() + end + + ## + # Adds +comment+ to this ClassModule's list of comments at +location+. This + # method is preferred over #comment= since it allows ri data to be updated + # across multiple runs. + + def add_comment comment, location + return unless document_self + + original = comment + + comment = case comment + when RDoc::Comment then + comment.normalize + else + normalize_comment comment + end + + @comment_location.delete_if { |(_, l)| l == location } + + @comment_location << [comment, location] + + self.comment = original + end + + def add_things my_things, other_things # :nodoc: + other_things.each do |group, things| + my_things[group].each { |thing| yield false, thing } if + my_things.include? group + + things.each do |thing| + yield true, thing + end + end + end + + ## + # Ancestors list for this ClassModule: the list of included modules + # (classes will add their superclass if any). + # + # Returns the included classes or modules, not the includes + # themselves. The returned values are either String or + # RDoc::NormalModule instances (see RDoc::Include#module). + # + # The values are returned in reverse order of their inclusion, + # which is the order suitable for searching methods/attributes + # in the ancestors. The superclass, if any, comes last. + + def ancestors + includes.map { |i| i.module }.reverse + end + + def aref_prefix # :nodoc: + raise NotImplementedError, "missing aref_prefix for #{self.class}" + end + + ## + # HTML fragment reference for this module or class. See + # RDoc::NormalClass#aref and RDoc::NormalModule#aref + + def aref + "#{aref_prefix}-#{full_name}" + end + + ## + # Ancestors of this class or module only + + alias direct_ancestors ancestors + + ## + # Clears the comment. Used by the Ruby parser. + + def clear_comment + @comment = '' + end + + ## + # This method is deprecated, use #add_comment instead. + # + # Appends +comment+ to the current comment, but separated by a rule. Works + # more like +=. + + def comment= comment # :nodoc: + comment = case comment + when RDoc::Comment then + comment.normalize + else + normalize_comment comment + end + + comment = "#{@comment}\n---\n#{comment}" unless @comment.empty? + + super comment + end + + ## + # Prepares this ClassModule for use by a generator. + # + # See RDoc::Store#complete + + def complete min_visibility + update_aliases + remove_nodoc_children + update_includes + remove_invisible min_visibility + end + + ## + # Does this ClassModule or any of its methods have document_self set? + + def document_self_or_methods + document_self || method_list.any?{ |m| m.document_self } + end + + ## + # Does this class or module have a comment with content or is + # #received_nodoc true? + + def documented? + return true if @received_nodoc + return false if @comment_location.empty? + @comment_location.any? { |comment, _| not comment.empty? } + end + + ## + # Iterates the ancestors of this class or module for which an + # RDoc::ClassModule exists. + + def each_ancestor # :yields: module + return enum_for __method__ unless block_given? + + ancestors.each do |mod| + next if String === mod + next if self == mod + yield mod + end + end + + ## + # Looks for a symbol in the #ancestors. See Context#find_local_symbol. + + def find_ancestor_local_symbol symbol + each_ancestor do |m| + res = m.find_local_symbol(symbol) + return res if res + end + + nil + end + + ## + # Finds a class or module with +name+ in this namespace or its descendants + + def find_class_named name + return self if full_name == name + return self if @name == name + + @classes.values.find do |klass| + next if klass == self + klass.find_class_named name + end + end + + ## + # Return the fully qualified name of this class or module + + def full_name + @full_name ||= if RDoc::ClassModule === parent then + "#{parent.full_name}::#{@name}" + else + @name + end + end + + ## + # TODO: filter included items by #display? + + def marshal_dump # :nodoc: + attrs = attributes.sort.map do |attr| + next unless attr.display? + [ attr.name, attr.rw, + attr.visibility, attr.singleton, attr.file_name, + ] + end.compact + + method_types = methods_by_type.map do |type, visibilities| + visibilities = visibilities.map do |visibility, methods| + method_names = methods.map do |method| + next unless method.display? + [method.name, method.file_name] + end.compact + + [visibility, method_names.uniq] + end + + [type, visibilities] + end + + [ MARSHAL_VERSION, + @name, + full_name, + @superclass, + parse(@comment_location), + attrs, + constants.select { |constant| constant.display? }, + includes.map do |incl| + next unless incl.display? + [incl.name, parse(incl.comment), incl.file_name] + end.compact, + method_types, + extends.map do |ext| + next unless ext.display? + [ext.name, parse(ext.comment), ext.file_name] + end.compact, + @sections.values, + @in_files.map do |tl| + tl.relative_name + end, + parent.full_name, + parent.class, + ] + end + + def marshal_load array # :nodoc: + initialize_visibility + initialize_methods_etc + @current_section = nil + @document_self = true + @done_documenting = false + @parent = nil + @temporary_section = nil + @visibility = nil + @classes = {} + @modules = {} + + @name = array[1] + @full_name = array[2] + @superclass = array[3] + @comment = array[4] + + @comment_location = if RDoc::Markup::Document === @comment.parts.first then + @comment + else + RDoc::Markup::Document.new @comment + end + + array[5].each do |name, rw, visibility, singleton, file| + singleton ||= false + visibility ||= :public + + attr = RDoc::Attr.new nil, name, rw, nil, singleton + + add_attribute attr + attr.visibility = visibility + attr.record_location RDoc::TopLevel.new file + end + + array[6].each do |constant, comment, file| + case constant + when RDoc::Constant then + add_constant constant + else + constant = add_constant RDoc::Constant.new(constant, nil, comment) + constant.record_location RDoc::TopLevel.new file + end + end + + array[7].each do |name, comment, file| + incl = add_include RDoc::Include.new(name, comment) + incl.record_location RDoc::TopLevel.new file + end + + array[8].each do |type, visibilities| + visibilities.each do |visibility, methods| + @visibility = visibility + + methods.each do |name, file| + method = RDoc::AnyMethod.new nil, name + method.singleton = true if type == 'class' + method.record_location RDoc::TopLevel.new file + add_method method + end + end + end + + array[9].each do |name, comment, file| + ext = add_extend RDoc::Extend.new(name, comment) + ext.record_location RDoc::TopLevel.new file + end if array[9] # Support Marshal version 1 + + sections = (array[10] || []).map do |section| + [section.title, section] + end + + @sections = Hash[*sections.flatten] + @current_section = add_section nil + + @in_files = [] + + (array[11] || []).each do |filename| + record_location RDoc::TopLevel.new filename + end + + @parent_name = array[12] + @parent_class = array[13] + end + + ## + # Merges +class_module+ into this ClassModule. + # + # The data in +class_module+ is preferred over the receiver. + + def merge class_module + @parent = class_module.parent + @parent_name = class_module.parent_name + + other_document = parse class_module.comment_location + + if other_document then + document = parse @comment_location + + document = document.merge other_document + + @comment = @comment_location = document + end + + cm = class_module + other_files = cm.in_files + + merge_collections attributes, cm.attributes, other_files do |add, attr| + if add then + add_attribute attr + else + @attributes.delete attr + @methods_hash.delete attr.pretty_name + end + end + + merge_collections constants, cm.constants, other_files do |add, const| + if add then + add_constant const + else + @constants.delete const + @constants_hash.delete const.name + end + end + + merge_collections includes, cm.includes, other_files do |add, incl| + if add then + add_include incl + else + @includes.delete incl + end + end + + @includes.uniq! # clean up + + merge_collections extends, cm.extends, other_files do |add, ext| + if add then + add_extend ext + else + @extends.delete ext + end + end + + @extends.uniq! # clean up + + merge_collections method_list, cm.method_list, other_files do |add, meth| + if add then + add_method meth + else + @method_list.delete meth + @methods_hash.delete meth.pretty_name + end + end + + merge_sections cm + + self + end + + ## + # Merges collection +mine+ with +other+ preferring other. +other_files+ is + # used to help determine which items should be deleted. + # + # Yields whether the item should be added or removed (true or false) and the + # item to be added or removed. + # + # merge_collections things, other.things, other.in_files do |add, thing| + # if add then + # # add the thing + # else + # # remove the thing + # end + # end + + def merge_collections mine, other, other_files, &block # :nodoc: + my_things = mine. group_by { |thing| thing.file } + other_things = other.group_by { |thing| thing.file } + + remove_things my_things, other_files, &block + add_things my_things, other_things, &block + end + + ## + # Merges the comments in this ClassModule with the comments in the other + # ClassModule +cm+. + + def merge_sections cm # :nodoc: + my_sections = sections.group_by { |section| section.title } + other_sections = cm.sections.group_by { |section| section.title } + + other_files = cm.in_files + + remove_things my_sections, other_files do |_, section| + @sections.delete section.title + end + + other_sections.each do |group, sections| + if my_sections.include? group + my_sections[group].each do |my_section| + other_section = cm.sections_hash[group] + + my_comments = my_section.comments + other_comments = other_section.comments + + other_files = other_section.in_files + + merge_collections my_comments, other_comments, other_files do |add, comment| + if add then + my_section.add_comment comment + else + my_section.remove_comment comment + end + end + end + else + sections.each do |section| + add_section group, section.comments + end + end + end + end + + ## + # Does this object represent a module? + + def module? + false + end + + ## + # Allows overriding the initial name. + # + # Used for modules and classes that are constant aliases. + + def name= new_name + @name = new_name + end + + ## + # Parses +comment_location+ into an RDoc::Markup::Document composed of + # multiple RDoc::Markup::Documents with their file set. + + def parse comment_location + case comment_location + when String then + super + when Array then + docs = comment_location.map do |comment, location| + doc = super comment + doc.file = location + doc + end + + RDoc::Markup::Document.new(*docs) + when RDoc::Comment then + doc = super comment_location.text, comment_location.format + doc.file = comment_location.location + doc + when RDoc::Markup::Document then + return comment_location + else + raise ArgumentError, "unknown comment class #{comment_location.class}" + end + end + + ## + # Path to this class or module for use with HTML generator output. + + def path + http_url @store.rdoc.generator.class_dir + end + + ## + # Name to use to generate the url: + # modules and classes that are aliases for another + # module or class return the name of the latter. + + def name_for_path + is_alias_for ? is_alias_for.full_name : full_name + end + + ## + # Returns the classes and modules that are not constants + # aliasing another class or module. For use by formatters + # only (caches its result). + + def non_aliases + @non_aliases ||= classes_and_modules.reject { |cm| cm.is_alias_for } + end + + ## + # Updates the child modules or classes of class/module +parent+ by + # deleting the ones that have been removed from the documentation. + # + # +parent_hash+ is either parent.modules_hash or + # parent.classes_hash and +all_hash+ is ::all_modules_hash or + # ::all_classes_hash. + + def remove_nodoc_children + prefix = self.full_name + '::' + + modules_hash.each_key do |name| + full_name = prefix + name + modules_hash.delete name unless @store.modules_hash[full_name] + end + + classes_hash.each_key do |name| + full_name = prefix + name + classes_hash.delete name unless @store.classes_hash[full_name] + end + end + + def remove_things my_things, other_files # :nodoc: + my_things.delete_if do |file, things| + next false unless other_files.include? file + + things.each do |thing| + yield false, thing + end + + true + end + end + + ## + # Search record used by RDoc::Generator::JsonIndex + + def search_record + [ + name, + full_name, + full_name, + '', + path, + '', + snippet(@comment_location), + ] + end + + ## + # Sets the store for this class or module and its contained code objects. + + def store= store + super + + @attributes .each do |attr| attr.store = store end + @constants .each do |const| const.store = store end + @includes .each do |incl| incl.store = store end + @extends .each do |ext| ext.store = store end + @method_list.each do |meth| meth.store = store end + end + + ## + # Get the superclass of this class. Attempts to retrieve the superclass + # object, returns the name if it is not known. + + def superclass + @store.find_class_named(@superclass) || @superclass + end + + ## + # Set the superclass of this class to +superclass+ + + def superclass=(superclass) + raise NoMethodError, "#{full_name} is a module" if module? + @superclass = superclass + end + + def to_s # :nodoc: + if is_alias_for then + "#{self.class.name} #{self.full_name} -> #{is_alias_for}" + else + super + end + end + + ## + # 'module' or 'class' + + def type + module? ? 'module' : 'class' + end + + ## + # Updates the child modules & classes by replacing the ones that are + # aliases through a constant. + # + # The aliased module/class is replaced in the children and in + # RDoc::Store#modules_hash or RDoc::Store#classes_hash + # by a copy that has RDoc::ClassModule#is_alias_for set to + # the aliased module/class, and this copy is added to #aliases + # of the aliased module/class. + # + # Formatters can use the #non_aliases method to retrieve children that + # are not aliases, for instance to list the namespace content, since + # the aliased modules are included in the constants of the class/module, + # that are listed separately. + + def update_aliases + constants.each do |const| + next unless cm = const.is_alias_for + cm_alias = cm.dup + cm_alias.name = const.name + + # Don't move top-level aliases under Object, they look ugly there + unless RDoc::TopLevel === cm_alias.parent then + cm_alias.parent = self + cm_alias.full_name = nil # force update for new parent + end + + cm_alias.aliases.clear + cm_alias.is_alias_for = cm + + if cm.module? then + @store.modules_hash[cm_alias.full_name] = cm_alias + modules_hash[const.name] = cm_alias + else + @store.classes_hash[cm_alias.full_name] = cm_alias + classes_hash[const.name] = cm_alias + end + + cm.aliases << cm_alias + end + end + + ## + # Deletes from #includes those whose module has been removed from the + # documentation. + #-- + # FIXME: includes are not reliably removed, see _possible_bug test case + + def update_includes + includes.reject! do |include| + mod = include.module + !(String === mod) && @store.modules_hash[mod.full_name].nil? + end + + includes.uniq! + end + + ## + # Deletes from #extends those whose module has been removed from the + # documentation. + #-- + # FIXME: like update_includes, extends are not reliably removed + + def update_extends + extends.reject! do |ext| + mod = ext.module + + !(String === mod) && @store.modules_hash[mod.full_name].nil? + end + + extends.uniq! + end + +end + diff --git a/lib/ruby/shared/rdoc/code_object.rb b/lib/ruby/shared/rdoc/code_object.rb new file mode 100644 index 00000000000..4620fa586da --- /dev/null +++ b/lib/ruby/shared/rdoc/code_object.rb @@ -0,0 +1,429 @@ +## +# Base class for the RDoc code tree. +# +# We contain the common stuff for contexts (which are containers) and other +# elements (methods, attributes and so on) +# +# Here's the tree of the CodeObject subclasses: +# +# * RDoc::Context +# * RDoc::TopLevel +# * RDoc::ClassModule +# * RDoc::AnonClass (never used so far) +# * RDoc::NormalClass +# * RDoc::NormalModule +# * RDoc::SingleClass +# * RDoc::MethodAttr +# * RDoc::Attr +# * RDoc::AnyMethod +# * RDoc::GhostMethod +# * RDoc::MetaMethod +# * RDoc::Alias +# * RDoc::Constant +# * RDoc::Mixin +# * RDoc::Require +# * RDoc::Include + +class RDoc::CodeObject + + include RDoc::Text + + ## + # Our comment + + attr_reader :comment + + ## + # Do we document our children? + + attr_reader :document_children + + ## + # Do we document ourselves? + + attr_reader :document_self + + ## + # Are we done documenting (ie, did we come across a :enddoc:)? + + attr_reader :done_documenting + + ## + # Which file this code object was defined in + + attr_reader :file + + ## + # Force documentation of this CodeObject + + attr_reader :force_documentation + + ## + # Line in #file where this CodeObject was defined + + attr_accessor :line + + ## + # Hash of arbitrary metadata for this CodeObject + + attr_reader :metadata + + ## + # Offset in #file where this CodeObject was defined + #-- + # TODO character or byte? + + attr_accessor :offset + + ## + # Sets the parent CodeObject + + attr_writer :parent + + ## + # Did we ever receive a +:nodoc:+ directive? + + attr_reader :received_nodoc + + ## + # Set the section this CodeObject is in + + attr_writer :section + + ## + # The RDoc::Store for this object. + + attr_reader :store + + ## + # We are the model of the code, but we know that at some point we will be + # worked on by viewers. By implementing the Viewable protocol, viewers can + # associated themselves with these objects. + + attr_accessor :viewer + + ## + # Creates a new CodeObject that will document itself and its children + + def initialize + @metadata = {} + @comment = '' + @parent = nil + @parent_name = nil # for loading + @parent_class = nil # for loading + @section = nil + @section_title = nil # for loading + @file = nil + @full_name = nil + @store = nil + @track_visibility = true + + initialize_visibility + end + + ## + # Initializes state for visibility of this CodeObject and its children. + + def initialize_visibility # :nodoc: + @document_children = true + @document_self = true + @done_documenting = false + @force_documentation = false + @received_nodoc = false + @ignored = false + @suppressed = false + @track_visibility = true + end + + ## + # Replaces our comment with +comment+, unless it is empty. + + def comment=(comment) + @comment = case comment + when NilClass then '' + when RDoc::Markup::Document then comment + when RDoc::Comment then comment.normalize + else + if comment and not comment.empty? then + normalize_comment comment + else + # HACK correct fix is to have #initialize create @comment + # with the correct encoding + if String === @comment and + Object.const_defined? :Encoding and @comment.empty? then + @comment.force_encoding comment.encoding + end + @comment + end + end + end + + ## + # Should this CodeObject be displayed in output? + # + # A code object should be displayed if: + # + # * The item didn't have a nodoc or wasn't in a container that had nodoc + # * The item wasn't ignored + # * The item has documentation and was not suppressed + + def display? + @document_self and not @ignored and + (documented? or not @suppressed) + end + + ## + # Enables or disables documentation of this CodeObject's children unless it + # has been turned off by :enddoc: + + def document_children=(document_children) + return unless @track_visibility + + @document_children = document_children unless @done_documenting + end + + ## + # Enables or disables documentation of this CodeObject unless it has been + # turned off by :enddoc:. If the argument is +nil+ it means the + # documentation is turned off by +:nodoc:+. + + def document_self=(document_self) + return unless @track_visibility + return if @done_documenting + + @document_self = document_self + @received_nodoc = true if document_self.nil? + end + + ## + # Does this object have a comment with content or is #received_nodoc true? + + def documented? + @received_nodoc or !@comment.empty? + end + + ## + # Turns documentation on/off, and turns on/off #document_self + # and #document_children. + # + # Once documentation has been turned off (by +:enddoc:+), + # the object will refuse to turn #document_self or + # #document_children on, so +:doc:+ and +:start_doc:+ directives + # will have no effect in the current file. + + def done_documenting=(value) + return unless @track_visibility + @done_documenting = value + @document_self = !value + @document_children = @document_self + end + + ## + # Yields each parent of this CodeObject. See also + # RDoc::ClassModule#each_ancestor + + def each_parent + code_object = self + + while code_object = code_object.parent do + yield code_object + end + + self + end + + ## + # File name where this CodeObject was found. + # + # See also RDoc::Context#in_files + + def file_name + return unless @file + + @file.absolute_name + end + + ## + # Force the documentation of this object unless documentation + # has been turned off by :enddoc: + #-- + # HACK untested, was assigning to an ivar + + def force_documentation=(value) + @force_documentation = value unless @done_documenting + end + + ## + # Sets the full_name overriding any computed full name. + # + # Set to +nil+ to clear RDoc's cached value + + def full_name= full_name + @full_name = full_name + end + + ## + # Use this to ignore a CodeObject and all its children until found again + # (#record_location is called). An ignored item will not be displayed in + # documentation. + # + # See github issue #55 + # + # The ignored status is temporary in order to allow implementation details + # to be hidden. At the end of processing a file RDoc allows all classes + # and modules to add new documentation to previously created classes. + # + # If a class was ignored (via stopdoc) then reopened later with additional + # documentation it should be displayed. If a class was ignored and never + # reopened it should not be displayed. The ignore flag allows this to + # occur. + + def ignore + return unless @track_visibility + + @ignored = true + + stop_doc + end + + ## + # Has this class been ignored? + # + # See also #ignore + + def ignored? + @ignored + end + + ## + # The options instance from the store this CodeObject is attached to, or a + # default options instance if the CodeObject is not attached. + # + # This is used by Text#snippet + + def options + if @store and @store.rdoc then + @store.rdoc.options + else + RDoc::Options.new + end + end + + ## + # Our parent CodeObject. The parent may be missing for classes loaded from + # legacy RI data stores. + + def parent + return @parent if @parent + return nil unless @parent_name + + if @parent_class == RDoc::TopLevel then + @parent = @store.add_file @parent_name + else + @parent = @store.find_class_or_module @parent_name + + return @parent if @parent + + begin + @parent = @store.load_class @parent_name + rescue RDoc::Store::MissingFileError + nil + end + end + end + + ## + # File name of our parent + + def parent_file_name + @parent ? @parent.base_name : '(unknown)' + end + + ## + # Name of our parent + + def parent_name + @parent ? @parent.full_name : '(unknown)' + end + + ## + # Records the RDoc::TopLevel (file) where this code object was defined + + def record_location top_level + @ignored = false + @suppressed = false + @file = top_level + end + + ## + # The section this CodeObject is in. Sections allow grouping of constants, + # attributes and methods inside a class or module. + + def section + return @section if @section + + @section = parent.add_section @section_title if parent + end + + ## + # Enable capture of documentation unless documentation has been + # turned off by :enddoc: + + def start_doc + return if @done_documenting + + @document_self = true + @document_children = true + @ignored = false + @suppressed = false + end + + ## + # Disable capture of documentation + + def stop_doc + return unless @track_visibility + + @document_self = false + @document_children = false + end + + ## + # Sets the +store+ that contains this CodeObject + + def store= store + @store = store + + return unless @track_visibility + + if :nodoc == options.visibility then + initialize_visibility + @track_visibility = false + end + end + + ## + # Use this to suppress a CodeObject and all its children until the next file + # it is seen in or documentation is discovered. A suppressed item with + # documentation will be displayed while an ignored item with documentation + # may not be displayed. + + def suppress + return unless @track_visibility + + @suppressed = true + + stop_doc + end + + ## + # Has this class been suppressed? + # + # See also #suppress + + def suppressed? + @suppressed + end + +end + diff --git a/lib/ruby/shared/rdoc/code_objects.rb b/lib/ruby/shared/rdoc/code_objects.rb new file mode 100644 index 00000000000..f1a626cd2e1 --- /dev/null +++ b/lib/ruby/shared/rdoc/code_objects.rb @@ -0,0 +1,5 @@ +# This file was used to load all the RDoc::CodeObject subclasses at once. Now +# autoload handles this. + +require 'rdoc' + diff --git a/lib/ruby/shared/rdoc/comment.rb b/lib/ruby/shared/rdoc/comment.rb new file mode 100644 index 00000000000..33ced18b5ac --- /dev/null +++ b/lib/ruby/shared/rdoc/comment.rb @@ -0,0 +1,229 @@ +## +# A comment holds the text comment for a RDoc::CodeObject and provides a +# unified way of cleaning it up and parsing it into an RDoc::Markup::Document. +# +# Each comment may have a different markup format set by #format=. By default +# 'rdoc' is used. The :markup: directive tells RDoc which format to use. +# +# See RDoc::Markup@Other+directives for instructions on adding an alternate +# format. + +class RDoc::Comment + + include RDoc::Text + + ## + # The format of this comment. Defaults to RDoc::Markup + + attr_reader :format + + ## + # The RDoc::TopLevel this comment was found in + + attr_accessor :location + + ## + # For duck-typing when merging classes at load time + + alias file location # :nodoc: + + ## + # The text for this comment + + attr_reader :text + + ## + # Overrides the content returned by #parse. Use when there is no #text + # source for this comment + + attr_writer :document + + ## + # Creates a new comment with +text+ that is found in the RDoc::TopLevel + # +location+. + + def initialize text = nil, location = nil + @location = location + @text = text + + @document = nil + @format = 'rdoc' + @normalized = false + end + + ## + #-- + # TODO deep copy @document + + def initialize_copy copy # :nodoc: + @text = copy.text.dup + end + + def == other # :nodoc: + self.class === other and + other.text == @text and other.location == @location + end + + ## + # Look for a 'call-seq' in the comment to override the normal parameter + # handling. The :call-seq: is indented from the baseline. All lines of the + # same indentation level and prefix are consumed. + # + # For example, all of the following will be used as the :call-seq: + # + # # :call-seq: + # # ARGF.readlines(sep=$/) -> array + # # ARGF.readlines(limit) -> array + # # ARGF.readlines(sep, limit) -> array + # # + # # ARGF.to_a(sep=$/) -> array + # # ARGF.to_a(limit) -> array + # # ARGF.to_a(sep, limit) -> array + + def extract_call_seq method + # we must handle situations like the above followed by an unindented first + # comment. The difficulty is to make sure not to match lines starting + # with ARGF at the same indent, but that are after the first description + # paragraph. + if @text =~ /^\s*:?call-seq:(.*?(?:\S).*?)^\s*$/m then + all_start, all_stop = $~.offset(0) + seq_start, seq_stop = $~.offset(1) + + # we get the following lines that start with the leading word at the + # same indent, even if they have blank lines before + if $1 =~ /(^\s*\n)+^(\s*\w+)/m then + leading = $2 # ' * ARGF' in the example above + re = %r% + \A( + (^\s*\n)+ + (^#{Regexp.escape leading}.*?\n)+ + )+ + ^\s*$ + %xm + + if @text[seq_stop..-1] =~ re then + all_stop = seq_stop + $~.offset(0).last + seq_stop = seq_stop + $~.offset(1).last + end + end + + seq = @text[seq_start..seq_stop] + seq.gsub!(/^\s*(\S|\n)/m, '\1') + @text.slice! all_start...all_stop + + method.call_seq = seq.chomp + + elsif @text.sub!(/^\s*:?call-seq:(.*?)(^\s*$|\z)/m, '') then + seq = $1 + seq.gsub!(/^\s*/, '') + method.call_seq = seq + end + + method + end + + ## + # A comment is empty if its text String is empty. + + def empty? + @text.empty? + end + + ## + # HACK dubious + + def force_encoding encoding + @text.force_encoding encoding + end + + ## + # Sets the format of this comment and resets any parsed document + + def format= format + @format = format + @document = nil + end + + def inspect # :nodoc: + location = @location ? @location.relative_name : '(unknown)' + + "#<%s:%x %s %p>" % [self.class, object_id, location, @text] + end + + ## + # Normalizes the text. See RDoc::Text#normalize_comment for details + + def normalize + return self unless @text + return self if @normalized # TODO eliminate duplicate normalization + + @text = normalize_comment @text + + @normalized = true + + self + end + + ## + # Was this text normalized? + + def normalized? # :nodoc: + @normalized + end + + ## + # Parses the comment into an RDoc::Markup::Document. The parsed document is + # cached until the text is changed. + + def parse + return @document if @document + + @document = super @text, @format + @document.file = @location + @document + end + + ## + # Removes private sections from this comment. Private sections are flush to + # the comment marker and start with -- and end with ++. + # For C-style comments, a private marker may not start at the opening of the + # comment. + # + # /* + # *-- + # * private + # *++ + # * public + # */ + + def remove_private + # Workaround for gsub encoding for Ruby 1.9.2 and earlier + empty = '' + empty.force_encoding @text.encoding if Object.const_defined? :Encoding + + @text = @text.gsub(%r%^\s*([#*]?)--.*?^\s*(\1)\+\+\n?%m, empty) + @text = @text.sub(%r%^\s*[#*]?--.*%m, '') + end + + ## + # Replaces this comment's text with +text+ and resets the parsed document. + # + # An error is raised if the comment contains a document but no text. + + def text= text + raise RDoc::Error, 'replacing document-only comment is not allowed' if + @text.nil? and @document + + @document = nil + @text = text + end + + ## + # Returns true if this comment is in TomDoc format. + + def tomdoc? + @format == 'tomdoc' + end + +end + diff --git a/lib/ruby/shared/rdoc/constant.rb b/lib/ruby/shared/rdoc/constant.rb new file mode 100644 index 00000000000..97985cbf998 --- /dev/null +++ b/lib/ruby/shared/rdoc/constant.rb @@ -0,0 +1,186 @@ +## +# A constant + +class RDoc::Constant < RDoc::CodeObject + + MARSHAL_VERSION = 0 # :nodoc: + + ## + # Sets the module or class this is constant is an alias for. + + attr_writer :is_alias_for + + ## + # The constant's name + + attr_accessor :name + + ## + # The constant's value + + attr_accessor :value + + ## + # The constant's visibility + + attr_accessor :visibility + + ## + # Creates a new constant with +name+, +value+ and +comment+ + + def initialize(name, value, comment) + super() + + @name = name + @value = value + + @is_alias_for = nil + @visibility = nil + + self.comment = comment + end + + ## + # Constants are ordered by name + + def <=> other + return unless self.class === other + + [parent_name, name] <=> [other.parent_name, other.name] + end + + ## + # Constants are equal when their #parent and #name is the same + + def == other + self.class == other.class and + @parent == other.parent and + @name == other.name + end + + ## + # A constant is documented if it has a comment, or is an alias + # for a documented class or module. + + def documented? + return true if super + return false unless @is_alias_for + case @is_alias_for + when String then + found = @store.find_class_or_module @is_alias_for + return false unless found + @is_alias_for = found + end + @is_alias_for.documented? + end + + ## + # Full constant name including namespace + + def full_name + @full_name ||= "#{parent_name}::#{@name}" + end + + ## + # The module or class this constant is an alias for + + def is_alias_for + case @is_alias_for + when String then + found = @store.find_class_or_module @is_alias_for + @is_alias_for = found if found + @is_alias_for + else + @is_alias_for + end + end + + def inspect # :nodoc: + "#<%s:0x%x %s::%s>" % [ + self.class, object_id, + parent_name, @name, + ] + end + + ## + # Dumps this Constant for use by ri. See also #marshal_load + + def marshal_dump + alias_name = case found = is_alias_for + when RDoc::CodeObject then found.full_name + else found + end + + [ MARSHAL_VERSION, + @name, + full_name, + @visibility, + alias_name, + parse(@comment), + @file.relative_name, + parent.name, + parent.class, + section.title, + ] + end + + ## + # Loads this Constant from +array+. For a loaded Constant the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize array[1], nil, array[5] + + @full_name = array[2] + @visibility = array[3] + @is_alias_for = array[4] + # 5 handled above + # 6 handled below + @parent_name = array[7] + @parent_class = array[8] + @section_title = array[9] + + @file = RDoc::TopLevel.new array[6] + end + + ## + # Path to this constant for use with HTML generator output. + + def path + "#{@parent.path}##{@name}" + end + + def pretty_print q # :nodoc: + q.group 2, "[#{self.class.name} #{full_name}", "]" do + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + ## + # Sets the store for this class or module and its contained code objects. + + def store= store + super + + @file = @store.add_file @file.full_name if @file + end + + def to_s # :nodoc: + parent_name = parent ? parent.full_name : '(unknown)' + if is_alias_for + "constant #{parent_name}::#@name -> #{is_alias_for}" + else + "constant #{parent_name}::#@name" + end + end + +end + diff --git a/lib/ruby/shared/rdoc/context.rb b/lib/ruby/shared/rdoc/context.rb new file mode 100644 index 00000000000..bb583488a91 --- /dev/null +++ b/lib/ruby/shared/rdoc/context.rb @@ -0,0 +1,1209 @@ +require 'cgi' + +## +# A Context is something that can hold modules, classes, methods, attributes, +# aliases, requires, and includes. Classes, modules, and files are all +# Contexts. + +class RDoc::Context < RDoc::CodeObject + + include Comparable + + ## + # Types of methods + + TYPES = %w[class instance] + + ## + # If a context has these titles it will be sorted in this order. + + TOMDOC_TITLES = [nil, 'Public', 'Internal', 'Deprecated'] # :nodoc: + TOMDOC_TITLES_SORT = TOMDOC_TITLES.sort_by { |title| title.to_s } # :nodoc: + + ## + # Class/module aliases + + attr_reader :aliases + + ## + # All attr* methods + + attr_reader :attributes + + ## + # Block params to be used in the next MethodAttr parsed under this context + + attr_accessor :block_params + + ## + # Constants defined + + attr_reader :constants + + ## + # Sets the current documentation section of documentation + + attr_writer :current_section + + ## + # Files this context is found in + + attr_reader :in_files + + ## + # Modules this context includes + + attr_reader :includes + + ## + # Modules this context is extended with + + attr_reader :extends + + ## + # Methods defined in this context + + attr_reader :method_list + + ## + # Name of this class excluding namespace. See also full_name + + attr_reader :name + + ## + # Files this context requires + + attr_reader :requires + + ## + # Use this section for the next method, attribute or constant added. + + attr_accessor :temporary_section + + ## + # Hash old_name => [aliases], for aliases + # that haven't (yet) been resolved to a method/attribute. + # (Not to be confused with the aliases of the context.) + + attr_accessor :unmatched_alias_lists + + ## + # Aliases that could not be resolved. + + attr_reader :external_aliases + + ## + # Current visibility of this context + + attr_accessor :visibility + + ## + # Hash of registered methods. Attributes are also registered here, + # twice if they are RW. + + attr_reader :methods_hash + + ## + # Params to be used in the next MethodAttr parsed under this context + + attr_accessor :params + + ## + # Hash of registered constants. + + attr_reader :constants_hash + + ## + # Creates an unnamed empty context with public current visibility + + def initialize + super + + @in_files = [] + + @name ||= "unknown" + @parent = nil + @visibility = :public + + @current_section = Section.new self, nil, nil + @sections = { nil => @current_section } + @temporary_section = nil + + @classes = {} + @modules = {} + + initialize_methods_etc + end + + ## + # Sets the defaults for methods and so-forth + + def initialize_methods_etc + @method_list = [] + @attributes = [] + @aliases = [] + @requires = [] + @includes = [] + @extends = [] + @constants = [] + @external_aliases = [] + + # This Hash maps a method name to a list of unmatched aliases (aliases of + # a method not yet encountered). + @unmatched_alias_lists = {} + + @methods_hash = {} + @constants_hash = {} + + @params = nil + + @store ||= nil + end + + ## + # Contexts are sorted by full_name + + def <=>(other) + full_name <=> other.full_name + end + + ## + # Adds an item of type +klass+ with the given +name+ and +comment+ to the + # context. + # + # Currently only RDoc::Extend and RDoc::Include are supported. + + def add klass, name, comment + if RDoc::Extend == klass then + ext = RDoc::Extend.new name, comment + add_extend ext + elsif RDoc::Include == klass then + incl = RDoc::Include.new name, comment + add_include incl + else + raise NotImplementedError, "adding a #{klass} is not implemented" + end + end + + ## + # Adds +an_alias+ that is automatically resolved + + def add_alias an_alias + return an_alias unless @document_self + + method_attr = find_method(an_alias.old_name, an_alias.singleton) || + find_attribute(an_alias.old_name, an_alias.singleton) + + if method_attr then + method_attr.add_alias an_alias, self + else + add_to @external_aliases, an_alias + unmatched_alias_list = + @unmatched_alias_lists[an_alias.pretty_old_name] ||= [] + unmatched_alias_list.push an_alias + end + + an_alias + end + + ## + # Adds +attribute+ if not already there. If it is (as method(s) or attribute), + # updates the comment if it was empty. + # + # The attribute is registered only if it defines a new method. + # For instance, attr_reader :foo will not be registered + # if method +foo+ exists, but attr_accessor :foo will be registered + # if method +foo+ exists, but foo= does not. + + def add_attribute attribute + return attribute unless @document_self + + # mainly to check for redefinition of an attribute as a method + # TODO find a policy for 'attr_reader :foo' + 'def foo=()' + register = false + + key = nil + + if attribute.rw.index 'R' then + key = attribute.pretty_name + known = @methods_hash[key] + + if known then + known.comment = attribute.comment if known.comment.empty? + elsif registered = @methods_hash[attribute.pretty_name << '='] and + RDoc::Attr === registered then + registered.rw = 'RW' + else + @methods_hash[key] = attribute + register = true + end + end + + if attribute.rw.index 'W' then + key = attribute.pretty_name << '=' + known = @methods_hash[key] + + if known then + known.comment = attribute.comment if known.comment.empty? + elsif registered = @methods_hash[attribute.pretty_name] and + RDoc::Attr === registered then + registered.rw = 'RW' + else + @methods_hash[key] = attribute + register = true + end + end + + if register then + attribute.visibility = @visibility + add_to @attributes, attribute + resolve_aliases attribute + end + + attribute + end + + ## + # Adds a class named +given_name+ with +superclass+. + # + # Both +given_name+ and +superclass+ may contain '::', and are + # interpreted relative to the +self+ context. This allows handling correctly + # examples like these: + # class RDoc::Gauntlet < Gauntlet + # module Mod + # class Object # implies < ::Object + # class SubObject < Object # this is _not_ ::Object + # + # Given class Container::Item RDoc assumes +Container+ is a module + # unless it later sees class Container. +add_class+ automatically + # upgrades +given_name+ to a class in this case. + + def add_class class_type, given_name, superclass = '::Object' + # superclass +nil+ is passed by the C parser in the following cases: + # - registering Object in 1.8 (correct) + # - registering BasicObject in 1.9 (correct) + # - registering RubyVM in 1.9 in iseq.c (incorrect: < Object in vm.c) + # + # If we later find a superclass for a registered class with a nil + # superclass, we must honor it. + + # find the name & enclosing context + if given_name =~ /^:+(\w+)$/ then + full_name = $1 + enclosing = top_level + name = full_name.split(/:+/).last + else + full_name = child_name given_name + + if full_name =~ /^(.+)::(\w+)$/ then + name = $2 + ename = $1 + enclosing = @store.classes_hash[ename] || @store.modules_hash[ename] + # HACK: crashes in actionpack/lib/action_view/helpers/form_helper.rb (metaprogramming) + unless enclosing then + # try the given name at top level (will work for the above example) + enclosing = @store.classes_hash[given_name] || + @store.modules_hash[given_name] + return enclosing if enclosing + # not found: create the parent(s) + names = ename.split('::') + enclosing = self + names.each do |n| + enclosing = enclosing.classes_hash[n] || + enclosing.modules_hash[n] || + enclosing.add_module(RDoc::NormalModule, n) + end + end + else + name = full_name + enclosing = self + end + end + + # fix up superclass + if full_name == 'BasicObject' then + superclass = nil + elsif full_name == 'Object' then + superclass = defined?(::BasicObject) ? '::BasicObject' : nil + end + + # find the superclass full name + if superclass then + if superclass =~ /^:+/ then + superclass = $' #' + else + if superclass =~ /^(\w+):+(.+)$/ then + suffix = $2 + mod = find_module_named($1) + superclass = mod.full_name + '::' + suffix if mod + else + mod = find_module_named(superclass) + superclass = mod.full_name if mod + end + end + + # did we believe it was a module? + mod = @store.modules_hash.delete superclass + + upgrade_to_class mod, RDoc::NormalClass, mod.parent if mod + + # e.g., Object < Object + superclass = nil if superclass == full_name + end + + klass = @store.classes_hash[full_name] + + if klass then + # if TopLevel, it may not be registered in the classes: + enclosing.classes_hash[name] = klass + + # update the superclass if needed + if superclass then + existing = klass.superclass + existing = existing.full_name unless existing.is_a?(String) if existing + if existing.nil? || + (existing == 'Object' && superclass != 'Object') then + klass.superclass = superclass + end + end + else + # this is a new class + mod = @store.modules_hash.delete full_name + + if mod then + klass = upgrade_to_class mod, RDoc::NormalClass, enclosing + + klass.superclass = superclass unless superclass.nil? + else + klass = class_type.new name, superclass + + enclosing.add_class_or_module(klass, enclosing.classes_hash, + @store.classes_hash) + end + end + + klass.parent = self + + klass + end + + ## + # Adds the class or module +mod+ to the modules or + # classes Hash +self_hash+, and to +all_hash+ (either + # TopLevel::modules_hash or TopLevel::classes_hash), + # unless #done_documenting is +true+. Sets the #parent of +mod+ + # to +self+, and its #section to #current_section. Returns +mod+. + + def add_class_or_module mod, self_hash, all_hash + mod.section = current_section # TODO declaring context? something is + # wrong here... + mod.parent = self + mod.store = @store + + unless @done_documenting then + self_hash[mod.name] = mod + # this must be done AFTER adding mod to its parent, so that the full + # name is correct: + all_hash[mod.full_name] = mod + end + + mod + end + + ## + # Adds +constant+ if not already there. If it is, updates the comment, + # value and/or is_alias_for of the known constant if they were empty/nil. + + def add_constant constant + return constant unless @document_self + + # HACK: avoid duplicate 'PI' & 'E' in math.c (1.8.7 source code) + # (this is a #ifdef: should be handled by the C parser) + known = @constants_hash[constant.name] + + if known then + known.comment = constant.comment if known.comment.empty? + + known.value = constant.value if + known.value.nil? or known.value.strip.empty? + + known.is_alias_for ||= constant.is_alias_for + else + @constants_hash[constant.name] = constant + add_to @constants, constant + end + + constant + end + + ## + # Adds included module +include+ which should be an RDoc::Include + + def add_include include + add_to @includes, include + + include + end + + ## + # Adds extension module +ext+ which should be an RDoc::Extend + + def add_extend ext + add_to @extends, ext + + ext + end + + ## + # Adds +method+ if not already there. If it is (as method or attribute), + # updates the comment if it was empty. + + def add_method method + return method unless @document_self + + # HACK: avoid duplicate 'new' in io.c & struct.c (1.8.7 source code) + key = method.pretty_name + known = @methods_hash[key] + + if known then + if @store then # otherwise we are loading + known.comment = method.comment if known.comment.empty? + previously = ", previously in #{known.file}" unless + method.file == known.file + @store.rdoc.options.warn \ + "Duplicate method #{known.full_name} in #{method.file}#{previously}" + end + else + @methods_hash[key] = method + method.visibility = @visibility + add_to @method_list, method + resolve_aliases method + end + + method + end + + ## + # Adds a module named +name+. If RDoc already knows +name+ is a class then + # that class is returned instead. See also #add_class. + + def add_module(class_type, name) + mod = @classes[name] || @modules[name] + return mod if mod + + full_name = child_name name + mod = @store.modules_hash[full_name] || class_type.new(name) + + add_class_or_module mod, @modules, @store.modules_hash + end + + ## + # Adds an alias from +from+ (a class or module) to +name+ which was defined + # in +file+. + + def add_module_alias from, name, file + return from if @done_documenting + + to_name = child_name name + + # if we already know this name, don't register an alias: + # see the metaprogramming in lib/active_support/basic_object.rb, + # where we already know BasicObject is a class when we find + # BasicObject = BlankSlate + return from if @store.find_class_or_module to_name + + to = from.dup + to.name = name + to.full_name = nil + + if to.module? then + @store.modules_hash[to_name] = to + @modules[name] = to + else + @store.classes_hash[to_name] = to + @classes[name] = to + end + + # Registers a constant for this alias. The constant value and comment + # will be updated later, when the Ruby parser adds the constant + const = RDoc::Constant.new name, nil, to.comment + const.record_location file + const.is_alias_for = from + add_constant const + + to + end + + ## + # Adds +require+ to this context's top level + + def add_require(require) + return require unless @document_self + + if RDoc::TopLevel === self then + add_to @requires, require + else + parent.add_require require + end + end + + ## + # Returns a section with +title+, creating it if it doesn't already exist. + # +comment+ will be appended to the section's comment. + # + # A section with a +title+ of +nil+ will return the default section. + # + # See also RDoc::Context::Section + + def add_section title, comment = nil + if section = @sections[title] then + section.add_comment comment if comment + else + section = Section.new self, title, comment + @sections[title] = section + end + + section + end + + ## + # Adds +thing+ to the collection +array+ + + def add_to array, thing + array << thing if @document_self + + thing.parent = self + thing.store = @store if @store + thing.section = current_section + end + + ## + # Is there any content? + # + # This means any of: comment, aliases, methods, attributes, external + # aliases, require, constant. + # + # Includes and extends are also checked unless includes == false. + + def any_content(includes = true) + @any_content ||= !( + @comment.empty? && + @method_list.empty? && + @attributes.empty? && + @aliases.empty? && + @external_aliases.empty? && + @requires.empty? && + @constants.empty? + ) + @any_content || (includes && !(@includes + @extends).empty? ) + end + + ## + # Creates the full name for a child with +name+ + + def child_name name + if name =~ /^:+/ + $' #' + elsif RDoc::TopLevel === self then + name + else + "#{self.full_name}::#{name}" + end + end + + ## + # Class attributes + + def class_attributes + @class_attributes ||= attributes.select { |a| a.singleton } + end + + ## + # Class methods + + def class_method_list + @class_method_list ||= method_list.select { |a| a.singleton } + end + + ## + # Array of classes in this context + + def classes + @classes.values + end + + ## + # All classes and modules in this namespace + + def classes_and_modules + classes + modules + end + + ## + # Hash of classes keyed by class name + + def classes_hash + @classes + end + + ## + # The current documentation section that new items will be added to. If + # temporary_section is available it will be used. + + def current_section + if section = @temporary_section then + @temporary_section = nil + else + section = @current_section + end + + section + end + + ## + # Is part of this thing was defined in +file+? + + def defined_in?(file) + @in_files.include?(file) + end + + def display(method_attr) # :nodoc: + if method_attr.is_a? RDoc::Attr + "#{method_attr.definition} #{method_attr.pretty_name}" + else + "method #{method_attr.pretty_name}" + end + end + + ## + # Iterator for ancestors for duck-typing. Does nothing. See + # RDoc::ClassModule#each_ancestor. + # + # This method exists to make it easy to work with Context subclasses that + # aren't part of RDoc. + + def each_ancestor # :nodoc: + end + + ## + # Iterator for attributes + + def each_attribute # :yields: attribute + @attributes.each { |a| yield a } + end + + ## + # Iterator for classes and modules + + def each_classmodule(&block) # :yields: module + classes_and_modules.sort.each(&block) + end + + ## + # Iterator for constants + + def each_constant # :yields: constant + @constants.each {|c| yield c} + end + + ## + # Iterator for included modules + + def each_include # :yields: include + @includes.each do |i| yield i end + end + + ## + # Iterator for extension modules + + def each_extend # :yields: extend + @extends.each do |e| yield e end + end + + ## + # Iterator for methods + + def each_method # :yields: method + return enum_for __method__ unless block_given? + + @method_list.sort.each { |m| yield m } + end + + ## + # Iterator for each section's contents sorted by title. The +section+, the + # section's +constants+ and the sections +attributes+ are yielded. The + # +constants+ and +attributes+ collections are sorted. + # + # To retrieve methods in a section use #methods_by_type with the optional + # +section+ parameter. + # + # NOTE: Do not edit collections yielded by this method + + def each_section # :yields: section, constants, attributes + return enum_for __method__ unless block_given? + + constants = @constants.group_by do |constant| constant.section end + attributes = @attributes.group_by do |attribute| attribute.section end + + constants.default = [] + attributes.default = [] + + sort_sections.each do |section| + yield section, constants[section].sort, attributes[section].sort + end + end + + ## + # Finds an attribute +name+ with singleton value +singleton+. + + def find_attribute(name, singleton) + name = $1 if name =~ /^(.*)=$/ + @attributes.find { |a| a.name == name && a.singleton == singleton } + end + + ## + # Finds an attribute with +name+ in this context + + def find_attribute_named(name) + case name + when /\A#/ then + find_attribute name[1..-1], false + when /\A::/ then + find_attribute name[2..-1], true + else + @attributes.find { |a| a.name == name } + end + end + + ## + # Finds a class method with +name+ in this context + + def find_class_method_named(name) + @method_list.find { |meth| meth.singleton && meth.name == name } + end + + ## + # Finds a constant with +name+ in this context + + def find_constant_named(name) + @constants.find {|m| m.name == name} + end + + ## + # Find a module at a higher scope + + def find_enclosing_module_named(name) + parent && parent.find_module_named(name) + end + + ## + # Finds an external alias +name+ with singleton value +singleton+. + + def find_external_alias(name, singleton) + @external_aliases.find { |m| m.name == name && m.singleton == singleton } + end + + ## + # Finds an external alias with +name+ in this context + + def find_external_alias_named(name) + case name + when /\A#/ then + find_external_alias name[1..-1], false + when /\A::/ then + find_external_alias name[2..-1], true + else + @external_aliases.find { |a| a.name == name } + end + end + + ## + # Finds a file with +name+ in this context + + def find_file_named name + @store.find_file_named name + end + + ## + # Finds an instance method with +name+ in this context + + def find_instance_method_named(name) + @method_list.find { |meth| !meth.singleton && meth.name == name } + end + + ## + # Finds a method, constant, attribute, external alias, module or file + # named +symbol+ in this context. + + def find_local_symbol(symbol) + find_method_named(symbol) or + find_constant_named(symbol) or + find_attribute_named(symbol) or + find_external_alias_named(symbol) or + find_module_named(symbol) or + find_file_named(symbol) + end + + ## + # Finds a method named +name+ with singleton value +singleton+. + + def find_method(name, singleton) + @method_list.find { |m| m.name == name && m.singleton == singleton } + end + + ## + # Finds a instance or module method with +name+ in this context + + def find_method_named(name) + case name + when /\A#/ then + find_method name[1..-1], false + when /\A::/ then + find_method name[2..-1], true + else + @method_list.find { |meth| meth.name == name } + end + end + + ## + # Find a module with +name+ using ruby's scoping rules + + def find_module_named(name) + res = @modules[name] || @classes[name] + return res if res + return self if self.name == name + find_enclosing_module_named name + end + + ## + # Look up +symbol+, first as a module, then as a local symbol. + + def find_symbol(symbol) + find_symbol_module(symbol) || find_local_symbol(symbol) + end + + ## + # Look up a module named +symbol+. + + def find_symbol_module(symbol) + result = nil + + # look for a class or module 'symbol' + case symbol + when /^::/ then + result = @store.find_class_or_module symbol + when /^(\w+):+(.+)$/ + suffix = $2 + top = $1 + searched = self + while searched do + mod = searched.find_module_named(top) + break unless mod + result = @store.find_class_or_module "#{mod.full_name}::#{suffix}" + break if result || searched.is_a?(RDoc::TopLevel) + searched = searched.parent + end + else + searched = self + while searched do + result = searched.find_module_named(symbol) + break if result || searched.is_a?(RDoc::TopLevel) + searched = searched.parent + end + end + + result + end + + ## + # The full name for this context. This method is overridden by subclasses. + + def full_name + '(unknown)' + end + + ## + # Does this context and its methods and constants all have documentation? + # + # (Yes, fully documented doesn't mean everything.) + + def fully_documented? + documented? and + attributes.all? { |a| a.documented? } and + method_list.all? { |m| m.documented? } and + constants.all? { |c| c.documented? } + end + + ## + # URL for this with a +prefix+ + + def http_url(prefix) + path = name_for_path + path = path.gsub(/<<\s*(\w*)/, 'from-\1') if path =~ /<'class' or + # 'instance') and visibility (+:public+, +:protected+, +:private+). + # + # If +section+ is provided only methods in that RDoc::Context::Section will + # be returned. + + def methods_by_type section = nil + methods = {} + + TYPES.each do |type| + visibilities = {} + RDoc::VISIBILITIES.each do |vis| + visibilities[vis] = [] + end + + methods[type] = visibilities + end + + each_method do |method| + next if section and not method.section == section + methods[method.type][method.visibility] << method + end + + methods + end + + ## + # Yields AnyMethod and Attr entries matching the list of names in +methods+. + + def methods_matching(methods, singleton = false, &block) + (@method_list + @attributes).each do |m| + yield m if methods.include?(m.name) and m.singleton == singleton + end + + each_ancestor do |parent| + parent.methods_matching(methods, singleton, &block) + end + end + + ## + # Array of modules in this context + + def modules + @modules.values + end + + ## + # Hash of modules keyed by module name + + def modules_hash + @modules + end + + ## + # Name to use to generate the url. + # #full_name by default. + + def name_for_path + full_name + end + + ## + # Changes the visibility for new methods to +visibility+ + + def ongoing_visibility=(visibility) + @visibility = visibility + end + + ## + # Record +top_level+ as a file +self+ is in. + + def record_location(top_level) + @in_files << top_level unless @in_files.include?(top_level) + end + + ## + # Should we remove this context from the documentation? + # + # The answer is yes if: + # * #received_nodoc is +true+ + # * #any_content is +false+ (not counting includes) + # * All #includes are modules (not a string), and their module has + # #remove_from_documentation? == true + # * All classes and modules have #remove_from_documentation? == true + + def remove_from_documentation? + @remove_from_documentation ||= + @received_nodoc && + !any_content(false) && + @includes.all? { |i| !i.module.is_a?(String) && i.module.remove_from_documentation? } && + classes_and_modules.all? { |cm| cm.remove_from_documentation? } + end + + ## + # Removes methods and attributes with a visibility less than +min_visibility+. + #-- + # TODO mark the visibility of attributes in the template (if not public?) + + def remove_invisible min_visibility + return if [:private, :nodoc].include? min_visibility + remove_invisible_in @method_list, min_visibility + remove_invisible_in @attributes, min_visibility + end + + ## + # Only called when min_visibility == :public or :private + + def remove_invisible_in array, min_visibility # :nodoc: + if min_visibility == :public then + array.reject! { |e| + e.visibility != :public and not e.force_documentation + } + else + array.reject! { |e| + e.visibility == :private and not e.force_documentation + } + end + end + + ## + # Tries to resolve unmatched aliases when a method or attribute has just + # been added. + + def resolve_aliases added + # resolve any pending unmatched aliases + key = added.pretty_name + unmatched_alias_list = @unmatched_alias_lists[key] + return unless unmatched_alias_list + unmatched_alias_list.each do |unmatched_alias| + added.add_alias unmatched_alias, self + @external_aliases.delete unmatched_alias + end + @unmatched_alias_lists.delete key + end + + ## + # Returns RDoc::Context::Section objects referenced in this context for use + # in a table of contents. + + def section_contents + used_sections = {} + + each_method do |method| + next unless method.display? + + used_sections[method.section] = true + end + + # order found sections + sections = sort_sections.select do |section| + used_sections[section] + end + + # only the default section is used + return [] if + sections.length == 1 and not sections.first.title + + sections + end + + ## + # Sections in this context + + def sections + @sections.values + end + + def sections_hash # :nodoc: + @sections + end + + ## + # Sets the current section to a section with +title+. See also #add_section + + def set_current_section title, comment + @current_section = add_section title, comment + end + + ## + # Given an array +methods+ of method names, set the visibility of each to + # +visibility+ + + def set_visibility_for(methods, visibility, singleton = false) + methods_matching methods, singleton do |m| + m.visibility = visibility + end + end + + ## + # Sorts sections alphabetically (default) or in TomDoc fashion (none, + # Public, Internal, Deprecated) + + def sort_sections + titles = @sections.map { |title, _| title } + + if titles.length > 1 and + TOMDOC_TITLES_SORT == + (titles | TOMDOC_TITLES).sort_by { |title| title.to_s } then + @sections.values_at(*TOMDOC_TITLES).compact + else + @sections.sort_by { |title, _| + title.to_s + }.map { |_, section| + section + } + end + end + + def to_s # :nodoc: + "#{self.class.name} #{self.full_name}" + end + + ## + # Return the TopLevel that owns us + #-- + # FIXME we can be 'owned' by several TopLevel (see #record_location & + # #in_files) + + def top_level + return @top_level if defined? @top_level + @top_level = self + @top_level = @top_level.parent until RDoc::TopLevel === @top_level + @top_level + end + + ## + # Upgrades NormalModule +mod+ in +enclosing+ to a +class_type+ + + def upgrade_to_class mod, class_type, enclosing + enclosing.modules_hash.delete mod.name + + klass = RDoc::ClassModule.from_module class_type, mod + klass.store = @store + + # if it was there, then we keep it even if done_documenting + @store.classes_hash[mod.full_name] = klass + enclosing.classes_hash[mod.name] = klass + + klass + end + + autoload :Section, 'rdoc/context/section' + +end + diff --git a/lib/ruby/shared/rdoc/context/section.rb b/lib/ruby/shared/rdoc/context/section.rb new file mode 100644 index 00000000000..580f07deff8 --- /dev/null +++ b/lib/ruby/shared/rdoc/context/section.rb @@ -0,0 +1,238 @@ +## +# A section of documentation like: +# +# # :section: The title +# # The body +# +# Sections can be referenced multiple times and will be collapsed into a +# single section. + +class RDoc::Context::Section + + include RDoc::Text + + MARSHAL_VERSION = 0 # :nodoc: + + ## + # Section comment + + attr_reader :comment + + ## + # Section comments + + attr_reader :comments + + ## + # Context this Section lives in + + attr_reader :parent + + ## + # Section title + + attr_reader :title + + @@sequence = "SEC00000" + + ## + # Creates a new section with +title+ and +comment+ + + def initialize parent, title, comment + @parent = parent + @title = title ? title.strip : title + + @@sequence.succ! + @sequence = @@sequence.dup + + @comments = [] + + add_comment comment + end + + ## + # Sections are equal when they have the same #title + + def == other + self.class === other and @title == other.title + end + + ## + # Adds +comment+ to this section + + def add_comment comment + comment = extract_comment comment + + return if comment.empty? + + case comment + when RDoc::Comment then + @comments << comment + when RDoc::Markup::Document then + @comments.concat comment.parts + when Array then + @comments.concat comment + else + raise TypeError, "unknown comment type: #{comment.inspect}" + end + end + + ## + # Anchor reference for linking to this section + + def aref + title = @title || '[untitled]' + + CGI.escape(title).gsub('%', '-').sub(/^-/, '') + end + + ## + # Extracts the comment for this section from the original comment block. + # If the first line contains :section:, strip it and use the rest. + # Otherwise remove lines up to the line containing :section:, and look + # for those lines again at the end and remove them. This lets us write + # + # # :section: The title + # # The body + + def extract_comment comment + case comment + when Array then + comment.map do |c| + extract_comment c + end + when nil + RDoc::Comment.new '' + when RDoc::Comment then + if comment.text =~ /^#[ \t]*:section:.*\n/ then + start = $` + rest = $' + + comment.text = if start.empty? then + rest + else + rest.sub(/#{start.chomp}\Z/, '') + end + end + + comment + when RDoc::Markup::Document then + comment + else + raise TypeError, "unknown comment #{comment.inspect}" + end + end + + def inspect # :nodoc: + "#<%s:0x%x %p>" % [self.class, object_id, title] + end + + ## + # The files comments in this section come from + + def in_files + return [] if @comments.empty? + + case @comments + when Array then + @comments.map do |comment| + comment.file + end + when RDoc::Markup::Document then + @comment.parts.map do |document| + document.file + end + else + raise RDoc::Error, "BUG: unknown comment class #{@comments.class}" + end + end + + ## + # Serializes this Section. The title and parsed comment are saved, but not + # the section parent which must be restored manually. + + def marshal_dump + [ + MARSHAL_VERSION, + @title, + parse, + ] + end + + ## + # De-serializes this Section. The section parent must be restored manually. + + def marshal_load array + @parent = nil + + @title = array[1] + @comments = array[2] + end + + ## + # Parses +comment_location+ into an RDoc::Markup::Document composed of + # multiple RDoc::Markup::Documents with their file set. + + def parse + case @comments + when String then + super + when Array then + docs = @comments.map do |comment, location| + doc = super comment + doc.file = location if location + doc + end + + RDoc::Markup::Document.new(*docs) + when RDoc::Comment then + doc = super @comments.text, comments.format + doc.file = @comments.location + doc + when RDoc::Markup::Document then + return @comments + else + raise ArgumentError, "unknown comment class #{comments.class}" + end + end + + ## + # The section's title, or 'Top Section' if the title is nil. + # + # This is used by the table of contents template so the name is silly. + + def plain_html + @title || 'Top Section' + end + + ## + # Removes a comment from this section if it is from the same file as + # +comment+ + + def remove_comment comment + return if @comments.empty? + + case @comments + when Array then + @comments.delete_if do |my_comment| + my_comment.file == comment.file + end + when RDoc::Markup::Document then + @comments.parts.delete_if do |document| + document.file == comment.file.name + end + else + raise RDoc::Error, "BUG: unknown comment class #{@comments.class}" + end + end + + ## + # Section sequence number (deprecated) + + def sequence + warn "RDoc::Context::Section#sequence is deprecated, use #aref" + @sequence + end + +end + diff --git a/lib/ruby/shared/rdoc/cross_reference.rb b/lib/ruby/shared/rdoc/cross_reference.rb new file mode 100644 index 00000000000..5b08d5202de --- /dev/null +++ b/lib/ruby/shared/rdoc/cross_reference.rb @@ -0,0 +1,183 @@ +## +# RDoc::CrossReference is a reusable way to create cross references for names. + +class RDoc::CrossReference + + ## + # Regular expression to match class references + # + # 1. There can be a '\\' in front of text to suppress the cross-reference + # 2. There can be a '::' in front of class names to reference from the + # top-level namespace. + # 3. The method can be followed by parenthesis (not recommended) + + CLASS_REGEXP_STR = '\\\\?((?:\:{2})?[A-Z]\w*(?:\:\:\w+)*)' + + ## + # Regular expression to match method references. + # + # See CLASS_REGEXP_STR + + METHOD_REGEXP_STR = '([a-z]\w*[!?=]?|%|===|\[\]=?|<<|>>)(?:\([\w.+*/=<>-]*\))?' + + ## + # Regular expressions matching text that should potentially have + # cross-reference links generated are passed to add_special. Note that + # these expressions are meant to pick up text for which cross-references + # have been suppressed, since the suppression characters are removed by the + # code that is triggered. + + CROSSREF_REGEXP = /(?:^|\s) + ( + (?: + # A::B::C.meth + #{CLASS_REGEXP_STR}(?:[.#]|::)#{METHOD_REGEXP_STR} + + # Stand-alone method (preceded by a #) + | \\?\##{METHOD_REGEXP_STR} + + # Stand-alone method (preceded by ::) + | ::#{METHOD_REGEXP_STR} + + # A::B::C + # The stuff after CLASS_REGEXP_STR is a + # nasty hack. CLASS_REGEXP_STR unfortunately matches + # words like dog and cat (these are legal "class" + # names in Fortran 95). When a word is flagged as a + # potential cross-reference, limitations in the markup + # engine suppress other processing, such as typesetting. + # This is particularly noticeable for contractions. + # In order that words like "can't" not + # be flagged as potential cross-references, only + # flag potential class cross-references if the character + # after the cross-reference is a space, sentence + # punctuation, tag start character, or attribute + # marker. + | #{CLASS_REGEXP_STR}(?=[@\s).?!,;<\000]|\z) + + # Things that look like filenames + # The key thing is that there must be at least + # one special character (period, slash, or + # underscore). + | (?:\.\.\/)*[-\/\w]+[_\/.][-\w\/.]+ + + # Things that have markup suppressed + # Don't process things like '\<' in \, though. + # TODO: including < is a hack, not very satisfying. + | \\[^\s<] + ) + + # labels for headings + (?:@[\w+%-]+(?:\.[\w|%-]+)?)? + )/x + + ## + # Version of CROSSREF_REGEXP used when --hyperlink-all is specified. + + ALL_CROSSREF_REGEXP = / + (?:^|\s) + ( + (?: + # A::B::C.meth + #{CLASS_REGEXP_STR}(?:[.#]|::)#{METHOD_REGEXP_STR} + + # Stand-alone method + | \\?#{METHOD_REGEXP_STR} + + # A::B::C + | #{CLASS_REGEXP_STR}(?=[@\s).?!,;<\000]|\z) + + # Things that look like filenames + | (?:\.\.\/)*[-\/\w]+[_\/.][-\w\/.]+ + + # Things that have markup suppressed + | \\[^\s<] + ) + + # labels for headings + (?:@[\w+%-]+)? + )/x + + ## + # Hash of references that have been looked-up to their replacements + + attr_accessor :seen + + ## + # Allows cross-references to be created based on the given +context+ + # (RDoc::Context). + + def initialize context + @context = context + @store = context.store + + @seen = {} + end + + ## + # Returns a reference to +name+. + # + # If the reference is found and +name+ is not documented +text+ will be + # returned. If +name+ is escaped +name+ is returned. If +name+ is not + # found +text+ is returned. + + def resolve name, text + return @seen[name] if @seen.include? name + + if /#{CLASS_REGEXP_STR}([.#]|::)#{METHOD_REGEXP_STR}/o =~ name then + type = $2 + type = '' if type == '.' # will find either #method or ::method + method = "#{type}#{$3}" + container = @context.find_symbol_module($1) + elsif /^([.#]|::)#{METHOD_REGEXP_STR}/o =~ name then + type = $1 + type = '' if type == '.' + method = "#{type}#{$2}" + container = @context + else + container = nil + end + + if container then + ref = container.find_local_symbol method + + unless ref || RDoc::TopLevel === container then + ref = container.find_ancestor_local_symbol method + end + end + + ref = case name + when /^\\(#{CLASS_REGEXP_STR})$/o then + @context.find_symbol $1 + else + @context.find_symbol name + end unless ref + + # Try a page name + ref = @store.page name if not ref and name =~ /^\w+$/ + + ref = nil if RDoc::Alias === ref # external alias, can't link to it + + out = if name == '\\' then + name + elsif name =~ /^\\/ then + # we remove the \ only in front of what we know: + # other backslashes are treated later, only outside of + ref ? $' : name + elsif ref then + if ref.display? then + ref + else + text + end + else + text + end + + @seen[name] = out + + out + end + +end + diff --git a/lib/ruby/shared/rdoc/encoding.rb b/lib/ruby/shared/rdoc/encoding.rb new file mode 100644 index 00000000000..9fe35394123 --- /dev/null +++ b/lib/ruby/shared/rdoc/encoding.rb @@ -0,0 +1,97 @@ +# coding: US-ASCII + +## +# This class is a wrapper around File IO and Encoding that helps RDoc load +# files and convert them to the correct encoding. + +module RDoc::Encoding + + ## + # Reads the contents of +filename+ and handles any encoding directives in + # the file. + # + # The content will be converted to the +encoding+. If the file cannot be + # converted a warning will be printed and nil will be returned. + # + # If +force_transcode+ is true the document will be transcoded and any + # unknown character in the target encoding will be replaced with '?' + + def self.read_file filename, encoding, force_transcode = false + content = open filename, "rb" do |f| f.read end + content.gsub!("\r\n", "\n") if RUBY_PLATFORM =~ /mswin|mingw/ + + utf8 = content.sub!(/\A\xef\xbb\xbf/, '') + + RDoc::Encoding.set_encoding content + + if Object.const_defined? :Encoding then + begin + encoding ||= Encoding.default_external + orig_encoding = content.encoding + + if utf8 then + content.force_encoding Encoding::UTF_8 + content.encode! encoding + else + # assume the content is in our output encoding + content.force_encoding encoding + end + + unless content.valid_encoding? then + # revert and try to transcode + content.force_encoding orig_encoding + content.encode! encoding + end + + unless content.valid_encoding? then + warn "unable to convert #{filename} to #{encoding}, skipping" + content = nil + end + rescue Encoding::InvalidByteSequenceError, + Encoding::UndefinedConversionError => e + if force_transcode then + content.force_encoding orig_encoding + content.encode!(encoding, + :invalid => :replace, :undef => :replace, + :replace => '?') + return content + else + warn "unable to convert #{e.message} for #{filename}, skipping" + return nil + end + end + end + + content + rescue ArgumentError => e + raise unless e.message =~ /unknown encoding name - (.*)/ + warn "unknown encoding name \"#{$1}\" for #{filename}, skipping" + nil + rescue Errno::EISDIR, Errno::ENOENT + nil + end + + ## + # Sets the encoding of +string+ based on the magic comment + + def self.set_encoding string + string =~ /\A(?:#!.*\n)?(.*\n)/ + + first_line = $1 + + name = case first_line + when /^<\?xml[^?]*encoding=(["'])(.*?)\1/ then $2 + when /\b(?:en)?coding[=:]\s*([^\s;]+)/i then $1 + else return + end + + string.sub! first_line, '' + + return unless Object.const_defined? :Encoding + + enc = Encoding.find name + string.force_encoding enc if enc + end + +end + diff --git a/lib/ruby/shared/rdoc/erb_partial.rb b/lib/ruby/shared/rdoc/erb_partial.rb new file mode 100644 index 00000000000..910d1e0351b --- /dev/null +++ b/lib/ruby/shared/rdoc/erb_partial.rb @@ -0,0 +1,18 @@ +## +# Allows an ERB template to be rendered in the context (binding) of an +# existing ERB template evaluation. + +class RDoc::ERBPartial < ERB + + ## + # Overrides +compiler+ startup to set the +eoutvar+ to an empty string only + # if it isn't already set. + + def set_eoutvar compiler, eoutvar = '_erbout' + super + + compiler.pre_cmd = ["#{eoutvar} ||= ''"] + end + +end + diff --git a/lib/ruby/shared/rdoc/erbio.rb b/lib/ruby/shared/rdoc/erbio.rb new file mode 100644 index 00000000000..04a89fbd348 --- /dev/null +++ b/lib/ruby/shared/rdoc/erbio.rb @@ -0,0 +1,37 @@ +require 'erb' + +## +# A subclass of ERB that writes directly to an IO. Credit to Aaron Patterson +# and Masatoshi SEKI. +# +# To use: +# +# erbio = RDoc::ERBIO.new '<%= "hello world" %>', nil, nil +# +# open 'hello.txt', 'w' do |io| +# erbio.result binding +# end +# +# Note that binding must enclose the io you wish to output on. + +class RDoc::ERBIO < ERB + + ## + # Defaults +eoutvar+ to 'io', otherwise is identical to ERB's initialize + + def initialize str, safe_level = nil, trim_mode = nil, eoutvar = 'io' + super + end + + ## + # Instructs +compiler+ how to write to +io_variable+ + + def set_eoutvar compiler, io_variable + compiler.put_cmd = "#{io_variable}.write" + compiler.insert_cmd = "#{io_variable}.write" + compiler.pre_cmd = [] + compiler.post_cmd = [] + end + +end + diff --git a/lib/ruby/shared/rdoc/extend.rb b/lib/ruby/shared/rdoc/extend.rb new file mode 100644 index 00000000000..efa2c69bee0 --- /dev/null +++ b/lib/ruby/shared/rdoc/extend.rb @@ -0,0 +1,9 @@ +## +# A Module extension to a class with \#extend +# +# RDoc::Extend.new 'Enumerable', 'comment ...' + +class RDoc::Extend < RDoc::Mixin + +end + diff --git a/lib/ruby/shared/rdoc/generator.rb b/lib/ruby/shared/rdoc/generator.rb new file mode 100644 index 00000000000..9051f8a6589 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator.rb @@ -0,0 +1,50 @@ +## +# RDoc uses generators to turn parsed source code in the form of an +# RDoc::CodeObject tree into some form of output. RDoc comes with the HTML +# generator RDoc::Generator::Darkfish and an ri data generator +# RDoc::Generator::RI. +# +# == Registering a Generator +# +# Generators are registered by calling RDoc::RDoc.add_generator with the class +# of the generator: +# +# class My::Awesome::Generator +# RDoc::RDoc.add_generator self +# end +# +# == Adding Options to +rdoc+ +# +# Before option processing in +rdoc+, RDoc::Options will call ::setup_options +# on the generator class with an RDoc::Options instance. The generator can +# use RDoc::Options#option_parser to add command-line options to the +rdoc+ +# tool. See RDoc::Options@Custom+Options for an example and see OptionParser +# for details on how to add options. +# +# You can extend the RDoc::Options instance with additional accessors for your +# generator. +# +# == Generator Instantiation +# +# After parsing, RDoc::RDoc will instantiate a generator by calling +# #initialize with an RDoc::Store instance and an RDoc::Options instance. +# +# The RDoc::Store instance holds documentation for parsed source code. In +# RDoc 3 and earlier the RDoc::TopLevel class held this data. When upgrading +# a generator from RDoc 3 and earlier you should only need to replace +# RDoc::TopLevel with the store instance. +# +# RDoc will then call #generate on the generator instance. You can use the +# various methods on RDoc::Store and in the RDoc::CodeObject tree to create +# your desired output format. + +module RDoc::Generator + + autoload :Markup, 'rdoc/generator/markup' + + autoload :Darkfish, 'rdoc/generator/darkfish' + autoload :JsonIndex, 'rdoc/generator/json_index' + autoload :RI, 'rdoc/generator/ri' + +end + diff --git a/lib/ruby/shared/rdoc/generator/darkfish.rb b/lib/ruby/shared/rdoc/generator/darkfish.rb new file mode 100644 index 00000000000..bd37b606689 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/darkfish.rb @@ -0,0 +1,759 @@ +# -*- mode: ruby; ruby-indent-level: 2; tab-width: 2 -*- + +require 'erb' +require 'fileutils' +require 'pathname' +require 'rdoc/generator/markup' + +## +# Darkfish RDoc HTML Generator +# +# $Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $ +# +# == Author/s +# * Michael Granger (ged@FaerieMUD.org) +# +# == Contributors +# * Mahlon E. Smith (mahlon@martini.nu) +# * Eric Hodel (drbrain@segment7.net) +# +# == License +# +# Copyright (c) 2007, 2008, Michael Granger. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the author/s, nor the names of the project's +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# == Attributions +# +# Darkfish uses the {Silk Icons}[http://www.famfamfam.com/lab/icons/silk/] set +# by Mark James. + +class RDoc::Generator::Darkfish + + RDoc::RDoc.add_generator self + + include ERB::Util + + ## + # Stylesheets, fonts, etc. that are included in RDoc. + + BUILTIN_STYLE_ITEMS = # :nodoc: + %w[ + fonts.css + fonts/Lato-Light.ttf + fonts/Lato-LightItalic.ttf + fonts/Lato-Regular.ttf + fonts/Lato-RegularItalic.ttf + fonts/SourceCodePro-Bold.ttf + fonts/SourceCodePro-Regular.ttf + rdoc.css + ] + + ## + # Path to this file's parent directory. Used to find templates and other + # resources. + + GENERATOR_DIR = File.join 'rdoc', 'generator' + + ## + # Release Version + + VERSION = '3' + + ## + # Description of this generator + + DESCRIPTION = 'HTML generator, written by Michael Granger' + + ## + # The relative path to style sheets and javascript. By default this is set + # the same as the rel_prefix. + + attr_accessor :asset_rel_path + + ## + # The path to generate files into, combined with --op from the + # options for a full path. + + attr_reader :base_dir + + ## + # Classes and modules to be used by this generator, not necessarily + # displayed. See also #modsort + + attr_reader :classes + + ## + # No files will be written when dry_run is true. + + attr_accessor :dry_run + + ## + # When false the generate methods return a String instead of writing to a + # file. The default is true. + + attr_accessor :file_output + + ## + # Files to be displayed by this generator + + attr_reader :files + + ## + # The JSON index generator for this Darkfish generator + + attr_reader :json_index + + ## + # Methods to be displayed by this generator + + attr_reader :methods + + ## + # Sorted list of classes and modules to be displayed by this generator + + attr_reader :modsort + + ## + # The RDoc::Store that is the source of the generated content + + attr_reader :store + + ## + # The directory where the template files live + + attr_reader :template_dir # :nodoc: + + ## + # The output directory + + attr_reader :outputdir + + ## + # Initialize a few instance variables before we start + + def initialize store, options + @store = store + @options = options + + @asset_rel_path = '' + @base_dir = Pathname.pwd.expand_path + @dry_run = @options.dry_run + @file_output = true + @template_dir = Pathname.new options.template_dir + @template_cache = {} + + @classes = nil + @context = nil + @files = nil + @methods = nil + @modsort = nil + + @json_index = RDoc::Generator::JsonIndex.new self, options + end + + ## + # Output progress information if debugging is enabled + + def debug_msg *msg + return unless $DEBUG_RDOC + $stderr.puts(*msg) + end + + ## + # Directory where generated class HTML files live relative to the output + # dir. + + def class_dir + nil + end + + ## + # Directory where generated class HTML files live relative to the output + # dir. + + def file_dir + nil + end + + ## + # Create the directories the generated docs will live in if they don't + # already exist. + + def gen_sub_directories + @outputdir.mkpath + end + + ## + # Copy over the stylesheet into the appropriate place in the output + # directory. + + def write_style_sheet + debug_msg "Copying static files" + options = { :verbose => $DEBUG_RDOC, :noop => @dry_run } + + BUILTIN_STYLE_ITEMS.each do |item| + install_rdoc_static_file @template_dir + item, "./#{item}", options + end + + @options.template_stylesheets.each do |stylesheet| + FileUtils.cp stylesheet, '.', options + end + + Dir[(@template_dir + "{js,images}/**/*").to_s].each do |path| + next if File.directory? path + next if File.basename(path) =~ /^\./ + + dst = Pathname.new(path).relative_path_from @template_dir + + install_rdoc_static_file @template_dir + path, dst, options + end + end + + ## + # Build the initial indices and output objects based on an array of TopLevel + # objects containing the extracted information. + + def generate + setup + + write_style_sheet + generate_index + generate_class_files + generate_file_files + generate_table_of_contents + @json_index.generate + + copy_static + + rescue => e + debug_msg "%s: %s\n %s" % [ + e.class.name, e.message, e.backtrace.join("\n ") + ] + + raise + end + + ## + # Copies static files from the static_path into the output directory + + def copy_static + return if @options.static_path.empty? + + fu_options = { :verbose => $DEBUG_RDOC, :noop => @dry_run } + + @options.static_path.each do |path| + unless File.directory? path then + FileUtils.install path, @outputdir, fu_options.merge(:mode => 0644) + next + end + + Dir.chdir path do + Dir[File.join('**', '*')].each do |entry| + dest_file = @outputdir + entry + + if File.directory? entry then + FileUtils.mkdir_p entry, fu_options + else + FileUtils.install entry, dest_file, fu_options.merge(:mode => 0644) + end + end + end + end + end + + ## + # Return a list of the documented modules sorted by salience first, then + # by name. + + def get_sorted_module_list classes + classes.select do |klass| + klass.display? + end.sort + end + + ## + # Generate an index page which lists all the classes which are documented. + + def generate_index + setup + + template_file = @template_dir + 'index.rhtml' + return unless template_file.exist? + + debug_msg "Rendering the index page..." + + out_file = @base_dir + @options.op_dir + 'index.html' + rel_prefix = @outputdir.relative_path_from out_file.dirname + search_index_rel_prefix = rel_prefix + search_index_rel_prefix += @asset_rel_path if @file_output + + # suppress 1.9.3 warning + asset_rel_prefix = asset_rel_prefix = rel_prefix + @asset_rel_path + + @title = @options.title + + render_template template_file, out_file do |io| binding end + rescue => e + error = RDoc::Error.new \ + "error generating index.html: #{e.message} (#{e.class})" + error.set_backtrace e.backtrace + + raise error + end + + ## + # Generates a class file for +klass+ + + def generate_class klass, template_file = nil + setup + + current = klass + + template_file ||= @template_dir + 'class.rhtml' + + debug_msg " working on %s (%s)" % [klass.full_name, klass.path] + out_file = @outputdir + klass.path + rel_prefix = @outputdir.relative_path_from out_file.dirname + search_index_rel_prefix = rel_prefix + search_index_rel_prefix += @asset_rel_path if @file_output + + # suppress 1.9.3 warning + asset_rel_prefix = asset_rel_prefix = rel_prefix + @asset_rel_path + svninfo = svninfo = get_svninfo(current) + + @title = "#{klass.type} #{klass.full_name} - #{@options.title}" + + debug_msg " rendering #{out_file}" + render_template template_file, out_file do |io| binding end + end + + ## + # Generate a documentation file for each class and module + + def generate_class_files + setup + + template_file = @template_dir + 'class.rhtml' + template_file = @template_dir + 'classpage.rhtml' unless + template_file.exist? + return unless template_file.exist? + debug_msg "Generating class documentation in #{@outputdir}" + + current = nil + + @classes.each do |klass| + current = klass + + generate_class klass, template_file + end + rescue => e + error = RDoc::Error.new \ + "error generating #{current.path}: #{e.message} (#{e.class})" + error.set_backtrace e.backtrace + + raise error + end + + ## + # Generate a documentation file for each file + + def generate_file_files + setup + + page_file = @template_dir + 'page.rhtml' + fileinfo_file = @template_dir + 'fileinfo.rhtml' + + # for legacy templates + filepage_file = @template_dir + 'filepage.rhtml' unless + page_file.exist? or fileinfo_file.exist? + + return unless + page_file.exist? or fileinfo_file.exist? or filepage_file.exist? + + debug_msg "Generating file documentation in #{@outputdir}" + + out_file = nil + current = nil + + @files.each do |file| + current = file + + if file.text? and page_file.exist? then + generate_page file + next + end + + template_file = nil + out_file = @outputdir + file.path + debug_msg " working on %s (%s)" % [file.full_name, out_file] + rel_prefix = @outputdir.relative_path_from out_file.dirname + search_index_rel_prefix = rel_prefix + search_index_rel_prefix += @asset_rel_path if @file_output + + # suppress 1.9.3 warning + asset_rel_prefix = asset_rel_prefix = rel_prefix + @asset_rel_path + + unless filepage_file then + if file.text? then + next unless page_file.exist? + template_file = page_file + @title = file.page_name + else + next unless fileinfo_file.exist? + template_file = fileinfo_file + @title = "File: #{file.base_name}" + end + end + + @title += " - #{@options.title}" + template_file ||= filepage_file + + render_template template_file, out_file do |io| binding end + end + rescue => e + error = + RDoc::Error.new "error generating #{out_file}: #{e.message} (#{e.class})" + error.set_backtrace e.backtrace + + raise error + end + + ## + # Generate a page file for +file+ + + def generate_page file + setup + + template_file = @template_dir + 'page.rhtml' + + out_file = @outputdir + file.path + debug_msg " working on %s (%s)" % [file.full_name, out_file] + rel_prefix = @outputdir.relative_path_from out_file.dirname + search_index_rel_prefix = rel_prefix + search_index_rel_prefix += @asset_rel_path if @file_output + + # suppress 1.9.3 warning + current = current = file + asset_rel_prefix = asset_rel_prefix = rel_prefix + @asset_rel_path + + @title = "#{file.page_name} - #{@options.title}" + + debug_msg " rendering #{out_file}" + render_template template_file, out_file do |io| binding end + end + + ## + # Generates the 404 page for the RDoc servlet + + def generate_servlet_not_found message + setup + + template_file = @template_dir + 'servlet_not_found.rhtml' + return unless template_file.exist? + + debug_msg "Rendering the servlet 404 Not Found page..." + + rel_prefix = rel_prefix = '' + search_index_rel_prefix = rel_prefix + search_index_rel_prefix += @asset_rel_path if @file_output + + # suppress 1.9.3 warning + asset_rel_prefix = asset_rel_prefix = '' + + @title = 'Not Found' + + render_template template_file do |io| binding end + rescue => e + error = RDoc::Error.new \ + "error generating servlet_not_found: #{e.message} (#{e.class})" + error.set_backtrace e.backtrace + + raise error + end + + ## + # Generates the servlet root page for the RDoc servlet + + def generate_servlet_root installed + setup + + template_file = @template_dir + 'servlet_root.rhtml' + return unless template_file.exist? + + debug_msg 'Rendering the servlet root page...' + + rel_prefix = '.' + asset_rel_prefix = rel_prefix + search_index_rel_prefix = asset_rel_prefix + search_index_rel_prefix += @asset_rel_path if @file_output + + @title = 'Local RDoc Documentation' + + render_template template_file do |io| binding end + rescue => e + error = RDoc::Error.new \ + "error generating servlet_root: #{e.message} (#{e.class})" + error.set_backtrace e.backtrace + + raise error + end + + ## + # Generate an index page which lists all the classes which are documented. + + def generate_table_of_contents + setup + + template_file = @template_dir + 'table_of_contents.rhtml' + return unless template_file.exist? + + debug_msg "Rendering the Table of Contents..." + + out_file = @outputdir + 'table_of_contents.html' + rel_prefix = @outputdir.relative_path_from out_file.dirname + search_index_rel_prefix = rel_prefix + search_index_rel_prefix += @asset_rel_path if @file_output + + # suppress 1.9.3 warning + asset_rel_prefix = asset_rel_prefix = rel_prefix + @asset_rel_path + + @title = "Table of Contents - #{@options.title}" + + render_template template_file, out_file do |io| binding end + rescue => e + error = RDoc::Error.new \ + "error generating table_of_contents.html: #{e.message} (#{e.class})" + error.set_backtrace e.backtrace + + raise error + end + + def install_rdoc_static_file source, destination, options # :nodoc: + return unless source.exist? + + begin + FileUtils.mkdir_p File.dirname(destination), options + + begin + FileUtils.ln source, destination, options + rescue Errno::EEXIST + FileUtils.rm destination + retry + end + rescue + FileUtils.cp source, destination, options + end + end + + ## + # Prepares for generation of output from the current directory + + def setup + return if instance_variable_defined? :@outputdir + + @outputdir = Pathname.new(@options.op_dir).expand_path @base_dir + + return unless @store + + @classes = @store.all_classes_and_modules.sort + @files = @store.all_files.sort + @methods = @classes.map { |m| m.method_list }.flatten.sort + @modsort = get_sorted_module_list @classes + end + + ## + # Return a string describing the amount of time in the given number of + # seconds in terms a human can understand easily. + + def time_delta_string seconds + return 'less than a minute' if seconds < 60 + return "#{seconds / 60} minute#{seconds / 60 == 1 ? '' : 's'}" if + seconds < 3000 # 50 minutes + return 'about one hour' if seconds < 5400 # 90 minutes + return "#{seconds / 3600} hours" if seconds < 64800 # 18 hours + return 'one day' if seconds < 86400 # 1 day + return 'about one day' if seconds < 172800 # 2 days + return "#{seconds / 86400} days" if seconds < 604800 # 1 week + return 'about one week' if seconds < 1209600 # 2 week + return "#{seconds / 604800} weeks" if seconds < 7257600 # 3 months + return "#{seconds / 2419200} months" if seconds < 31536000 # 1 year + return "#{seconds / 31536000} years" + end + + # %q$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $" + SVNID_PATTERN = / + \$Id:\s + (\S+)\s # filename + (\d+)\s # rev + (\d{4}-\d{2}-\d{2})\s # Date (YYYY-MM-DD) + (\d{2}:\d{2}:\d{2}Z)\s # Time (HH:MM:SSZ) + (\w+)\s # committer + \$$ + /x + + ## + # Try to extract Subversion information out of the first constant whose + # value looks like a subversion Id tag. If no matching constant is found, + # and empty hash is returned. + + def get_svninfo klass + constants = klass.constants or return {} + + constants.find { |c| c.value =~ SVNID_PATTERN } or return {} + + filename, rev, date, time, committer = $~.captures + commitdate = Time.parse "#{date} #{time}" + + return { + :filename => filename, + :rev => Integer(rev), + :commitdate => commitdate, + :commitdelta => time_delta_string(Time.now - commitdate), + :committer => committer, + } + end + + ## + # Creates a template from its components and the +body_file+. + # + # For backwards compatibility, if +body_file+ contains " + + + +#{head_file.read} + +#{body} + +#{footer_file.read} + TEMPLATE + end + + ## + # Renders the ERb contained in +file_name+ relative to the template + # directory and returns the result based on the current context. + + def render file_name + template_file = @template_dir + file_name + + template = template_for template_file, false, RDoc::ERBPartial + + template.filename = template_file.to_s + + template.result @context + end + + ## + # Load and render the erb template in the given +template_file+ and write + # it out to +out_file+. + # + # Both +template_file+ and +out_file+ should be Pathname-like objects. + # + # An io will be yielded which must be captured by binding in the caller. + + def render_template template_file, out_file = nil # :yield: io + io_output = out_file && !@dry_run && @file_output + erb_klass = io_output ? RDoc::ERBIO : ERB + + template = template_for template_file, true, erb_klass + + if io_output then + debug_msg "Outputting to %s" % [out_file.expand_path] + + out_file.dirname.mkpath + out_file.open 'w', 0644 do |io| + io.set_encoding @options.encoding if Object.const_defined? :Encoding + + @context = yield io + + template_result template, @context, template_file + end + else + @context = yield nil + + output = template_result template, @context, template_file + + debug_msg " would have written %d characters to %s" % [ + output.length, out_file.expand_path + ] if @dry_run + + output + end + end + + ## + # Creates the result for +template+ with +context+. If an error is raised a + # Pathname +template_file+ will indicate the file where the error occurred. + + def template_result template, context, template_file + template.filename = template_file.to_s + template.result context + rescue NoMethodError => e + raise RDoc::Error, "Error while evaluating %s: %s" % [ + template_file.expand_path, + e.message, + ], e.backtrace + end + + ## + # Retrieves a cache template for +file+, if present, or fills the cache. + + def template_for file, page = true, klass = ERB + template = @template_cache[file] + + return template if template + + if page then + template = assemble_template file + erbout = 'io' + else + template = file.read + template = template.encode @options.encoding if + Object.const_defined? :Encoding + + file_var = File.basename(file).sub(/\..*/, '') + + erbout = "_erbout_#{file_var}" + end + + template = klass.new template, nil, '<>', erbout + @template_cache[file] = template + template + end + +end + diff --git a/lib/ruby/shared/rdoc/generator/json_index.rb b/lib/ruby/shared/rdoc/generator/json_index.rb new file mode 100644 index 00000000000..c303b2effb4 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/json_index.rb @@ -0,0 +1,248 @@ +require 'json' + +## +# The JsonIndex generator is designed to complement an HTML generator and +# produces a JSON search index. This generator is derived from sdoc by +# Vladimir Kolesnikov and contains verbatim code written by him. +# +# This generator is designed to be used with a regular HTML generator: +# +# class RDoc::Generator::Darkfish +# def initialize options +# # ... +# @base_dir = Pathname.pwd.expand_path +# +# @json_index = RDoc::Generator::JsonIndex.new self, options +# end +# +# def generate +# # ... +# @json_index.generate +# end +# end +# +# == Index Format +# +# The index is output as a JSON file assigned to the global variable +# +search_data+. The structure is: +# +# var search_data = { +# "index": { +# "searchIndex": +# ["a", "b", ...], +# "longSearchIndex": +# ["a", "a::b", ...], +# "info": [ +# ["A", "A", "A.html", "", ""], +# ["B", "A::B", "A::B.html", "", ""], +# ... +# ] +# } +# } +# +# The same item is described across the +searchIndex+, +longSearchIndex+ and +# +info+ fields. The +searchIndex+ field contains the item's short name, the +# +longSearchIndex+ field contains the full_name (when appropriate) and the +# +info+ field contains the item's name, full_name, path, parameters and a +# snippet of the item's comment. +# +# == LICENSE +# +# Copyright (c) 2009 Vladimir Kolesnikov +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +class RDoc::Generator::JsonIndex + + include RDoc::Text + + ## + # Where the search index lives in the generated output + + SEARCH_INDEX_FILE = File.join 'js', 'search_index.js' + + attr_reader :index # :nodoc: + + ## + # Creates a new generator. +parent_generator+ is used to determine the + # class_dir and file_dir of links in the output index. + # + # +options+ are the same options passed to the parent generator. + + def initialize parent_generator, options + @parent_generator = parent_generator + @store = parent_generator.store + @options = options + + @template_dir = File.expand_path '../template/json_index', __FILE__ + @base_dir = @parent_generator.base_dir + + @classes = nil + @files = nil + @index = nil + end + + ## + # Builds the JSON index as a Hash. + + def build_index + reset @store.all_files.sort, @store.all_classes_and_modules.sort + + index_classes + index_methods + index_pages + + { :index => @index } + end + + ## + # Output progress information if debugging is enabled + + def debug_msg *msg + return unless $DEBUG_RDOC + $stderr.puts(*msg) + end + + ## + # Writes the JSON index to disk + + def generate + debug_msg "Generating JSON index" + + debug_msg " writing search index to %s" % SEARCH_INDEX_FILE + data = build_index + + return if @options.dry_run + + out_dir = @base_dir + @options.op_dir + index_file = out_dir + SEARCH_INDEX_FILE + + FileUtils.mkdir_p index_file.dirname, :verbose => $DEBUG_RDOC + + index_file.open 'w', 0644 do |io| + io.set_encoding Encoding::UTF_8 if Object.const_defined? :Encoding + io.write 'var search_data = ' + + JSON.dump data, io, 0 + end + + Dir.chdir @template_dir do + Dir['**/*.js'].each do |source| + dest = File.join out_dir, source + + FileUtils.install source, dest, :mode => 0644, :verbose => $DEBUG_RDOC + end + end + end + + ## + # Adds classes and modules to the index + + def index_classes + debug_msg " generating class search index" + + documented = @classes.uniq.select do |klass| + klass.document_self_or_methods + end + + documented.each do |klass| + debug_msg " #{klass.full_name}" + record = klass.search_record + @index[:searchIndex] << search_string(record.shift) + @index[:longSearchIndex] << search_string(record.shift) + @index[:info] << record + end + end + + ## + # Adds methods to the index + + def index_methods + debug_msg " generating method search index" + + list = @classes.uniq.map do |klass| + klass.method_list + end.flatten.sort_by do |method| + [method.name, method.parent.full_name] + end + + list.each do |method| + debug_msg " #{method.full_name}" + record = method.search_record + @index[:searchIndex] << "#{search_string record.shift}()" + @index[:longSearchIndex] << "#{search_string record.shift}()" + @index[:info] << record + end + end + + ## + # Adds pages to the index + + def index_pages + debug_msg " generating pages search index" + + pages = @files.select do |file| + file.text? + end + + pages.each do |page| + debug_msg " #{page.page_name}" + record = page.search_record + @index[:searchIndex] << search_string(record.shift) + @index[:longSearchIndex] << '' + record.shift + @index[:info] << record + end + end + + ## + # The directory classes are written to + + def class_dir + @parent_generator.class_dir + end + + ## + # The directory files are written to + + def file_dir + @parent_generator.file_dir + end + + def reset files, classes # :nodoc: + @files = files + @classes = classes + + @index = { + :searchIndex => [], + :longSearchIndex => [], + :info => [] + } + end + + ## + # Removes whitespace and downcases +string+ + + def search_string string + string.downcase.gsub(/\s/, '') + end + +end + diff --git a/lib/ruby/shared/rdoc/generator/markup.rb b/lib/ruby/shared/rdoc/generator/markup.rb new file mode 100644 index 00000000000..788e5a485df --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/markup.rb @@ -0,0 +1,169 @@ +## +# Handle common RDoc::Markup tasks for various CodeObjects +# +# This module is loaded by generators. It allows RDoc's CodeObject tree to +# avoid loading generator code to improve startup time for +ri+. + +module RDoc::Generator::Markup + + ## + # Generates a relative URL from this object's path to +target_path+ + + def aref_to(target_path) + RDoc::Markup::ToHtml.gen_relative_url path, target_path + end + + ## + # Generates a relative URL from +from_path+ to this object's path + + def as_href(from_path) + RDoc::Markup::ToHtml.gen_relative_url from_path, path + end + + ## + # Handy wrapper for marking up this object's comment + + def description + markup @comment + end + + ## + # Creates an RDoc::Markup::ToHtmlCrossref formatter + + def formatter + return @formatter if defined? @formatter + + options = @store.rdoc.options + this = RDoc::Context === self ? self : @parent + + @formatter = RDoc::Markup::ToHtmlCrossref.new options, this.path, this + @formatter.code_object = self + @formatter + end + + ## + # Build a webcvs URL starting for the given +url+ with +full_path+ appended + # as the destination path. If +url+ contains '%s' +full_path+ will be + # will replace the %s using sprintf on the +url+. + + def cvs_url(url, full_path) + if /%s/ =~ url then + sprintf url, full_path + else + url + full_path + end + end + +end + +class RDoc::CodeObject + + include RDoc::Generator::Markup + +end + +class RDoc::MethodAttr + + @add_line_numbers = false + + class << self + ## + # Allows controlling whether #markup_code adds line numbers to + # the source code. + + attr_accessor :add_line_numbers + end + + ## + # Prepend +src+ with line numbers. Relies on the first line of a source + # code listing having: + # + # # File xxxxx, line dddd + # + # If it has this comment then line numbers are added to +src+ and the , + # line dddd portion of the comment is removed. + + def add_line_numbers(src) + return unless src.sub!(/\A(.*)(, line (\d+))/, '\1') + first = $3.to_i - 1 + last = first + src.count("\n") + size = last.to_s.length + + line = first + src.gsub!(/^/) do + res = if line == first then + " " * (size + 1) + else + "%2$*1$d " % [size, line] + end + + line += 1 + res + end + end + + ## + # Turns the method's token stream into HTML. + # + # Prepends line numbers if +add_line_numbers+ is true. + + def markup_code + return '' unless @token_stream + + src = RDoc::TokenStream.to_html @token_stream + + # dedent the source + indent = src.length + lines = src.lines.to_a + lines.shift if src =~ /\A.*#\ *File/i # remove '# File' comment + lines.each do |line| + if line =~ /^ *(?=\S)/ + n = $&.length + indent = n if n < indent + break if n == 0 + end + end + src.gsub!(/^#{' ' * indent}/, '') if indent > 0 + + add_line_numbers(src) if RDoc::MethodAttr.add_line_numbers + + src + end + +end + +class RDoc::ClassModule + + ## + # Handy wrapper for marking up this class or module's comment + + def description + markup @comment_location + end + +end + +class RDoc::Context::Section + + include RDoc::Generator::Markup + +end + +class RDoc::TopLevel + + ## + # Returns a URL for this source file on some web repository. Use the -W + # command line option to set. + + def cvs_url + url = @store.rdoc.options.webcvs + + if /%s/ =~ url then + url % @relative_name + else + url + @relative_name + end + end + +end + diff --git a/lib/ruby/shared/rdoc/generator/ri.rb b/lib/ruby/shared/rdoc/generator/ri.rb new file mode 100644 index 00000000000..b9c4141a5e2 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/ri.rb @@ -0,0 +1,30 @@ +## +# Generates ri data files + +class RDoc::Generator::RI + + RDoc::RDoc.add_generator self + + ## + # Description of this generator + + DESCRIPTION = 'creates ri data files' + + ## + # Set up a new ri generator + + def initialize store, options #:not-new: + @options = options + @store = store + @store.path = '.' + end + + ## + # Writes the parsed data store to disk for use by ri. + + def generate + @store.save + end + +end + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/.document b/lib/ruby/shared/rdoc/generator/template/darkfish/.document new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_footer.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_footer.rhtml new file mode 100644 index 00000000000..3d9526f02a0 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_footer.rhtml @@ -0,0 +1,5 @@ + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_head.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_head.rhtml new file mode 100644 index 00000000000..062160a7517 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_head.rhtml @@ -0,0 +1,22 @@ + + +<%= h @title %> + + + +<% if @options.template_stylesheets.flatten.any? then %> +<% @options.template_stylesheets.flatten.each do |stylesheet| %> + +<% end %> +<% end %> + + + + + + + + + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_VCS_info.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_VCS_info.rhtml new file mode 100644 index 00000000000..e889f8063dc --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_VCS_info.rhtml @@ -0,0 +1,19 @@ +<% if !svninfo.empty? then %> + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_classes.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_classes.rhtml new file mode 100644 index 00000000000..fe54d8339f9 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_classes.rhtml @@ -0,0 +1,9 @@ + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_extends.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_extends.rhtml new file mode 100644 index 00000000000..2bd8efee996 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_extends.rhtml @@ -0,0 +1,15 @@ +<% unless klass.extends.empty? then %> + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_in_files.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_in_files.rhtml new file mode 100644 index 00000000000..0ba1d2be804 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_in_files.rhtml @@ -0,0 +1,9 @@ + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_includes.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_includes.rhtml new file mode 100644 index 00000000000..d141098ecd1 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_includes.rhtml @@ -0,0 +1,15 @@ +<% unless klass.includes.empty? then %> + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_installed.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_installed.rhtml new file mode 100644 index 00000000000..1285bfd7322 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_installed.rhtml @@ -0,0 +1,15 @@ + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_methods.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_methods.rhtml new file mode 100644 index 00000000000..45df08d8fe7 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_methods.rhtml @@ -0,0 +1,12 @@ +<% unless klass.method_list.empty? then %> + + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_navigation.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_navigation.rhtml new file mode 100644 index 00000000000..d7f330840a4 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_navigation.rhtml @@ -0,0 +1,11 @@ + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_pages.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_pages.rhtml new file mode 100644 index 00000000000..5f39825f08c --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_pages.rhtml @@ -0,0 +1,12 @@ +<% simple_files = @files.select { |f| f.text? } %> +<% unless simple_files.empty? then %> + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_parent.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_parent.rhtml new file mode 100644 index 00000000000..cc048526523 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_parent.rhtml @@ -0,0 +1,11 @@ +<% if klass.type == 'class' then %> + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_search.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_search.rhtml new file mode 100644 index 00000000000..9c49b31376a --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_search.rhtml @@ -0,0 +1,14 @@ + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_sections.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_sections.rhtml new file mode 100644 index 00000000000..15ff78ba91e --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_sections.rhtml @@ -0,0 +1,11 @@ +<% unless klass.sections.length == 1 then %> + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml new file mode 100644 index 00000000000..b58e6b3c616 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml @@ -0,0 +1,18 @@ +<% comment = if current.respond_to? :comment_location then + current.comment_location + else + current.comment + end + table = current.parse(comment).table_of_contents + + if table.length > 1 then %> + +<% end %> diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/class.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/class.rhtml new file mode 100644 index 00000000000..b4970001122 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/class.rhtml @@ -0,0 +1,174 @@ + + + +
+

+ <%= klass.type %> <%= klass.full_name %> +

+ +
+ <%= klass.description %> +
+ + <% klass.each_section do |section, constants, attributes| %> + <% constants = constants.select { |const| const.display? } %> + <% attributes = attributes.select { |attr| attr.display? } %> +
+ <% if section.title then %> +
+

+ <%= section.title %> +

+ + ↑ top + +
+ <% end %> + + <% if section.comment then %> +
+ <%= section.description %> +
+ <% end %> + + <% unless constants.empty? then %> +
+
+

Constants

+
+
+ <% constants.each do |const| %> +
<%= const.name %> + <% if const.comment then %> +
<%= const.description.strip %> + <% else %> +
(Not documented) + <% end %> + <% end %> +
+
+ <% end %> + + <% unless attributes.empty? then %> +
+
+

Attributes

+
+ + <% attributes.each do |attrib| %> +
+
+ <%= h attrib.name %>[<%= attrib.rw %>] +
+ +
+ <% if attrib.comment then %> + <%= attrib.description.strip %> + <% else %> +

(Not documented) + <% end %> +

+
+ <% end %> +
+ <% end %> + + <% klass.methods_by_type(section).each do |type, visibilities| + next if visibilities.empty? + visibilities.each do |visibility, methods| + next if methods.empty? %> +
+
+

<%= visibility.to_s.capitalize %> <%= type.capitalize %> Methods

+
+ + <% methods.each do |method| %> +
"> + <% if method.call_seq then %> + <% method.call_seq.strip.split("\n").each_with_index do |call_seq, i| %> +
+ + <%= h(call_seq.strip. + gsub( /^\w+\./m, '')). + gsub(/(.*)[-=]>/, '\1→') %> + + <% if i == 0 and method.token_stream then %> + click to toggle source + <% end %> +
+ <% end %> + <% else %> +
+ <%= h method.name %><%= method.param_seq %> + <% if method.token_stream then %> + click to toggle source + <% end %> +
+ <% end %> + +
+ <% if method.comment then %> + <%= method.description.strip %> + <% else %> +

(Not documented) + <% end %> + <% if method.calls_super then %> +

+ Calls superclass method + <%= + method.superclass_method ? + method.formatter.link(method.superclass_method.full_name, method.superclass_method.full_name) : nil + %> +
+ <% end %> + + <% if method.token_stream then %> +
+
<%= method.markup_code %>
+
+ <% end %> +
+ + <% unless method.aliases.empty? then %> +
+ Also aliased as: <%= method.aliases.map do |aka| + if aka.parent then # HACK lib/rexml/encodings + %{#{h aka.name}} + else + h aka.name + end + end.join ", " %> +
+ <% end %> + + <% if method.is_alias_for then %> + + <% end %> +
+ + <% end %> +
+ <% end + end %> +
+<% end %> +
diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/fonts.css b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts.css new file mode 100644 index 00000000000..e9e721183b0 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts.css @@ -0,0 +1,167 @@ +/* + * Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), + * with Reserved Font Name "Source". All Rights Reserved. Source is a + * trademark of Adobe Systems Incorporated in the United States and/or other + * countries. + * + * This Font Software is licensed under the SIL Open Font License, Version + * 1.1. + * + * This license is copied below, and is also available with a FAQ at: + * http://scripts.sil.org/OFL + */ + +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 400; + src: local("Source Code Pro"), + local("SourceCodePro-Regular"), + url("fonts/SourceCodePro-Regular.ttf") format("truetype"); +} + +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 700; + src: local("Source Code Pro Bold"), + local("SourceCodePro-Bold"), + url("fonts/SourceCodePro-Bold.ttf") format("truetype"); +} + +/* + * Copyright (c) 2010, Łukasz Dziedzic (dziedzic@typoland.com), + * with Reserved Font Name Lato. + * + * This Font Software is licensed under the SIL Open Font License, Version + * 1.1. + * + * This license is copied below, and is also available with a FAQ at: + * http://scripts.sil.org/OFL + */ + +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 300; + src: local("Lato Light"), + local("Lato-Light"), + url("fonts/Lato-Light.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: italic; + font-weight: 300; + src: local("Lato Light Italic"), + local("Lato-LightItalic"), + url("fonts/Lato-LightItalic.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 700; + src: local("Lato Regular"), + local("Lato-Regular"), + url("fonts/Lato-Regular.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: italic; + font-weight: 700; + src: local("Lato Italic"), + local("Lato-Italic"), + url("fonts/Lato-RegularItalic.ttf") format("truetype"); +} + +/* + * ----------------------------------------------------------- + * SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + * ----------------------------------------------------------- + * + * PREAMBLE + * The goals of the Open Font License (OFL) are to stimulate worldwide + * development of collaborative font projects, to support the font creation + * efforts of academic and linguistic communities, and to provide a free and + * open framework in which fonts may be shared and improved in partnership + * with others. + * + * The OFL allows the licensed fonts to be used, studied, modified and + * redistributed freely as long as they are not sold by themselves. The + * fonts, including any derivative works, can be bundled, embedded, + * redistributed and/or sold with any software provided that any reserved + * names are not used by derivative works. The fonts and derivatives, + * however, cannot be released under any other type of license. The + * requirement for fonts to remain under this license does not apply + * to any document created using the fonts or their derivatives. + * + * DEFINITIONS + * "Font Software" refers to the set of files released by the Copyright + * Holder(s) under this license and clearly marked as such. This may + * include source files, build scripts and documentation. + * + * "Reserved Font Name" refers to any names specified as such after the + * copyright statement(s). + * + * "Original Version" refers to the collection of Font Software components as + * distributed by the Copyright Holder(s). + * + * "Modified Version" refers to any derivative made by adding to, deleting, + * or substituting -- in part or in whole -- any of the components of the + * Original Version, by changing formats or by porting the Font Software to a + * new environment. + * + * "Author" refers to any designer, engineer, programmer, technical + * writer or other person who contributed to the Font Software. + * + * PERMISSION & CONDITIONS + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of the Font Software, to use, study, copy, merge, embed, modify, + * redistribute, and sell modified and unmodified copies of the Font + * Software, subject to the following conditions: + * + * 1) Neither the Font Software nor any of its individual components, + * in Original or Modified Versions, may be sold by itself. + * + * 2) Original or Modified Versions of the Font Software may be bundled, + * redistributed and/or sold with any software, provided that each copy + * contains the above copyright notice and this license. These can be + * included either as stand-alone text files, human-readable headers or + * in the appropriate machine-readable metadata fields within text or + * binary files as long as those fields can be easily viewed by the user. + * + * 3) No Modified Version of the Font Software may use the Reserved Font + * Name(s) unless explicit written permission is granted by the corresponding + * Copyright Holder. This restriction only applies to the primary font name as + * presented to the users. + * + * 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + * Software shall not be used to promote, endorse or advertise any + * Modified Version, except to acknowledge the contribution(s) of the + * Copyright Holder(s) and the Author(s) or with their explicit written + * permission. + * + * 5) The Font Software, modified or unmodified, in part or in whole, + * must be distributed entirely under this license, and must not be + * distributed under any other license. The requirement for fonts to + * remain under this license does not apply to any document created + * using the Font Software. + * + * TERMINATION + * This license becomes null and void if any of the above conditions are + * not met. + * + * DISCLAIMER + * THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + * OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + * DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM + * OTHER DEALINGS IN THE FONT SOFTWARE. + */ + diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-Light.ttf b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-Light.ttf new file mode 100644 index 00000000000..b49dd43729d Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-Light.ttf differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-LightItalic.ttf b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-LightItalic.ttf new file mode 100644 index 00000000000..7959fef0756 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-LightItalic.ttf differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-Regular.ttf b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-Regular.ttf new file mode 100644 index 00000000000..839cd589dc5 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-Regular.ttf differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-RegularItalic.ttf b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-RegularItalic.ttf new file mode 100644 index 00000000000..bababa09e3f Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/Lato-RegularItalic.ttf differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/SourceCodePro-Bold.ttf b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/SourceCodePro-Bold.ttf new file mode 100644 index 00000000000..61e3090c1c6 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/SourceCodePro-Bold.ttf differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/SourceCodePro-Regular.ttf b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/SourceCodePro-Regular.ttf new file mode 100644 index 00000000000..85686d967df Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/fonts/SourceCodePro-Regular.ttf differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/add.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/add.png new file mode 100644 index 00000000000..6332fefea4b Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/add.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/arrow_up.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/arrow_up.png new file mode 100644 index 00000000000..1ebb1932437 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/arrow_up.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/brick.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/brick.png new file mode 100644 index 00000000000..7851cf34c94 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/brick.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/brick_link.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/brick_link.png new file mode 100644 index 00000000000..9ebf013a23a Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/brick_link.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/bug.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bug.png new file mode 100644 index 00000000000..2d5fb90ec6e Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bug.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_black.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_black.png new file mode 100644 index 00000000000..57619706d10 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_black.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_toggle_minus.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_toggle_minus.png new file mode 100644 index 00000000000..b47ce55f685 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_toggle_minus.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_toggle_plus.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_toggle_plus.png new file mode 100644 index 00000000000..9ab4a89664e Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/bullet_toggle_plus.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/date.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/date.png new file mode 100644 index 00000000000..783c83357fd Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/date.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/delete.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/delete.png new file mode 100644 index 00000000000..08f249365af Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/delete.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/find.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/find.png new file mode 100644 index 00000000000..15474796467 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/find.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/loadingAnimation.gif b/lib/ruby/shared/rdoc/generator/template/darkfish/images/loadingAnimation.gif new file mode 100644 index 00000000000..82290f48334 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/loadingAnimation.gif differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/macFFBgHack.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/macFFBgHack.png new file mode 100644 index 00000000000..c6473b324ee Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/macFFBgHack.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/package.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/package.png new file mode 100644 index 00000000000..da3c2a2d74b Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/package.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_green.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_green.png new file mode 100644 index 00000000000..de8e003f9fb Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_green.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_white_text.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_white_text.png new file mode 100644 index 00000000000..813f712f726 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_white_text.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_white_width.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_white_width.png new file mode 100644 index 00000000000..1eb880947dd Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/page_white_width.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/plugin.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/plugin.png new file mode 100644 index 00000000000..6187b15aec0 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/plugin.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/ruby.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/ruby.png new file mode 100644 index 00000000000..f763a168807 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/ruby.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/tag_blue.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/tag_blue.png new file mode 100644 index 00000000000..3f02b5f8f8b Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/tag_blue.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/tag_green.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/tag_green.png new file mode 100644 index 00000000000..83ec984bd73 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/tag_green.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/transparent.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/transparent.png new file mode 100644 index 00000000000..d665e179efd Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/transparent.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/wrench.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/wrench.png new file mode 100644 index 00000000000..5c8213fef5a Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/wrench.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/wrench_orange.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/wrench_orange.png new file mode 100644 index 00000000000..565a9330e0a Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/wrench_orange.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/images/zoom.png b/lib/ruby/shared/rdoc/generator/template/darkfish/images/zoom.png new file mode 100644 index 00000000000..908612e3945 Binary files /dev/null and b/lib/ruby/shared/rdoc/generator/template/darkfish/images/zoom.png differ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/index.rhtml b/lib/ruby/shared/rdoc/generator/template/darkfish/index.rhtml new file mode 100644 index 00000000000..7d1c74807b1 --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/index.rhtml @@ -0,0 +1,23 @@ + + + +
+<% if @options.main_page and + main_page = @files.find { |f| f.full_name == @options.main_page } then %> +<%= main_page.description %> +<% else %> +

This is the API documentation for <%= @title %>. +<% end %> +

+ diff --git a/lib/ruby/shared/rdoc/generator/template/darkfish/js/darkfish.js b/lib/ruby/shared/rdoc/generator/template/darkfish/js/darkfish.js new file mode 100644 index 00000000000..06fef3b215b --- /dev/null +++ b/lib/ruby/shared/rdoc/generator/template/darkfish/js/darkfish.js @@ -0,0 +1,140 @@ +/** + * + * Darkfish Page Functions + * $Id: darkfish.js 53 2009-01-07 02:52:03Z deveiant $ + * + * Author: Michael Granger + * + */ + +/* Provide console simulation for firebug-less environments */ +if (!("console" in window) || !("firebug" in console)) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", + "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +}; + + +/** + * Unwrap the first element that matches the given @expr@ from the targets and return them. + */ +$.fn.unwrap = function( expr ) { + return this.each( function() { + $(this).parents( expr ).eq( 0 ).after( this ).remove(); + }); +}; + + +function showSource( e ) { + var target = e.target; + var codeSections = $(target). + parents('.method-detail'). + find('.method-source-code'); + + $(target). + parents('.method-detail'). + find('.method-source-code'). + slideToggle(); +}; + +function hookSourceViews() { + $('.method-heading').click( showSource ); +}; + +function toggleDebuggingSection() { + $('.debugging-section').slideToggle(); +}; + +function hookDebuggingToggle() { + $('#debugging-toggle img').click( toggleDebuggingSection ); +}; + +function hookSearch() { + var input = $('#search-field').eq(0); + var result = $('#search-results').eq(0); + $(result).show(); + + var search_section = $('#search-section').get(0); + $(search_section).show(); + + var search = new Search(search_data, input, result); + + search.renderItem = function(result) { + var li = document.createElement('li'); + var html = ''; + + // TODO add relative path to