summaryrefslogtreecommitdiffstats
path: root/lang/ruby/ruby_find_pkgsdeps
blob: 57187a72144906c977e1812af7260ab9fe2082b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
#!/usr/bin/ruby -Eutf-8
# encoding: utf-8
#
# -*- mode: ruby -*-
# vi: set tabstop=8 shiftwidth=8 noexpandtab:
#
#
# Find dependencies between ruby packages
#
# Must run inside a openwrt with all *ruby* packages installed
#
#
$debug=$stderr
$info=$stderr
$error=$stderr

#$debug=File.open("/dev/null")

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

$info.puts "Loading all installed gems (unstable after external gems are instaled/update)"
require 'rubygems'
Gem::Specification.collect{ |g| g.name.downcase }.uniq.each {|g| gem g }

$info.puts "Looking for installed ruby packages (and its files)..."
packages = []
package_files = {}
package_depends = {}
packages_json=`apk info --format json --contents --depends --match url 'http://www.ruby-lang.org/'`
require "json"
JSON.parse(packages_json).each do |pkg|
  next if not pkg["contents"]
  packages << pkg["name"]
  package_files[pkg["name"]] = pkg["contents"].map() {|file| "/#{file}" }
  package_depends[pkg["name"]] = pkg["depends"].reject{|dep| dep =~ /^lib/ or dep == "ruby" }
end
# Fake enc/utf_16 to dummy enc:
package_files["ruby-enc"]+=[RbConfig::CONFIG["rubylibdir"] + "/enc/utf_16.rb" ]

# These are Encodings that does not require extra files
builtin_enc=[
	Encoding.find("ASCII-8BIT"),
	Encoding.find("UTF-8"),
	Encoding.find("UTF-7"),
	Encoding.find("US-ASCII"),
]

# List of requires that can be simply ignored, normally because they are conditional
# requirements or to break loops
require_ignore=%w{
	bundler
	capistrano/version
	dbm
	ffi
	fiber
	gettext/mo
	gettext/po_parser
	graphviz
	iconv
	java
	json/truffle_ruby/generator
	jruby
	minitest/proveit
	nkf.jar
	open3/jruby_windows
	parser
	prism/prism
	profile
	psych_jars
	racc/cparse-jruby.jar
	ruby_parser
	rubygems/defaults/operating_system
	rubygems/net/http
	rubygems/timeout
	simplecov
	sexp
	sorted_set
	stackprof
	tracer
	uconv
	webrick
	webrick/https
	win32api
	win32/resolv
	win32/sspi
	xml/encoding-ja
	xmlencoding-ja
	xml/parser
	xmlparser
	xmlscan/scanner
}
# builtin requires
require_ignore+=%w{
	enumerator
	thread
	fiber
	rational
	complex
	set
}
# Keep track of which of these ignores really matters
require_ignore_that_matched={}

files_ignore=%w{
	extconf.rb
}

