require 'rgl/adjacency'
require 'rgl/dot'
=begin
rdep - The Ruby Dependency Tool
Version 1.4
Hal E. Fulton
2 November 2002
Ruby's license
Purpose
Determine the library files on which a specified Ruby file is dependent
(and their location and availability).
Usage notes
Usage: ruby rdep.rb sourcefile
The sourcefile may or may not have a .rb extension.
The directories in the $: array (which includes the RUBYLIB environment
variable) are searched first. File extensions are currently searched for
in this order: no extension, .rb, .o, .so, .dll (this may not be correct).
If there are no detected dependencies, the program will give the
message, "No dependencies found."
If the program finds [auto]load and require statements that it can
understand, it searches for the specified files. Any recognized Ruby
source files (*.rb) are processed recursively in the same way. No attempt
is made to open the files that appear to be binary.
The program will print up to four lists (any or all may be omitted):
1. A list of files it found by going through RUBYLIB.;
2. A list of files found under the searchroot (or under '.');
3. A list of directories under searchroot which should perhaps be
added to RUBYLIB; and
4. A list of files (without extensions) which could not be found.
If there were unparseable [auto]load or require statements, a warning
will be issued.
Between lists 3 and 4, the program will give an opinion about the overall
situation. The worst case is that files were not found; the uncertain
case is when there were unparseable statements; and the best case is
when all files could be found (lists 1 and 2).
Exit codes
0 - Usage or successful execution
1 - Nonexistent sourcefile specified
2 - Improper sourcefile (pipe, special file, ...)
3 - Some kind of problem reading a file
Limitations
Requires Ruby 1.6.0 or higher
No recursion on binaries
Can't look at dynamically built names
Can't detect "tested" requires (e.g.: flag = require "foo.rb")
[auto]load/require can be preceded only by whitespace on the line
Only recognizes simple strings ("file" or 'file')
Does not recognized named constants (e.g.: require MyFile)
Assumes every directory entry is either a file or subdirectory
Does not handle the Windows variable RUBYLIB_PREFIX
May be SLOW if a directory structure is deep (especially
on Windows with 1.6.x)
Known bugs:
Logic may be incorrect in terms of search order, file extensions, etc.
Injected a bug in 1.3: In rare cases will recurse until stack overflow
Revision history
Version 1.0 - 13 October 2000 - Initial release
Version 1.1 - 10 July 2001 - Bug fixes
Version 1.2 - 15 August 2002 - Works correctly on Win98
Version 1.3 - 21 October 2002 - Removed globals; removed search root;
added $: instead of RUBYLIB; etc.
Version 1.4 - 2 November 2002 - Fixed autoload recursion bug
To-do list
Possibly change extension search order?
Possibly add extensions to list?
Are explicit extensions allowed other than .rb?
Is a null extension really legal?
Additional tests/safeguards? (file permissions, non-empty files,...)
Change inconsistent expansion of tilde, dot, etc.?
Make it smarter somehow??
=end
class File
def doc_skip
loop do
str = gets
break if not str
if str =~ /^=begin([ \t]|$)/
loop do
str = gets
break if not str
break if str =~ /^=end([ \t]|$)/
end
else
yield str
end
end
end
end
class Dependency
attr_reader :graph
def unquote(str)
return nil if str == nil
if [?', ?"].include? str[0] str = str[1..-2]
else
""
end
end
def scan(line)
line.strip!
if line =~ /^load/ or line =~ /^auto/ or line =~ /^require/
@has_dep = true junk = %w[ require load autoload ( ) , ] + [""]
temp = line.split(/[ \t\(\),]/) - junk
if temp[2] and temp[2][0].chr =~ /[#;]/ temp = temp[0..1]
end
if temp[-1] =~ /\#\{/ str = ""
else
str = unquote(temp[-1]) end
str
else
nil
end
end
def find_files(source)
loadable = false
files = [] found = []
begin
File.open(source).doc_skip { |line| files << scan(line) }
rescue => err
puts "Problem processing file #{source}: #{err}"
caller.each { |x| puts " #{x}" }
exit 3
end
if !@has_dep
puts "No dependencies found."
exit 0
end
files.compact!
catch(:skip) do
for file in files
if file == "" @warnfiles << source
next
end
throw :skip if (@inpath.include? file) || (@cantfind.include? file)
if file =~ /\.rb$/ suffixes = [""] else
suffixes = @suffixes end
for dir in @search_path
for suf in suffixes
filename = dir + file + suf
loadable = test ?e, filename
break if loadable
end
if loadable
@inpath << filename found << filename if filename =~ /\.rb$/
break
end
end
@cantfind << file if !loadable
end
end
found.uniq!
found.compact!
@graph.add_vertex(source)
list = found
found.each { |x|
@graph.add_edge(source, x)
list += find_files(x)
}
list
end
def print_list(, list)
return if list.empty?
puts + "\n\n" list.each { |x| puts " #{x}" }
puts "\n" end
SEP = File::Separator
DIRSEP = SEP == "/" ? ":" : ";"
def execute
@has_dep = false
@warnfiles = []
@newdirs = []
@inpath = []
@cantfind = []
@suffixes = [""] + %w[ .rb .o .so .dll ]
@rdirs = []
@global_found = []
@graph = RGL::DirectedAdjacencyGraph.new
if not ARGV[0]
puts "Usage: ruby rdep.rb sourcefile [searchroot]"
exit 0
end
if !test ?e, ARGV[0]
puts "#{ARGV[0]} does not exist."
exit 1
end
if !test ?f, ARGV[0]
puts "#{ARGV[0]} is not a regular file."
exit 2
end
@proghome = File.dirname(File.expand_path(ARGV[0]))
if @proghome != File.expand_path(".")
$: << @proghome
end
@search_path = $:
@search_path.collect! { |x| x[-1] == SEP ? x : x + SEP }
find_files(ARGV[0])
@warnfiles.uniq!
@cantfind.uniq!
@newdirs.uniq!
@inpath.map! { |x| File.expand_path(x) }
@inpath.uniq!
if @inpath[0]
print_list("Found in search path:", @inpath)
if !@cantfind.empty? && @warnfiles.empty?
puts "This will probably be sufficient.\n"
end
end
homedirs = @inpath.find_all { |x| x =~ Regexp.new("^"+@proghome) }
if homedirs[0] homedirs.map! { |x| File.dirname(x) }.uniq!
puts "Consider adding these directories to RUBYPATH:\n\n"
homedirs.each { |x| puts " #{x}" }
puts
if @warnfiles[0] and homedirs == [] puts "This will probably NOT be sufficient. See below.\n\n"
end
end
if @cantfind[0] puts "This will probably NOT be sufficient. See below.\n\n"
elsif @warnfiles[0] and homedirs == [] puts "Files may still be missing. See below.\n\n"
else puts "This will probably be sufficient."
end
print_list("Not located anywhere:", @cantfind)
print_list("Warning: Unparseable usages of 'load' or 'require' in:",
@warnfiles)
end
end
d = Dependency.new
d.execute
begin
d.graph.write_to_graphic_file('png',
File.basename(ARGV[0]),
'label' => "Dependencies of #{ARGV[0]}")
rescue ArgumentError
d.graph.write_to_graphic_file('png',
File.basename(ARGV[0]))
end
exit 0