#!/bin/bash # SPDX-License-Identifier: GPL-2.0-only # "spp" preprocessor script. # Copyright (c) 2010-2013 Wind River Systems, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. version="0.8" usage() { cat << EOF spp [--help] [--force] [-i] [--fuzz] [-w] [-o outfile] [-D=] [-I] [-v] infiles --help: This message --force: force overwrite output file if it already exists --find: Find the .scc file that matches the passed defines (-D) --fuzz: Use fuzzy logic to find the target file (off by default). Only for advanced use cases, since partial/close matches will be returned -D: define to which will be available to sub scripts -I: include path will be searched for files -i: leave intermediate files on failure -w: only warn on missing files -v: verbose output -o: outfile file for processed results. stdout is used if not passed infiles files to preprocess EOF } if [ -z "$1" ]; then usage exit 1 fi while [ $# -gt 0 ]; do case "$1" in --help) usage exit ;; --find) do_find=t ;; -D*|--D*) if [ "$1" == "-D" ] || [ "$1" == "--D" ]; then x=$2; shift; else x=`echo $1 | sed s%^-*D%%` fi defines="$defines $x" ;; -I*|--I*) if [ "$1" == "-I" ] || [ "$1" == "--I" ]; then x=$2; shift; else x=`echo $1 | sed s%^-*I%%` fi if [ -n "$x" ] && [ -d "$x" ]; then include_paths="$include_paths $(readlink -f $x)" fi ;; --force|-f) force=t ;; --fuzz) fuzz=t ;; -i) intermediate=t ;; -w) warn_on_missing=t ;; -o) outfile=$2 shift ;; -v) verbose=t ;; *) break ;; esac shift done err() { echo "$@" 1>&2 } warn() { err "$@" } # removes any common prefixes from a name (path, patch, etc). With # these removed, the resulting name is now relative to a set of # search paths, and can be found later. strip_common_prefix() { in_name=$1 # this takes an input name and searches all known paths. # the relocation that removes the MOST from the original is # the one we want, since it is the best match by definition out_len=${#in_name} relocated_name=$in_name for r in $include_paths; do t=${in_name/$r} this_len=${#t} if [ $this_len -lt $out_len ]; then relocated_name=$t out_len=$this_len break fi # add a trailing slash to get corner cases where one may # have been added or not dropped t=$in_name/ t=${t/$r} this_len=${#t} if [ $this_len -lt $out_len ]; then relocated_name=$t out_len=$this_len break fi done if [ ! -f ${relocated_name} ]; then # remove any leading slashes relocated_name=$(echo ${relocated_name} | sed 's%^//*%%g') fi echo "$relocated_name" } # looks for a .scc file that matches a set of defines (keywords) search_includes_for_defines() { defines=$1 found_scc= declare -A define_map if [ -n "$defines" ]; then # this could totally just be a loop .. but we only need three, so # keep it simple. The array allows us to easily weed out duplicate # defines define_tgt=$(echo $defines | cut -d: -f1) if [ -n "${define_tgt}" ]; then define_map[${define_tgt}]=t fi define_tgt=$(echo $defines | cut -d: -f2) if [ -n "${define_tgt}" ]; then define_map[${define_tgt}]=t fi define_tgt=$(echo $defines | cut -d: -f3) if [ -n "${define_tgt}" ]; then define_map[${define_tgt}]=t fi fi max_score=0 sccs_that_define=`find $include_paths -name '*.scc' \ | xargs grep -l -e '^[[:space:]]*define.*' | sort | uniq` for scc in $sccs_that_define; do score=0 for tgt in ${!define_map[@]}; do if [ -n "$tgt" ]; then f=`grep -l -e '^[[:space:]]*define.*'$tgt'[[:space:]]*\$' $scc` if [ -n "$f" ]; then score=`expr $score + 1` fi fi done if [ -n "${verbose}" ]; then >&2 echo "checking $scc ($score)" >&2 fi if [ $score -gt $max_score ]; then found_scc="$scc" max_score=$score elif [ $score -eq $max_score ]; then found_scc="$found_scc $scc" fi done if [ -n "${verbose}" ]; then >&2 echo "score: $max_score" fi if [ $max_score -gt 0 ]; then # return the first target found (among equals) rstring="$(echo $found_scc | cut -f2 -d' ')" # .. and add the score echo "$rstring:$max_score" fi } search_include_paths() { local tgt=$1 shift local includes="$@" local exclude_files="" local base local feature_ext=".scc" local possible="" if [ -n "$verbose" ]; then echo "search_includes: $tgt" >&2 echo "include paths: " >&2 for i in $includes; do echo " $i" >&2 done fi # if the path is absolute, we are done if [ -f "${tgt}" ]; then echo ${tgt} return fi case $tgt in *.patch) feature_ext=".patch" ;; *.cfg) feature_ext=".cfg" ;; *.scc) feature_ext=".scc" ;; *defconfig) feature_ext="" ;; esac # remove the feature extension (if present) from the input name tgt=${tgt%$feature_ext} base=`basename $tgt` if [ -z "$exclude_files" ]; then exclude_files="/dev/null" fi for p in "." $includes; do # target file + default feature extension if [ -f $p/$tgt$feature_ext ]; then possible=`readlink -f "$p/$tgt$feature_ext"` # raw target file elif [ -f $p/$tgt ]; then possible=`readlink -f "$p/$tgt"` # special processing test the include directory + # the name of the feature. This saves us doing a massive # set of includes for sub categories includes elif [ -f $p/$tgt/$tgt$feature_ext ]; then possible=`readlink -f "$p/$tgt/$tgt$feature_ext"` elif [ -f $p/$tgt/$tgt ]; then possible=`readlink -f "$p/$tgt/$tgt"` # more special processing. test if the included # feature is actually just the name of a directory # AND there is not file with the same name present. # if that is true, then test for: # /.extension # in that directory elif [ -f $p/$tgt/$base$feature_ext ]; then possible=`readlink -f "$p/$tgt/$base$feature_ext"` elif [ -f $p/$tgt/$base ]; then possible=`readlink -f "$p/$tgt/$base$feature_ext"` fi if [ -n "$possible" ]; then echo "$exclude_files" | grep -q "$possible" if [ $? -ne 0 ]; then echo $possible return fi possible= fi done } # args are the input files infiles=$@ processed_files="" # this function also removes duplicated lines by `sort -u` sort_by_len_dec() { for i in $@; do echo $i done | sort -u | awk '{ print length($0) " " $0; }' | sort -nr | cut -d ' ' -f 2- } include_paths=$(sort_by_len_dec $include_paths) ## ## create variables for use in scripts ## if [ -n "$defines" ]; then vars=$(echo $defines | sed 's/,/ /g') for v in "$vars"; do # eval makes it available for this script eval $v done fi # we are just trying to locate a file on the search path if [ -n "${do_find}" ]; then if [ -z "${KMACHINE}" ] && [ -z "${KTYPE}" ]; then if [ -z "${infiles}" ]; then echo "[ERROR]: No files provided, and no KMACHINE and KTYPE defined" exit 1 fi fi # are we looking for a machine/kernel match ? if [ -n "${KMACHINE}" ] && [ -n "${KTYPE}" ]; then found=$(search_includes_for_defines ${KMACHINE}) found_file=$(echo $found | cut -d: -f1) found_score=$(echo $found | cut -d: -f2) # now do the same search with the ktype AND the machine. If we # find a different file with a score of "2" (both), we'll use it # instead, otherwise, stick with the one that matched the machine found=$(search_includes_for_defines ${KMACHINE}:${KTYPE}) found_file2=$(echo $found | cut -d: -f1) found_score2=$(echo $found | cut -d: -f2) if [ "${found_score2}" == "2" ]; then found_file=${found_file2} found_score=${found_score2} else # if --fuzz is passed, we'll continue with a score of 1, otherwise # we exit if [ -z "${fuzz}" ]; then exit 1 fi fi if [ -n "${found_file}" ]; then echo "${found_file}" else exit 1 fi fi # or are we looking for a file in particular ? if [ -n "${infiles}" ]; then for f in ${infiles}; do if [ -n "${verbose}" ]; then echo "[INFO]: searching for $f" fi search_include_paths $f $include_paths done fi exit 0 fi if [ -z "$infiles" ]; then err "ERROR: at least one input file must be supplied" exit 1 else for f in $infiles; do expanded_file=$(search_include_paths $f $include_paths) if [ -z "${expanded_file}" ]; then err "ERROR. input file \"$f\" does not exist" exit 1 fi done fi if [ -n "$outfile" ] && [ -f "$outfile" ]; then if [ -z "$force" ]; then err "ERROR: output file \"$outfile\" exists, and --force was not passed" exit 1 fi fi # used by preprocessor directives to define values, very # similar to the block above which processes command line # values. These could be unified, but it is easier to keep the # separate for now define() { var=$1 value="$2" # make the variable real eval $var="\"$value\"" } header() { echo "#" echo "# spp v$version" echo "# processed: `date`" echo "#" echo "# This is a preprocessor output file, do not edit" echo "#" for i in $include_paths; do # strip a trailing / abs_dir=`readlink -f $i` abs_dir2=`cd $i; pwd` if [ x"$abs_dir" != x"$abs_dir2" ]; then # there is some sort of symlink trickery going on. # add both dirs to the relocation list abs_dir="$abs_dir $abs_dir2" fi for d in $abs_dir; do one_less_dir=${d%/} # strip last path component one_less_dir=${one_less_dir%/*} echo "reloc_dir $one_less_dir" done done echo "#" } # arg1: duration # remaining: the processed files footer() { local duration=$1 shift local infiles=$@ echo "# run time: $duration seconds" echo "# processed files:" for f in $infiles; do echo "# _cfg $f" done } process_file() { local in=$1 local containing_file=$2 shift shift local flags=$@ local inherited_inhibit_cfg="" local inherited_inhibit_patch="" local ret=0 local done="" local kconf_type local kconf_name local arg1 local fline local include_name local inhibit_cfg local inhibit_patch local working_dir=$(dirname ${containing_file}) if [ -z "$in" ]; then return fi if [ -n "${verbose}" ]; then echo "[INFO]: processing $in" fi case $in in *.scc) # do nothing, the rest of this function handles things ;; *.patch) patch_name=$in patch_name_new=$(search_include_paths $patch_name ${working_dir} ${include_paths}) if [ -z "${patch_name_new}" ]; then echo "[ERROR]: could not find patch: $in" exit 1 fi # output the name, and exit relative_patch=$(strip_common_prefix $(readlink -f $patch_name)) containing_dir=${patch_name/$relative_patch} echo "prefix ${containing_dir}" echo "patch \"${patch_name_new}\"" return ;; *.cfg) # output the name, and exit kconf_name=$in kconf_name_new=$(search_include_paths $kconf_name ${working_dir} ${include_paths}) if [ -z "${kconf_name_new}" ]; then echo "[ERROR]: could not find config fragment: $in" exit 1 fi relative_kconf=$(strip_common_prefix $(readlink -f $kconf_name_new)) containing_dir=${kconf_name_new/$relative_kconf} echo "prefix ${containing_dir}" echo "kconf non-hardware ${kconf_name_new}" return ;; *defconfig) # output the name, and exit kconf_name=$in kconf_name_new=$(readlink -f $(search_include_paths $kconf_name ${working_dir} ${include_paths})) if [ -z "${kconf_name_new}" ]; then echo "[ERROR]: could not find defconfig: $in" exit 1 fi relative_kconf=$(strip_common_prefix $(readlink -f $kconf_name_new)) containing_dir=${kconf_name_new/$relative_kconf} echo "prefix ${containing_dir}" echo "kconf non-hardware ${kconf_name_new}" return ;; esac # process the flags to this file processing for flag in $flags; do case $flag in nocfg) inherited_inhibit_cfg=nocfg ;; nopatch) inherited_inhibit_patch=nopatch ;; esac done if [ ! -f "$in" ]; then local_includes=`dirname $containing_file` new_in=`search_include_paths $in $include_paths $local_includes` if [ ! -f "$new_in" ]; then err "ERROR: could not find file $in, included from $containing_file" return 1 fi in=$new_in else in=`readlink -f $in` fi if [[ "$processed_files" =~ $in ]]; then echo "# NOTE: feature `basename $in` has already been processed" fi processed_files="$processed_files $in" echo "# --> file: $in" echo "# flags: $flags" echo "mark `basename $in` start" while read line || [ -n "$line" ]; do fline=$line [[ $fline ]] || continue # include if [[ $fline =~ ^[[:space:]]*include ]]; then set $fline include_name=$2 # if we were called with inhibit flags, passing them along is # the default inhibit_cfg=$inherited_inhibit_cfg inhibit_patch=$inherited_inhibit_patch # if we have a "nocfg" or "nopatch" on the include directive, # then we need to set the variables so they'll be passed down to # the nested process call if [[ $fline =~ [[:space:]]+nocfg ]]; then inhibit_cfg=nocfg fi if [[ $fline =~ [[:space:]]+nopatch ]]; then inhibit_patch=nopatch fi process_file $include_name $in $inhibit_cfg $inhibit_patch ret=$? if [ $ret -eq 1 ]; then return $ret fi # we can clear the inhbit flag, only if it wasn't passed into # us from above. This allows local .cfg files to be processed. if [ -z "$inherited_inhibit_cfg" ]; then inhibit_cfg="" fi if [ -z "$inherited_inhibit_patch" ]; then inhibit_patch="" fi continue fi ## preprocessor define if [[ $fline =~ ^[[:space:]]*#define ]]; then set $fline local define_name=$2 local define_value=$3 if [ -z "$define_value" ]; then define_value=t fi define $define_name $define_value continue fi ## patch if [[ $fline =~ ^[[:space:]]*patch ]]; then set $fline local patch_name=$2 if [ -n "$inherited_inhibit_patch" ]; then echo "# inhibited patch" echo "true \"`dirname $in`/$patch_name\"" else if [ ! -f `dirname $in`/$patch_name ]; then local patch_name_new=`search_include_paths $patch_name $include_paths` if [ ! -f "$patch_name_new" ]; then err "ERROR: could not find patch $patch_name, included from $containing_file" return 1 fi patch_name=$patch_name_new else patch_name="`dirname $in`/$patch_name" fi # output the patch relative_patch=$(strip_common_prefix $patch_name) containing_dir=${patch_name/$relative_patch} echo "prefix ${containing_dir}" echo "patch \"${patch_name}\"" fi continue fi ## kconf if [[ $fline =~ ^[[:space:]]*kconf || $fline =~ ^force[[:space:]]*kconf ]]; then set $fline arg1=$1 if [ "$arg1" = "force" ]; then # "force" will insist that its config be processed kconf_type=$3 kconf_name=$4 else arg1="" kconf_type=$2 kconf_name=$3 fi if [ -n "$inherited_inhibit_cfg" ] && [ -z "$arg1" ]; then echo "# inhibited kconf" echo "true \"`dirname $in`/$kconf_name\"" else if [ ! -f `dirname $in`/$kconf_name ]; then local kconf_name_new=`search_include_paths $kconf_name $include_paths` if [ ! -f "$kconf_name_new" ]; then if [ -n "$lazy_filenames" ]; then warn "WARNING: could not find kconf $kconf_name, included from $containing_file" kconf_name_new=$kconf_name else err "ERROR: could not find kconf $kconf_name, included from $containing_file" return 1 fi fi kconf_name="${kconf_name_new}" else kconf_name="`dirname $in`/$kconf_name" fi # output the kconfig relative_kconf=$(strip_common_prefix $kconf_name) containing_dir=${kconf_name/$relative_kconf} echo "prefix ${containing_dir}" echo "kconf $kconf_type ${kconf_name} # $arg1" fi continue fi echo "$fline" done < $in echo "mark `basename $in` end" echo "# <-- done file: $in" echo "#" return 0 } start_time=`date +"%s"` if [ -n "$outfile" ]; then header > $outfile else header fi for f in $infiles; do if [ -z "$fail" ]; then if [ -n "$outfile" ]; then process_file $f $f >> $outfile ret=$? if [ $ret -eq 1 ]; then fail=t fi else process_file $f $f ret=$? if [ $ret -eq 1 ]; then fail=t fi fi fi done if [ -n "$fail" ]; then if [ -z "$intermediate" ]; then rm -f $outfile fi exit $ret fi stop_time=`date +"%s"` duration=`expr $stop_time - $start_time` if [ -n "$outfile" ]; then footer $duration $infiles >> $outfile else footer $duration $infiles fi exit $ret