2007年 10月 13日

Ruby に use を実装する。

RubyGems みたいに、あらかじめ spec をキャッシュしておく、みたいなのが許されるなら、こういうのもありなんじゃないかなぁと思った。以下の実装はいろいろだめなところがあるだろうけど…… (プラットホーム依存のところがほげほげとか)

# module 名 -> ファイル名へのハッシュテーブルを作る
# ruby 起動しまくって時間がかかるので前もってキャッシュする
require "pathname"
module_files = {}
$LOAD_PATH.each do |path|
	path = Pathname.new(path)
	next if path.relative?
	Pathname.glob(path + "**/*").each do |t|
		next unless t.file?
		next if t.to_s =~ /tk/
		puts t
		ENV.delete("RUBYOPT")
		ret = nil
		IO.popen("ruby", "r+") do |io|
			io.puts <<-CODE
				# 自分自身はロード済みにしないと、ループして require している場合にまずい
				$LOADED_FEATURES << '#{t.relative_path_from(path)}'
				$depend_lib = []
				def __modules
					ret = []
					ObjectSpace.each_object(Module) do |o|
						ret << o
					end
					ret
				end

				alias _require_orig require
				def require(lib)
					prev = __modules
					ret = _require_orig(lib)
					if ret
						$depend_lib.concat(__modules - prev)
					end
					ret
				end

				def set_trace_func(*)
				end

				prev = __modules
				stdout = STDOUT.dup
				STDOUT.reopen($stderr)
				begin
					_require_orig '#{t}'
				ensure
					STDOUT.reopen(stdout)
					print Marshal.dump((__modules - prev - $depend_lib).map {|i| i.to_s })
					STDOUT.flush
					exit!
				end
			CODE
			io.close_write
			ret = io.read
			ret = Marshal.load(ret)
		end
		ret.each do |m|
			(module_files[m] ||= []) << t.to_s
		end
	end
end

require "pp"
File.open("module.cache", "wb") do |f|
	Marshal.dump(module_files, f)
end

本体

def use(mod)
	mod  = mod.to_s
	mods = Marshal.load(File.read("module.cache"))
	raise LoadError, "#{mod} is not found." unless mods[mod]
	mods = mods[mod].map {|i|
		[i, i.split(File::ALT_SEPARATOR || File::SEPARATOR).size]
	}.sort_by {|i,ii|
		ii
	}
	mods.select {|i,ii| ii == mods.first[1] }.each do |i,ii|
		require i
	end
end

use %(Test::Unit::AutoRunner)
use %(Test::Unit::TestCase)

class UseTEST < Test::Unit::TestCase
	def test_success
		use %(Pathname)
		assert_kind_of Class, Pathname

		use %(Test::Unit::AutoRunner)
		assert_kind_of Class, Test::Unit::AutoRunner

		use %(ERB)
		assert_kind_of Class, ERB

		use %(ERB::Util)
		assert_kind_of Module, ERB::Util

		use %(WEBrick)
		assert_kind_of Module, WEBrick

		use %(Net::HTTP)
		assert_kind_of Module, Net
		assert_kind_of Class, Net::HTTP

		assert_raise(LoadError) do
			use %(NotInTableModule)
		end
	end
end

Test::Unit::AutoRunner.run($0 != "-e" && $0)

テーブルは require しているファイルに定義されているクラスを除いているので、test/unit.rb みたいな require をしている場合、そのファイルにいくら定義があっても「クラスの再定義/上書き」という判断しかできないので、use "Test::Unit" とかしても test/unit.rb は読みこまれない。

あとは webrick なんかを例にとると webrick.rb は他のファイルを require するだけなので、直接はこのファイルはモジュールハッシュ上にあらわれない。use では同じ深さを持っていて、WEBrick を定義している全てのファイルを require してる。

もっとたくさんテスト書いて、実装精密にすればつかえるかなぁ……

どっかのサーバで rubyforge の gems をミラーして解析して、このテーブルをつくっておいて、パッケージ検索できるようにするとか、あるいはそのままパッケージシステムにしてしまうとかがおもしろいかなぁ。既存の gems をそのままもってこれてたのしそう。