blob: c8e1a5de185cc530360c613f8ef875855f0fd581 [file] [edit]
#! /usr/bin/env ruby
# frozen_string_literal: true
# Compare the four libraries' public exports with the canonical ELF symbol
# manifests. Native inspection tools supply the platform-specific records;
# command failures are fatal, so partial output cannot pass a comparison.
require 'open3'
def command_output(*command)
output, status = Open3.capture2({'LANG' => 'C', 'LC_ALL' => 'C'}, *command)
abort "Command failed (#{status}): #{command.join(' ')}" unless status.success?
output
end
def find_dumpbin
# An activated MSVC environment may already provide dumpbin. Otherwise use
# Visual Studio's locator; building with CMake does not activate that shell.
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |directory|
candidate = File.join(directory, 'dumpbin.exe')
return candidate if File.file?(candidate)
end
vswhere = File.join(ENV.fetch('ProgramFiles(x86)', 'C:/Program Files (x86)'),
'Microsoft Visual Studio/Installer/vswhere.exe')
paths = command_output(vswhere, '-utf8', '-latest', '-requires',
'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
'-find', 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe')
abort 'vswhere did not find dumpbin.exe' if paths.strip.empty?
paths.lines.first.strip
end
def solaris_symbols(versions_output, symbols_output)
# Solaris nm omits symbol versions. elfdump supplies a version-name table
# and dynamic-symbol rows: index/value/size/type/bind/oth/ver/shndx/name.
versions = {}
versions_output.each_line do |line|
match = line.match(/^\s*\[(\d+)\]\s+(\S+)/)
versions[match[1]] = match[2] if match
end
symbols = []
symbols_output.each_line do |line|
next unless line.match?(/^\s*\[\d+\]/)
fields = line.split
abort "Unrecognized elfdump symbol: #{line}" if fields.length < 9
version, section, name = fields.values_at(6, 7, 8)
next if section == 'UNDEF' # Includes the unnamed null symbol.
abort "Missing name in elfdump symbol: #{line}" unless name
type = {'.text' => 'T', 'ABS' => 'A'}.fetch(section, section)
# Keep unmapped version indexes visible: unversioned exports must not
# accidentally compare equal to the versioned public API.
symbols << "#{type} #{name}@@#{versions.fetch(version, version)}"
end
symbols
end
def nm_symbols(output)
# Addresses vary with the build. Keep only the symbol type and name,
# including any ELF version suffix, for comparison with the public API.
output.lines.map {|line| line.strip.sub(/\A[0-9a-fA-F]+\s+/, '') }.reject(&:empty?)
end
def dll_symbols(output)
# dumpbin's export rows begin with ordinal, hint and RVA; ignore its headings
# and section summary, preserving the actual exported name verbatim.
output.lines.each_with_object([]) do |line, symbols|
match = line.match(/^\s*\d+\s+[0-9a-fA-F]+\s+[0-9a-fA-F]+\s+(\S+)/)
symbols << "T #{match[1]}" if match
end
end
def public_symbols(symbols, darwin)
symbols.each_with_object([]) do |line, result|
type, name = line.split(' ', 2)
abort "Unrecognized symbol record: #{line}" unless type && name
# Imports and unresolved weak references are not exports. Absolute PCRE2_
# symbols name ELF version definitions rather than public functions/data.
next if %w[U w].include?(type)
next if type == 'A' && name.start_with?('PCRE2_')
# These are linker-generated ELF bookkeeping symbols, not PCRE2 exports.
next if %w[_init _fini __bss_start _end _DYNAMIC _GLOBAL_OFFSET_TABLE_
_PROCEDURE_LINKAGE_TABLE_ _edata _etext].include?(name.split('@', 2).first)
name = name.delete_prefix('_') if darwin
result << "#{type} #{name}"
end.sort_by(&:b)
end
def main
abort "Usage: ruby #{$PROGRAM_NAME} <library dir> <manifest dir>" unless ARGV.length == 2
input_dir, manifest_dir = ARGV
windows = RUBY_PLATFORM.match?(/mswin|mingw/)
darwin = RUBY_PLATFORM.include?('darwin')
solaris = RUBY_PLATFORM.match?(/solaris|sunos/)
dumpbin = find_dumpbin if windows
extension = windows ? 'dll' : (darwin ? 'dylib' : 'so')
%w[8 16 32 posix].each do |width|
base = "manifest-libpcre2-#{width}.so"
library = File.join(input_dir, "#{windows ? '' : 'lib'}pcre2-#{width}.#{extension}")
abort "Library does not exist: #{library}" unless File.file?(library)
if windows
symbols = dll_symbols(command_output(dumpbin, '/exports', library))
elsif solaris
symbols = solaris_symbols(command_output('elfdump', '-v', library),
command_output('elfdump', '-T', 'SHT_DYNSYM', library))
else
tool = RUBY_PLATFORM.include?('freebsd') ? 'llvm-nm' : 'nm'
arguments = ['-B', darwin ? '-g' : '-D']
arguments << '--with-symbol-versions' if RUBY_PLATFORM.include?('linux')
symbols = nm_symbols(command_output(tool, *arguments, library))
end
actual = public_symbols(symbols, darwin)
expected = File.readlines(File.join(manifest_dir, base), encoding: 'UTF-8').map(&:chomp)
abort "Empty symbol manifest: #{base}" if expected.empty?
# Mach-O and PE do not carry the ELF symbol-version suffixes.
expected = expected.map {|line| line.sub(/@.*$/, '') } if windows || darwin
File.binwrite("#{base}.expected", expected.join("\n") + "\n")
File.binwrite("#{base}.actual", actual.join("\n") + "\n")
if expected != actual
warn 'Missing or changed exports (-), unexpected exports (+):'
(expected - actual).each {|line| warn "- #{line}" }
(actual - expected).each {|line| warn "+ #{line}" }
abort "Symbols for #{library} differ; compare #{base}.expected and #{base}.actual"
end
puts "Shared object contents for #{library} match expected"
File.delete("#{base}.expected", "#{base}.actual")
end
end
main if $PROGRAM_NAME == __FILE__