Unverified Commit 31e53468 authored by Mislav Marohnić's avatar Mislav Marohnić
Browse files

Automatically link to Homebrew OpenSSL

If a system OpenSSL version was not found or is at version that is incompatible with a Ruby being installed, ruby-build would typically download and compile a new OpenSSL version scoped to that Ruby installation.

Now the `needs_openssl` condition will also check for Homebrew-installed OpenSSL and automatically link to the first one found that satisfies the version requirement. This primarily helps speed up Ruby installation on macOS where the system OpenSSL is never compatible and where Homebrew is a de-facto standard package manager.
parent 8f294c43
Loading
Loading
Loading
Loading
+88 −30
Original line number Diff line number Diff line
@@ -1023,6 +1023,8 @@ use_freebsd_libffi() {
  fi
}

# macOS prevents linking to its system OpenSSL/LibreSSL installation, so
# it's basically useless for Ruby purposes.
has_broken_mac_openssl() {
  is_mac || return 1
  local openssl_version
@@ -1030,47 +1032,103 @@ has_broken_mac_openssl() {
  [[ $openssl_version = "OpenSSL 0.9.8"?* || $openssl_version = "LibreSSL"* ]]
}

# Detect the OpenSSL version that a compiler can reasonably link to.
system_openssl_version() {
  local version_text
  version_text=$(printf '#include <openssl/opensslv.h>\nOPENSSL_VERSION_TEXT\n' | cc -xc -E - 2>/dev/null || true)
  if [[ $version_text == *"OpenSSL "* ]]; then
    local version=${version_text#*OpenSSL }
    # shellcheck disable=SC2001
    sed 's/[^0-9]//g' <<<"${version%% *}" | sed 's/^0*//'
  else
    echo "No system openssl version was found, ensure openssl headers are installed (https://github.com/rbenv/ruby-build/wiki#suggested-build-environment)" >&2
    echo 000
  cc -xc -E - <<EOF 2>/dev/null | sed -n 's/OpenSSL \([0-9][0-9.]*\).*/\1/p'
#include <openssl/opensslv.h>
OPENSSL_VERSION_TEXT
EOF
}

# List all Homebrew-installed OpenSSL versions and their filesystem prefixes.
homebrew_openssl_versions() {
  local formula version prefix
  # https://github.com/orgs/Homebrew/discussions/4845
  brew list 2>/dev/null | grep '^openssl@' | while read -r formula; do
    prefix="$(brew --prefix "$formula" 2>/dev/null || true)"
    [ -n "$prefix" ] || continue
    version="$("$prefix"/bin/openssl version 2>/dev/null | sed -n 's/OpenSSL \([0-9][0-9.]*\).*/\1/p')"
    [ -z "$version" ] || printf '%s %s %s\n' "$formula" "$version" "$prefix"
  done
}

# Normalizes "X.Y.Z" into a comparable numeric value. Does not support prereleases.
# See also osx_version, require_java
normalize_semver() {
  local ver
  IFS=. read -d "" -r -a ver <<<"$1" || true
  IFS="$OLDIFS"
  # 3.1.23 -> 300_123
  echo $(( ver[0]*100000 + ver[1]*100 + ver[2] ))
}

# Checks if system OpenSSL does NOT satisfy the version requirement
# between lower and upper bounds. This is used by build definitions to
# conditionally install per-ruby OpenSSL.
#
# If a compatible Homebrew-installed OpenSSL version is found during
# checking, Ruby will be linked to it and the check will return false.
needs_openssl() {
  [[ "$RUBY_CONFIGURE_OPTS ${RUBY_CONFIGURE_OPTS_ARRAY[*]}" != *--with-openssl-dir=* ]] || return 1

  local system_version
  if ! has_broken_mac_openssl; then
    system_version="$(system_openssl_version)"
  fi

  # With no arguments, any system OpenSSL satisfies the check.
  if [ $# -lt 2 ]; then
    [ -z "$system_version" ] || return 1
    return 0
  fi

  local lower_bound upper_bound
  lower_bound="$(normalize_semver "${2%-*}")"
  upper_bound="${2#*-}"
  upper_bound="$(normalize_semver "${upper_bound//.x/.99}")"
  system_version="$(normalize_semver "$system_version")"

  # Return early if system openssl satisfies the requirement.
  (( system_version < lower_bound || system_version >= upper_bound )) || return 1

  # Look for the latest Homebrew-installed OpenSSL that satisfies the requirement
  local brew_installs
  brew_installs="$(homebrew_openssl_versions)"
  [ -n "$brew_installs" ] || return 0

  # Link to the highest-matching Homebrew OpenSSL
  local versions homebrew_version formula version prefix
  # shellcheck disable=SC2207
  versions=( $(awk '{print $2}' <<<"$brew_installs" | sort_versions) )
  local index="${#versions[@]}"
  while [ $((index--)) -ge 0 ]; do
    homebrew_version="$(normalize_semver "${versions[index]}")"
    (( homebrew_version >= lower_bound && homebrew_version < upper_bound )) || continue
    while read -r formula version prefix; do
      [ "$version" = "${versions[index]}" ] || continue
      echo "ruby-build: using $formula from homebrew"
      package_option ruby configure --with-openssl-dir="$prefix"
      return 1
    done <<<"$brew_installs"
  done
}

# openssl gem 1.1.1
# Kept for backward compatibility with 3rd-party Ruby definitions.
needs_openssl_096_102() {
  [[ "$RUBY_CONFIGURE_OPTS ${RUBY_CONFIGURE_OPTS_ARRAY[*]}" == *--with-openssl-dir=* ]] && return 1
  has_broken_mac_openssl && return 0

  local version
  version="$(system_openssl_version)"
  (( version < 96 || version >= 110 ))
  # openssl gem 1.1.1
  needs_openssl "$1" "0.9.6-1.0.x"
}

# openssl gem 2.2.1
# Kept for backward compatibility with 3rd-party Ruby definitions.
needs_openssl_101_111() {
  [[ "$RUBY_CONFIGURE_OPTS ${RUBY_CONFIGURE_OPTS_ARRAY[*]}" == *--with-openssl-dir=* ]] && return 1
  has_broken_mac_openssl && return 0

  local version
  version="$(system_openssl_version)"
  (( version < 101 || version >= 300 ))
  # openssl gem 2.2.1
  needs_openssl "$1" "1.0.1-1.x.x"
}

# openssl gem 3.0.0
# Kept for backward compatibility with 3rd-party Ruby definitions.
needs_openssl_102_300() {
  [[ "$RUBY_CONFIGURE_OPTS ${RUBY_CONFIGURE_OPTS_ARRAY[*]}" == *--with-openssl-dir=* ]] && return 1
  has_broken_mac_openssl && return 0

  local version
  version="$(system_openssl_version)"
  (( version < 102 || version >= 400 ))
  # openssl gem 3.0.0
  needs_openssl "$1" "1.0.2-3.x.x"
}

# Kept for backward compatibility with 3rd-party Ruby definitions.
+79 −0
Original line number Diff line number Diff line
@@ -316,6 +316,33 @@ make install
OUT
}

@test "use system OpenSSL" {
  cached_tarball "ruby-2.0.0" configure

  stub_repeated uname '-s : echo Linux'
  stub_repeated brew false
  # shellcheck disable=SC2016
  stub cc '-xc -E - : [[ "$(cat)" == *OPENSSL_VERSION_TEXT* ]] && printf "# <unrelated> 4.0.2\nOpenSSL 1.0.3a  1 Aug 202\n0 errors.\n"'
  stub_make_install

  mkdir -p "$INSTALL_ROOT"/openssl/ssl # OPENSSLDIR
  run_inline_definition <<DEF
install_package "openssl-1.1.1w" "https://www.openssl.org/source/openssl-1.1.1w.tar.gz" openssl --if needs_openssl_102_300
install_package "ruby-2.0.0" "http://ruby-lang.org/ruby/2.0/ruby-2.0.0.tar.gz"
DEF
  assert_success

  unstub uname
  unstub brew
  unstub make

  assert_build_log <<OUT
ruby-2.0.0: [--prefix=$INSTALL_ROOT]
make -j 2
make install
OUT
}

@test "install bundled OpenSSL" {
  cached_tarball "openssl-1.1.1w" config
  cached_tarball "ruby-2.0.0" configure
@@ -339,6 +366,8 @@ DEF

  unstub uname
  unstub brew
  unstub cc
  # unstub openssl
  unstub make

  assert_build_log <<OUT
@@ -375,6 +404,56 @@ make install
OUT
}

@test "link to Homebrew OpenSSL" {
  cached_tarball "ruby-2.0.0" configure

  local homebrew_prefix="${TMP}/homebrew"
  executable "${homebrew_prefix}/opt/openssl@3/bin/openssl" <<EXE
#!/$BASH
[ "\$1" = "version" ] || exit 1
echo 'OpenSSL 3.2.1  20 Dec 2019'
EXE
  executable "${homebrew_prefix}/opt/openssl@3.1/bin/openssl" <<EXE
#!/$BASH
[ "\$1" = "version" ] || exit 1
echo 'OpenSSL 3.1.22  20 Dec 2019'
EXE
  executable "${homebrew_prefix}/opt/openssl@3.0/bin/openssl" <<EXE
#!/$BASH
[ "\$1" = "version" ] || exit 1
echo 'OpenSSL 3.0.2  20 Dec 2019'
EXE
  executable "${homebrew_prefix}/opt/openssl@1.1/bin/openssl" <<EXE
#!/$BASH
[ "\$1" = "version" ] || exit 1
echo 'OpenSSL 1.1.1v  20 Dec 2019'
EXE

  stub_repeated uname '-s : echo Linux'
  stub cc '-xc -E - : echo "OpenSSL 1.0.1a  1 Aug 2023"'
  stub_repeated brew \
    'list : printf "git\nopenssl@3\nopenssl-utils\nopenssl@1.1\nopenssl@3.0\nwget\nopenssl@3.1"' \
    "--prefix : echo '$homebrew_prefix'/opt/\$2 "
  stub_make_install

  run_inline_definition <<DEF
install_package "openssl-1.1.1w" "https://www.openssl.org/source/openssl-1.1.1w.tar.gz" openssl --if needs_openssl:1.1.0-3.0.x
install_package "ruby-2.0.0" "http://ruby-lang.org/ruby/2.0/ruby-2.0.0.tar.gz"
DEF
  assert_success

  unstub uname
  unstub cc
  unstub brew
  unstub make

  assert_build_log <<OUT
ruby-2.0.0: [--prefix=$INSTALL_ROOT,--with-openssl-dir=$TMP/homebrew/opt/openssl@3.0]
make -j 2
make install
OUT
}

@test "forward extra command-line arguments as configure flags" {
  cached_tarball "ruby-2.0.0" configure

+3 −2
Original line number Diff line number Diff line
@@ -44,8 +44,9 @@ inspect_args() {
}

# Loop over each line in the plan.
IFS=$'\n' read -d '' -r -a lines < "${!_STUB_PLAN}" || true
index=0
while IFS= read -r line; do
for line in "${lines[@]}"; do
  index=$(($index + 1))

  if [[ -z "${!_STUB_END}" && -n "${!_STUB_NOINDEX}" || $index -eq "${!_STUB_INDEX}" ]]; then
@@ -93,7 +94,7 @@ while IFS= read -r line; do
      eval "${_STUB_RESULT}"=1
    fi
  fi
done < "${!_STUB_PLAN}"
done


if [ -n "${!_STUB_END}" ]; then