# The digestor that parsers the ruby source code for requires or use of Encodings
require "ripper"
def parse_requires_ripper(source)
	requires = []
	encodings = []

	ast = Ripper.sexp(source)
	stack = [ast]

	until stack.empty?
		node = stack.pop
		next unless node.is_a?(Array)

		case node[0]
		when :command
			req = nil
			case node[1][1]
			when "require"
				#[:command, [:@ident, "require", [3, 0]], [:args_add_block, [[:string_literal, [:string_content, [:@tstring_content, "pathname", [3, 9]]]]], false]]
				#[:command, [:@ident, "require", [3, 0]], [:args_add_block, [[:string_literal, [:string_content, [:@tstring_content, "rbconfig", [3, 9]]]]], false]]
				#[:command, [:@ident, "require", [3, 0]], [:args_add_block, [[:string_literal, [:string_content, [:@tstring_content, "rubygems/dependency", [3, 9]]]]], false]]
				# Only accepts requires with only a literal strings (without embeded expressions)
				req = node[2][1][0][1][1][1] if node[2][1][0][1][1][0] == :@tstring_content and node[2][1][0][1].length == 2 rescue nil
				requires << req if req
			end

			# node = [:command, [:@ident, "pp", [ln,col]], [:args_add_block, ...]]
			if node[1][0] == :@ident && node[1][1] == "pp"
				requires << "pp"
			end

		when :command_call
			req = nil
			# node = [:command_call, receiver, [:@period, ".", [ln,col]], [:@ident, "require", [ln,col]], args]
			if node[3][1] == "require"
				# Only accepts requires with only a literal strings (without embedded expressions)
				args = node[4] rescue nil
				if args && args[1] && args[1][0] && args[1][0][1] && args[1][0][1][1][0] == :@tstring_content && args[1][0][1].length == 2
					req = args[1][0][1][1][1] rescue nil
					requires << req if req
				end
			end			
		# The args in Encoding.find (if a string constant) should also requires the encoding (but,in practice,
		# there is no case for it current ruby code
		when :const_path_ref
			# [:const_path_ref, [:var_ref, [:@const, "Encoding", [136, 9]]], [:@const, "US_ASCII", [136, 19]]]
			if node[1][0]=:const_ref and node[1][1][1] == "Encoding" and node[2][0] = :@const
				enc = node[2][1]

				enc = eval("Encoding::#{enc}")
				encodings << enc if enc.kind_of? Encoding

				# The builtin encodings do not populate Encoding::XXX constants
				requires << "enc/encdb"
			end

			node.each do |child|
				stack << child if child.is_a?(Array)
			end

		when :method_add_arg
			# Detects fcall :pp => [:method_add_arg, [:fcall, [:@ident, "pp", [1,0]]], ...]
			if node[1][0] == :fcall && node[1][1][0] == :@ident && node[1][1][1] == "pp"
				requires << "pp"
			end

			# Detects Kernel.pp => [:method_add_arg, [:call, [:var_ref, [:@const, "Kernel", [1,0]]], :".", [:@ident, "pp", [1,8]]], ...]
			if node[1][0] == :call &&
			   node[1][1][0] == :var_ref && node[1][1][1][1] == "Kernel" &&
			   node[1][3][0] == :@ident && node[1][3][1] == "pp"
				requires << "pp"
			end
		end

		node.each do |child|
			stack << child if child.is_a?(Array)
		end
	end

	return requires, encodings
end

require "prism"
def parse_requires_prism(source)
	requires  = []
	encodings = []

	result = Prism.parse(source)
	stack  = [result.value] # root node

	until stack.empty?
		node = stack.pop
		next unless node.is_a?(Prism::Node)

		case node
		when Prism::CallNode
			# e.g. `require "foo"`
			if node.name == :require && node.arguments&.arguments&.size == 1
				arg = node.arguments.arguments.first
				if arg.is_a?(Prism::StringNode)
					requires << arg.unescaped
				end
			end

			# Detects Kernel.pp or pp(...)
			if node.name == :pp
				if node.receiver.nil? # just `pp(...)`
					requires << "pp"
				elsif node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :Kernel
					requires << "pp"
				end
			end

		when Prism::ConstantPathNode
			# e.g. Encoding::US_ASCII
			if node.parent.is_a?(Prism::ConstantReadNode) &&
			   node.parent.name == :Encoding &&
			   node.child.is_a?(Prism::ConstantReadNode)

				enc = node.child.name.to_s
				begin
					enc_obj = Encoding.const_get(enc)
					encodings << enc_obj if enc_obj.is_a?(Encoding)
					requires << "enc/encdb"
				rescue NameError
					# ignore if unknown
				end
			end


			# e.g. ::Encoding::XXX
			if node.parent.is_a?(Prism::ConstantPathNode) &&
			   node.parent.child.is_a?(Prism::ConstantReadNode) &&
			   node.parent.child.name == :Encoding &&
			   node.child.is_a?(Prism::ConstantReadNode)

				enc = node.child.name.to_s
				begin
					enc_obj = Encoding.const_get(enc)
					encodings << enc_obj if enc_obj.is_a?(Encoding)
					requires << "enc/encdb"
				rescue NameError
				end
			end
		end

		# Recurse through children
		node.child_nodes.compact.each do |child|
			stack << child
		end
	end

	[requires, encodings]
end

