#! /usr/bin/env bash #===-- tools/f18/flang-to-external-fc.sh --------------------------*- sh -*-===# # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # #===------------------------------------------------------------------------===# # A wrapper script for Flang's compiler driver that was developed for testing and # experimenting. You should be able to use it as a regular compiler driver. It # will: # * run Flang's compiler driver to unparse the input source files # * use the external compiler (defined via FLANG_FC environment variable) to # compile the unparsed source files # # Tested with Bash 4.4. This script will exit immediately if you use an # older version of Bash. #===------------------------------------------------------------------------===# set -euo pipefail # Global variables to make the parsing of input arguments a bit easier INPUT_FILES=() OPTIONS=() OUTPUT_FILE="" MODULE_DIR="" INTRINSICS_MOD_DIR="" COMPILE_ONLY="False" PREPROCESS_ONLY="False" PRINT_VERSION="False" # === check_bash_version ====================================================== # # Checks the Bash version that's used to run this script. Exits immediately # with a non-zero return code if it's lower than 4.4. Otherwise returns 0 # (success). # ============================================================================= check_bash_version() { message="Error: Your Bash is too old. Please use Bash >= 4.4" # Major version [[ "${BASH_VERSINFO[0]:-0}" -lt 4 ]] && echo $message && exit 1 # Minor version if [[ "${BASH_VERSINFO[0]}" == 4 ]]; then [[ "${BASH_VERSINFO[1]:-0}" -lt 4 ]] && echo $message && exit 1 fi return 0 } # === parse_args ============================================================== # # Parse the input arguments passed to this script. Sets the global variables # declared at the top. # # INPUTS: # $1 - all input arguments # OUTPUTS: # Saved in the global variables for this script # ============================================================================= parse_args() { while [ "${1:-}" != "" ]; do # CASE 1: Compiler option if [[ "${1:0:1}" == "-" ]] ; then # Output file - extract it into a global variable if [[ "$1" == "-o" ]] ; then shift OUTPUT_FILE="$1" shift continue fi # Module directory - extract it into a global variable if [[ "$1" == "-module-dir" ]]; then shift MODULE_DIR="$1" shift continue fi # Intrinsics module dir - extract it into a global var if [[ "$1" == "-intrinsics-module-directory" ]]; then shift INTRINSICS_MOD_DIR=$1 shift continue fi # Module suffix cannot be modified - this script defines it before # calling the driver. if [[ "$1" == "-module-suffix" ]]; then echo "ERROR: \'-module-suffix\' is not available when using the \'flang\' script" exit 1 fi # Special treatment for `J ` and `-I `. We translate these # into `J` and `-I` respectively. if [[ "$1" == "-J" ]] || [[ "$1" == "-I" ]]; then opt=$1 shift OPTIONS+=("$opt$1") shift continue fi # This is a regular option - just add it to the list. OPTIONS+=($1) if [[ $1 == "-c" ]]; then COMPILE_ONLY="True" fi if [[ $1 == "-S" ]]; then COMPILE_ONLY="True" fi if [[ $1 == "-E" ]]; then PREPROCESS_ONLY="True" fi if [[ $1 == "-v" || $1 == "--version" ]]; then PRINT_VERSION="True" fi shift continue # CASE 2: A regular file (either source or a library file) elif [[ -f "$1" ]]; then INPUT_FILES+=($1) shift continue else # CASE 3: Unsupported echo "ERROR: unrecognised option format: \`$1\`. Perhaps non-existent file?" exit 1 fi done } # === categorise_files ======================================================== # # Categorises input files into: # * Fortran source files (to be compiled) # * library files (to be linked into the final executable) # # INPUTS: # $1 - all input files to be categorised (array, name reference) # OUTPUTS: # $2 - Fortran source files extracted from $1 (array, name reference) # $3 - other source files extracted from $1 (array, name reference) # $4 - object files extracted from $1 (array, name reference) # $5 - lib files extracted from $1 (array, name reference) # ============================================================================= categorise_files() { local -n -r all_files=$1 local -n fortran_sources=$2 local -n other_sources=$3 local -n objects=$4 local -n libs=$5 for current_file in "${all_files[@]}"; do file_ext=${current_file##*.} if [[ $file_ext == "f" ]] || [[ $file_ext == "f90" ]] || [[ $file_ext == "f" ]] || [[ $file_ext == "F" ]] || [[ $file_ext == "ff" ]] || [[ $file_ext == "f90" ]] || [[ $file_ext == "F90" ]] || [[ $file_ext == "ff90" ]] || [[ $file_ext == "f95" ]] || [[ $file_ext == "F95" ]] || [[ $file_ext == "ff95" ]] || [[ $file_ext == "cuf" ]] || [[ $file_ext == "CUF" ]] || [[ $file_ext == "f18" ]] || [[ $file_ext == "F18" ]] || [[ $file_ext == "ff18" ]]; then fortran_sources+=($current_file) elif [[ $file_ext == "a" ]] || [[ $file_ext == "so" ]]; then libs+=($current_file) elif [[ $file_ext == "o" ]]; then objects+=($current_file) else other_sources+=($current_file) fi done } # === categorise_opts ========================================================== # # Categorises compiler options into options for: # * the Flang driver (either new or the "throwaway" driver) # * the external Fortran driver that will generate the code # Most options accepted by Flang will be claimed by it. The only exceptions are # `-I` and `-J`. # # INPUTS: # $1 - all compiler options (array, name reference) # OUTPUTS: # $2 - compiler options for the Flang driver (array, name reference) # $3 - compiler options for the external driver (array, name reference) # ============================================================================= categorise_opts() { local -n all_opts=$1 local -n flang_opts=$2 local -n fc_opts=$3 for opt in "${all_opts[@]}"; do # These options are claimed by Flang, but should've been dealt with in parse_args. if [[ $opt == "-module-dir" ]] || [[ $opt == "-o" ]] || [[ $opt == "-fintrinsic-modules-path" ]] ; then echo "ERROR: $opt should've been fully processed by \`parse_args\`" exit 1 fi if # The options claimed by Flang. This list needs to be compatible with # what's supported by Flang's compiler driver (i.e. `flang-new`). [[ $opt == "-cpp" ]] || [[ $opt =~ ^-D.* ]] || [[ $opt == "-E" ]] || [[ $opt == "-falternative-parameter-statement" ]] || [[ $opt == "-fbackslash" ]] || [[ $opt == "-fcolor-diagnostics" ]] || [[ $opt == "-fdefault-double-8" ]] || [[ $opt == "-fdefault-integer-8" ]] || [[ $opt == "-fdefault-real-8" ]] || [[ $opt == "-ffixed-form" ]] || [[ $opt =~ ^-ffixed-line-length=.* ]] || [[ $opt == "-ffree-form" ]] || [[ $opt == "-fimplicit-none" ]] || [[ $opt =~ ^-finput-charset=.* ]] || [[ $opt == "-flarge-sizes" ]] || [[ $opt == "-flogical-abbreviations" ]] || [[ $opt == "-fno-color-diagnostics" ]] || [[ $opt == "-fxor-operator" ]] || [[ $opt == "-help" ]] || [[ $opt == "-nocpp" ]] || [[ $opt == "-pedantic" ]] || [[ $opt =~ ^-std=.* ]] || [[ $opt =~ ^-U.* ]] || [[ $opt == "-Werror" ]]; then flang_opts+=($opt) elif # We translate the following into equivalents understood by `flang-new` [[ $opt == "-Mfixed" ]] || [[ $opt == "-Mfree" ]]; then case $opt in -Mfixed) flang_opts+=("-ffixed-form") ;; -Mfree) flang_opts+=("-ffree-form") ;; *) echo "ERROR: $opt has no equivalent in 'flang-new'" exit 1 ;; esac elif # Options that are needed for both Flang and the external driver. [[ $opt =~ -I.* ]] || [[ $opt =~ -J.* ]] || [[ $opt == "-fopenmp" ]] || [[ $opt == "-fopenacc" ]]; then flang_opts+=($opt) fc_opts+=($opt) else # All other options are claimed for the external driver. fc_opts+=($opt) fi done } # === get_external_fc_name ==================================================== # # Returns the name of external Fortran compiler based on values of # environment variables. # ============================================================================= get_external_fc_name() { if [[ -v FLANG_FC ]]; then echo ${FLANG_FC} elif [[ -v F18_FC ]]; then # We support F18_FC for backwards compatibility. echo ${F18_FC} else echo gfortran fi } # === preprocess ============================================================== # # Runs the preprocessing. Fortran files are preprocessed using Flang. Other # files are preprocessed using the external Fortran compiler. # # INPUTS: # $1 - Fortran source files (array, name reference) # $2 - other source files (array, name reference) # $3 - compiler flags (array, name reference) # ============================================================================= preprocess() { local -n fortran_srcs=$1 local -n other_srcs=$2 local -n opts=$3 local ext_fc="$(get_external_fc_name)" local -r wd=$(cd "$(dirname "$0")/.." && pwd) # Use the provided output file name. if [[ ! -z ${OUTPUT_FILE:+x} ]]; then output_definition="-o $OUTPUT_FILE" fi # Preprocess fortran sources using Flang for idx in "${!fortran_srcs[@]}"; do if ! "$wd/bin/flang-new" -E "${opts[@]}" "${fortran_srcs[$idx]}" ${output_definition:+$output_definition} then status=$? echo flang: in "$PWD", flang-new failed with exit status $status: "$wd/bin/flang-new" "${opts[@]}" "$@" >&2 exit $status fi done # Preprocess other sources using Flang for idx in "${!other_srcs[@]}"; do if ! $ext_fc -E "${opts[@]}" "${other_srcs[$idx]}" ${output_definition:+$output_definition} then status=$? echo flang: in "$PWD", flang-new failed with exit status $status: "$wd/bin/flang-new" "${opts[@]}" "$@" >&2 exit $status fi done } # === get_relocatable_name ====================================================== # This method generates the name of the output file for the compilation phase # (triggered with `-c`). If the user of this script is only interested in # compilation (`flang -c`), use $OUTPUT_FILE provided that it was defined. # Otherwise, use the usual heuristics: # * file.f --> file.o # * file.c --> file.o # # INPUTS: # $1 - input source file for which to generate the output name # ============================================================================= get_relocatable_name() { local -r src_file=$1 if [[ $COMPILE_ONLY == "True" ]] && [[ ! -z ${OUTPUT_FILE:+x} ]]; then out_file="$OUTPUT_FILE" else current_ext=${src_file##*.} new_ext="o" out_file=$(basename "${src_file}" "$current_ext")${new_ext} fi echo "$out_file" } # === main ==================================================================== # Main entry point for this script # ============================================================================= main() { check_bash_version parse_args "$@" if [[ $PRINT_VERSION == "True" ]]; then echo "flang version 18.1.8" exit 0 fi # Source, object and library files provided by the user local fortran_source_files=() local other_source_files=() local object_files=() local lib_files=() categorise_files INPUT_FILES fortran_source_files other_source_files object_files lib_files if [[ $PREPROCESS_ONLY == "True" ]]; then preprocess fortran_source_files other_source_files OPTIONS exit 0 fi # Options for the Flang driver. # NOTE: We need `-fc1` to make sure that the frontend driver rather than # compiler driver is used. We also need to make sure that that's the first # flag that the driver will see (otherwise it assumes compiler/toolchain # driver mode). local flang_options=("-fc1") # Options for the external Fortran Compiler local ext_fc_options=() categorise_opts OPTIONS flang_options ext_fc_options local -r wd=$(cd "$(dirname "$0")/.." && pwd) # uuidgen is common but not installed by default on some distros if ! command -v uuidgen &> /dev/null then echo "uuidgen is required for generating unparsed file names." exit 1 fi # STEP 1: Unparse # Base-name for the unparsed files. These are just temporary files that are # first generated and then deleted by this script. # NOTE: We need to make sure that the base-name is unique to every # invocation. Otherwise we can't use this script in parallel. local -r unique_id=$(uuidgen | cut -b25-36) local -r unparsed_file_base="flang_unparsed_file_$unique_id" flang_options+=("-module-suffix") flang_options+=(".f18.mod") flang_options+=("-fdebug-unparse") flang_options+=("-fno-analyzed-objects-for-unparse") [[ ! -z ${MODULE_DIR} ]] && flang_options+=("-module-dir ${MODULE_DIR}") [[ ! -z ${INTRINSICS_MOD_DIR} ]] && flang_options+=("-intrinsics-module-directory ${INTRINSICS_MOD_DIR}") for idx in "${!fortran_source_files[@]}"; do set +e "$wd/bin/flang-new" "${flang_options[@]}" "${fortran_source_files[$idx]}" -o "${unparsed_file_base}_${idx}.f90" ret_status=$? set -e if [[ $ret_status != 0 ]]; then echo flang: in "$PWD", flang-new failed with exit status "$ret_status": "$wd/bin/flang-new" "${flang_options[@]}" "$@" >&2 exit "$ret_status" fi done # STEP 2: Compile Fortran Source Files local ext_fc="$(get_external_fc_name)" # Temporary object files generated by this script. To be deleted at the end. local temp_object_files=() for idx in "${!fortran_source_files[@]}"; do # We always have to specify the output name with `-o `. This # is because we are using the unparsed rather than the original source file # below. As a result, we cannot rely on the compiler-generated output name. out_obj_file=$(get_relocatable_name "${fortran_source_files[$idx]}") set +e $ext_fc "-c" "${ext_fc_options[@]}" "${unparsed_file_base}_${idx}.f90" "-o" "${out_obj_file}" ret_status=$? set -e if [[ $ret_status != 0 ]]; then echo flang: in "$PWD", "$ext_fc" failed with exit status "$ret_status": "$ext_fc" "${ext_fc_options[@]}" "$@" >&2 exit "$ret_status" fi temp_object_files+=(${out_obj_file}) done # Delete the unparsed files for idx in "${!fortran_source_files[@]}"; do rm "${unparsed_file_base}_${idx}.f90" done # STEP 3: Compile Other Source Files for idx in "${!other_source_files[@]}"; do # We always specify the output name with `-o `. The user # might have used `-o`, but we never add it to $OPTIONS (or # $ext_fc_options). Hence we need to use `get_relocatable_name`. out_obj_file=$(get_relocatable_name "${other_source_files[$idx]}") set +e $ext_fc "-c" "${ext_fc_options[@]}" "${other_source_files[${idx}]}" "-o" "${out_obj_file}" ret_status=$? set -e if [[ $ret_status != 0 ]]; then echo flang: in "$PWD", "$ext_fc" failed with exit status "$ret_status": "$ext_fc" "${ext_fc_options[@]}" "$@" >&2 exit "$ret_status" fi temp_object_files+=(${out_obj_file}) done # STEP 4: Link if [[ $COMPILE_ONLY == "True" ]]; then exit 0; fi if [[ ${#temp_object_files[@]} -ge 1 ]] || [[ ${#object_files[@]} -ge 1 ]] ; then # If $OUTPUT_FILE was specified, use it for the output name. if [[ ! -z ${OUTPUT_FILE:+x} ]]; then output_definition="-o $OUTPUT_FILE" else output_definition="" fi set +e $ext_fc "${ext_fc_options[@]}" "${object_files[@]}" "${temp_object_files[@]}" "${lib_files[@]}" ${output_definition:+$output_definition} ret_status=$? set -e if [[ $ret_status != 0 ]]; then echo flang: in "$PWD", "$ext_fc" failed with exit status "$ret_status": "$ext_fc" "${ext_fc_options[@]}" "$@" >&2 exit "$ret_status" fi fi # Delete intermediate object files for idx in "${!fortran_source_files[@]}"; do rm "${temp_object_files[$idx]}" done } main "${@}"