build 13 KB

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