uprb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CLAUDE.md +55 -0
- data/LICENSE.txt +21 -0
- data/README.md +59 -0
- data/Rakefile +8 -0
- data/examples/rls/.gitignore +8 -0
- data/examples/rls/Gemfile +9 -0
- data/examples/rls/README.md +9 -0
- data/examples/rls/Rakefile +4 -0
- data/examples/rls/bin/console +11 -0
- data/examples/rls/bin/setup +8 -0
- data/examples/rls/exe/rls +7 -0
- data/examples/rls/lib/rls/cli.rb +48 -0
- data/examples/rls/lib/rls/version.rb +5 -0
- data/examples/rls/lib/rls.rb +8 -0
- data/examples/rls/rls.gemspec +28 -0
- data/examples/rls/sig/rls.rbs +4 -0
- data/examples/s3-ls/.gitignore +8 -0
- data/examples/s3-ls/Gemfile +9 -0
- data/examples/s3-ls/Gemfile.lock +91 -0
- data/examples/s3-ls/README.md +19 -0
- data/examples/s3-ls/Rakefile +4 -0
- data/examples/s3-ls/bin/console +11 -0
- data/examples/s3-ls/bin/setup +8 -0
- data/examples/s3-ls/exe/s3-ls +7 -0
- data/examples/s3-ls/lib/s3/ls/cli.rb +78 -0
- data/examples/s3-ls/lib/s3/ls/version.rb +7 -0
- data/examples/s3-ls/lib/s3/ls.rb +10 -0
- data/examples/s3-ls/s3-ls.gemspec +36 -0
- data/examples/s3-ls/sig/s3/ls.rbs +6 -0
- data/exe/uprb +7 -0
- data/lib/uprb/cli.rb +176 -0
- data/lib/uprb/require_replacer.rb +171 -0
- data/lib/uprb/require_tracker.rb +91 -0
- data/lib/uprb/static_require_tracker.rb +197 -0
- data/lib/uprb/version.rb +5 -0
- data/lib/uprb.rb +15 -0
- data/sig/uprb.rbs +4 -0
- data/tmp/.keep +0 -0
- metadata +95 -0
data/lib/uprb/cli.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../uprb"
|
|
6
|
+
|
|
7
|
+
module Uprb
|
|
8
|
+
class CLI
|
|
9
|
+
USAGE = <<~USAGE.chomp
|
|
10
|
+
Usage:
|
|
11
|
+
uprb pack <src.rb> <dest>
|
|
12
|
+
uprb gem install <gem> [--path DIR]
|
|
13
|
+
uprb gem pack <gem> [--path DIR]
|
|
14
|
+
USAGE
|
|
15
|
+
|
|
16
|
+
def self.start(argv = ARGV)
|
|
17
|
+
new(argv).run
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(argv)
|
|
21
|
+
@argv = argv.dup
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run
|
|
25
|
+
command = @argv.shift
|
|
26
|
+
|
|
27
|
+
case command
|
|
28
|
+
when "pack"
|
|
29
|
+
pack_command
|
|
30
|
+
when "gem"
|
|
31
|
+
gem_command
|
|
32
|
+
when "--version", "-v"
|
|
33
|
+
$stdout.puts(Uprb::VERSION)
|
|
34
|
+
when "--help", "-h", nil
|
|
35
|
+
$stdout.puts(USAGE)
|
|
36
|
+
else
|
|
37
|
+
$stderr.puts(USAGE)
|
|
38
|
+
end
|
|
39
|
+
rescue Uprb::Error => e
|
|
40
|
+
$stderr.puts("uprb: #{e.message}")
|
|
41
|
+
exit 1
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
$stderr.puts("uprb: #{e.class}: #{e.message}")
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def pack_command
|
|
50
|
+
options, args = parse_pack_options(@argv)
|
|
51
|
+
src = args.shift or raise Uprb::Error, "missing <src.rb>"
|
|
52
|
+
dest = args.shift or raise Uprb::Error, "missing <dist>"
|
|
53
|
+
|
|
54
|
+
src_path = File.expand_path(src)
|
|
55
|
+
dest_path = File.expand_path(dest)
|
|
56
|
+
|
|
57
|
+
raise Uprb::Error, "source not found: #{src}" unless File.file?(src_path)
|
|
58
|
+
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
60
|
+
return unless confirm_overwrite(dest_path, options)
|
|
61
|
+
|
|
62
|
+
Uprb::RequireReplacer.pack(src_path, dest_path:, requires: options[:requires], dynamic: options[:dynamic], script_argv: options[:script_argv], skip_disable_gems: options[:skip_disable_gems], skip_ruby_path_replace: options[:skip_ruby_path_replace])
|
|
63
|
+
|
|
64
|
+
$stdout.puts("Packed #{dest_path}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def gem_command
|
|
68
|
+
subcommand = @argv.shift
|
|
69
|
+
|
|
70
|
+
case subcommand
|
|
71
|
+
when "install"
|
|
72
|
+
options, args = parse_pack_options(@argv)
|
|
73
|
+
gem_name = args.shift or raise Uprb::Error, "missing <gem>"
|
|
74
|
+
install_gem(gem_name)
|
|
75
|
+
pack_gem_executables(gem_name, options)
|
|
76
|
+
when "pack"
|
|
77
|
+
options, args = parse_pack_options(@argv)
|
|
78
|
+
gem_name = args.shift or raise Uprb::Error, "missing <gem>"
|
|
79
|
+
pack_gem_executables(gem_name, options)
|
|
80
|
+
else
|
|
81
|
+
$stdout.puts(USAGE)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_pack_options(argv)
|
|
86
|
+
if (separator_index = argv.index("--"))
|
|
87
|
+
script_argv = argv[(separator_index + 1)..]
|
|
88
|
+
argv = argv[0...separator_index]
|
|
89
|
+
else
|
|
90
|
+
script_argv = []
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
options = {
|
|
94
|
+
path: nil,
|
|
95
|
+
force: false,
|
|
96
|
+
requires: [],
|
|
97
|
+
dynamic: false,
|
|
98
|
+
script_argv: script_argv,
|
|
99
|
+
skip_disable_gems: false,
|
|
100
|
+
skip_ruby_path_replace: false,
|
|
101
|
+
}
|
|
102
|
+
parser = OptionParser.new
|
|
103
|
+
parser.on("--path DIR") do |dir|
|
|
104
|
+
options[:path] = dir
|
|
105
|
+
end
|
|
106
|
+
parser.on("-f", "--force", "overwrite existing destination without prompting") do
|
|
107
|
+
options[:force] = true
|
|
108
|
+
end
|
|
109
|
+
parser.on("-r", "--require LIB", "pre-require LIB at pack time and at runtime (repeatable)") do |lib|
|
|
110
|
+
options[:requires] << lib
|
|
111
|
+
end
|
|
112
|
+
parser.on("--with-rubygems", "shortcut for --require rubygems") do
|
|
113
|
+
options[:requires] << "rubygems"
|
|
114
|
+
end
|
|
115
|
+
parser.on("--dynamic", "execute the entry script at pack time to observe runtime-only requires") do
|
|
116
|
+
options[:dynamic] = true
|
|
117
|
+
end
|
|
118
|
+
parser.on("--skip-disable-gems", "drop --disable-gems from the shebang (vendoring only; trades fast startup for normal Ruby startup)") do
|
|
119
|
+
options[:skip_disable_gems] = true
|
|
120
|
+
end
|
|
121
|
+
parser.on("--skip-ruby-path-replace", "keep the source file's shebang ruby invocation instead of rewriting it to an absolute RbConfig.ruby path (vendoring only; source must have a shebang)") do
|
|
122
|
+
options[:skip_ruby_path_replace] = true
|
|
123
|
+
end
|
|
124
|
+
args = parser.parse(argv)
|
|
125
|
+
|
|
126
|
+
[options, args]
|
|
127
|
+
rescue OptionParser::ParseError => e
|
|
128
|
+
raise Uprb::Error, e.message
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def confirm_overwrite(dest_path, options)
|
|
132
|
+
return true if options[:force]
|
|
133
|
+
return true unless File.exist?(dest_path)
|
|
134
|
+
|
|
135
|
+
unless $stdin.tty?
|
|
136
|
+
raise Uprb::Error, "destination already exists: #{dest_path} (pass --force to overwrite)"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
$stderr.print("uprb: #{dest_path} already exists. overwrite? [y/N]: ")
|
|
140
|
+
answer = $stdin.gets
|
|
141
|
+
return true if answer && answer.strip.match?(/\A(y|yes)\z/i)
|
|
142
|
+
|
|
143
|
+
$stdout.puts("skipped #{dest_path}")
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def install_gem(gem_name)
|
|
148
|
+
command = [RbConfig.ruby, "-S", "gem", "install", gem_name]
|
|
149
|
+
system(*command) or raise Uprb::Error, "gem install failed: #{gem_name}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def pack_gem_executables(gem_name, options)
|
|
153
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
|
154
|
+
executables = spec.executables
|
|
155
|
+
raise Uprb::Error, "no executables for gem: #{gem_name}" if executables.empty?
|
|
156
|
+
bindir = spec.bindir
|
|
157
|
+
|
|
158
|
+
dest_dir = options[:path] ? File.expand_path(options[:path]) : Gem.bindir
|
|
159
|
+
FileUtils.mkdir_p(dest_dir)
|
|
160
|
+
|
|
161
|
+
executables.each do |exe|
|
|
162
|
+
source_path = File.join(spec.full_gem_path, bindir, exe)
|
|
163
|
+
raise Uprb::Error, "executable not found: #{source_path}" unless File.file?(source_path)
|
|
164
|
+
|
|
165
|
+
dest_path = File.join(dest_dir, exe)
|
|
166
|
+
|
|
167
|
+
next unless confirm_overwrite(dest_path, options)
|
|
168
|
+
|
|
169
|
+
Uprb::RequireReplacer.pack(source_path, dest_path:, requires: options[:requires], dynamic: options[:dynamic], script_argv: options[:script_argv], skip_disable_gems: options[:skip_disable_gems], skip_ruby_path_replace: options[:skip_ruby_path_replace])
|
|
170
|
+
$stdout.puts("Packed #{dest_path}")
|
|
171
|
+
end
|
|
172
|
+
rescue Gem::LoadError => e
|
|
173
|
+
raise Uprb::Error, e.message
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
|
|
7
|
+
module Uprb
|
|
8
|
+
module RequireReplacer
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :mapping
|
|
11
|
+
|
|
12
|
+
def pack(source_path, dest_path: nil, requires: [], dynamic: false, script_argv: [], skip_disable_gems: false, skip_ruby_path_replace: false)
|
|
13
|
+
source = File.read(source_path)
|
|
14
|
+
mapping = build_mapping(source_path, requires, dynamic, script_argv)
|
|
15
|
+
embedded, external = build_payload(mapping)
|
|
16
|
+
ruby_source = source_with_require_hook(source, requires)
|
|
17
|
+
main_iseq = RubyVM::InstructionSequence.compile(ruby_source, source_path, source_path)
|
|
18
|
+
payload = Marshal.dump({
|
|
19
|
+
embedded: embedded,
|
|
20
|
+
external: external,
|
|
21
|
+
main: main_iseq.to_binary
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
shebang = resolve_shebang(source, skip_ruby_path_replace: skip_ruby_path_replace, skip_disable_gems: skip_disable_gems)
|
|
25
|
+
body = <<~RUBY
|
|
26
|
+
DATA.binmode
|
|
27
|
+
data = Marshal.load(DATA)
|
|
28
|
+
|
|
29
|
+
EMBEDDED_ISEQ = data.fetch(:embedded)
|
|
30
|
+
REQUIRE_MAP = data.fetch(:external)
|
|
31
|
+
|
|
32
|
+
iseq = RubyVM::InstructionSequence.load_from_binary(data.fetch(:main))
|
|
33
|
+
iseq.eval
|
|
34
|
+
__END__
|
|
35
|
+
RUBY
|
|
36
|
+
|
|
37
|
+
if shebang
|
|
38
|
+
program = "#{shebang}\n#{body}#{payload}"
|
|
39
|
+
return program unless dest_path
|
|
40
|
+
File.write(dest_path, program)
|
|
41
|
+
FileUtils.chmod("+x", dest_path)
|
|
42
|
+
else
|
|
43
|
+
program = body + payload
|
|
44
|
+
return program unless dest_path
|
|
45
|
+
File.write(dest_path, program)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def resolve_shebang(source, skip_ruby_path_replace:, skip_disable_gems:)
|
|
52
|
+
first_line = source.lines.first&.chomp
|
|
53
|
+
return nil unless first_line&.start_with?("#!")
|
|
54
|
+
|
|
55
|
+
ruby_command = skip_ruby_path_replace ? first_line[2..] : RbConfig.ruby
|
|
56
|
+
skip_disable_gems ? "#!#{ruby_command}" : "#!#{ruby_command} --disable-gems"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `--dynamic` alone would miss literal requires in branches the
|
|
60
|
+
# execution didn't take (rescued `LoadError` alternates, unused
|
|
61
|
+
# autoloads, feature-flag branches); the static walk fills those in.
|
|
62
|
+
def build_mapping(source_path, requires, dynamic, script_argv)
|
|
63
|
+
return Uprb::StaticRequireTracker.trace(source_path, requires: requires) unless dynamic
|
|
64
|
+
|
|
65
|
+
dynamic_map = execute_with_tracker(source_path, requires, script_argv)
|
|
66
|
+
static_map = Uprb::StaticRequireTracker::StaticWalker.new.walk(File.expand_path(source_path))
|
|
67
|
+
static_map.merge(dynamic_map)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rewind_read_tempfile(file)
|
|
71
|
+
file.flush
|
|
72
|
+
file.rewind
|
|
73
|
+
file.read
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def execute_with_tracker(path, requires = [], script_argv = [])
|
|
77
|
+
original_stdout, original_stderr = STDOUT.dup, STDERR.dup
|
|
78
|
+
original_argv = ARGV.dup
|
|
79
|
+
original_program_name = $PROGRAM_NAME
|
|
80
|
+
tmp_stdout = Tempfile.new("uprb-stdout")
|
|
81
|
+
tmp_stderr = Tempfile.new("uprb-stderr")
|
|
82
|
+
mapping = nil
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
STDOUT.reopen(tmp_stdout)
|
|
86
|
+
STDERR.reopen(tmp_stderr)
|
|
87
|
+
ARGV.replace(script_argv)
|
|
88
|
+
$PROGRAM_NAME = path
|
|
89
|
+
Uprb::RequireTracker.start
|
|
90
|
+
requires.each {|lib| require lib }
|
|
91
|
+
load path
|
|
92
|
+
rescue SystemExit => e
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
stdout_content = rewind_read_tempfile(tmp_stdout)
|
|
95
|
+
stderr_content = rewind_read_tempfile(tmp_stderr)
|
|
96
|
+
message = ["execution failed: #{e.class}: #{e.message}"]
|
|
97
|
+
message << "stdout: #{stdout_content}" unless stdout_content.empty?
|
|
98
|
+
message << "stderr: #{stderr_content}" unless stderr_content.empty?
|
|
99
|
+
raise Uprb::Error, message.join("\n")
|
|
100
|
+
ensure
|
|
101
|
+
mapping = Uprb::RequireTracker.stop
|
|
102
|
+
STDOUT.reopen(original_stdout)
|
|
103
|
+
STDERR.reopen(original_stderr)
|
|
104
|
+
ARGV.replace(original_argv)
|
|
105
|
+
$PROGRAM_NAME = original_program_name
|
|
106
|
+
tmp_stdout.close!
|
|
107
|
+
tmp_stderr.close!
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
mapping
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def source_with_require_hook(source, requires = [])
|
|
114
|
+
preload_lines = requires.map {|lib| "require #{lib.inspect}" }.join("\n")
|
|
115
|
+
pre_code = <<~RUBY
|
|
116
|
+
module FixedRequire
|
|
117
|
+
SUFFIXES = #{Uprb::SUFFIXES.inspect}.freeze
|
|
118
|
+
|
|
119
|
+
def require(name)
|
|
120
|
+
entry = EMBEDDED_ISEQ[name]
|
|
121
|
+
if entry
|
|
122
|
+
path, binary = entry
|
|
123
|
+
return false if $LOADED_FEATURES.include?(path) || $LOADED_FEATURES.include?(name)
|
|
124
|
+
$LOADED_FEATURES << path
|
|
125
|
+
$LOADED_FEATURES << name unless $LOADED_FEATURES.include?(name)
|
|
126
|
+
mark_runtime_resolved(name, path)
|
|
127
|
+
RubyVM::InstructionSequence.load_from_binary(binary).eval
|
|
128
|
+
true
|
|
129
|
+
elsif (path = REQUIRE_MAP[name])
|
|
130
|
+
result = super(path)
|
|
131
|
+
mark_runtime_resolved(name, path) if result
|
|
132
|
+
result
|
|
133
|
+
else
|
|
134
|
+
super(name)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# C extensions bypass this hook via rb_require(); pre-mark the path
|
|
139
|
+
# $LOAD_PATH would resolve `name` to so they see it as already loaded.
|
|
140
|
+
def mark_runtime_resolved(name, loaded_path)
|
|
141
|
+
resolved = $LOAD_PATH.lazy.flat_map {|d| SUFFIXES.map {|s| File.join(d, "\#{name}\#{s}") } }.find {|p| File.file?(p) }
|
|
142
|
+
return unless resolved && resolved != loaded_path && !$LOADED_FEATURES.include?(resolved)
|
|
143
|
+
$LOADED_FEATURES << resolved
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Kernel.prepend(FixedRequire)
|
|
148
|
+
#{preload_lines}
|
|
149
|
+
RUBY
|
|
150
|
+
pre_code + source
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_payload(mapping)
|
|
154
|
+
embedded = {}
|
|
155
|
+
external = {}
|
|
156
|
+
|
|
157
|
+
mapping.each do |name, path|
|
|
158
|
+
if path.is_a?(String) && File.file?(path) && File.extname(path) == ".rb"
|
|
159
|
+
source = File.read(path)
|
|
160
|
+
iseq = RubyVM::InstructionSequence.compile(source, path, path)
|
|
161
|
+
embedded[name] = [path, iseq.to_binary]
|
|
162
|
+
else
|
|
163
|
+
external[name] = path
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
[embedded, external]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uprb
|
|
4
|
+
module RequireTracker
|
|
5
|
+
class << self
|
|
6
|
+
attr_reader :mapping
|
|
7
|
+
|
|
8
|
+
def start
|
|
9
|
+
install_require_hook
|
|
10
|
+
@mapping = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stop
|
|
14
|
+
recorded = @mapping
|
|
15
|
+
@mapping = nil
|
|
16
|
+
return recorded
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def record_require(name, features)
|
|
20
|
+
return if !@mapping || @mapping[name]
|
|
21
|
+
|
|
22
|
+
path = find_loaded_feature(name, features)
|
|
23
|
+
@mapping[name] = path if path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def install_require_hook
|
|
29
|
+
return if defined?(@require_hook_installed) && @require_hook_installed
|
|
30
|
+
|
|
31
|
+
if Kernel.private_method_defined?(:uprb_original_require)
|
|
32
|
+
@require_hook_installed = true
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Kernel.module_eval do
|
|
37
|
+
alias_method :uprb_original_require, :require
|
|
38
|
+
alias_method :uprb_original_require_relative, :require_relative
|
|
39
|
+
|
|
40
|
+
def require(name)
|
|
41
|
+
before_size = $LOADED_FEATURES.size
|
|
42
|
+
required = uprb_original_require(name)
|
|
43
|
+
features = required ? $LOADED_FEATURES[before_size..] : $LOADED_FEATURES
|
|
44
|
+
Uprb::RequireTracker.record_require(name, features)
|
|
45
|
+
required
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def require_relative(path)
|
|
49
|
+
caller_path = caller_locations(1, 1).first.path
|
|
50
|
+
absolute_path = File.expand_path(path, File.dirname(caller_path))
|
|
51
|
+
before_size = $LOADED_FEATURES.size
|
|
52
|
+
required = uprb_original_require(absolute_path)
|
|
53
|
+
features = required ? $LOADED_FEATURES[before_size..] : $LOADED_FEATURES
|
|
54
|
+
Uprb::RequireTracker.record_require(absolute_path, features)
|
|
55
|
+
required
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private :require, :uprb_original_require
|
|
59
|
+
private :require_relative, :uprb_original_require_relative
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@require_hook_installed = true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def find_loaded_feature(name, entries)
|
|
66
|
+
extname = File.extname(name)
|
|
67
|
+
if Uprb::SUFFIXES.include?(extname)
|
|
68
|
+
target_name = name.delete_suffix(extname)
|
|
69
|
+
suffixes = Uprb::DL_SUFFIXES.include?(extname) ? Uprb::DL_SUFFIXES : Uprb::SUFFIXES
|
|
70
|
+
else
|
|
71
|
+
target_name = name
|
|
72
|
+
suffixes = Uprb::SUFFIXES
|
|
73
|
+
end
|
|
74
|
+
suffixes.each do |suffix|
|
|
75
|
+
pattern = /(?:\A|#{File::SEPARATOR})#{target_name}#{suffix}\z/
|
|
76
|
+
# Ruby appends the parent after its nested children; search from the tail to hit it first.
|
|
77
|
+
entries.reverse_each do |f|
|
|
78
|
+
return f if pattern.match?(f)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def absolute_path?(name)
|
|
86
|
+
name.start_with?(File::SEPARATOR) ||
|
|
87
|
+
(File::ALT_SEPARATOR && name.start_with?(File::ALT_SEPARATOR))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Uprb
|
|
7
|
+
# Avoids running the entry's top-level code (CLI scripts often start
|
|
8
|
+
# `App.start(ARGV)` at load time) while still letting dependencies load
|
|
9
|
+
# normally so rubygems resolution, autoload registration, etc. happen.
|
|
10
|
+
module StaticRequireTracker
|
|
11
|
+
class << self
|
|
12
|
+
def trace(source_path, requires: [])
|
|
13
|
+
Tracer.new(requires: requires).trace(source_path)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Tracer
|
|
18
|
+
def initialize(requires: [])
|
|
19
|
+
@requires = requires
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def trace(source_path)
|
|
23
|
+
entry_path = File.expand_path(source_path)
|
|
24
|
+
|
|
25
|
+
dynamic = dynamic_phase(entry_path)
|
|
26
|
+
static = StaticWalker.new.walk(entry_path)
|
|
27
|
+
|
|
28
|
+
static.merge(dynamic)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def dynamic_phase(entry_path)
|
|
34
|
+
Uprb::RequireTracker.start
|
|
35
|
+
begin
|
|
36
|
+
@requires.each {|lib| require(lib) }
|
|
37
|
+
entry_dir = File.dirname(entry_path)
|
|
38
|
+
extract_requires(entry_path).each do |kind, name|
|
|
39
|
+
trigger(kind, name, entry_dir)
|
|
40
|
+
end
|
|
41
|
+
ensure
|
|
42
|
+
mapping = Uprb::RequireTracker.stop
|
|
43
|
+
end
|
|
44
|
+
mapping
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def trigger(kind, name, entry_dir)
|
|
48
|
+
case kind
|
|
49
|
+
when :require
|
|
50
|
+
require(name)
|
|
51
|
+
when :require_relative
|
|
52
|
+
require(File.expand_path(name, entry_dir))
|
|
53
|
+
end
|
|
54
|
+
rescue LoadError
|
|
55
|
+
# The packed output falls through to runtime require for names we
|
|
56
|
+
# can't resolve here (e.g. local libs the entry would have added to
|
|
57
|
+
# $LOAD_PATH if it had been allowed to run).
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_requires(path)
|
|
61
|
+
source = File.read(path)
|
|
62
|
+
result = Prism.parse(source, filepath: path)
|
|
63
|
+
return [] if result.failure?
|
|
64
|
+
|
|
65
|
+
visitor = RequireVisitor.new
|
|
66
|
+
result.value.accept(visitor)
|
|
67
|
+
visitor.requires
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class StaticWalker
|
|
72
|
+
def initialize
|
|
73
|
+
@mapping = {}
|
|
74
|
+
@visited = Set.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def walk(entry_path)
|
|
78
|
+
parse_file(entry_path)
|
|
79
|
+
@mapping
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def parse_file(path)
|
|
85
|
+
return unless @visited.add?(path)
|
|
86
|
+
return unless File.file?(path)
|
|
87
|
+
|
|
88
|
+
source = File.read(path)
|
|
89
|
+
result = Prism.parse(source, filepath: path)
|
|
90
|
+
return if result.failure?
|
|
91
|
+
|
|
92
|
+
visitor = RequireVisitor.new
|
|
93
|
+
result.value.accept(visitor)
|
|
94
|
+
|
|
95
|
+
visitor.requires.each do |kind, name|
|
|
96
|
+
case kind
|
|
97
|
+
when :require
|
|
98
|
+
record_require(name)
|
|
99
|
+
when :require_relative
|
|
100
|
+
absolute = File.expand_path(name, File.dirname(path))
|
|
101
|
+
resolved = resolve_file(absolute)
|
|
102
|
+
next unless resolved
|
|
103
|
+
|
|
104
|
+
@mapping[absolute] ||= resolved
|
|
105
|
+
parse_file(resolved) if resolved.end_with?(".rb")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def record_require(name)
|
|
111
|
+
resolved = resolve_in_load_path(name)
|
|
112
|
+
return unless resolved
|
|
113
|
+
|
|
114
|
+
@mapping[name] ||= resolved
|
|
115
|
+
parse_file(resolved) if resolved.end_with?(".rb")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Skips rubygems' pre-activation steps (default-gem / unresolved-dep
|
|
119
|
+
# resolution) that run before the $LOAD_PATH search — so in environments
|
|
120
|
+
# with multi-version conflicts this may pick a different version than
|
|
121
|
+
# runtime would.
|
|
122
|
+
def resolve_in_load_path(name)
|
|
123
|
+
return resolve_file(name) if File.absolute_path?(name)
|
|
124
|
+
|
|
125
|
+
resolved = search_load_path(name)
|
|
126
|
+
return resolved if resolved
|
|
127
|
+
|
|
128
|
+
return nil unless defined?(Gem) && Gem.respond_to?(:try_activate)
|
|
129
|
+
return nil unless Gem.try_activate(name)
|
|
130
|
+
|
|
131
|
+
search_load_path(name)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def search_load_path(name)
|
|
135
|
+
$LOAD_PATH.each do |dir|
|
|
136
|
+
candidate = resolve_file(File.join(dir, name))
|
|
137
|
+
return candidate if candidate
|
|
138
|
+
end
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_file(path)
|
|
143
|
+
extname = File.extname(path)
|
|
144
|
+
if Uprb::SUFFIXES.include?(extname)
|
|
145
|
+
if Uprb::DL_SUFFIXES.include?(extname)
|
|
146
|
+
base = path.delete_suffix(extname)
|
|
147
|
+
Uprb::DL_SUFFIXES.each do |suffix|
|
|
148
|
+
candidate = "#{base}#{suffix}"
|
|
149
|
+
return candidate if File.file?(candidate)
|
|
150
|
+
end
|
|
151
|
+
nil
|
|
152
|
+
else
|
|
153
|
+
File.file?(path) ? path : nil
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
Uprb::SUFFIXES.each do |suffix|
|
|
157
|
+
candidate = "#{path}#{suffix}"
|
|
158
|
+
return candidate if File.file?(candidate)
|
|
159
|
+
end
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class RequireVisitor < Prism::Visitor
|
|
166
|
+
attr_reader :requires
|
|
167
|
+
|
|
168
|
+
def initialize
|
|
169
|
+
super
|
|
170
|
+
@requires = []
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def visit_call_node(node)
|
|
174
|
+
case node.name
|
|
175
|
+
when :require, :require_relative
|
|
176
|
+
if node.receiver.nil?
|
|
177
|
+
name = extract_string_argument(node, index: 0)
|
|
178
|
+
@requires << [node.name, name] if name
|
|
179
|
+
end
|
|
180
|
+
when :autoload
|
|
181
|
+
name = extract_string_argument(node, index: 1)
|
|
182
|
+
@requires << [:require, name] if name
|
|
183
|
+
end
|
|
184
|
+
super
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def extract_string_argument(node, index:)
|
|
190
|
+
args = node.arguments&.arguments
|
|
191
|
+
return nil unless args && args[index].is_a?(Prism::StringNode)
|
|
192
|
+
|
|
193
|
+
args[index].unescaped
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
data/lib/uprb/version.rb
ADDED
data/lib/uprb.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Uprb
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
DL_SUFFIXES = [".#{RbConfig::CONFIG['DLEXT']}", ".so", ".o"].uniq.freeze
|
|
9
|
+
SUFFIXES = ([".rb"] + DL_SUFFIXES).freeze
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative "uprb/version"
|
|
13
|
+
require_relative "uprb/require_tracker"
|
|
14
|
+
require_relative "uprb/static_require_tracker"
|
|
15
|
+
require_relative "uprb/require_replacer"
|
data/sig/uprb.rbs
ADDED
data/tmp/.keep
ADDED
|
File without changes
|