Module: Debendencies::Private
- Defined in:
- lib/debendencies/utils.rb,
lib/debendencies/elf_analysis.rb,
lib/debendencies/package_finding.rb,
lib/debendencies/package_version.rb,
lib/debendencies/symbols_file_parsing.rb
Defined Under Namespace
Classes: PackageVersion
Constant Summary collapse
- ELF_MAGIC =
String.new("\x7FELF").force_encoding("binary").freeze
Class Method Summary collapse
- .dpkg_architecture ⇒ Object
- .elf_file?(path) ⇒ Boolean
-
.extract_dynamic_symbols(paths, cache = {}) ⇒ Set<String>
Extracts dynamic symbols from ELF files using ‘nm`.
-
.extract_soname_and_dependency_libs(path) ⇒ String, Array<Array<String>>
Extracts from an ELF file using ‘objdump`:.
-
.find_min_package_version(soname, symbols_file_path, dependent_elf_file_paths, symbol_extraction_cache = {}, logger = nil) ⇒ Object
Finds the minimum version of the package that provides the necessary library symbols used by the given ELF files.
-
.find_package_providing_lib(soname) ⇒ String
Finds the package providing a specific library soname.
- .find_symbols_file(package_name, architecture) ⇒ Object
-
.list_symbols(path, soname) {|String, PackageVersion| ... } ⇒ Object
Parses a symbols file.
- .path_resembles_library?(path) ⇒ Boolean
-
.popen(command_args, spawn_error_message:, fail_error_message:) ⇒ Object
Runs a command and yields its standard output as an IO object.
Class Method Details
.dpkg_architecture ⇒ Object
19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/debendencies/utils.rb', line 19 def dpkg_architecture read_string_envvar("DEB_HOST_ARCH") || read_string_envvar("DEB_BUILD_ARCH") || @dpkg_architecture ||= begin popen(["dpkg", "--print-architecture"], spawn_error_message: "Error getting dpkg architecture: cannot spawn 'dpkg'", fail_error_message: "Error getting dpkg architecture: 'dpkg --print-architecture' failed") do |io| io.read.chomp end end end |
.elf_file?(path) ⇒ Boolean
9 10 11 12 13 |
# File 'lib/debendencies/utils.rb', line 9 def elf_file?(path) File.open(path, "rb") do |f| f.read(4) == ELF_MAGIC end end |
.extract_dynamic_symbols(paths, cache = {}) ⇒ Set<String>
Extracts dynamic symbols from ELF files using ‘nm`.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/debendencies/elf_analysis.rb', line 43 def extract_dynamic_symbols(paths, cache = {}) result = Set.new paths.each do |path| subresult = cache[path] ||= popen(["nm", "-D", path], spawn_error_message: "Error extracting dynamic symbols from #{path}: cannot spawn 'nm'", fail_error_message: "Error extracting dynamic symbols from #{path}: 'nm' failed") do |io| io.each_line.lazy.map do |line| # Line is in the following format: # # U waitpid # 0000000000126190 B want_pending_command # ^^^^^^^^^^^^^^^^^^^^ # we want to extract this $1 if line =~ /^\S*\s+[A-Za-z]\s+(.+)/ end.compact.to_set end result.merge(subresult) end result end |
.extract_soname_and_dependency_libs(path) ⇒ String, Array<Array<String>>
Extracts from an ELF file using ‘objdump`:
-
The ELF file’s own soname, if possible. Can be nil.
-
The list of shared library dependencies (sonames).
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/debendencies/elf_analysis.rb', line 17 def extract_soname_and_dependency_libs(path) popen(["objdump", "-p", path], spawn_error_message: "Error scanning ELF file dependencies: cannot spawn 'objdump'", fail_error_message: "Error scanning ELF file dependencies: 'objdump' failed") do |io| soname = nil dependent_libs = [] io.each_line do |line| case line when /^\s*SONAME\s+(.+)$/ soname = $1.strip when /^\s*NEEDED\s+(.+)$/ dependent_libs << $1.strip end end [soname, dependent_libs] end end |
.find_min_package_version(soname, symbols_file_path, dependent_elf_file_paths, symbol_extraction_cache = {}, logger = nil) ⇒ Object
Finds the minimum version of the package that provides the necessary library symbols used by the given ELF files.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/debendencies/package_finding.rb', line 41 def find_min_package_version(soname, symbols_file_path, dependent_elf_file_paths, symbol_extraction_cache = {}, logger = nil) dependent_symbols = extract_dynamic_symbols(dependent_elf_file_paths, symbol_extraction_cache) return nil if dependent_symbols.empty? max_used_package_version = nil list_symbols(symbols_file_path, soname) do |dependency_symbol, package_version| if dependent_symbols.include?(dependency_symbol) logger&.info("Found in-use dependency symbol: #{dependency_symbol} (version: #{package_version})") if max_used_package_version.nil? || package_version > max_used_package_version max_used_package_version = package_version end end end max_used_package_version end |
.find_package_providing_lib(soname) ⇒ String
Finds the package providing a specific library soname. This is done using ‘dpkg-query -S`.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/debendencies/package_finding.rb', line 14 def find_package_providing_lib(soname) output, error_output, status = Open3.capture3("dpkg-query", "-S", "*/#{soname}") if !status.success? if !status.signaled? && error_output.include?("no path found matching pattern") return nil else raise Error, "Error finding packages that provide #{soname}: 'dpkg-query' failed: #{status}: #{error_output.chomp}" end end # Output is in the following format: # libfoo1:amd64: /usr/lib/x86_64-linux-gnu/libfoo.so.1 # # The architecture could be omitted, like so: # libfoo1: /usr/lib/x86_64-linux-gnu/libfoo1.so.1 # # In theory, the output could contain multiple results, indicating alternatives. # We don't support alternatives, so we just return the first result. # See rationale in HOW-IT-WORKS.md. return nil if output.empty? line = output.split("\n").first line.split(":", 2).first end |
.find_symbols_file(package_name, architecture) ⇒ Object
45 46 47 48 |
# File 'lib/debendencies/symbols_file_parsing.rb', line 45 def find_symbols_file(package_name, architecture) path = File.join(symbols_dir, "#{package_name}:#{architecture}.symbols") path if File.exist?(path) end |
.list_symbols(path, soname) {|String, PackageVersion| ... } ⇒ Object
Parses a symbols file. Yields:
-
All symbols for the specified library soname.
-
The package version that provides that symbol.
For example, it yields ‘[“fopen@GLIBC_1.0”, “5”]`.
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 |
# File 'lib/debendencies/symbols_file_parsing.rb', line 17 def list_symbols(path, soname) File.open(path, "r:utf-8") do |f| # Skips lines in the symbols file until we encounter the start of the section for the given library f.each_line do |line| break if line.start_with?("#{soname} ") end f.each_line do |line| # Ignore alternative package specifiers and metadata fields like these: # # | libtinfo6 #MINVER#, libtinfo6 (<< 6.2~) # * Build-Depends-Package: libncurses-dev next if line =~ /^\s*[\|\*]/ # We look for a line like this: # # NCURSES6_TIC_5.0.19991023@NCURSES6_TIC_5.0.19991023 6.1 # # Stop when we reach the section for next library break if line !~ /^\s+(\S+)\s+(\S+)/ raw_symbol = $1 package_version_string = $2 yield [raw_symbol.sub(/@Base$/, ""), PackageVersion.new(package_version_string)] end end end |
.path_resembles_library?(path) ⇒ Boolean
15 16 17 |
# File 'lib/debendencies/utils.rb', line 15 def path_resembles_library?(path) !!(path =~ /\.so($|\.\d+)/) end |
.popen(command_args, spawn_error_message:, fail_error_message:) ⇒ Object
Runs a command and yields its standard output as an IO object. Like IO.popen but with better error handling. On success, returns the result of the block, otherwise raises an Error.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/debendencies/utils.rb', line 34 def popen(command_args, spawn_error_message:, fail_error_message:) begin begin io = IO.popen(command_args) rescue SystemCallError => e raise Error, "#{spawn_error_message}: #{e}" end result = yield io ensure io.close if io end if $?.success? result else raise Error, "#{fail_error_message}: #{$?}" end end |