build 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. #!/bin/sh
  2. #
  3. # build
  4. #
  5. # Build script for the Dragora GNU/Linux-Libre website
  6. # (https://www.dragora.org)
  7. #
  8. #
  9. # Copyright (C) 2020 Michael Siegel
  10. #
  11. # Licensed under the Apache License, Version 2.0 (the "License");
  12. # you may not use this file except in compliance with the License.
  13. # You may obtain a copy of the License at
  14. #
  15. # http://www.apache.org/licenses/LICENSE-2.0
  16. #
  17. # Unless required by applicable law or agreed to in writing, software
  18. # distributed under the License is distributed on an "AS IS" BASIS,
  19. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  20. # See the License for the specific language governing permissions and
  21. # limitations under the License.
  22. #### "CONSTANTS" ####
  23. PROGNAME=build
  24. TOOLBOX='cat cmp cut find grep head mkdir sed sort stat rm rsync wc'
  25. ## Source directories and files
  26. SOURCE_DIR=source
  27. COMMON_DIR="$SOURCE_DIR"/common
  28. PAGES_DIR="$SOURCE_DIR"/pages
  29. PAGES_DIR_MASTER="$PAGES_DIR"/en
  30. TMP_DIR="$SOURCE_DIR"/tmp
  31. FOOTER_PARAMS=footer_params
  32. HEADER_PARAMS=header_params
  33. INDEX_PAGE=index.html.in
  34. ## Common directories and files
  35. CSS_DIR=css
  36. STYLESHEET=main.css
  37. STYLESHEET_PATH="$CSS_DIR/$STYLESHEET"
  38. IMG_DIR=img
  39. FAVICON=dragora.ico
  40. FAVICON_PATH="$IMG_DIR/$FAVICON"
  41. #LOGO=dragora_logo.png
  42. LOGO=dragora_logo-211x211.png
  43. LOGO_PATH="$IMG_DIR/$LOGO"
  44. ## Output directory
  45. OUTPUT_DIR=output
  46. ## Misc
  47. COPYRIGHT_HOLDERS='The Dragora Team'
  48. COPYRIGHT_YEARS='2019, 2020'
  49. DIR_UP='../'
  50. SEPARATOR='|' # Used in <title></title>.
  51. SITE_TITLE=Dragora
  52. TZ=UTC
  53. #### GLOBAL VARIABLES ####
  54. backpath=
  55. css_path=
  56. err_msg=
  57. favicon_path=
  58. header_type=
  59. lang=
  60. logo_path=
  61. subheader_class= # make "local" to _mk_header?
  62. mod_date=
  63. page_title=
  64. pg=
  65. pg_basename_in=
  66. pg_basename_out=
  67. pg_path_in=
  68. pg_path_out=
  69. sublevel=
  70. title=
  71. #### FUNCTIONS ####
  72. _abort() {
  73. ## Run _cleanup if "$TMP_DIR" exists and exit the script with a return code
  74. ## indicating an error
  75. [ -d "$TMP_DIR" ] && _cleanup
  76. exit 1
  77. }
  78. _cleanup() {
  79. ## Remove any temporary files
  80. rm -rf "${TMP_DIR:?}"/*
  81. }
  82. _get_lang_dirs() {
  83. ## Find all language-specific page directories
  84. find "$PAGES_DIR" -maxdepth 1 -type d -not -name '.*' -name '??' | \
  85. sed 's/^.*\///'
  86. }
  87. _get_page_title() {
  88. ## Retrieve page title from input file
  89. grep '^<!--PAGETITLE:.*-->$' "$1" | cut -d ':' -f 2 | sed 's/-->$//'
  90. }
  91. _get_regular_pages() {
  92. ## Find all HTML pages in "$PAGES_DIR"/"$lang", except those that belong to
  93. ## the Dragora Handbook
  94. find "$PAGES_DIR"/"$lang" -path "$PAGES_DIR"/"$lang"/doc/manual -prune -o \
  95. -type f -name '*.html.in' -print
  96. }
  97. _mk_header() {
  98. ## Compile HTML page header
  99. case "$header_type" in
  100. frontpage)
  101. subheader_class='subheader_frontpage'
  102. ;;
  103. regular)
  104. subheader_class='subheader_regular'
  105. ;;
  106. esac
  107. printf '<!DOCTYPE html>\n<html lang="%s">\n<head>\n' "$lang"
  108. printf ' <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n'
  109. printf ' <link rel="stylesheet" type="text/css" href="%s">\n' "$css_path"
  110. printf ' <link rel="icon" type="image/gif" href="%s">\n' "$favicon_path"
  111. printf ' <title>%s</title>\n' "$title"
  112. printf ' <meta name="description" content="%s">\n' "$lang_description"
  113. printf '</head>\n<body>\n'
  114. printf ' <div class="header" role="banner">\n'
  115. printf ' <img src="%s" height="160" alt="Dragora logo">\n' "$logo_path"
  116. printf ' <div class="header_text">\n'
  117. printf ' <h1>Dragora</h1>\n'
  118. printf ' <p class="tagline">%s</p>\n' "$lang_tagline"
  119. printf ' </div> <!-- close header_text -->\n'
  120. printf ' </div> <!-- close header -->\n'
  121. printf ' <div class="page_wrapper">\n'
  122. printf ' <div class="torso">\n'
  123. }
  124. _mk_nav() {
  125. ## Compile HTML navigation menu
  126. printf ' <div class="site_nav" role="navigation">\n'
  127. printf ' <ul>\n'
  128. printf ' <li><div class="site_nav_item"><a href="%s/">%s</a></div></li>\n' "${DIR_UP}${backpath}${lang}" "$(_get_page_title "${PAGES_DIR}/${lang}/index.html.in")"
  129. printf ' <li><div class="site_nav_item"><a href="%snews/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/news/index.html.in")"
  130. printf ' <li><div class="site_nav_item"><a href="%sget/">%s</a></div>\n' "${backpath%}" "$(_get_page_title "${PAGES_DIR}/${lang}/get/index.html.in")"
  131. if [ "$pg_branch" = get ]
  132. then
  133. printf ' <ul>\n'
  134. printf ' <li><div class="site_nav_item"><a href="%sget/mirrors/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/get/mirrors/index.html.in")"
  135. printf ' </ul>\n'
  136. fi
  137. printf ' </li>\n'
  138. printf ' <li><div class="site_nav_item"><a href="%sdoc/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/doc/index.html.in")"
  139. printf ' <li><div class="site_nav_item"><a href="%scontribute/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/contribute/index.html.in")"
  140. printf ' <li><div class="site_nav_item"><a href="%scommunity/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/community/index.html.in")"
  141. printf ' <li><div class="site_nav_item"><a href="%sreport_bug/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/report_bug/index.html.in")"
  142. printf ' <li><div class="site_nav_item"><a href="%sdevel/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/devel/index.html.in")"
  143. printf ' <li><div class="site_nav_item"><a href="%stestbed/">%s</a></div>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/testbed/index.html.in")"
  144. if [ "$pg_branch" = testbed ]
  145. then
  146. printf ' <ul>\n'
  147. printf ' <li><div class="site_nav_item"><a href="%stestbed/coffee/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/testbed/coffee/index.html.in")"
  148. printf ' <li><div class="site_nav_item"><a href="%stestbed/icecream/">%s</a></div></li>\n' "${backpath}" "$(_get_page_title "${PAGES_DIR}/${lang}/testbed/icecream/index.html.in")"
  149. printf ' </ul>\n'
  150. fi
  151. printf ' </li>\n'
  152. printf ' </ul>\n'
  153. printf ' </div> <!-- close site_nav -->\n'
  154. printf ' <div class="main" role="main">\n'
  155. }
  156. _mk_footer() {
  157. ## Compile HTML page footer
  158. printf '\n <p class="back_top"><a href="#">%s</a></p>\n' "$lang_back_to_top"
  159. printf ' </div> <!-- close main -->\n'
  160. printf ' </div> <!-- close torso -->\n'
  161. printf ' <hr class="footer_hr">\n'
  162. printf ' <div class="footer" role="contentinfo">\n'
  163. printf ' <p>%s: %s</p>\n' "$lang_mod_label" "$mod_date"
  164. printf ' <p>Copyright © %s %s<br>\n' "$COPYRIGHT_YEARS" "$COPYRIGHT_HOLDERS"
  165. printf ' This work is licensed under a\n'
  166. printf ' <a href="https://creativecommons.org/licenses/by-sa/4.0/">\n'
  167. printf ' Creative Commons Attribution-ShareAlike 4.0 International\n'
  168. printf ' License</a>.\n'
  169. printf ' </p>\n'
  170. printf ' </div> <!-- close footer -->\n'
  171. printf ' </div> <!-- close page_wrapper -->\n'
  172. printf '</body>\n</html>\n'
  173. }
  174. _perr() {
  175. ## Print a message to stderr
  176. printf '%s\n' "$PROGNAME: $1" >&2
  177. }
  178. _probe_dirs_files() {
  179. ## Probe for existence of needed directories and files
  180. # Printing error messages right away and only returning at the end because we
  181. # want to catch all missing files and directories at once.
  182. # Local variables
  183. d=
  184. sd=
  185. err=0
  186. # Probe for top-level source directory
  187. if [ ! -d "$SOURCE_DIR" ]
  188. then
  189. _perr "directory not found -- '$SOURCE_DIR'"
  190. err=1
  191. else
  192. # Probe for sub-directories and files
  193. for d in "$COMMON_DIR" "$PAGES_DIR"
  194. do
  195. if [ ! -d "$d" ]
  196. then
  197. _perr "directory not found -- '$d'"
  198. err=1
  199. continue
  200. fi
  201. case "$d" in
  202. "$COMMON_DIR")
  203. for sd in "$d/$CSS_DIR" "$d/$IMG_DIR"
  204. do
  205. if [ ! -d "$sd" ]
  206. then
  207. _perr "directory not found -- '$sd'"
  208. err=1
  209. continue
  210. fi
  211. case "$sd" in
  212. "$d/$CSS_DIR")
  213. f="$d/$CSS_DIR/$STYLESHEET"
  214. [ -f "$f" ] || { _perr "stylesheet not found -- '$f'"; err=1; }
  215. ;;
  216. "$d/$IMG_DIR")
  217. f="$d/$IMG_DIR/$FAVICON"
  218. [ -f "$f" ] || { _perr "favicon not found -- '$f'"; err=1; }
  219. f="$d/$IMG_DIR/$LOGO"
  220. [ -f "$f" ] || { _perr "logo not found -- '$f'"; err=1; }
  221. ;;
  222. esac
  223. done
  224. ;;
  225. "$PAGES_DIR")
  226. [ -z "$(find "$PAGES_DIR" -mindepth 1 -maxdepth 1)" ] && \
  227. { _perr "directory must not be empty -- '$PAGES_DIR'"; err=1; }
  228. if [ ! -d "$PAGES_DIR_MASTER" ]
  229. then
  230. _perr "master page directory not found -- '$PAGES_DIR_MASTER'"
  231. err=1
  232. else
  233. # Check whether master tree is basically sane
  234. for f in "$HEADER_PARAMS" "$FOOTER_PARAMS" "$INDEX_PAGE"
  235. do
  236. f="$PAGES_DIR_MASTER/$f"
  237. [ -f "$f" ] || { _perr "file not found -- '$f'"; err=1; }
  238. # Improve error message.
  239. done
  240. # Determine if subtrees of all page directories are identical to
  241. # master subtree.
  242. find "$PAGES_DIR_MASTER" -printf '%P\n' | sort \
  243. > "$TMP_DIR"/treemaster
  244. for pd in $(_get_lang_dirs)
  245. do
  246. find "$PAGES_DIR/$pd" -printf '%P\n' | sort \
  247. > "$TMP_DIR"/treecopy
  248. cmp -s "$TMP_DIR"/treemaster "$TMP_DIR"/treecopy || { _perr \
  249. "'$PAGES_DIR/$pd': subtree not identical to '$PAGES_DIR_MASTER'";
  250. err=1; }
  251. # Improve error message
  252. done
  253. fi
  254. ;;
  255. esac
  256. done
  257. fi
  258. [ "$err" -gt 0 ] && { unset d sd f pd err; return 1; }
  259. unset d sd f pd err
  260. return 0
  261. }
  262. _probe_toolbox() {
  263. ## Probe for external commands
  264. err_msg=
  265. cmd=
  266. cmd_first_char=
  267. for cmd in $TOOLBOX
  268. do
  269. command -v "$cmd" >/dev/null || \
  270. { err_msg="command not found -- '$cmd'"; break; }
  271. cmd_first_char="$(printf '%s' "$cmd" | cut -c 1)"
  272. if [ "$cmd_first_char" = '/' ]
  273. then
  274. [ -x "$cmd" ] || \
  275. { err_msg="command found, but not executable -- '$cmd'"; break; }
  276. fi
  277. done
  278. unset cmd cmd_first_char
  279. [ -n "$err_msg" ] && return 1
  280. # Find GNU version commands that need it
  281. for cmd in $TOOLBOX
  282. do
  283. case "$cmd" in
  284. find)
  285. is_gnu="$(find --version | head -n1 | \
  286. sed 's/.*\(GNU findutils\).*/\1/')"
  287. [ -n "$is_gnu" ] || { err_msg="GNU version of '$cmd' required"; break; }
  288. ;;
  289. stat)
  290. is_gnu="$(stat --version | head -n1 | \
  291. sed 's/.*\(GNU coreutils\).*/\1/')"
  292. [ -n "$is_gnu" ] || { err_msg="GNU version of '$cmd' required"; break; }
  293. ;;
  294. esac
  295. done
  296. unset cmd is_gnu
  297. [ -n "$err_msg" ] && return 1
  298. return 0
  299. }
  300. _set_backpath() {
  301. backpath= # If you don't, the string will grow with every invocation.
  302. while [ "$sublevel" -gt 0 ]
  303. do
  304. backpath="${backpath}$DIR_UP"
  305. sublevel="$((sublevel-1))"
  306. done
  307. }
  308. #### MAIN ####
  309. # TODO: implement traps
  310. ## Environment checks
  311. _probe_toolbox || { _perr "$err_msg"; _abort; }
  312. _probe_dirs_files || { _cleanup; _abort; }
  313. mkdir -p "$TMP_DIR" "$OUTPUT_DIR" || _abort
  314. # Sync static common files
  315. rsync -av "$COMMON_DIR"/ "$OUTPUT_DIR" || _abort
  316. # Compile pages
  317. for lang in $(_get_lang_dirs)
  318. do
  319. # Get values for language-specific parameters in header and footer
  320. . "$PAGES_DIR/$lang/$HEADER_PARAMS" || _abort
  321. . "$PAGES_DIR/$lang/$FOOTER_PARAMS" || _abort
  322. # Compile pages
  323. for pg in $(_get_regular_pages)
  324. do
  325. # Set path and file names
  326. pg_path_in="${pg%/*}"
  327. pg_path_out="$(printf '%s' "$pg_path_in" | sed "s,$PAGES_DIR/,,")"
  328. pg_branch="$(printf '%s' "$pg_path_in" | \
  329. sed -e "s,$PAGES_DIR/$lang,," -e 's,/,,' -e 's,/.*,,')"
  330. # The language-specific index page is not in any branch (i.e., $pg_path_in
  331. # has no trailing slash in this case). The 'sed' command above ensures that
  332. # 'branch' will be empty for the index page but have a meaningful value
  333. # otherwise.
  334. pg_basename_in="${pg##*/}"
  335. pg_basename_out="${pg_basename_in%.in}" # .html.in → .html
  336. sublevel="$(printf '%s' "$pg_path_out" | sed 's/[^\/]//g' | wc -m)"
  337. _set_backpath
  338. # these should, maybe, be 'local' to _mk_header
  339. css_path="${DIR_UP}${backpath}${STYLESHEET_PATH}"
  340. favicon_path="${DIR_UP}${backpath}${FAVICON_PATH}"
  341. logo_path="${DIR_UP}${backpath}${LOGO_PATH}"
  342. # Determine header type
  343. if [ "$pg" = "$PAGES_DIR"/"$lang"/index.html.in ]
  344. then
  345. header_type=frontpage
  346. else
  347. header_type=regular
  348. fi
  349. page_title="$(_get_page_title "$pg")"
  350. title="$SITE_TITLE $SEPARATOR $page_title"
  351. # Determine modification date (requires GNU version of `stat')
  352. mod_date="$(TZ="$TZ" stat -c '%y' "$pg" | cut -d '.' -f 1) $TZ"
  353. # Action
  354. mkdir -p "$OUTPUT_DIR"/"$pg_path_out" || _abort
  355. _mk_header > "$TMP_DIR"/header.html.in || _abort
  356. _mk_nav "$pg_path_in" > "$TMP_DIR/nav.html.in" || _abort
  357. _mk_footer > "$TMP_DIR"/footer.html.in || _abort
  358. cat "$TMP_DIR"/header.html.in "$TMP_DIR"/nav.html.in "$pg" \
  359. "$TMP_DIR"/footer.html.in \
  360. > "$OUTPUT_DIR"/"$pg_path_out"/"$pg_basename_out" || _abort
  361. done
  362. done
  363. _cleanup