# Now check what each files in packages needs
$info.puts "Looking for requires in files..."
files_requires=Hash.new { |h,k| h[k]=[] }
packages.each do
	|pkg|
	$debug.puts "Checking pkg #{pkg}..."

	package_files[pkg].each do
		|file|
		next if not File.file?(file) or not File.readable?(file)

		next if files_ignore.include?(file) or files_ignore.include?(file.sub(/.*\//,""))

		#file = "/usr/lib/ruby/3.4/bundler/rubygems_ext.rb"
		#file = "/usr/lib/ruby/3.4/openssl/buffering.rb"
		#file = "/usr/lib/ruby/3.4/unicode_normalize/normalize.rb"
		#file = "/usr/lib/ruby/3.4/rdoc/encoding.rb"
		#file = "/usr/lib/ruby/3.4/bundler.rb"
		#file = "/usr/lib/ruby/3.4/bundler/rubygems_ext.rb"
		#file = "/usr/lib/ruby/gems/3.4/gems/debug-1.11.0/lib/debug/server.rb"

		f = File.open(file,"r")
		line1 = f.gets()

		if not file =~ /\.rb$/
			next if not File.executable?(file)
			next if not line1[0..1] == "#!"
			next if not line1 =~ /^#!.*ruby/
			$debug.puts "File #{pkg}:#{file} is a ruby script"
		end
		# Ignore the shebang if present
		line1 = f.gets() if line1 =~ /#!.*ruby/

		# Rewind to parse it all again
		f.rewind()

		#$debug.puts "Checking file #{pkg}:#{file}..."
		requires, encodings = parse_requires_prism(f.read)
		#requires2, encodings2 = parse_requires_ripper(f.read)
		#if requires != requires2 or encodings != encodings2
		#	p pkg
		#	p file
		#	pp requires, encodings
		#	pp requires2, encodings2
		#	exit
		#end

		requires.reject! {|req| require_ignore_that_matched[req]=1 if require_ignore.include?(req) }
		# Relative paths are always internal
		requires.reject! {|req| req =~ /^\./ }

		# get the magic encoding if present
		if line1 =~ /^#\s*(en)?coding\s*[:=]\s*([a-zA-Z0-9_\-]+)\n/i
			encodings << Encoding.find($2)
		end

		# ignore buildin encodings
		encodings -= builtin_enc

		# convert encodings to requires
		requires += encodings.collect {|enc| "enc/#{enc.name.downcase.gsub("-","_")}" }
		requires << "enc/encdb" if not encodings.empty?

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

# Check which require_ignore arr not in use
missed_ignored = (require_ignore - require_ignore_that_matched.keys).sort.join(",")
if not missed_ignored.empty?
	$error.puts "These 'require_ignore' didn't match anything: ",(require_ignore - require_ignore_that_matched.keys).sort.join(","),""
end

# Add dependencies of ruby files from ruby lib.so
package_files.each do |(pkg,files)| files.each do |file|
	case file
	when /\/nkf\.so$/    ; files_requires[file]=files_requires[file] + ["enc/encdb"]
	when /\/json\/ext\/generator\.so$/ ; files_requires[file]=files_requires[file] + ["enc/encdb"]
	when /\/json\/ext\/parser\.so$/ ; files_requires[file]=files_requires[file] + ["enc/encdb"]
	when /\/nkf\.so$/    ; files_requires[file]=files_requires[file] + ["enc/encdb"]
	when /\/objspace\.so$/; files_requires[file]=files_requires[file] + ["tempfile"]	# dump_output from ext/objspace/objspace_dump.c
	when /\/openssl\.so$/; files_requires[file]=files_requires[file] + ["digest"]		# Init_ossl_digest from ext/openssl/ossl_digest.c
	end
end; end

$info.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

# For optional require or for breaking cycle dependencies
weak_dependency=Hash.new { |h,k| h[k]=[] }
weak_dependency.merge!({
	"ruby-irb"      =>%w{ruby-rdoc ruby-readline ruby-debug}, # irb/cmd/help.rb irb/cmd/debug.rb,3.2/irb/cmd/debug.rb
	"ruby-gems"     =>%w{ruby-bundler ruby-rdoc},             # rubygems.rb rubygems/server.rb rdoc/rubygems_hook
	"ruby-racc"     =>%w{ruby-gems},			  # /usr/bin/racc*
	"ruby-rake"     =>%w{ruby-gems ruby-debug},               # /usr/bin/rake gems/3.3/gems/rake-13.1.0/lib/rake/application.rb
	"ruby-rdoc"     =>%w{ruby-readline},			  # rdoc/ri/driver.rb
	"ruby-testunit" =>%w{ruby-io-console},			  # gems/test-unit-3.1.5/lib/test/unit/ui/console/testrunner.rb
	"ruby-net-http" =>%w{ruby-open-uri}			  # net/http/status.rb
})

# Identify which files a package requires
$info.puts "Looking for package dependencies..."
provided_by = {}
package_provides = Hash[package_files.map() {|(pkg,files)| [pkg,files.map() {|file| file.sub(/\.(so|rb)$/,"")}]}]
package_dependencies = Hash.new { |h,k| h[k]=[] }
package_requires_files.each do
	|(pkg,requires_files)|

	requires_files.each do
		|(require,files)|

		found = provided_by[require]
		if not found
			# local dir or in search path are acceptables
			#search_paths = (files.map() {|file| file.sub(/\/[^\/]+$/,"") } + $:).uniq
			search_files = $:.map() {|path| "#{path}/#{require.sub(/\.(so|rb)$/,"")}" }

			found = package_provides.detect {|(_pkg,_files)| not (_files & search_files).empty? }

			if not found
				$error.puts "#{pkg}: Nothing provides #{require} for #{files.collect {|file| file.sub("/usr/lib/ruby/","") }.join(",")}"
				failed = true
				next
			end
			found = found.first
			provided_by[require] = found
		end

		if weak_dependency[pkg].include?(found)
			$debug.puts "#{pkg}: #{found} provides #{require} (weak depedendency ignored) for #{files.collect {|file| file.sub("/usr/lib/ruby/","") }.join(",")}"
		else
			$debug.puts "#{pkg}: #{found} provides #{require} for #{files.collect {|file| file.sub("/usr/lib/ruby/","") }.join(",")}"
			package_dependencies[pkg] += [found]
		end
	end
	#break if pkg =~ /ruby-bundler.*/
end
if failed
	$error.puts "There is some missing requirements not mapped to files in packages."
	$error.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 = []

# Add explicity dependency
package_dependencies["ruby-enc-extra"]+=["ruby-enc"]

# Expanding dependencies, including the depedencies from required packages
$info.puts "Expanding dependencies..."
begin
	changed=false
	package_dependencies.each do
		|(pkg,deps)|
		next if deps.empty?
		deps.each {|dep| $info.puts "#{pkg}: #{dep} also depends on #{pkg}" if package_dependencies[dep].include?(pkg) }
		deps_new = deps.collect {|dep| [dep] + package_dependencies[dep] }.inject([],:+).uniq.sort
		if not deps == deps_new
			$debug.puts "#{pkg}: #{deps.join(",")} (OLD)"
			$debug.puts "#{pkg}: #{deps_new.join(",")} (NEW)"
			package_dependencies[pkg]=deps_new

			if deps_new.include?(pkg)
				$error.puts "#{pkg}: Circular dependency detected (#1)!"
				exit 1
			end
			changed=true
		end
	end
end if not changed

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

$info.puts "Checking for mutual dependencies..."
package_dependencies.each do
	|(pkg,deps)|
	if deps.include? pkg
		$error.puts "#{pkg}: Circular dependency detected (#2)!"
		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)
			$info.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

$info.puts "Checking current packages dependencies..."
ok=true
package_dependencies.each do
	|(pkg,deps)|
	extra_dep = package_depends[pkg] - deps
	$info.puts "Package #{pkg} does not need to depend on #{extra_dep.join(" ")} " if not extra_dep.empty?
	missing_dep = deps - package_depends[pkg]
	$info.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?
		puts "define Package/#{pkg}"
		puts "  DEPENDS:=ruby#{([""] +deps).join(" +")}"
		ok=false
	end
end

puts "All dependencies are OK." if ok

__END__