#!/bin/bash # # Copyright 2015 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This is a wrapper script around gcc/clang that adjusts linker flags for # Haskell library and binary targets. # # Load commands that attempt to load dynamic libraries relative to the working # directory in their package output path (bazel-out/...) are converted to load # commands relative to @rpath. rules_haskell passes the corresponding # -Wl,-rpath,... flags itself. # # rpath commands that attempt to add rpaths relative to the working directory # to look for libraries in their package output path (bazel-out/...) are # omitted, since rules_haskell adds more appropriate rpaths itself. # # GHC generates intermediate dynamic libraries outside the build tree. # Additional RPATH entries are provided for those to make dynamic library # dependencies in the Bazel build tree available at runtime. # # See https://blogs.oracle.com/dipol/entry/dynamic_libraries_rpath_and_mac # on how to set those paths for Mach-O binaries. # set -euo pipefail INSTALL_NAME_TOOL="/usr/bin/install_name_tool" OTOOL="/usr/bin/otool" # Collect arguments to forward in a fresh response file. RESPONSE_FILE="$(mktemp osx_cc_args_XXXX.rsp)" rm_response_file() { rm -f "$RESPONSE_FILE" } trap rm_response_file EXIT add_args() { # Add the given arguments to the fresh response file. We follow GHC's # example in storing one argument per line, wrapped in double quotes. Double # quotes in the argument itself are escaped. for arg in "$@"; do printf '"%s"\n' "${arg//\"/\\\"}" >> "$RESPONSE_FILE" done } # Collect library, library dir, and rpath arguments. LIBS=() LIB_DIRS=() RPATHS=() # Parser state. # Parsing response file - unquote arguments. QUOTES= # Upcoming linker argument. LINKER= # Upcoming rpath argument. RPATH= # Upcoming install-name argument. INSTALL= # Upcoming output argument. OUTPUT= parse_arg() { # Parse the given argument. Decide whether to pass it on to the compiler, # and how it affects the parser state. local arg="$1" # Unquote response file arguments. if [[ "$QUOTES" = "1" && "$arg" =~ ^\"(.*)\"$ ]]; then # Take GHC's argument quoting into account when parsing a response # file. Note, no indication was found that GHC would pass multiline # arguments, or insert escape codes into the quoted arguments. If you # observe ill-formed arguments being passed to the compiler, then this # logic may need to be extended. arg="${BASH_REMATCH[1]}" fi # Parse given argument. if [[ "$OUTPUT" = "1" ]]; then # The previous argument was -o. Read output file. OUTPUT="$arg" add_args "$arg" elif [[ "$LINKER" = "1" ]]; then # The previous argument was -Xlinker. Read linker argument. if [[ "$RPATH" = "1" ]]; then # The previous argument was -rpath. Read RPATH. parse_rpath "$arg" RPATH=0 elif [[ "$arg" = "-rpath" ]]; then # rpath is coming RPATH=1 else # Unrecognized linker argument. Pass it on. add_args "-Xlinker" "$arg" fi LINKER= elif [[ "$INSTALL" = "1" ]]; then INSTALL= add_args "$arg" elif [[ "$arg" =~ ^@(.*)$ ]]; then # Handle response file argument. Parse the arguments contained in the # response file one by one. Take GHC's argument quoting into account. # Note, assumes that response file arguments are not nested in other # response files. QUOTES=1 while read line; do parse_arg "$line" done < "${BASH_REMATCH[1]}" QUOTES= elif [[ "$arg" = "-install_name" ]]; then # Install name is coming. We don't use it, but it can start with an @ # and be mistaken for a response file. INSTALL=1 add_args "$arg" elif [[ "$arg" = "-o" ]]; then # output is coming OUTPUT=1 add_args "$arg" elif [[ "$arg" = "-Xlinker" ]]; then # linker flag is coming LINKER=1 elif [[ "$arg" =~ ^-l(.*)$ ]]; then LIBS+=("${BASH_REMATCH[1]}") add_args "$arg" elif [[ "$arg" =~ ^-L(.*)$ ]]; then LIB_DIRS+=("${BASH_REMATCH[1]}") add_args "$arg" elif [[ "$arg" =~ ^-Wl,-rpath,(.*)$ ]]; then parse_rpath "${BASH_REMATCH[1]}" else # Unrecognized argument. Pass it on. add_args "$arg" fi } parse_rpath() { # Parse the given -rpath argument and decide whether it should be # forwarded to the compiler/linker. local rpath="$1" if [[ "$rpath" =~ ^/ || "$rpath" =~ ^@ ]]; then # Absolute rpaths or rpaths relative to @loader_path or similar, are # passed on to the linker. Other relative rpaths are dropped, these # are auto-generated by GHC, but are useless because rules_haskell # constructs dedicated rpaths to the _solib or _hssolib directory. # See https://github.com/tweag/rules_haskell/issues/689 add_args "-Wl,-rpath,$rpath" RPATHS+=("$rpath") fi } # Parse all given arguments. for arg in "$@"; do parse_arg "$arg" done get_library_in() { # Find the given library in the given directory. # Returns empty string if the library is not found. local lib="$1" local dir="$2" local solib="${dir}${dir:+/}lib${lib}.so" local dylib="${dir}${dir:+/}lib${lib}.dylib" if [[ -f "$solib" ]]; then echo "$solib" elif [[ -f "$dylib" ]]; then echo "$dylib" fi } get_library_path() { # Find the given library in the specified library search paths. # Returns empty string if the library is not found. if [[ ${#LIB_DIRS[@]} -gt 0 ]]; then local libpath for libdir in "${LIB_DIRS[@]}"; do libpath="$(get_library_in "$1" "$libdir")" if [[ -n "$libpath" ]]; then echo "$libpath" return fi done fi } resolve_rpath() { # Resolve the given rpath. I.e. if it is an absolute path, just return it. # If it is relative to the output, then prepend the output path. local rpath="$1" if [[ "$rpath" =~ ^/ ]]; then echo "$rpath" elif [[ "$rpath" =~ ^@loader_path/(.*)$ || "$rpath" =~ ^@executable_path/(.*)$ ]]; then echo "$(dirname "$OUTPUT")/${BASH_REMATCH[1]}" else echo "$rpath" fi } get_library_rpath() { # Find the given library in the specified rpaths. # Returns empty string if the library is not found. if [[ ${#RPATHS[@]} -gt 0 ]]; then local libdir libpath for rpath in "${RPATHS[@]}"; do libdir="$(resolve_rpath "$rpath")" libpath="$(get_library_in "$1" "$libdir")" if [[ -n "$libpath" ]]; then echo "$libpath" return fi done fi } get_library_name() { # Get the "library name" of the given library. "$OTOOL" -D "$1" | tail -1 } relpath() { # Find relative path from the first to the second path. Assuming the first # is a directory. If either is an absolute path, then we return the # absolute path to the second. local from="$1" local to="$2" if [[ "$to" =~ ^/ ]]; then echo "$to" elif [[ "$from" =~ ^/ ]]; then echo "$PWD/$to" else # Split path and store components in bash array. IFS=/ read -a fromarr <<<"$from" IFS=/ read -a toarr <<<"$to" # Drop common prefix. for ((i=0; i < ${#fromarr[@]}; ++i)); do if [[ "${fromarr[$i]}" != "${toarr[$i]}" ]]; then break fi done # Construct relative path. local common=$i local out= for ((i=$common; i < ${#fromarr[@]}; ++i)); do out="$out${out:+/}.." done for ((i=$common; i < ${#toarr[@]}; ++i)); do out="$out${out:+/}${toarr[$i]}" done echo $out fi } generate_rpath() { # Generate an rpath entry for the given library path. local rpath="$(relpath "$(dirname "$OUTPUT")" "$(dirname "$1")")" if [[ "$rpath" =~ ^/ ]]; then echo "$rpath" else # Relative rpaths are relative to the binary. echo "@loader_path${rpath:+/}$rpath" fi } if [[ ! "$OUTPUT" =~ ^bazel-out/ && ${#LIBS[@]} -gt 0 ]]; then # GHC generates temporary dynamic libraries during compilation outside of # the build directory. References to dynamic C libraries are broken in this # case. Here we add additional RPATHs to fix these references. The Hazel # package for swagger2 is an example that triggers this issue. for lib in "${LIBS[@]}"; do librpath="$(get_library_rpath "$lib")" if [[ -z "$librpath" ]]; then # The given library was not found in any of the rpaths. # Find it in the library search paths. libpath="$(get_library_path "$lib")" if [[ "$libpath" =~ ^bazel-out/ ]]; then # The library is Bazel generated and loaded relative to PWD. # Add an RPATH entry, so it is found at runtime. rpath="$(generate_rpath "$libpath")" parse_rpath "$rpath" fi fi done fi # Call the C++ compiler with the fresh response file. %{cc} "@$RESPONSE_FILE" if [[ ${#LIBS[@]} -gt 0 ]]; then # Replace load commands relative to the working directory, by load commands # relative to the rpath, if the library can be found relative to an rpath. for lib in "${LIBS[@]}"; do librpath="$(get_library_rpath "$lib")" if [[ -n "$librpath" ]]; then libname="$(get_library_name "$librpath")" if [[ "$libname" =~ ^bazel-out/ ]]; then "${INSTALL_NAME_TOOL}" -change \ "$libname" \ "@rpath/$(basename "$librpath")" \ "$OUTPUT" fi fi done fi # vim: ft=sh