1#!/bin/bash
2#
3# Copyright (c) 2022 R. Diez - Licensed under the GNU AGPLv3
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as
7# published by the Free Software Foundation, either version 3 of the
8# License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with this program.  If not, see <https://www.gnu.org/licenses/>.
17#
18
19set -o errexit
20set -o nounset
21set -o pipefail
22
23# set -x  # Enable tracing of this script.
24
25declare -r SCRIPT_NAME="GeneratePicolibcCrossFile.sh"
26declare -r VERSION_NUMBER="1.02"
27
28declare -r -i EXIT_CODE_SUCCESS=0
29declare -r -i EXIT_CODE_ERROR=1
30
31
32abort ()
33{
34  echo >&2 && echo "Error in script \"$0\": $*" >&2
35  exit $EXIT_CODE_ERROR
36}
37
38
39display_help ()
40{
41cat - <<EOF
42
43This script generates a file with the cross-compilation settings needed to build Picolibc.
44
45Copyright (c) 2022 R. Diez - Licensed under the GNU AGPLv3
46
47Syntax:
48  $SCRIPT_NAME  [options]
49
50The generated file contents are sent to stdout.
51
52Options:
53 --help     displays this help text
54 --version  displays this tool's version number (currently $VERSION_NUMBER)
55 --license  prints license information
56 --target-arch=triplet    The GCC cross-compilation triplet. Example: arm-none-eabi
57 --system=name            The system name does not really matter. The default is 'unknown-system'.
58 --cpu-family=name        The Meson build system documents all supported CPU family names.
59                          Example CPU family name: arm
60 --cpu=name               The CPU name does not really matter. The default is 'unknown-cpu'.
61                          Example CPU name: cortex-m3
62 --endianness=little/big  The CPU endianness. Example: 'little'.
63 --c-compiler=xxx         The default value of Meson setting 'c' is <triplet>-gcc.
64                          This option allows you to override it. Specify --c-compiler=xxx multiple
65                          times to add further compiler arguments.
66 --linker=xxxx            Add Meson setting 'c_ld'. Specify --linker=xxx multiple times
67                          to add further linker arguments.
68 --cflag=xxx              Add a C compiler flag for the target. Example: --cflag="-O2"
69                          These flags land in Meson setting 'c_args'.
70 --lflag=xxx              Add a linker     flag for the target. Example: --lflag="-L/target/libs"
71                          These flags land in Meson setting 'c_link_args'.
72 --exewrap=xxx            The first time, add Meson setting 'exe_wrapper' with the given
73                          argument, and add Meson setting "needs_exe_wrapper = true".
74                          Subsequent --exewrap=xxx options add further arguments to 'exe_wrapper'.
75 --meson-compat=0.55      Provides compability with Meson versions up to 0.55.
76
77Usage example:
78  ./$SCRIPT_NAME \\
79    --target-arch=arm-none-eabi \\
80    --cpu-family=arm            \\
81    --cpu=cortex-m3             \\
82    --endianness=little         \\
83    --cflag="-g"                \\
84    --cflag="-O2"               \\
85    >cross-build-settings.txt
86
87If you are calling this script from a GNU Make makefile, and you have a variable with all compiler flags,
88you can generate the corresponding --cflags=xxx options like this:
89  \$(patsubst %,--cflag=%,\$(PICOLIBC_C_FLAGS_FOR_TARGET))
90
91Exit status: 0 means success. Any other value means error.
92
93EOF
94}
95
96
97display_license()
98{
99cat - <<EOF
100
101Copyright (c) 2022 R. Diez
102
103This program is free software: you can redistribute it and/or modify
104it under the terms of the GNU Affero General Public License version 3 as published by
105the Free Software Foundation.
106
107This program is distributed in the hope that it will be useful,
108but WITHOUT ANY WARRANTY; without even the implied warranty of
109MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
110GNU Affero General Public License version 3 for more details.
111
112You should have received a copy of the GNU Affero General Public License version 3
113along with this program.  If not, see L<http://www.gnu.org/licenses/>.
114
115EOF
116}
117
118
119process_command_line_argument ()
120{
121  case "$OPTION_NAME" in
122    help)
123        display_help
124        exit $EXIT_CODE_SUCCESS
125        ;;
126
127    version)
128        echo "$VERSION_NUMBER"
129        exit $EXIT_CODE_SUCCESS
130        ;;
131
132    license)
133        display_license
134        exit $EXIT_CODE_SUCCESS
135        ;;
136
137    target-arch)
138        if [[ $OPTARG = "" ]]; then
139          abort "Option --target-arch has an empty value.";
140        fi
141        TARGET_ARCH="$OPTARG"
142        ;;
143
144    cpu-family)
145        if [[ $OPTARG = "" ]]; then
146          abort "Option --cpu-family has an empty value.";
147        fi
148        CPU_FAMILY_NAME="$OPTARG"
149        ;;
150
151    cpu)
152        if [[ $OPTARG = "" ]]; then
153          abort "Option --cpu has an empty value.";
154        fi
155        CPU_NAME="$OPTARG"
156        ;;
157
158    system)
159        if [[ $OPTARG = "" ]]; then
160          abort "Option --system has an empty value.";
161        fi
162        SYSTEM_NAME="$OPTARG"
163        ;;
164
165    c-compiler)
166        if [[ $OPTARG = "" ]]; then
167          abort "Option --c-compiler has an empty value.";
168        fi
169        C_COMPILER+=("$OPTARG")
170        ;;
171
172    linker)
173        if [[ $OPTARG = "" ]]; then
174          abort "Option --linker has an empty value.";
175        fi
176        LINKER+=("$OPTARG")
177        ;;
178
179    endianness)
180        if [[ $OPTARG = "" ]]; then
181          abort "Option --endianness has an empty value.";
182        fi
183        ENDIANNESS="$OPTARG"
184        ;;
185
186    cflag)
187        if [[ $OPTARG = "" ]]; then
188          abort "Option --cflag has an empty value.";
189        fi
190        CFLAGS_FOR_TARGET+=("$OPTARG")
191        ;;
192
193    lflag)
194        if [[ $OPTARG = "" ]]; then
195          abort "Option --lflag has an empty value.";
196        fi
197        LINKER_FLAGS_FOR_TARGET+=("$OPTARG")
198        ;;
199
200    exewrap)
201        if [[ $OPTARG = "" ]]; then
202          abort "Option --exewrap has an empty value.";
203        fi
204        EXE_WRAPPER+=("$OPTARG")
205        ;;
206
207    meson-compat)
208        if [[ $OPTARG = "" ]]; then
209          abort "Option --meson-compat has an empty value.";
210        fi
211        MESON_COMPATIBILITY="$OPTARG"
212        ;;
213
214    *)  # We should actually never land here, because parse_command_line_arguments() already checks if an option is known.
215        abort "Unknown command-line option \"--${OPTION_NAME}\".";;
216  esac
217}
218
219
220parse_command_line_arguments ()
221{
222  # The way command-line arguments are parsed below was originally described on the following page:
223  #   http://mywiki.wooledge.org/ComplexOptionParsing
224  # But over the years I have rewritten or amended most of the code myself.
225
226  if false; then
227    echo "USER_SHORT_OPTIONS_SPEC: $USER_SHORT_OPTIONS_SPEC"
228    echo "Contents of USER_LONG_OPTIONS_SPEC:"
229    for key in "${!USER_LONG_OPTIONS_SPEC[@]}"; do
230      printf -- "- %s=%s\\n" "$key" "${USER_LONG_OPTIONS_SPEC[$key]}"
231    done
232  fi
233
234  # The first colon (':') means "use silent error reporting".
235  # The "-:" means an option can start with '-', which helps parse long options which start with "--".
236  local MY_OPT_SPEC=":-:$USER_SHORT_OPTIONS_SPEC"
237
238  local OPTION_NAME
239  local OPT_ARG_COUNT
240  local OPTARG  # This is a standard variable in Bash. Make it local just in case.
241  local OPTARG_AS_ARRAY
242
243  while getopts "$MY_OPT_SPEC" OPTION_NAME; do
244
245    case "$OPTION_NAME" in
246
247      -) # This case triggers for options beginning with a double hyphen ('--').
248         # If the user specified "--longOpt"   , OPTARG is then "longOpt".
249         # If the user specified "--longOpt=xx", OPTARG is then "longOpt=xx".
250
251         if [[ "$OPTARG" =~ .*=.* ]]  # With this --key=value format, only one argument is possible.
252         then
253
254           OPTION_NAME=${OPTARG/=*/}
255           OPTARG=${OPTARG#*=}
256           OPTARG_AS_ARRAY=("")
257
258           if ! test "${USER_LONG_OPTIONS_SPEC[$OPTION_NAME]+string_returned_if_exists}"; then
259             abort "Unknown command-line option \"--$OPTION_NAME\"."
260           fi
261
262           # Retrieve the number of arguments for this option.
263           OPT_ARG_COUNT=${USER_LONG_OPTIONS_SPEC[$OPTION_NAME]}
264
265           if (( OPT_ARG_COUNT != 1 )); then
266             abort "Command-line option \"--$OPTION_NAME\" does not take 1 argument."
267           fi
268
269           process_command_line_argument
270
271         else  # With this format, multiple arguments are possible, like in "--key value1 value2".
272
273           OPTION_NAME="$OPTARG"
274
275           if ! test "${USER_LONG_OPTIONS_SPEC[$OPTION_NAME]+string_returned_if_exists}"; then
276             abort "Unknown command-line option \"--$OPTION_NAME\"."
277           fi
278
279           # Retrieve the number of arguments for this option.
280           OPT_ARG_COUNT=${USER_LONG_OPTIONS_SPEC[$OPTION_NAME]}
281
282           if (( OPT_ARG_COUNT == 0 )); then
283             OPTARG=""
284             OPTARG_AS_ARRAY=("")
285             process_command_line_argument
286           elif (( OPT_ARG_COUNT == 1 )); then
287             # If this is the last option, and its argument is missing, then OPTIND is out of bounds.
288             if (( OPTIND > $# )); then
289               abort "Option '--$OPTION_NAME' expects one argument, but it is missing."
290             fi
291             OPTARG="${!OPTIND}"
292             OPTARG_AS_ARRAY=("")
293             process_command_line_argument
294           else
295             OPTARG=""
296             # OPTARG_AS_ARRAY is not standard in Bash. I have introduced it to make it clear that
297             # arguments are passed as an array in this case. It also prevents many Shellcheck warnings.
298             OPTARG_AS_ARRAY=("${@:OPTIND:OPT_ARG_COUNT}")
299
300             if [ ${#OPTARG_AS_ARRAY[@]} -ne "$OPT_ARG_COUNT" ]; then
301               abort "Command-line option \"--$OPTION_NAME\" needs $OPT_ARG_COUNT arguments."
302             fi
303
304             process_command_line_argument
305           fi;
306
307           ((OPTIND+=OPT_ARG_COUNT))
308         fi
309         ;;
310
311      *) # This processes only single-letter options.
312         # getopts knows all valid single-letter command-line options, see USER_SHORT_OPTIONS_SPEC above.
313         # If it encounters an unknown one, it returns an option name of '?'.
314         if [[ "$OPTION_NAME" = "?" ]]; then
315           abort "Unknown command-line option \"$OPTARG\"."
316         else
317           # Process a valid single-letter option.
318           OPTARG_AS_ARRAY=("")
319           process_command_line_argument
320         fi
321         ;;
322    esac
323  done
324
325  shift $((OPTIND-1))
326  ARGS=("$@")
327}
328
329
330escape_for_meson_string ()
331{
332  local    STR="$1"
333  local -r RESULT_VAR_NAME="$2"
334
335  if [[ $STR =~ [[:cntrl:]] ]]; then
336    abort "The argument to escape for a meson string contains ASCII control characters."
337  fi
338
339  STR="${STR//\\/\\\\}"  # Replace \ with \\
340  STR="${STR//\'/\\\'}"  # Replace ' with \'
341
342  printf -v "$RESULT_VAR_NAME" "%s" "$STR"
343}
344
345
346# Examples of the 3 types of output:
347#
348# c_args = []
349#
350# c_args = [ '-O2' ]
351#
352# c_args = [
353#            '-g',
354#            '-O2'
355#          ]
356
357generate_setting_as_meson_array ()
358{
359  local SETTING_NAME="$1"
360  local -n ARRAY="$2"  # Create a reference to the array passed by name.
361
362  local -i ARRAY_LEN="${#ARRAY[@]}"
363
364  SETTING_AS_MESON_ARRAY="$SETTING_NAME = ["
365
366  if (( ARRAY_LEN == 0 )); then
367    SETTING_AS_MESON_ARRAY+="]"$'\n'
368    return
369  fi
370
371  local ESCAPED_ARG
372
373  if (( ARRAY_LEN == 1 )); then
374    escape_for_meson_string "${ARRAY[0]}" "ESCAPED_ARG"
375    SETTING_AS_MESON_ARRAY+=" '${ESCAPED_ARG}' ]"$'\n'
376    return
377  fi
378
379  SETTING_AS_MESON_ARRAY+=$'\n'
380
381  local -i BASE_INDENTATION_LEN="$(( ${#SETTING_NAME} + 3 ))"
382  local BASE_INDENTATION
383  printf -v BASE_INDENTATION "%*s" "$BASE_INDENTATION_LEN" ""
384
385  local -r EXTRA_INDENTATION="  "
386
387  local -i INDEX
388
389  for ((INDEX=0; INDEX < ARRAY_LEN; INDEX++)); do
390
391    escape_for_meson_string "${ARRAY[$INDEX]}" "ESCAPED_ARG"
392
393    SETTING_AS_MESON_ARRAY+="${BASE_INDENTATION}${EXTRA_INDENTATION}'$ESCAPED_ARG'"
394
395    if (( INDEX != ARRAY_LEN - 1 )); then
396      SETTING_AS_MESON_ARRAY+=","
397    fi
398
399    SETTING_AS_MESON_ARRAY+=$'\n'
400
401  done
402
403  SETTING_AS_MESON_ARRAY+="${BASE_INDENTATION}]"$'\n'
404}
405
406
407# If the command argument array is empty, no setting is generated.
408
409generate_command_setting ()
410{
411  local SETTING_NAME="$1"
412  local ARRAY_NAME="$2"
413
414  local -n ARRAY="$ARRAY_NAME"  # Create a reference to the array passed by name.
415
416  local -i ARRAY_LEN="${#ARRAY[@]}"
417
418  if (( ARRAY_LEN == 0 )); then
419    COMMAND_SETTING=""
420    return
421  fi
422
423  if (( ARRAY_LEN == 1 )); then
424
425    # We could generate an array with just one element, but a string looks nicer.
426    # Other sibling settings like 'ar' or 'strip' are usually given as strings too.
427
428    local ESCAPED_ARG
429    escape_for_meson_string "${ARRAY[0]}" "ESCAPED_ARG"
430
431    COMMAND_SETTING="$SETTING_NAME = '${ESCAPED_ARG}'"$'\n'
432    return
433  fi
434
435  local SETTING_AS_MESON_ARRAY
436
437  generate_setting_as_meson_array "$SETTING_NAME" "$ARRAY_NAME"
438
439  COMMAND_SETTING="$SETTING_AS_MESON_ARRAY"
440}
441
442
443# ----- Entry point -----
444
445USER_SHORT_OPTIONS_SPEC=""
446
447# Use an associative array to declare how many arguments every long option expects.
448# All known options must be listed, even those with 0 arguments.
449declare -A USER_LONG_OPTIONS_SPEC
450USER_LONG_OPTIONS_SPEC+=( [help]=0 )
451USER_LONG_OPTIONS_SPEC+=( [version]=0 )
452USER_LONG_OPTIONS_SPEC+=( [license]=0 )
453USER_LONG_OPTIONS_SPEC+=( [target-arch]=1 )
454USER_LONG_OPTIONS_SPEC+=( [cpu-family]=1 )
455USER_LONG_OPTIONS_SPEC+=( [cpu]=1 )
456USER_LONG_OPTIONS_SPEC+=( [system]=1 )
457USER_LONG_OPTIONS_SPEC+=( [endianness]=1 )
458USER_LONG_OPTIONS_SPEC+=( [c-compiler]=1 )
459USER_LONG_OPTIONS_SPEC+=( [linker]=1 )
460USER_LONG_OPTIONS_SPEC+=( [cflag]=1 )
461USER_LONG_OPTIONS_SPEC+=( [lflag]=1 )
462USER_LONG_OPTIONS_SPEC+=( [exewrap]=1 )
463USER_LONG_OPTIONS_SPEC+=( [meson-compat]=1 )
464
465declare -a C_COMPILER=()
466declare -a LINKER=()
467declare -a CFLAGS_FOR_TARGET=()
468declare -a LINKER_FLAGS_FOR_TARGET=()
469declare -a EXE_WRAPPER=()
470
471TARGET_ARCH=""
472CPU_FAMILY_NAME=""
473CPU_NAME=""
474ENDIANNESS=""
475MESON_COMPATIBILITY=""
476
477# The system name is not really used by Meson.
478SYSTEM_NAME="unknown-system"
479
480# The CPU name is not really used by Meson, see:
481#   https://github.com/mesonbuild/meson/issues/7037
482
483parse_command_line_arguments "$@"
484
485if (( ${#ARGS[@]} != 0 )); then
486  abort "Invalid number of command-line arguments. Run this tool with the --help option for usage information."
487fi
488
489if [[ $TARGET_ARCH = "" ]]; then
490  abort "Option --target-arch is required."
491fi
492
493if [[ $CPU_FAMILY_NAME = "" ]]; then
494  CPU_FAMILY_NAME=`expr "$TARGET_ARCH" : '\([^-]*\)-.*'`
495fi
496
497if [[ $ENDIANNESS = "" ]]; then
498  case "$CPU_FAMILY_NAME" in
499    arm*|aarch*|x86*|riscv*)
500      ENDIANNESS=little
501      ;;
502    68*|sparc*)
503      ENDIANNESS=big
504      ;;
505    *)
506      abort "Option --endianness is required."
507      ;;
508  esac
509fi
510
511if (( ${#C_COMPILER[@]} == 0)); then
512  C_COMPILER+=("$TARGET_ARCH-gcc")
513fi
514
515if [[ "$CPU_NAME" = "" ]]; then
516  CPU_NAME="$CPU_FAMILY_NAME"
517fi
518
519escape_for_meson_string "$TARGET_ARCH"     "ESCAPED_TARGET_ARCH"
520escape_for_meson_string "$SYSTEM_NAME"     "ESCAPED_SYSTEM_NAME"
521escape_for_meson_string "$CPU_FAMILY_NAME" "ESCAPED_CPU_FAMILY_NAME"
522escape_for_meson_string "$CPU_NAME"        "ESCAPED_CPU_NAME"
523escape_for_meson_string "$ENDIANNESS"      "ESCAPED_ENDIANNESS"
524
525
526FILE_CONTENTS=""
527
528FILE_CONTENTS+="[binaries]"$'\n'
529
530
531generate_command_setting "c" "C_COMPILER"
532
533FILE_CONTENTS+="$COMMAND_SETTING"
534
535
536generate_command_setting "c_ld" "LINKER"
537
538FILE_CONTENTS+="$COMMAND_SETTING"
539
540
541set +o errexit  # When 'read' reaches end of file, a non-zero status code is returned.
542
543read -r -d '' PART_1 <<EOF
544ar = '$ESCAPED_TARGET_ARCH-ar'
545as = '$ESCAPED_TARGET_ARCH-as'
546nm = '$ESCAPED_TARGET_ARCH-nm'
547strip = '$ESCAPED_TARGET_ARCH-strip'
548EOF
549
550set -o errexit
551
552FILE_CONTENTS+="$PART_1"$'\n'
553
554
555generate_command_setting "exe_wrapper" "EXE_WRAPPER"
556
557FILE_CONTENTS+="$COMMAND_SETTING"
558
559
560set +o errexit  # When 'read' reaches end of file, a non-zero status code is returned.
561
562read -r -d '' PART_2 <<EOF
563[host_machine]
564system = '$ESCAPED_SYSTEM_NAME'
565cpu_family = '$ESCAPED_CPU_FAMILY_NAME'
566cpu = '$ESCAPED_CPU_NAME'
567endian = '$ESCAPED_ENDIANNESS'
568
569[properties]
570skip_sanity_check = true
571EOF
572
573set -o errexit
574
575FILE_CONTENTS+=$'\n'"$PART_2"$'\n'
576
577
578if (( ${#EXE_WRAPPER[@]} > 0 )); then
579  FILE_CONTENTS+="needs_exe_wrapper = true"$'\n'
580fi
581
582
583case "$MESON_COMPATIBILITY" in
584
585  '') FILE_CONTENTS+=$'\n'"[built-in options]"$'\n';;
586
587  0.55)  # Since Meson version 0.56.0, released on 2020-10-30, you get the following warning:
588         # DEPRECATION: c_args in the [properties] section of the machine file is deprecated, use the [built-in options] section.
589         # See this commit: https://github.com/mesonbuild/meson/pull/6597
590         ;;
591
592  *) abort "Unsupported value of --meson-compat: $MESON_COMPATIBILITY";;
593esac
594
595
596if (( ${#CFLAGS_FOR_TARGET[@]} > 0 )); then
597
598  generate_setting_as_meson_array "c_args" "CFLAGS_FOR_TARGET"
599
600  FILE_CONTENTS+="$SETTING_AS_MESON_ARRAY"
601
602fi
603
604
605if (( ${#LINKER_FLAGS_FOR_TARGET[@]} > 0 )); then
606
607  generate_setting_as_meson_array "c_link_args" "LINKER_FLAGS_FOR_TARGET"
608
609  FILE_CONTENTS+="$SETTING_AS_MESON_ARRAY"
610
611fi
612
613echo -n "$FILE_CONTENTS"
614