diff --git a/.github/workflows/nodejs-workflow.yml b/.github/workflows/nodejs-workflow.yml index c11fabcc3..3e2c46e24 100644 --- a/.github/workflows/nodejs-workflow.yml +++ b/.github/workflows/nodejs-workflow.yml @@ -313,6 +313,14 @@ jobs: working-directory: tools/nodejs_api run: npm test + - name: Make macOS OpenSSL lookup package-manager independent + if: ${{ matrix.platform == 'darwin' }} + working-directory: tools/nodejs_api + run: | + ../../scripts/relocate-macos-openssl.sh build/lbugjs.node + ../../scripts/verify-macos-openssl-rpaths.sh build/lbugjs.node + node -e "require('./build/lbugjs.node')" + - name: Move Node.js native module (Linux) if: ${{ matrix.platform == 'linux' }} working-directory: tools/nodejs_api diff --git a/scripts/relocate-macos-openssl.sh b/scripts/relocate-macos-openssl.sh new file mode 100755 index 000000000..c3f71a222 --- /dev/null +++ b/scripts/relocate-macos-openssl.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +binary="$1" +if [ ! -f "$binary" ]; then + echo "Mach-O binary not found: $binary" >&2 + exit 1 +fi + +dependency_for() { + local library="$1" + otool -L "$binary" | awk -v library="$library" \ + 'index($1, library) && substr($1, length($1) - length(library) + 1) == library { print $1; exit }' +} + +for library in libssl.3.dylib libcrypto.3.dylib; do + dependency="$(dependency_for "$library")" + if [ -z "$dependency" ]; then + echo "OpenSSL dependency $library not found in $binary" >&2 + exit 1 + fi + if [ "$dependency" != "@rpath/$library" ]; then + install_name_tool -change "$dependency" "@rpath/$library" "$binary" + fi +done + +for rpath in \ + /opt/homebrew/opt/openssl@3/lib \ + /usr/local/opt/openssl@3/lib \ + /opt/local/lib; do + existing_rpaths="$(otool -l "$binary" | awk '/cmd LC_RPATH/{getline; getline; print $2}')" + if ! grep -Fxq "$rpath" <<<"$existing_rpaths"; then + install_name_tool -add_rpath "$rpath" "$binary" + fi +done + +# install_name_tool invalidates the existing signature. +# Ad-hoc signing ("-") is sufficient for development and CI. If this binary is +# distributed to end-users (e.g., via npm), a proper Developer ID certificate +# should be used instead for notarization compatibility. +codesign --force --sign - "$binary" diff --git a/scripts/verify-macos-openssl-rpaths.sh b/scripts/verify-macos-openssl-rpaths.sh new file mode 100755 index 000000000..b6af6598b --- /dev/null +++ b/scripts/verify-macos-openssl-rpaths.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +binary="$1" +if [ ! -f "$binary" ]; then + echo "Mach-O binary not found: $binary" >&2 + exit 1 +fi + +dependencies="$(otool -L "$binary")" + +for library in libssl.3.dylib libcrypto.3.dylib; do + if ! grep -Fq "@rpath/$library" <<<"$dependencies"; then + echo "missing @rpath dependency for $library in $binary" >&2 + exit 1 + fi +done + +# The leading '/' is matched by the '^[[:space:]]+/' portion of the regex, so the +# alternatives within the group omit it: 'opt/homebrew', 'usr/local', 'opt/local'. +if grep -Eq '^[[:space:]]+/(opt/homebrew|usr/local|opt/local)/.*lib(ssl|crypto)\.3\.dylib' \ + <<<"$dependencies"; then + echo "package-manager-specific OpenSSL dependency remains in $binary" >&2 + exit 1 +fi + +rpaths="$(otool -l "$binary" | awk '/cmd LC_RPATH/{getline; getline; print $2}')" +for required in \ + /opt/homebrew/opt/openssl@3/lib \ + /usr/local/opt/openssl@3/lib \ + /opt/local/lib; do + if ! grep -Fxq "$required" <<<"$rpaths"; then + echo "missing OpenSSL rpath $required in $binary" >&2 + exit 1 + fi +done