#!/usr/bin/ruby
require "pathname"
require "optparse"
require "parser/current"
# Always use the rubygems version of require
require "rubygems"
$options = { o: STDOUT, l: [] }
if method(:require) == method(:gem_original_require)
# Bundler
module Kernel
def require(path)
$LOAD_PATH.resolve_feature_path(path).last
rescue LoadError => e
raise e unless $options[:w]
warn e
path
end
end
else
# Rubygems
module Kernel
def gem_original_require(path)
$LOAD_PATH.resolve_feature_path(path).last
rescue LoadError => e
raise e unless $options[:w]
warn e
path
end
end
end
# opt-in to most recent AST format:
Parser::Builders::Default.emit_lambda = true
Parser::Builders::Default.emit_procarg0 = true
Parser::Builders::Default.emit_encoding = true
Parser::Builders::Default.emit_index = true
Parser::Builders::Default.emit_arg_inside_procarg0 = true
Parser::Builders::Default.emit_forward_arg = true
Parser::Builders::Default.emit_kwargs = true
Parser::Builders::Default.emit_match_pattern = true
op = OptionParser.new do |opts|
opts.banner = "Usage: rld [options] file.rb ..."
opts.accept(Pathname) do |path|
Pathname.new(path)
end
opts.on("-p PATH", Pathname, "Project root") do |v|
$options[:p] = v.realdirpath
end
opts.on("-L PATH", "-I PATH", Pathname, "Search directory") do |v|
$LOAD_PATH.unshift(v)
end
opts.on("-o FILE", Pathname, "Output file") do |v|
$options[:o] = v
end
opts.on("-l FILE", "-r FILE", Pathname, "Require this file in output") do |v|
$options[:l] << "require #{require(v).inspect}"
end
opts.on("-w", "Only warn on LoadError") do
$options[:w] = true
end
end
op.parse!
if ARGV.empty?
puts op
exit 1
end
class Linker < Parser::TreeRewriter
def on_send(node)
super
case node
in [:send, [:gvar, :$: | :$LOAD_PATH], (:<< | :push | :unshift) => m, arg]
loc = arg.location.expression
file = Pathname.new(loc.source_buffer.name)
path = eval(loc.source, binding, file.to_s, loc.line)
$LOAD_PATH.public_send(m, Pathname.new(path).relative? ? "#{file.dirname}/#{path}" : path)
remove(node.location.expression)
in [:send, nil, :require, [:str, arg] => argnode]
path = require(arg)
realpath = Pathname.new(path).realdirpath
if path.start_with?($options[:p].to_s)
path = Pathname.new(path).relative_path_from($options[:p]).to_s
replace(node.location.expression, "require_relative " + path.inspect)
elsif realpath.to_s.start_with?($options[:p].to_s)
path = realpath.relative_path_from($options[:p]).to_s
replace(node.location.expression, "require_relative " + path.inspect)
else
replace(argnode.location.expression, path.inspect)
end
in [:send, nil, :load, [:str, arg] => argnode]
return if arg.match?(/\A\.\.?\//)
path = require(arg)
replace(argnode.location.expression, path.inspect)
in [:send, nil, :require | :load, *args]
loc = node.location.expression
warn "#{loc}: WARNING: ignoring non-string-literal #{loc.source}"
else
end
end
end
$options[:p] ||= Pathname.new(ARGV[0]).dirname.realdirpath
$LOAD_PATH.unshift($options[:p])
$options[:l].each(&$options[:o].method(:puts))
ARGV.each do |file|
buffer = Parser::Source::Buffer.new(file, source: Pathname.new(file).read)
ast = Parser::CurrentRuby.new.parse(buffer)
$options[:o].write Linker.new.rewrite(buffer, ast)
end