#!/usr/bin/ruby -Eutf-8
# encoding: utf-8
#
# Find dependencies between ruby packages
#
# Must run inside a openwrt with all *ruby* packages installed
#

RUBY_SIMPLE_VERSION = RUBY_VERSION.split(".")[0..1].join(".")
failed = false

puts "Looking for installed ruby packages..."
packages=`opkg list-installed '*ruby*' | cut -d' ' -f 1`.split("\n")

puts "Looking for packages files..."
package_files=Hash.new { |h,k| h[k]=[] }
packages.each do
	|pkg|
	files=`opkg files "#{pkg}" | sed -e 1d`.split("\n")
	package_files[pkg]=files if files
end

require_regex=/^require ["']([^"']+)["'].*/
require_regex_ignore=/^require ([a-zA-Z\$]|["']$|.*\/$)/
require_ignore=%w{drb/invokemethod16 foo rubygems/defaults/operating_system win32console java Win32API
                  builder/xchar json/pure simplecov win32/sspi rdoc/markdown/literals_1_8 enumerator win32/resolv rbtree
                  nqxml/streamingparser nqxml/treeparser xmlscan/parser xmlscan/scanner xmltreebuilder xml/parser xmlparser xml/encoding-ja xmlencoding-ja
                  iconv uconv win32ole gettext/po_parser gettext/mo libxml psych.jar jar-dependencies}

builtin_enc=[
	Encoding.find("ASCII-8BIT"),
	Encoding.find("UTF-8"),
	Encoding.find("UTF-7"),
	Encoding.find("US-ASCII"),
]

puts "Looking for requires in files..."
files_requires=Hash.new { |h,k| h[k]=[] }
packages.each do
        |pkg|
	package_files[pkg].each do
		|file|
		next if not File.file?(file)

		if not file =~ /.rb$/
			if File.executable?(file)
				magic=`head -c50 '#{file}' | head -1`
				begin
					if not magic =~ /ruby/
						next
					end
				rescue
					next
				end
			else
				next
			end
		end
		#puts "Checking #{file}..."
		File.open(file, "r") do
			|f|
			lineno=0
			while line=f.gets() do
				lineno+=1; encs=[]; requires=[]; need_encdb=false

				line=line.chomp.gsub!(/^[[:blank:]]*/,"")

				case line
				when /^#.*coding *:/
					if lineno <= 2
						enc=line.sub(/.*coding *: */,"").sub(/ .*/,"")
						encs << Encoding.find(enc)
					end
				end
				line.gsub!(/#.*/,"")
				case line
				when "__END__"
					break
				when /^require /
					#puts "#{file}:#{line}"
					if require_regex_ignore =~ line
						#puts "Ignoring #{line} at #{file}:#{lineno} (REGEX)..."
						next
					end
					if not require_regex =~ line
						$stderr.puts "Unknown require: '#{line}' at file #{file}:#{lineno}"
						failed=true
					end
					require=line.gsub(require_regex,"\\1")
					require.gsub!(/\.(so|rb)$/,"")

					if require_ignore.include?(require)
						#puts "Ignoring #{line} at #{file}:#{lineno} (STR)..."
						next
					end

					files_requires[file] += [require]

				when /Encoding::/
					encs=line.scan(/Encoding::[[:alnum:]_]+/).collect {|enc| eval(enc) }.select {|enc| enc.kind_of? Encoding }
					need_encdb=true
				end

				next if encs.empty?
				required_encs = (encs - builtin_enc).collect {|enc| "enc/#{enc.name.downcase.gsub("-","_")}" }
				required_encs << "enc/encdb" if need_encdb

				files_requires[file] += required_encs
			end
		end
	end
end
exit(1) if failed

# Add deps from .so
package_files.each do |(pkg,files)| files.each do |file|
	case file
	when /\/nkf\.so$/
		files_requires[file]= files_requires[file] + ["enc/encdb"]
	end
end; end

puts "Grouping package requirements per package"
package_requires_files = Hash.new{|h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
package_files.each do |(pkg,files)|
	package_requires_files[pkg]
	files.each do |file|
		files_requires[file].each do |requires|
			package_requires_files[pkg][requires] << file
		end
	end
end

weak_dependency=Hash.new { |h,k| h[k]=[] }
weak_dependency.merge!({
"ruby-misc"=>["ruby-openssl","ruby-fiddle"],			#securerandom.rb
"ruby-debuglib"=>["ruby-readline"],				#debug.rb
"ruby-drb"=>["ruby-openssl"],				 	#drb/ssl.rb
"ruby-irb"=>["ruby-rdoc", "ruby-readline"],		 	#irb/cmd/help.rb
"ruby-gems"=>["ruby-openssl","ruby-io-console","ruby-webrick"], #rubygems/commands/cert_command.rb rubygems/user_interaction.rb rubygems/server.rb
"ruby-mkmf"=>["ruby-webrick"], 					#un.rb
"ruby-net"=>["ruby-openssl","ruby-io-console","ruby-zlib"], 	#net/*.rb
"ruby-optparse"=>["ruby-uri","ruby-datetime"],			#optparse/date.rb optparse/uri.rb
"ruby-rake"=>["ruby-net","ruby-gems"],				#rake/contrib/ftptools.rb /usr/bin/rake
"ruby-rdoc"=>["ruby-gems","ruby-readline","ruby-webrick",	#/usr/bin/rdoc and others
	       "ruby-io-console"],				#rdoc/stats/normal.rb
"ruby-webrick"=>["ruby-openssl"],				#webrick/ssl.rb
"ruby-testunit"=>["ruby-io-console"],			 	#gems/test-unit-3.1.5/lib/test/unit/ui/console/testrunner.rb
})

puts "Preloading gems..."
Gem::Specification.all.each{ |x| gem x.name }

puts "Looking for package dependencies..."
package_provides = {}
package_dependencies = Hash.new { |h,k| h[k]=[] }
package_requires_files.each do
	|(pkg,requires_files)|

	requires_files.each do
		|(require,files)|
		if package_provides.include?(require)
			found = package_provides[require]
		else
			found = package_files.detect {|(pkg,files)| files.detect {|file| $:.detect {|path| "#{path}/#{require}" == file.gsub(/\.(so|rb)$/,"") } } }
			if not found
				$stderr.puts "#{pkg}: Nothing provides #{require}"
				failed = true
				next
			end
			found = found.first
			package_provides[require] = found
		end
		if weak_dependency[pkg].include?(found)
			puts "#{pkg}: #{found} provides #{require} (weak depedendency ignored)"
		else
			puts "#{pkg}: #{found} provides #{require} for #{files.collect {|file| file.sub("/usr/lib/ruby/","") }.join(",")}"
			package_dependencies[pkg] += [found]
		end
	end
end
if failed
	puts "There is some missing requirements not mapped to files in packages."
	puts "Please, fix the missing files or ignore them on require_ignore var"
	exit(1)
end
# Remove self dependency
package_dependencies = Hash[package_dependencies.collect {|(pkg,deps)| [pkg,package_dependencies[pkg]=deps.uniq.sort - [pkg]]}]
package_dependencies.default = []

puts "Expanding dependencies..."
begin
	changed=false
	package_dependencies.each do
		|(pkg,deps)|
		next if deps.empty?
		deps_new = deps.collect {|dep| [dep] + package_dependencies[dep] }.inject([],:+).uniq.sort
		if not deps == deps_new
			puts "#{pkg}: #{deps.join(",")}"
			puts "#{pkg}: #{deps_new.join(",")}"
			package_dependencies[pkg]=deps_new
			changed=true
		end
	end
end if not changed

puts "Removing redundant dependencies..."
package_dependencies.each do
	|(pkg,deps)|
	package_dependencies[pkg]=deps.uniq - [pkg]
end

puts "Checking for mutual dependencies..."
package_dependencies.each do
	|(pkg,deps)|
	if deps.include? pkg
		$stderr.puts "#{pkg}: Cycle dependency detected! "
		failed = true
	end
end
exit(1) if failed


package_dependencies2=package_dependencies.dup
package_dependencies.each do
	|(pkg,deps)|

	# Ignore dependencies that are already required by another dependency
	deps_clean = deps.reject {|dep_suspect| deps.detect {|dep_provider|
			if package_dependencies[dep_provider].include?(dep_suspect)
				puts "#{pkg}: #{dep_suspect} is already required by #{dep_provider}"
				true
			end
		 } }

	if not deps==deps_clean
		puts "before: #{deps.join(",")}"
		puts "after: #{deps_clean.join(",")}"
		package_dependencies2[pkg]=deps_clean
	end
end
package_dependencies=package_dependencies2

puts "Checking current packages dependencies..."
ok=true
package_dependencies.each do
	|(pkg,deps)|
	current_deps=`opkg depends #{pkg} | sed -r -e '1d;s/^[[:blank:]]*//'`.split("\n")
	current_deps.reject!{|dep| dep =~ /^lib/ }
	current_deps -= ["ruby"]

	extra_dep = current_deps - deps
	$stderr.puts "Package #{pkg} does not need to depend on #{extra_dep.join(" ")} " if not extra_dep.empty?
	missing_dep = deps - current_deps
	$stderr.puts "Package #{pkg} needs to depend on #{missing_dep.join(" ")} " if not missing_dep.empty?

	if not extra_dep.empty? or not missing_dep.empty?
		$stderr.puts "define Package/#{pkg}"
		$stderr.puts "  DEPENDS:=ruby#{([""] +deps).join(" +")}"
		ok=false
	end
end

puts "All dependencies are OK." if ok

__END__
