Top Level Namespace

Defined Under Namespace

Modules: PQCrypto

Instance Method Summary collapse

Instance Method Details

#argument_value(name) ⇒ Object



100
101
102
103
104
105
106
107
# File 'ext/pq_crypto_seal/extconf.rb', line 100

def argument_value(name)
  prefix = "--#{name}"
  ARGV.each_with_index do |argument, index|
    return argument.split("=", 2)[1] if argument.start_with?("#{prefix}=")
    return ARGV[index + 1] if argument == prefix && ARGV[index + 1]
  end
  nil
end

#command_output(*command) ⇒ Object

Ruby 2.7 installations on macOS are often built against Homebrew OpenSSL 1.1. Their RbConfig can inject that old prefix into every extension build, even when OpenSSL 3 is installed separately. Select one complete OpenSSL 3 installation, remove competing OpenSSL header paths, and link the selected libcrypto by its absolute path so headers and library cannot silently come from different roots.



16
17
18
19
20
# File 'ext/pq_crypto_seal/extconf.rb', line 16

def command_output(*command)
  IO.popen(command, err: File::NULL, &:read).to_s.strip
rescue StandardError
  ""
end

#configure_selected_openssl!(installation) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'ext/pq_crypto_seal/extconf.rb', line 229

def configure_selected_openssl!(installation)
  include_directory = installation.fetch(:include_directory)
  library_directory = installation.fetch(:library_directory)
  crypto_library = installation.fetch(:crypto_library)

  remove_competing_openssl_headers!(include_directory)

  # Do not call dir_config("openssl") here. On Ruby 2.7 it can reuse the
  # --with-openssl-dir value from Ruby's own build and reintroduce OpenSSL 1.1.
  $INCFLAGS = "-I#{Shellwords.escape(include_directory)} #{$INCFLAGS}".strip
  $CPPFLAGS = "-I#{Shellwords.escape(include_directory)} #{$CPPFLAGS}".strip
  $LIBPATH.unshift(library_directory) unless $LIBPATH.include?(library_directory)
  $LOCAL_LIBS = "#{Shellwords.escape(crypto_library)} #{$LOCAL_LIBS}".strip

  unless windows_host? || library_directory.start_with?("/usr/lib")
    $LDFLAGS = "-Wl,-rpath,#{Shellwords.escape(library_directory)} #{$LDFLAGS}".strip
  end

  puts "OpenSSL source: #{installation.fetch(:source)}"
  puts "OpenSSL prefix: #{installation[:prefix]}" if installation[:prefix]
  puts "OpenSSL include dir: #{include_directory}"
  puts "OpenSSL library dir: #{library_directory}"
  puts "OpenSSL libcrypto: #{crypto_library}"
end

#discover_openssl3_installationObject



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
# File 'ext/pq_crypto_seal/extconf.rb', line 145

def discover_openssl3_installation
  explicit = explicit_openssl_installation
  return explicit if explicit

  candidates = []
  if host_os =~ /darwin/i && find_executable("brew")
    candidates << command_output("brew", "--prefix", "openssl@3")
    candidates << command_output("brew", "--prefix", "openssl")
  end
  candidates.concat(
    %w[
      /opt/homebrew/opt/openssl@3
      /usr/local/opt/openssl@3
      /opt/homebrew/opt/openssl
      /usr/local/opt/openssl
    ]
  ) if host_os =~ /darwin/i

  candidates.compact.map(&:strip).reject(&:empty?).uniq.each do |prefix|
    installation = openssl_prefix_installation(prefix)
    return installation.merge(source: "prefix", prefix: File.expand_path(prefix)) if installation
  end

  pkg_config_openssl_installation ||
    openssl_prefix_installation("/usr/local")&.merge(source: "system prefix", prefix: "/usr/local") ||
    openssl_prefix_installation("/usr")&.merge(source: "system prefix", prefix: "/usr")
end

