debendencies 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cfb4b703eb13f5256ed10c8608f31ff2fb3458301dde2d0e599f63994cb8cf78
4
+ data.tar.gz: da9f79cad3cf0dd3b62dcf63a6bcdb141da6155d6e618a4768a9a2b4c55f0828
5
+ SHA512:
6
+ metadata.gz: 8310ab69c81538f55178835ed89b479c8d3c46fc3e073537fd57007b7752055e343b77b811ad465c9365ec9c876da4620088762ab233674e461df260b101315b
7
+ data.tar.gz: acd7ed848689e4ced849bc5ae4227a6324b197268d64c35bd5d031e9f378725856742a3086e5206c1f8ac40485ac522a3fd4011f22e066be79f270acbf4bec94
data/bin/debendencies ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/debendencies/cli"
3
+ Debendencies::CLI.new.run
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+ require "optparse"
3
+ require_relative "../debendencies"
4
+ require_relative "version"
5
+
6
+ class Debendencies
7
+ class CLI
8
+ def initialize
9
+ @options = {
10
+ format: "oneline",
11
+ }
12
+ end
13
+
14
+ def run
15
+ option_parser.parse!
16
+ require "json" if @options[:format] == "json"
17
+
18
+ paths = ARGV
19
+ if paths.empty?
20
+ puts option_parser
21
+ exit 1
22
+ end
23
+
24
+ debendencies = Debendencies.new(logger: get_logger)
25
+ begin
26
+ debendencies.scan(*paths)
27
+ dependencies = debendencies.resolve
28
+ rescue Error => e
29
+ abort(e.message)
30
+ end
31
+
32
+ case @options[:format]
33
+ when "oneline"
34
+ puts dependencies.map { |d| d.to_s }.join(", ")
35
+ when "multiline"
36
+ dependencies.each { |d| puts d.to_s }
37
+ when "json"
38
+ puts JSON.generate(dependencies.map { |d| d.as_json })
39
+ else
40
+ puts "Invalid format: #{@options[:format]}"
41
+ exit 1
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def option_parser
48
+ @option_parser ||= OptionParser.new do |opts|
49
+ opts.banner = "Usage: debendencies <PATHS...>"
50
+
51
+ opts.on("-f", "--format FORMAT", "Output format (oneline|multiline|json). Default: oneline") do |format|
52
+ if !["oneline", "multiline", "json"].include?(format)
53
+ abort "Invalid format: #{format.inspect}"
54
+ end
55
+ @options[:format] = format
56
+ end
57
+
58
+ opts.on("--verbose", "Show verbose output") do
59
+ @options[:verbose] = true
60
+ end
61
+
62
+ opts.on("-h", "--help", "Show this help message") do
63
+ puts opts
64
+ exit
65
+ end
66
+
67
+ opts.on("--version", "Show version") do
68
+ puts VERSION_STRING
69
+ exit
70
+ end
71
+ end
72
+ end
73
+
74
+ def get_logger
75
+ if @options[:verbose]
76
+ require "logger"
77
+ Logger.new(STDERR)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ require "set"
3
+ require_relative "errors"
4
+ require_relative "utils"
5
+
6
+ class Debendencies
7
+ module Private
8
+ class << self
9
+ # Extracts from an ELF file using `objdump`:
10
+ #
11
+ # - The ELF file's own soname, if possible. Can be nil.
12
+ # - The list of shared library dependencies (sonames).
13
+ #
14
+ # @param path [String] Path to the ELF file to analyze.
15
+ # @return [String, Array<Array<String>>]
16
+ # @raise [Error] If `objdump` fails.
17
+ def extract_soname_and_dependency_libs(path)
18
+ popen(["objdump", "-p", path],
19
+ spawn_error_message: "Error scanning ELF file dependencies: cannot spawn 'objdump'",
20
+ fail_error_message: "Error scanning ELF file dependencies: 'objdump' failed") do |io|
21
+ soname = nil
22
+ dependent_libs = []
23
+
24
+ io.each_line do |line|
25
+ case line
26
+ when /^\s*SONAME\s+(.+)$/
27
+ soname = $1.strip
28
+ when /^\s*NEEDED\s+(.+)$/
29
+ dependent_libs << $1.strip
30
+ end
31
+ end
32
+
33
+ [soname, dependent_libs]
34
+ end
35
+ end
36
+
37
+ # Extracts dynamic symbols from ELF files using `nm`.
38
+ #
39
+ # @param paths [Array<String>] Paths to the ELF files to analyze.
40
+ # @param cache [Hash<String, Set<String>>]
41
+ # @return [Set<String>] Set of dynamic symbols.
42
+ # @raise [Error] If `nm` fails.
43
+ def extract_dynamic_symbols(paths, cache = {})
44
+ result = Set.new
45
+
46
+ paths.each do |path|
47
+ subresult = cache[path] ||=
48
+ popen(["nm", "-D", path],
49
+ spawn_error_message: "Error extracting dynamic symbols from #{path}: cannot spawn 'nm'",
50
+ fail_error_message: "Error extracting dynamic symbols from #{path}: 'nm' failed") do |io|
51
+ io.each_line.lazy.map do |line|
52
+ # Line is in the following format:
53
+ #
54
+ # U waitpid
55
+ # 0000000000126190 B want_pending_command
56
+ # ^^^^^^^^^^^^^^^^^^^^
57
+ # we want to extract this
58
+ $1 if line =~ /^\S*\s+[A-Za-z]\s+(.+)/
59
+ end.compact.to_set
60
+ end
61
+ result.merge(subresult)
62
+ end
63
+
64
+ result
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Debendencies
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Debendencies
4
+ # Represents a single Debian package dependency, e.g., `libc`.
5
+ # Could potentially have version constraints, e.g., `libc (>= 2.28, <= 2.30)`.
6
+ #
7
+ # `version_constraints` is either nil or non-empty.
8
+ class PackageDependency
9
+ attr_reader :name, :version_constraints
10
+
11
+ def initialize(name, version_constraints = nil)
12
+ @name = name
13
+ @version_constraints = version_constraints
14
+ end
15
+
16
+ def eql?(other)
17
+ @name == other.name && @version_constraints == other.version_constraints
18
+ end
19
+
20
+ alias_method :==, :eql?
21
+
22
+ def hash
23
+ @name.hash ^ @version_constraints.hash
24
+ end
25
+
26
+ def as_json
27
+ result = { name: name }
28
+ result[:version_constraints] = version_constraints.map { |vc| vc.as_json } if version_constraints
29
+ result
30
+ end
31
+
32
+ def to_s
33
+ if version_constraints.nil?
34
+ name
35
+ else
36
+ "#{name} (#{version_constraints.map { |vc| vc.to_s }.join(", ")})"
37
+ end
38
+ end
39
+ end
40
+
41
+ # Represents a version constraint, e.g., `>= 2.28-1`.
42
+ class VersionConstraint
43
+ # A comparison operator, e.g., `>=`.
44
+ #
45
+ # @return [String]
46
+ attr_reader :operator
47
+
48
+ # A Debian package version, e.g., `2.28-1`.
49
+ # @return [String]
50
+ attr_reader :version
51
+
52
+ def initialize(operator, version)
53
+ @operator = operator
54
+ @version = version
55
+ end
56
+
57
+ def eql?(other)
58
+ @operator == other.operator && @version == other.version
59
+ end
60
+
61
+ alias_method :==, :eql?
62
+
63
+ def hash
64
+ @operator.hash ^ @version.hash
65
+ end
66
+
67
+ def as_json
68
+ { operator: operator, version: version }
69
+ end
70
+
71
+ def to_s
72
+ "#{operator} #{version}"
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require "open3"
3
+ require_relative "errors"
4
+ require_relative "elf_analysis"
5
+ require_relative "symbols_file_parsing"
6
+ require_relative "utils"
7
+
8
+ class Debendencies
9
+ module Private
10
+ class << self
11
+ # Finds the package providing a specific library soname. This is done using `dpkg-query -S`.
12
+ #
13
+ # @return [String] The package name (like "libc6"), or nil if no package provides the library.
14
+ def find_package_providing_lib(soname)
15
+ output, error_output, status = Open3.capture3("dpkg-query", "-S", "*/#{soname}")
16
+ if !status.success?
17
+ if !status.signaled? && error_output.include?("no path found matching pattern")
18
+ return nil
19
+ else
20
+ raise Error, "Error finding packages that provide #{soname}: 'dpkg-query' failed: #{status}: #{error_output.chomp}"
21
+ end
22
+ end
23
+
24
+ # Output is in the following format:
25
+ # libfoo1:amd64: /usr/lib/x86_64-linux-gnu/libfoo.so.1
26
+ #
27
+ # The architecture could be omitted, like so:
28
+ # libfoo1: /usr/lib/x86_64-linux-gnu/libfoo1.so.1
29
+ #
30
+ # In theory, the output could contain multiple results, indicating alternatives.
31
+ # We don't support alternatives, so we just return the first result.
32
+ # See rationale in HOW-IT-WORKS.md.
33
+
34
+ return nil if output.empty?
35
+ line = output.split("\n").first
36
+ line.split(":", 2).first
37
+ end
38
+
39
+ # Finds the minimum version of the package that provides the necessary library symbols
40
+ # used by the given ELF files.
41
+ def find_min_package_version(soname, symbols_file_path, dependent_elf_file_paths, symbol_extraction_cache = {}, logger = nil)
42
+ dependent_symbols = extract_dynamic_symbols(dependent_elf_file_paths, symbol_extraction_cache)
43
+ return nil if dependent_symbols.empty?
44
+
45
+ max_used_package_version = nil
46
+
47
+ list_symbols(symbols_file_path, soname) do |dependency_symbol, package_version|
48
+ if dependent_symbols.include?(dependency_symbol)
49
+ logger&.info("Found in-use dependency symbol: #{dependency_symbol} (version: #{package_version})")
50
+ if max_used_package_version.nil? || package_version > max_used_package_version
51
+ max_used_package_version = package_version
52
+ end
53
+ end
54
+ end
55
+
56
+ max_used_package_version
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Debendencies
4
+ module Private
5
+ # Represents a package version in the format used by Debian packages.
6
+ # This class is only used internally to compare package versions.
7
+ # It's not exposed through the public API.
8
+ #
9
+ # Version number formats and comparison rules are defined in the Debian Policy Manual:
10
+ # https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
11
+ # https://pmhahn.github.io/dpkg-compare-versions/
12
+ class PackageVersion
13
+ include Comparable
14
+
15
+ attr_reader :epoch, :upstream_version, :debian_revision
16
+
17
+ def initialize(version_string)
18
+ @version_string = version_string
19
+ # Parse version into epoch, upstream_version, and debian_revision
20
+ @epoch, version = parse_epoch(version_string)
21
+ @upstream_version, @debian_revision = parse_upstream_and_revision(version)
22
+ end
23
+
24
+ def <=>(other)
25
+ # Compare epoch
26
+ result = @epoch <=> other.epoch
27
+ return result unless result == 0
28
+
29
+ # Compare upstream version
30
+ result = compare_upstream_version(@upstream_version, other.upstream_version)
31
+ return result unless result == 0
32
+
33
+ # Compare debian revision
34
+ compare_debian_revision(@debian_revision, other.debian_revision)
35
+ end
36
+
37
+ def to_s
38
+ @version_string
39
+ end
40
+
41
+ private
42
+
43
+ def parse_epoch(version)
44
+ if version.include?(":")
45
+ epoch, rest = version.split(":", 2)
46
+ [epoch.to_i, rest]
47
+ else
48
+ [0, version]
49
+ end
50
+ end
51
+
52
+ def parse_upstream_and_revision(version)
53
+ if version.include?("-")
54
+ upstream, debian_revision = version.rpartition("-").values_at(0, 2)
55
+ else
56
+ upstream = version
57
+ debian_revision = ""
58
+ end
59
+ [upstream, debian_revision]
60
+ end
61
+
62
+ def compare_upstream_version(ver1, ver2)
63
+ compare_version_parts(split_version(ver1), split_version(ver2))
64
+ end
65
+
66
+ def compare_debian_revision(ver1, ver2)
67
+ # Empty string counts as 0 in Debian revision comparison
68
+ ver1 = "0" if ver1.empty?
69
+ ver2 = "0" if ver2.empty?
70
+ compare_version_parts(split_version(ver1), split_version(ver2))
71
+ end
72
+
73
+ def split_version(version)
74
+ version.scan(/\d+|[a-zA-Z]+|~|[^\da-zA-Z~]+/)
75
+ end
76
+
77
+ def compare_version_parts(parts1, parts2)
78
+ parts1.zip(parts2).each do |part1, part2|
79
+ # Handle nil cases
80
+ part1 ||= ""
81
+ part2 ||= ""
82
+
83
+ if part1 =~ /^\d+$/ && part2 =~ /^\d+$/
84
+ result = part1.to_i <=> part2.to_i
85
+ else
86
+ result = compare_lexically(part1, part2)
87
+ end
88
+ return result unless result == 0
89
+ end
90
+
91
+ parts1.size <=> parts2.size
92
+ end
93
+
94
+ def compare_lexically(part1, part2)
95
+ # Special handling for '~' which sorts before everything else
96
+ return -1 if part1 == "~" && part2 != "~"
97
+ return 1 if part1 != "~" && part2 == "~"
98
+
99
+ part1 <=> part2
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ require_relative "package_version"
3
+
4
+ class Debendencies
5
+ module Private
6
+ class << self
7
+ # Parses a symbols file. Yields:
8
+ #
9
+ # - All symbols for the specified library soname.
10
+ # - The package version that provides that symbol.
11
+ #
12
+ # For example, it yields `["fopen@GLIBC_1.0", "5"]`.
13
+ #
14
+ # @param path [String] Path to the symbols file.
15
+ # @param soname [String] Soname of the library to yield symbols for.
16
+ # @yield [String, PackageVersion]
17
+ def list_symbols(path, soname)
18
+ File.open(path, "r:utf-8") do |f|
19
+ # Skips lines in the symbols file until we encounter the start of the section for the given library
20
+ f.each_line do |line|
21
+ break if line.start_with?("#{soname} ")
22
+ end
23
+
24
+ f.each_line do |line|
25
+ # Ignore alternative package specifiers and metadata fields like these:
26
+ #
27
+ # | libtinfo6 #MINVER#, libtinfo6 (<< 6.2~)
28
+ # * Build-Depends-Package: libncurses-dev
29
+ next if line =~ /^\s*[\|\*]/
30
+
31
+ # We look for a line like this:
32
+ #
33
+ # NCURSES6_TIC_5.0.19991023@NCURSES6_TIC_5.0.19991023 6.1
34
+ #
35
+ # Stop when we reach the section for next library
36
+ break if line !~ /^\s+(\S+)\s+(\S+)/
37
+
38
+ raw_symbol = $1
39
+ package_version_string = $2
40
+ yield [raw_symbol.sub(/@Base$/, ""), PackageVersion.new(package_version_string)]
41
+ end
42
+ end
43
+ end
44
+
45
+ def find_symbols_file(package_name, architecture)
46
+ path = File.join(symbols_dir, "#{package_name}:#{architecture}.symbols")
47
+ path if File.exist?(path)
48
+ end
49
+
50
+ private
51
+
52
+ # Mocked during tests.
53
+ def symbols_dir
54
+ "/var/lib/dpkg/info"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ require_relative "errors"
3
+
4
+ class Debendencies
5
+ module Private
6
+ ELF_MAGIC = String.new("\x7FELF").force_encoding("binary").freeze
7
+
8
+ class << self
9
+ def elf_file?(path)
10
+ File.open(path, "rb") do |f|
11
+ f.read(4) == ELF_MAGIC
12
+ end
13
+ end
14
+
15
+ def path_resembles_library?(path)
16
+ !!(path =~ /\.so($|\.\d+)/)
17
+ end
18
+
19
+ def dpkg_architecture
20
+ read_string_envvar("DEB_HOST_ARCH") ||
21
+ read_string_envvar("DEB_BUILD_ARCH") ||
22
+ @dpkg_architecture ||= begin
23
+ popen(["dpkg", "--print-architecture"],
24
+ spawn_error_message: "Error getting dpkg architecture: cannot spawn 'dpkg'",
25
+ fail_error_message: "Error getting dpkg architecture: 'dpkg --print-architecture' failed") do |io|
26
+ io.read.chomp
27
+ end
28
+ end
29
+ end
30
+
31
+ # Runs a command and yields its standard output as an IO object.
32
+ # Like IO.popen but with better error handling.
33
+ # On success, returns the result of the block, otherwise raises an Error.
34
+ def popen(command_args, spawn_error_message:, fail_error_message:)
35
+ begin
36
+ begin
37
+ io = IO.popen(command_args)
38
+ rescue SystemCallError => e
39
+ raise Error, "#{spawn_error_message}: #{e}"
40
+ end
41
+
42
+ result = yield io
43
+ ensure
44
+ io.close if io
45
+ end
46
+
47
+ if $?.success?
48
+ result
49
+ else
50
+ raise Error, "#{fail_error_message}: #{$?}"
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def read_string_envvar(name)
57
+ value = ENV[name]
58
+ value if value && !value.empty?
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Debendencies
4
+ VERSION_STRING = "1.0.0.pre1"
5
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+ require "set"
3
+ require_relative "debendencies/elf_analysis"
4
+ require_relative "debendencies/package_finding"
5
+ require_relative "debendencies/package_dependency"
6
+ require_relative "debendencies/errors"
7
+ require_relative "debendencies/utils"
8
+
9
+ class Debendencies
10
+ def initialize(logger: nil)
11
+ @logger = logger
12
+
13
+ # Shared libraries (sonames) that have been scanned.
14
+ @scanned_libs = Set.new
15
+
16
+ # Shared libraries (sonames) that the scanned ELF files depend on.
17
+ # Maps each soname to an array of ELF file path that depend on it (the dependents).
18
+ @dependency_libs = {}
19
+
20
+ @symbol_extraction_cache = {}
21
+ end
22
+
23
+ def scan(*paths)
24
+ paths.each do |path|
25
+ if File.directory?(path)
26
+ scan_directory(path)
27
+ else
28
+ scan_file(path)
29
+ end
30
+ end
31
+ end
32
+
33
+ # Resolves the Debian package dependencies of all scanned ELF files.
34
+ # Returns an array of PackageDependency objects:
35
+ #
36
+ # [
37
+ # PackageDependency.new('libc6', [VersionConstraint.new('>=', '2.28')]),
38
+ # PackageDependency.new('libfoo1'),
39
+ # ]
40
+ #
41
+ # @return [Array<PackageDependency>]
42
+ def resolve
43
+ result = []
44
+
45
+ @dependency_libs.each_pair do |dependency_soname, dependent_elf_file_paths|
46
+ # ELF files in a package could depend on libraries included in the same package,
47
+ # so omit resolving scanned libraries.
48
+ if @scanned_libs.include?(dependency_soname)
49
+ @logger&.info("Skipping dependency resolution for scanned library: #{dependency_soname}")
50
+ next
51
+ end
52
+
53
+ package_name = Private.find_package_providing_lib(dependency_soname)
54
+ raise Error, "Error resolving package dependencies: no package provides #{dependency_soname}" if package_name.nil?
55
+ @logger&.info("Resolved package providing #{dependency_soname}: #{package_name}")
56
+ version_constraints = maybe_create_version_constraints(package_name, dependency_soname, dependent_elf_file_paths)
57
+ @logger&.info("Resolved version constraints: #{version_constraints&.map { |vc| vc.as_json }.inspect}")
58
+
59
+ result << PackageDependency.new(package_name, version_constraints)
60
+ end
61
+
62
+ result.uniq!
63
+ result
64
+ end
65
+
66
+ private
67
+
68
+ def scan_directory(dir)
69
+ Dir.glob("**/*", base: dir) do |entry|
70
+ path = File.join(dir, entry)
71
+
72
+ if File.symlink?(path)
73
+ # Libraries tend to have multiple symlinks (e.g. libfoo.so -> libfoo.so.1 -> libfoo.so.1.2.3)
74
+ # and we only want to process libraries once, so ignore symlinks.
75
+ @logger&.warn("Skipping symlink: #{path}")
76
+ next
77
+ end
78
+
79
+ scan_file(path) if File.file?(path) && File.executable?(path)
80
+ end
81
+ end
82
+
83
+ def scan_file(path)
84
+ @logger&.info("Scanning ELF file: #{path}")
85
+ return @logger&.warn("Skipping non-ELF file: #{path}") if !Private.elf_file?(path)
86
+
87
+ soname, dependency_libs = Private.extract_soname_and_dependency_libs(path)
88
+ @logger&.info("Detected soname: #{soname || "(none)"}")
89
+ @logger&.info("Detected dependencies: #{dependency_libs.inspect}")
90
+ if Private.path_resembles_library?(path) && soname.nil?
91
+ raise Error, "Error scanning ELF file: cannot determine shared library name (soname) for #{path}"
92
+ end
93
+
94
+ @scanned_libs << soname if soname
95
+ dependency_libs.each do |dependency_soname|
96
+ dependents = (@dependency_libs[dependency_soname] ||= [])
97
+ dependents << path
98
+ end
99
+ end
100
+
101
+ def maybe_create_version_constraints(package_name, soname, dependent_elf_files)
102
+ symbols_file_path = Private.find_symbols_file(package_name, Private.dpkg_architecture)
103
+ if symbols_file_path
104
+ @logger&.info("Found symbols file for #{package_name}: #{symbols_file_path}")
105
+ min_version = Private.find_min_package_version(soname,
106
+ symbols_file_path,
107
+ dependent_elf_files,
108
+ @symbol_extraction_cache,
109
+ @logger)
110
+ [VersionConstraint.new(">=", min_version.to_s)] if min_version
111
+ else
112
+ @logger&.warn("No symbols file found for #{package_name}")
113
+ nil
114
+ end
115
+ end
116
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: debendencies
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre1
5
+ platform: ruby
6
+ authors:
7
+ - Hongli Lai
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: set
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: stringio
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: optparse
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0.4'
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '2'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0.4'
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '2'
75
+ - !ruby/object:Gem::Dependency
76
+ name: open3
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0.1'
82
+ - - "<"
83
+ - !ruby/object:Gem::Version
84
+ version: '2'
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0.1'
92
+ - - "<"
93
+ - !ruby/object:Gem::Version
94
+ version: '2'
95
+ - !ruby/object:Gem::Dependency
96
+ name: tmpdir
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0.1'
102
+ - - "<"
103
+ - !ruby/object:Gem::Version
104
+ version: '2'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0.1'
112
+ - - "<"
113
+ - !ruby/object:Gem::Version
114
+ version: '2'
115
+ - !ruby/object:Gem::Dependency
116
+ name: fileutils
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '1'
122
+ type: :runtime
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '1'
129
+ - !ruby/object:Gem::Dependency
130
+ name: tempfile
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0.1'
136
+ - - "<"
137
+ - !ruby/object:Gem::Version
138
+ version: '2'
139
+ type: :runtime
140
+ prerelease: false
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0.1'
146
+ - - "<"
147
+ - !ruby/object:Gem::Version
148
+ version: '2'
149
+ description: Scans executables and shared libraries for their shared library dependencies,
150
+ and outputs a list of Debian package names that provide those libraries.
151
+ email:
152
+ - hongli@hongli.nl
153
+ executables:
154
+ - debendencies
155
+ extensions: []
156
+ extra_rdoc_files: []
157
+ files:
158
+ - bin/debendencies
159
+ - lib/debendencies.rb
160
+ - lib/debendencies/cli.rb
161
+ - lib/debendencies/elf_analysis.rb
162
+ - lib/debendencies/errors.rb
163
+ - lib/debendencies/package_dependency.rb
164
+ - lib/debendencies/package_finding.rb
165
+ - lib/debendencies/package_version.rb
166
+ - lib/debendencies/symbols_file_parsing.rb
167
+ - lib/debendencies/utils.rb
168
+ - lib/debendencies/version.rb
169
+ homepage: https://github.com/FooBarWidget/debendencies
170
+ licenses:
171
+ - MIT
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.5.3
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: Debian package shared library dependencies inferer
192
+ test_files: []