extensions.sh 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. # global variables managing the state of the extension manager. treat as private.
  2. declare -A extension_function_info # maps a function name to a string with KEY=VALUEs information about the defining extension
  3. declare -i initialize_extension_manager_counter=0 # how many times has the extension manager initialized?
  4. declare -A defined_hook_point_functions # keeps a map of hook point functions that were defined and their extension info
  5. declare -A hook_point_function_trace_sources # keeps a map of hook point functions that were actually called and their source
  6. declare -A hook_point_function_trace_lines # keeps a map of hook point functions that were actually called and their source
  7. declare fragment_manager_cleanup_file # this is a file used to cleanup the manager's produced functions, for build_all_ng
  8. # configuration.
  9. export DEBUG_EXTENSION_CALLS=no # set to yes to log every hook function called to the main build log
  10. export LOG_ENABLE_EXTENSION=yes # colorful logs with stacktrace when enable_extension is called.
  11. # This is a helper function for calling hooks.
  12. # It follows the pattern long used in the codebase for hook-like behaviour:
  13. # [[ $(type -t name_of_hook_function) == function ]] && name_of_hook_function
  14. # but with the following added behaviors:
  15. # 1) it allows for many arguments, and will treat each as a hook point.
  16. # this allows for easily kept backwards compatibility when renaming hooks, for example.
  17. # 2) it will read the stdin and assume it's (Markdown) documentation for the hook point.
  18. # combined with heredoc in the call site, it allows for "inline" documentation about the hook
  19. # notice: this is not involved in how the hook functions came to be. read below for that.
  20. call_extension_method() {
  21. # First, consume the stdin and write metadata about the call.
  22. write_hook_point_metadata "$@" || true
  23. # @TODO: hack to handle stdin again, possibly with '< /dev/tty'
  24. # Then a sanity check, hook points should only be invoked after the manager has initialized.
  25. if [[ ${initialize_extension_manager_counter} -lt 1 ]]; then
  26. display_alert "Extension problem" "Call to call_extension_method() (in ${BASH_SOURCE[1]- $(get_extension_hook_stracktrace "${BASH_SOURCE[*]}" "${BASH_LINENO[*]}")}) before extension manager is initialized." "err"
  27. fi
  28. # With DEBUG_EXTENSION_CALLS, log the hook call. Users might be wondering what/when is a good hook point to use, and this is visual aid.
  29. [[ "${DEBUG_EXTENSION_CALLS}" == "yes" ]] &&
  30. display_alert "--> Extension Method '${1}' being called from" "$(get_extension_hook_stracktrace "${BASH_SOURCE[*]}" "${BASH_LINENO[*]}")" ""
  31. # Then call the hooks, if they are defined.
  32. for hook_name in "$@"; do
  33. echo "-- Extension Method being called: ${hook_name}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  34. # shellcheck disable=SC2086
  35. [[ $(type -t ${hook_name}) == function ]] && { ${hook_name}; }
  36. done
  37. }
  38. # what this does is a lot of bash mumbo-jumbo to find all board-,family-,config- or user-defined hook points.
  39. # the meat of this is 'compgen -A function', which is bash builtin that lists all defined functions.
  40. # it will then compose a full hook point (function) that calls all the implementing hooks.
  41. # this centralized function will then be called by the regular Armbian build system, which is oblivious to how
  42. # it came to be. (although it is encouraged to call hook points via call_extension_method() above)
  43. # to avoid hard coding the list of hook-points (eg: user_config, image_tweaks_pre_customize, etc) we use
  44. # a marker in the function names, namely "__" (two underscores) to determine the hook point.
  45. initialize_extension_manager() {
  46. # before starting, auto-add extensions specified (eg, on the command-line) via the ENABLE_EXTENSIONS env var. Do it only once.
  47. [[ ${initialize_extension_manager_counter} -lt 1 ]] && [[ "${ENABLE_EXTENSIONS}" != "" ]] && {
  48. local auto_extension
  49. for auto_extension in $(echo "${ENABLE_EXTENSIONS}" | tr "," " "); do
  50. ENABLE_EXTENSION_TRACE_HINT="ENABLE_EXTENSIONS -> " enable_extension "${auto_extension}"
  51. done
  52. }
  53. # This marks the manager as initialized, no more extensions are allowed to load after this.
  54. export initialize_extension_manager_counter=$((initialize_extension_manager_counter + 1))
  55. # Have a unique temporary dir, even if being built concurrently by build_all_ng.
  56. export EXTENSION_MANAGER_TMP_DIR="${SRC}/.tmp/.extensions/${LOG_SUBPATH}"
  57. mkdir -p "${EXTENSION_MANAGER_TMP_DIR}"
  58. # Log destination.
  59. export EXTENSION_MANAGER_LOG_FILE="${EXTENSION_MANAGER_TMP_DIR}/extensions.log"
  60. echo -n "" >"${EXTENSION_MANAGER_TMP_DIR}/hook_point_calls.txt"
  61. # globally initialize the extensions log.
  62. echo "-- lib/extensions.sh included. logs will be below, followed by the debug generated by the initialize_extension_manager() function." >"${EXTENSION_MANAGER_LOG_FILE}"
  63. # log whats happening.
  64. echo "-- initialize_extension_manager() called." >>"${EXTENSION_MANAGER_LOG_FILE}"
  65. # this is the all-important separator.
  66. local hook_extension_delimiter="__"
  67. # list all defined functions. filter only the ones that have the delimiter. get only the part before the delimiter.
  68. # sort them, and make them unique. the sorting is required for uniq to work, and does not affect the ordering of execution.
  69. # get them on a single line, space separated.
  70. local all_hook_points
  71. all_hook_points="$(compgen -A function | grep "${hook_extension_delimiter}" | awk -F "${hook_extension_delimiter}" '{print $1}' | sort | uniq | xargs echo -n)"
  72. declare -i hook_points_counter=0 hook_functions_counter=0 hook_point_functions_counter=0
  73. # initialize the cleanups file.
  74. fragment_manager_cleanup_file="${SRC}"/.tmp/extension_function_cleanup.sh
  75. echo "# cleanups: " >"${fragment_manager_cleanup_file}"
  76. local FUNCTION_SORT_OPTIONS="--general-numeric-sort --ignore-case" # --random-sort could be used to introduce chaos
  77. local hook_point=""
  78. # now loop over the hook_points.
  79. for hook_point in ${all_hook_points}; do
  80. echo "-- hook_point ${hook_point}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  81. # check if the hook point is already defined as a function.
  82. # that can happen for example with user_config(), that can be implemented itself directly by a userpatches config.
  83. # for now, just warn, but we could devise a way to actually integrate it in the call list.
  84. # or: advise the user to rename their user_config() function to something like user_config__make_it_awesome()
  85. local existing_hook_point_function
  86. existing_hook_point_function="$(compgen -A function | grep "^${hook_point}\$")"
  87. if [[ "${existing_hook_point_function}" == "${hook_point}" ]]; then
  88. echo "--- hook_point_functions (final sorted realnames): ${hook_point_functions}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  89. display_alert "Extension conflict" "function ${hook_point} already defined! ignoring functions: $(compgen -A function | grep "^${hook_point}${hook_extension_delimiter}")" "wrn"
  90. continue
  91. fi
  92. # for each hook_point, obtain the list of implementing functions.
  93. # the sort order here is (very) relevant, since it determines final execution order.
  94. # so the name of the functions actually determine the ordering.
  95. local hook_point_functions hook_point_functions_pre_sort hook_point_functions_sorted_by_sort_id
  96. # Sorting. Multiple extensions (or even the same extension twice) can implement the same hook point
  97. # as long as they have different function names (the part after the double underscore __).
  98. # the order those will be called depends on the name; eg:
  99. # 'hook_point__033_be_awesome()' would be caller sooner than 'hook_point__799_be_even_more_awesome()'
  100. # independent from where they were defined or in which order the extensions containing them were added.
  101. # since requiring specific ordering could hamper portability, we reward extension authors who
  102. # don't mind ordering for writing just: 'hook_point__be_just_awesome()' which is automatically rewritten
  103. # as 'hook_point__500_be_just_awesome()'.
  104. # extension authors who care about ordering can use the 3-digit number, and use the context variables
  105. # HOOK_ORDER and HOOK_POINT_TOTAL_FUNCS to confirm in which order they're being run.
  106. # gather the real names of the functions (after the delimiter).
  107. hook_point_functions_pre_sort="$(compgen -A function | grep "^${hook_point}${hook_extension_delimiter}" | awk -F "${hook_extension_delimiter}" '{print $2}' | xargs echo -n)"
  108. echo "--- hook_point_functions_pre_sort: ${hook_point_functions_pre_sort}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  109. # add "500_" to the names of function that do NOT start with a number.
  110. # keep a reference from the new names to the old names (we'll sort on the new, but invoke the old)
  111. declare -A hook_point_functions_sortname_to_realname
  112. declare -A hook_point_functions_realname_to_sortname
  113. for hook_point_function_realname in ${hook_point_functions_pre_sort}; do
  114. local sort_id="${hook_point_function_realname}"
  115. [[ ! $sort_id =~ ^[0-9] ]] && sort_id="500_${sort_id}"
  116. hook_point_functions_sortname_to_realname[${sort_id}]="${hook_point_function_realname}"
  117. hook_point_functions_realname_to_sortname[${hook_point_function_realname}]="${sort_id}"
  118. done
  119. # actually sort the sort_id's...
  120. # shellcheck disable=SC2086
  121. hook_point_functions_sorted_by_sort_id="$(echo "${hook_point_functions_realname_to_sortname[*]}" | tr " " "\n" | LC_ALL=C sort ${FUNCTION_SORT_OPTIONS} | xargs echo -n)"
  122. echo "--- hook_point_functions_sorted_by_sort_id: ${hook_point_functions_sorted_by_sort_id}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  123. # then map back to the real names, keeping the order..
  124. hook_point_functions=""
  125. for hook_point_function_sortname in ${hook_point_functions_sorted_by_sort_id}; do
  126. hook_point_functions="${hook_point_functions} ${hook_point_functions_sortname_to_realname[${hook_point_function_sortname}]}"
  127. done
  128. # shellcheck disable=SC2086
  129. hook_point_functions="$(echo -n ${hook_point_functions})"
  130. echo "--- hook_point_functions (final sorted realnames): ${hook_point_functions}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  131. hook_point_functions_counter=0
  132. hook_points_counter=$((hook_points_counter + 1))
  133. # determine the variables we'll pass to the hook function during execution.
  134. # this helps the extension author create extensions that are portable between userpatches and official Armbian.
  135. # shellcheck disable=SC2089
  136. local common_function_vars="HOOK_POINT=\"${hook_point}\""
  137. # loop over the functions for this hook_point (keep a total for the hook point and a grand running total)
  138. for hook_point_function in ${hook_point_functions}; do
  139. hook_point_functions_counter=$((hook_point_functions_counter + 1))
  140. hook_functions_counter=$((hook_functions_counter + 1))
  141. done
  142. common_function_vars="${common_function_vars} HOOK_POINT_TOTAL_FUNCS=\"${hook_point_functions_counter}\""
  143. echo "-- hook_point: ${hook_point} will run ${hook_point_functions_counter} functions: ${hook_point_functions}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  144. local temp_source_file_for_hook_point="${EXTENSION_MANAGER_TMP_DIR}/extension_function_definition.sh"
  145. hook_point_functions_loop_counter=0
  146. # prepare the cleanup for the function, so we can remove our mess at the end of the build.
  147. cat <<-FUNCTION_CLEANUP_FOR_HOOK_POINT >>"${fragment_manager_cleanup_file}"
  148. unset ${hook_point}
  149. FUNCTION_CLEANUP_FOR_HOOK_POINT
  150. # now compose a function definition. notice the heredoc. it will be written to tmp file, logged, then sourced.
  151. # theres a lot of opportunities here, but for now I keep it simple:
  152. # - execute functions in the order defined by ${hook_point_functions} above
  153. # - define call-specific environment variables, to help extension authors to write portable extensions (eg: EXTENSION_DIR)
  154. cat <<-FUNCTION_DEFINITION_HEADER >"${temp_source_file_for_hook_point}"
  155. ${hook_point}() {
  156. echo "*** Extension-managed hook starting '${hook_point}': will run ${hook_point_functions_counter} functions: '${hook_point_functions}'" >>"\${EXTENSION_MANAGER_LOG_FILE}"
  157. FUNCTION_DEFINITION_HEADER
  158. for hook_point_function in ${hook_point_functions}; do
  159. hook_point_functions_loop_counter=$((hook_point_functions_loop_counter + 1))
  160. # store the full name in a hash, so we can track which were actually called later.
  161. defined_hook_point_functions["${hook_point}${hook_extension_delimiter}${hook_point_function}"]="DEFINED=yes ${extension_function_info["${hook_point}${hook_extension_delimiter}${hook_point_function}"]}"
  162. # prepare the call context
  163. local hook_point_function_variables="${common_function_vars}" # start with common vars... (eg: HOOK_POINT_TOTAL_FUNCS)
  164. # add the contextual extension info for the function (eg, EXTENSION_DIR)
  165. hook_point_function_variables="${hook_point_function_variables} ${extension_function_info["${hook_point}${hook_extension_delimiter}${hook_point_function}"]}"
  166. # add the current execution counter, so the extension author can know in which order it is being actually called
  167. hook_point_function_variables="${hook_point_function_variables} HOOK_ORDER=\"${hook_point_functions_loop_counter}\""
  168. # add it to our (not the call site!) environment. if we export those in the call site, the stack is corrupted.
  169. eval "${hook_point_function_variables}"
  170. # output the call, passing arguments, and also logging the output to the extensions log.
  171. # attention: don't pipe here (eg, capture output), otherwise hook function cant modify the environment (which is mostly the point)
  172. # @TODO: better error handling. we have a good opportunity to 'set -e' here, and 'set +e' after, so that extension authors are encouraged to write error-free handling code
  173. cat <<-FUNCTION_DEFINITION_CALLSITE >>"${temp_source_file_for_hook_point}"
  174. hook_point_function_trace_sources["${hook_point}${hook_extension_delimiter}${hook_point_function}"]="\${BASH_SOURCE[*]}"
  175. hook_point_function_trace_lines["${hook_point}${hook_extension_delimiter}${hook_point_function}"]="\${BASH_LINENO[*]}"
  176. [[ "\${DEBUG_EXTENSION_CALLS}" == "yes" ]] && display_alert "---> Extension Method ${hook_point}" "${hook_point_functions_loop_counter}/${hook_point_functions_counter} (ext:${EXTENSION:-built-in}) ${hook_point_function}" ""
  177. echo "*** *** Extension-managed hook starting ${hook_point_functions_loop_counter}/${hook_point_functions_counter} '${hook_point}${hook_extension_delimiter}${hook_point_function}':" >>"\${EXTENSION_MANAGER_LOG_FILE}"
  178. ${hook_point_function_variables} ${hook_point}${hook_extension_delimiter}${hook_point_function} "\$@"
  179. echo "*** *** Extension-managed hook finished ${hook_point_functions_loop_counter}/${hook_point_functions_counter} '${hook_point}${hook_extension_delimiter}${hook_point_function}':" >>"\${EXTENSION_MANAGER_LOG_FILE}"
  180. FUNCTION_DEFINITION_CALLSITE
  181. # output the cleanup for the implementation as well.
  182. cat <<-FUNCTION_CLEANUP_FOR_HOOK_POINT_IMPLEMENTATION >>"${fragment_manager_cleanup_file}"
  183. unset ${hook_point}${hook_extension_delimiter}${hook_point_function}
  184. FUNCTION_CLEANUP_FOR_HOOK_POINT_IMPLEMENTATION
  185. # unset extension vars for the next loop.
  186. unset EXTENSION EXTENSION_DIR EXTENSION_FILE EXTENSION_ADDED_BY
  187. done
  188. cat <<-FUNCTION_DEFINITION_FOOTER >>"${temp_source_file_for_hook_point}"
  189. echo "*** Extension-managed hook ending '${hook_point}': completed." >>"\${EXTENSION_MANAGER_LOG_FILE}"
  190. } # end ${hook_point}() function
  191. FUNCTION_DEFINITION_FOOTER
  192. # unsets, lest the next loop inherits them
  193. unset hook_point_functions hook_point_functions_sortname_to_realname hook_point_functions_realname_to_sortname
  194. # log what was produced in our own debug logfile
  195. cat "${temp_source_file_for_hook_point}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  196. cat "${fragment_manager_cleanup_file}" >>"${EXTENSION_MANAGER_LOG_FILE}"
  197. # source the generated function.
  198. # shellcheck disable=SC1090
  199. source "${temp_source_file_for_hook_point}"
  200. rm -f "${temp_source_file_for_hook_point}"
  201. done
  202. # Dont show any output until we have more than 1 hook function (we implement one already, below)
  203. [[ ${hook_functions_counter} -gt 0 ]] &&
  204. display_alert "Extension manager" "processed ${hook_points_counter} Extension Methods calls and ${hook_functions_counter} Extension Method implementations" "info" | tee -a "${EXTENSION_MANAGER_LOG_FILE}"
  205. }
  206. cleanup_extension_manager() {
  207. if [[ -f "${fragment_manager_cleanup_file}" ]]; then
  208. display_alert "Cleaning up" "extension manager" "info"
  209. # this will unset all the functions.
  210. # shellcheck disable=SC1090 # dynamic source, thanks, shellcheck
  211. source "${fragment_manager_cleanup_file}"
  212. fi
  213. # reset/unset the variables used
  214. initialize_extension_manager_counter=0
  215. unset extension_function_info defined_hook_point_functions hook_point_function_trace_sources hook_point_function_trace_lines fragment_manager_cleanup_file
  216. }
  217. # why not eat our own dog food?
  218. # process everything that happened during extension related activities
  219. # and write it to the log. also, move the log from the .tmp dir to its
  220. # final location. this will make run_after_build() "hot" (eg, emit warnings)
  221. run_after_build__999_finish_extension_manager() {
  222. # export these maps, so the hook can access them and produce useful stuff.
  223. export defined_hook_point_functions hook_point_function_trace_sources
  224. # eat our own dog food, pt2.
  225. call_extension_method "extension_metadata_ready" <<'EXTENSION_METADATA_READY'
  226. *meta-Meta time!*
  227. Implement this hook to work with/on the meta-data made available by the extension manager.
  228. Interesting stuff to process:
  229. - `"${EXTENSION_MANAGER_TMP_DIR}/hook_point_calls.txt"` contains a list of all hook points called, in order.
  230. - For each hook_point in the list, more files will have metadata about that hook point.
  231. - `${EXTENSION_MANAGER_TMP_DIR}/hook_point.orig.md` contains the hook documentation at the call site (inline docs), hopefully in Markdown format.
  232. - `${EXTENSION_MANAGER_TMP_DIR}/hook_point.compat` contains the compatibility names for the hooks.
  233. - `${EXTENSION_MANAGER_TMP_DIR}/hook_point.exports` contains _exported_ environment variables.
  234. - `${EXTENSION_MANAGER_TMP_DIR}/hook_point.vars` contains _all_ environment variables.
  235. - `${defined_hook_point_functions}` is a map of _all_ the defined hook point functions and their extension information.
  236. - `${hook_point_function_trace_sources}` is a map of all the hook point functions _that were really called during the build_ and their BASH_SOURCE information.
  237. - `${hook_point_function_trace_lines}` is the same, but BASH_LINENO info.
  238. After this hook is done, the `${EXTENSION_MANAGER_TMP_DIR}` will be removed.
  239. EXTENSION_METADATA_READY
  240. # Move temporary log file over to final destination, and start writing to it instead (although 999 is pretty late in the game)
  241. mv "${EXTENSION_MANAGER_LOG_FILE}" "${DEST}/${LOG_SUBPATH:-debug}/extensions.log"
  242. export EXTENSION_MANAGER_LOG_FILE="${DEST}/${LOG_SUBPATH:-debug}/extensions.log"
  243. # Cleanup. Leave no trace...
  244. [[ -d "${EXTENSION_MANAGER_TMP_DIR}" ]] && rm -rf "${EXTENSION_MANAGER_TMP_DIR}"
  245. }
  246. # This is called by call_extension_method(). To say the truth, this should be in an extension. But then it gets too meta for anyone's head.
  247. write_hook_point_metadata() {
  248. local main_hook_point_name="$1"
  249. [[ ! -d "${EXTENSION_MANAGER_TMP_DIR}" ]] && mkdir -p "${EXTENSION_MANAGER_TMP_DIR}"
  250. cat - >"${EXTENSION_MANAGER_TMP_DIR}/${main_hook_point_name}.orig.md" # Write the hook point documentation received via stdin to a tmp file for later processing.
  251. shift
  252. echo -n "$@" >"${EXTENSION_MANAGER_TMP_DIR}/${main_hook_point_name}.compat" # log the 2nd+ arguments too (those are the alternative/compatibility names), separate file.
  253. compgen -A export >"${EXTENSION_MANAGER_TMP_DIR}/${main_hook_point_name}.exports" # capture the exported env vars.
  254. compgen -A variable >"${EXTENSION_MANAGER_TMP_DIR}/${main_hook_point_name}.vars" # capture all env vars.
  255. # add to the list of hook points called, in order.
  256. echo "${main_hook_point_name}" >>"${EXTENSION_MANAGER_TMP_DIR}/hook_point_calls.txt"
  257. }
  258. # Helper function, to get clean "stack traces" that do not include the hook/extension infrastructure code.
  259. get_extension_hook_stracktrace() {
  260. local sources_str="$1" # Give this ${BASH_SOURCE[*]} - expanded
  261. local lines_str="$2" # And this # Give this ${BASH_LINENO[*]} - expanded
  262. local sources lines index final_stack=""
  263. IFS=' ' read -r -a sources <<<"${sources_str}"
  264. IFS=' ' read -r -a lines <<<"${lines_str}"
  265. for index in "${!sources[@]}"; do
  266. local source="${sources[index]}" line="${lines[((index - 1))]}"
  267. # skip extension infrastructure sources, these only pollute the trace and add no insight to users
  268. [[ ${source} == */.tmp/extension_function_definition.sh ]] && continue
  269. [[ ${source} == *lib/extensions.sh ]] && continue
  270. [[ ${source} == */compile.sh ]] && continue
  271. # relativize the source, otherwise too long to display
  272. source="${source#"${SRC}/"}"
  273. # remove 'lib/'. hope this is not too confusing.
  274. source="${source#"lib/"}"
  275. # add to the list
  276. arrow="$([[ "$final_stack" != "" ]] && echo "-> ")"
  277. final_stack="${source}:${line} ${arrow} ${final_stack} "
  278. done
  279. # output the result, no newline
  280. # shellcheck disable=SC2086 # I wanna suppress double spacing, thanks
  281. echo -n $final_stack
  282. }
  283. show_caller_full() {
  284. local frame=0
  285. while caller $frame; do
  286. ((frame++))
  287. done
  288. }
  289. # can be called by board, family, config or user to make sure an extension is included.
  290. # single argument is the extension name.
  291. # will look for it in /userpatches/extensions first.
  292. # if not found there will look in /extensions
  293. # if not found will exit 17
  294. declare -i enable_extension_recurse_counter=0
  295. declare -a enable_extension_recurse_stack
  296. enable_extension() {
  297. local extension_name="$1"
  298. local extension_dir extension_file extension_file_in_dir extension_floating_file
  299. local stacktrace
  300. # capture the stack leading to this, possibly with a hint in front.
  301. stacktrace="${ENABLE_EXTENSION_TRACE_HINT}$(get_extension_hook_stracktrace "${BASH_SOURCE[*]}" "${BASH_LINENO[*]}")"
  302. # if LOG_ENABLE_EXTENSION, output useful stack, so user can figure out which extensions are being added where
  303. [[ "${LOG_ENABLE_EXTENSION}" == "yes" ]] &&
  304. display_alert "Extension being added" "${extension_name} :: added by ${stacktrace}" ""
  305. # first a check, has the extension manager already initialized? then it is too late to enable_extension(). bail.
  306. if [[ ${initialize_extension_manager_counter} -gt 0 ]]; then
  307. display_alert "Extension problem" "already initialized -- too late to add '${extension_name}' (trace: ${stacktrace})" "err"
  308. exit 2
  309. fi
  310. # check the counter. if recurring, add to the stack and return success
  311. if [[ $enable_extension_recurse_counter -gt 1 ]]; then
  312. enable_extension_recurse_stack+=("${extension_name}")
  313. return 0
  314. fi
  315. # increment the counter
  316. enable_extension_recurse_counter=$((enable_extension_recurse_counter + 1))
  317. # there are many opportunities here. too many, actually. let userpatches override just some functions, etc.
  318. for extension_base_path in "${SRC}/userpatches/extensions" "${EXTER}/extensions"; do
  319. extension_dir="${extension_base_path}/${extension_name}"
  320. extension_file_in_dir="${extension_dir}/${extension_name}.sh"
  321. extension_floating_file="${extension_base_path}/${extension_name}.sh"
  322. if [[ -d "${extension_dir}" ]] && [[ -f "${extension_file_in_dir}" ]]; then
  323. extension_file="${extension_file_in_dir}"
  324. break
  325. elif [[ -f "${extension_floating_file}" ]]; then
  326. extension_dir="${extension_base_path}" # this is misleading. only directory-based extensions should have this.
  327. extension_file="${extension_floating_file}"
  328. break
  329. fi
  330. done
  331. # After that, we should either have extension_file and extension_dir, or throw.
  332. if [[ ! -f "${extension_file}" ]]; then
  333. echo "ERR: Extension problem -- cant find extension '${extension_name}' anywhere - called by ${BASH_SOURCE[1]}"
  334. exit 17 # exit, forcibly. no way we can recover from this, and next extensions will get bogus errors as well.
  335. fi
  336. local before_function_list after_function_list new_function_list
  337. # store a list of existing functions at this point, before sourcing the extension.
  338. before_function_list="$(compgen -A function)"
  339. # error handling during a 'source' call is quite insane in bash after 4.3.
  340. # to be able to catch errors in sourced scripts the only way is to trap
  341. declare -i extension_source_generated_error=0
  342. trap 'extension_source_generated_error=1;' ERR
  343. # source the file. extensions are not supposed to do anything except export variables and define functions, so nothing should happen here.
  344. # there is no way to enforce it though, short of static analysis.
  345. # we could punish the extension authors who violate it by removing some essential variables temporarily from the environment during this source, and restore them later.
  346. # shellcheck disable=SC1090
  347. source "${extension_file}"
  348. # remove the trap we set.
  349. trap - ERR
  350. # decrement the recurse counter, so calls to this method are allowed again.
  351. enable_extension_recurse_counter=$((enable_extension_recurse_counter - 1))
  352. # test if it fell into the trap, and abort immediately with an error.
  353. if [[ $extension_source_generated_error != 0 ]]; then
  354. display_alert "Extension failed to load" "${extension_file}" "err"
  355. exit 4
  356. fi
  357. # get a new list of functions after sourcing the extension
  358. after_function_list="$(compgen -A function)"
  359. # compare before and after, thus getting the functions defined by the extension.
  360. # comm is oldskool. we like it. go "man comm" to understand -13 below
  361. new_function_list="$(comm -13 <(echo "$before_function_list" | sort) <(echo "$after_function_list" | sort))"
  362. # iterate over defined functions, store them in global associative array extension_function_info
  363. for newly_defined_function in ${new_function_list}; do
  364. extension_function_info["${newly_defined_function}"]="EXTENSION=\"${extension_name}\" EXTENSION_DIR=\"${extension_dir}\" EXTENSION_FILE=\"${extension_file}\" EXTENSION_ADDED_BY=\"${stacktrace}\""
  365. done
  366. # snapshot, then clear, the stack
  367. local -a stack_snapshot=("${enable_extension_recurse_stack[@]}")
  368. enable_extension_recurse_stack=()
  369. # process the stacked snapshot, finally enabling the extensions
  370. for stacked_extension in "${stack_snapshot[@]}"; do
  371. ENABLE_EXTENSION_TRACE_HINT="RECURSE ${stacktrace} ->" enable_extension "${stacked_extension}"
  372. done
  373. }