#explicit_openssl_installationObject



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'ext/pq_crypto_seal/extconf.rb', line 109

def explicit_openssl_installation
  explicit_root = argument_value("with-openssl-dir") || ENV["OPENSSL_ROOT_DIR"] || ENV["OPENSSL_DIR"]
  if explicit_root && !explicit_root.to_s.strip.empty?
    installation = openssl_prefix_installation(explicit_root)
    abort <<~MSG unless installation
      #{explicit_root.inspect} does not point to a complete OpenSSL 3 installation.

      Expected OpenSSL 3 headers below <prefix>/include/openssl and libcrypto
      below <prefix>/lib, <prefix>/lib64, or a Linux multiarch lib directory.
    MSG
    return installation.merge(source: "explicit prefix")
  end

  explicit_include = argument_value("with-openssl-include")
  explicit_library = argument_value("with-openssl-lib")
  return nil unless explicit_include || explicit_library

  abort "Both --with-openssl-include and --with-openssl-lib are required together" unless explicit_include && explicit_library
  installation = openssl_installation(explicit_include, explicit_library)
  abort "Explicit OpenSSL include/lib paths do not contain a matching OpenSSL 3 headers + libcrypto pair" unless installation

  installation.merge(source: "explicit include/lib")
end

#host_osObject



22
23
24
# File 'ext/pq_crypto_seal/extconf.rb', line 22

def host_os
  RbConfig::CONFIG.fetch("host_os", "")
end

#include_paths_from_flags(flags) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'ext/pq_crypto_seal/extconf.rb', line 179

def include_paths_from_flags(flags)
  tokens = Shellwords.split(flags.to_s)
  paths = []
  index = 0

  while index < tokens.length
    token = tokens[index]
    case token
    when "-I", "-isystem"
      paths << tokens[index + 1] if tokens[index + 1]
      index += 2
      next
    when /\A-I(.+)\z/
      paths << Regexp.last_match(1)
    when /\A-isystem(.+)\z/
      paths << Regexp.last_match(1)
    end
    index += 1
  end

  paths
rescue ArgumentError
  []
end

#normalize_existing_path(path) ⇒ Object



173
174
175
176
177
# File 'ext/pq_crypto_seal/extconf.rb', line 173

def normalize_existing_path(path)
  File.realpath(path)
rescue Errno::ENOENT
  File.expand_path(path)
end

#openssl_header_major(include_directory) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'ext/pq_crypto_seal/extconf.rb', line 55

def openssl_header_major(include_directory)
  header = File.join(include_directory, "openssl", "opensslv.h")
  return nil unless File.file?(header)

  contents = File.read(header)
  major = contents[/^\s*#\s*define\s+OPENSSL_VERSION_MAJOR\s+(\d+)/, 1]
  return major.to_i if major

  # OpenSSL 1.x has no OPENSSL_VERSION_MAJOR. Parse the high hexadecimal nibble
  # only to reject it cleanly; OpenSSL 3 installations always define the major.
  number = contents[/^\s*#\s*define\s+OPENSSL_VERSION_NUMBER\s+0x([0-9A-Fa-f]+)L?/, 1]
  number ? (number.to_i(16) >> 28) & 0xF : nil
rescue StandardError
  nil
end

#openssl_installation(include_directory, library_directory) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'ext/pq_crypto_seal/extconf.rb', line 71

def openssl_installation(include_directory, library_directory)
  return nil unless include_directory && library_directory

  include_directory = File.expand_path(include_directory)
  library_directory = File.expand_path(library_directory)
  return nil unless openssl_header_major(include_directory).to_i >= 3

  crypto_library = openssl_library_file(library_directory)
  return nil unless crypto_library

  {
    include_directory: include_directory,
    library_directory: library_directory,
    crypto_library: crypto_library
  }
end

#openssl_library_dirs(prefix) ⇒ Object



30
31
32
33
34
# File 'ext/pq_crypto_seal/extconf.rb', line 30

def openssl_library_dirs(prefix)
  dirs = Dir.glob(File.join(prefix, "lib", "*-linux-gnu"))
  dirs.concat([File.join(prefix, "lib64"), File.join(prefix, "lib")])
  dirs.select { |directory| File.directory?(directory) }.uniq
end

#openssl_library_file(directory) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'ext/pq_crypto_seal/extconf.rb', line 36

def openssl_library_file(directory)
  candidates = [
    File.join(directory, "libcrypto.so.3"),
    *Dir.glob(File.join(directory, "libcrypto.so.3.*")).sort,
    File.join(directory, "libcrypto.so"),
    File.join(directory, "libcrypto.3.dylib"),
    *Dir.glob(File.join(directory, "libcrypto.3.*.dylib")).sort,
    File.join(directory, "libcrypto.dylib"),
    File.join(directory, "libcrypto.a"),
    File.join(directory, "libcrypto.dll.a"),
    File.join(directory, "crypto.lib")
  ]

  path = candidates.find { |candidate| File.file?(candidate) }
  path && File.realpath(path)
rescue Errno::ENOENT
  nil
end

#openssl_prefix_installation(prefix) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
# File 'ext/pq_crypto_seal/extconf.rb', line 88

def openssl_prefix_installation(prefix)
  return nil if prefix.nil? || prefix.to_s.strip.empty?

  prefix = File.expand_path(prefix.to_s.strip)
  include_directory = File.join(prefix, "include")
  openssl_library_dirs(prefix).each do |library_directory|
    installation = openssl_installation(include_directory, library_directory)
    return installation.merge(prefix: prefix) if installation
  end
  nil
end

#pkg_config_openssl_installationObject



133
134
135
136
137
138
139
140
141
142
143
# File 'ext/pq_crypto_seal/extconf.rb', line 133

def pkg_config_openssl_installation
  return nil unless find_executable("pkg-config")

  version = command_output("pkg-config", "--modversion", "openssl")
  return nil if version.empty? || version.split(".").first.to_i < 3

  include_directory = command_output("pkg-config", "--variable=includedir", "openssl")
  library_directory = command_output("pkg-config", "--variable=libdir", "openssl")
  installation = openssl_installation(include_directory, library_directory)
  installation && installation.merge(source: "pkg-config #{version}")
end

#remove_competing_openssl_headers!(selected_include_directory) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'ext/pq_crypto_seal/extconf.rb', line 212

def remove_competing_openssl_headers!(selected_include_directory)
  selected = normalize_existing_path(selected_include_directory)
  competing = [$INCFLAGS, $CPPFLAGS, $CFLAGS]
              .flat_map { |flags| include_paths_from_flags(flags) }
              .uniq
              .select do |path|
    File.file?(File.join(path, "openssl", "opensslv.h")) && normalize_existing_path(path) != selected
  end

  competing.each do |path|
    $INCFLAGS = strip_include_path($INCFLAGS, path)
    $CPPFLAGS = strip_include_path($CPPFLAGS, path)
    $CFLAGS = strip_include_path($CFLAGS, path)
    puts "Ignoring competing OpenSSL headers: #{path}"
  end
end

#strip_include_path(flags, path) ⇒ Object



204
205
206
207
208
209
210
# File 'ext/pq_crypto_seal/extconf.rb', line 204

def strip_include_path(flags, path)
  escaped = Regexp.escape(path)
  flags.to_s
       .gsub(/(?:\A|\s)-I\s*#{escaped}(?=\s|\z)/, " ")
       .gsub(/(?:\A|\s)-isystem\s*#{escaped}(?=\s|\z)/, " ")
       .strip
end

#windows_host?Boolean

Returns:

  • (Boolean)


26
27
28
# File 'ext/pq_crypto_seal/extconf.rb', line 26

def windows_host?
  host_os =~ /mswin|mingw|cygwin/i
end