docs/spec, script/gentree: support whiteout list file
[CDN/Mosi.git] / script / gentree
1 #!/bin/sh
2
3 # Boilerplate
4 _root="$(dirname "${0}")"
5 . "${_root}/lib/env.sh"
6
7 # Load needed modules
8 want root ansi log ask
9
10 targets="prepwork admin overlay packages patch whiteout prepboot preptmp prepetc imgboot imgconf imgetc imgall custom"
11
12 pebkac() {
13   [ "${*}" ] && printf "${*}\n\n"
14   echo "********************************************************************************"
15   echo "Usage: $(basename "${0}") <-a arch> <-c conf> <-b basedir> <-t stagedir>"
16   echo '        <-i pkgsdir> <-p patchdir> <-r rootdir> <-l logfile> [-h] <targets>'
17   echo "  -a arch       Arch for image target (default: current arch: $(uname -m))"
18   echo '  -c conf       Conf name for image target (default: GENERIC)'
19   echo '  -b basedir    Basedir for automagic defaults'
20   echo '  -t stagedir   Staging directory name (${target}/tree)'
21   echo '  -r rootdir    Directory holding the virgin source tree (${target}/world/root)'
22   echo '  -o overlaydir Directory holding an overlay tree (${target}/config/overlay)'
23   echo '  -i pkgsdir    Directory holding packages to install (${target}/pkg)'
24   echo '  -p patchdir   Directory holding patches to apply (${target}/config/patch)'
25   echo '  -w whiteout   Listfile of paths to remove (${target}/config/whiteout.lst)'
26   echo '  -d confdir    Conf md_size files and old cpios (${target}/config/conf)'
27   echo '  -l logfile    File to hold stderr spam (${stage}/gentree.log'
28   echo '  -h            Hello! >^-^<'
29   echo ''
30   echo 'Available targets:'
31   for target in ${targets}
32   do
33     echo "  ${target}"
34   done
35   exit 1
36 }
37
38 while getopts "a:c:b:t:r:o:i:p:w:l:h" opt
39 do
40   case "${opt}" in
41     a) arch="${OPTARG}" ;;
42     c) conf="${OPTARG}" ;;
43     b) base="${OPTARG}" ;;
44     t) tree="${OPTARG}" ;;
45     r) root="${OPTARG}" ;;
46     o) ovly="${OPTARG}" ;;
47     i) pkgs="${OPTARG}" ;;
48     p) ptch="${OPTARG}" ;;
49     w) rmrf="${OPTARG}" ;;
50     d) cnfd="${OPTARG}" ;;
51     l) logf="${OPTARG}" ;;
52     h) pebkac ;;
53     [?]) pebkac "Unrecognized option ${opt}" ;;
54   esac
55 done
56 shift $(( $OPTIND - 1 ))
57
58 sequence="${*:-${targets}}"
59
60 base="${base:-$(dirname "${0}")/..}" #"
61 base="$(realpath "${base}")"
62
63 arch="${arch:-$(uname -m)}"
64 conf="${conf:-GENERIC}"
65
66 target="${base}/targets/${arch}/${conf}"
67
68 tree="${tree:-${target}/tree}"
69 root="${root:-${target}/world/root}"
70 ovly="${ovly:-${target}/config/overlay}"
71 pkgs="${pkgs:-${target}/pkg}"
72 ptch="${ptch:-${target}/config/patch}"
73 rmrf="${rmrf:-${target}/config/whiteout.lst}"
74 cnfd="${cnfd:-${target}/config/conf}"
75 logf="${logf:-${tree}/gentree.log}"
76
77 stage="${stage:-${base}/tree}"
78 pkgs="${pkgs:-${base}/pkg}"
79 patch="${patch:-${base}/patch}"
80 overlay="${overlay:-${base}/overlay}"
81 root="${root:-${base}/root}"
82 logfile="${logfile:-${stage}/gentree.log}"
83
84 mkdir -p "${tree}"
85 tree="$(realpath "${tree}")"
86 sroot="${tree}/root"
87 sboot="${tree}/boot"
88 sconf="${tree}/conf"
89
90 exec 2>>"${logf}"
91 _log_to_stderr=yes
92
93 # Helper functions
94 onelink() {
95   # Make sure the provided file(s) have only one link!
96   while [ -n "${1}" ]
97   do
98     if [ -f "${1}" -a "$(stat -f '%l' "${1}")" -gt 1 ]
99     then
100       cp -p "${1}" "${1}.tmp" && mv "${1}.tmp" "${1}" || err "breaklink failed"
101     fi
102     shift
103   done
104 }
105
106 # Build steps
107 do_prepwork() {
108   log Prepare workspace
109   if [ -d "${tree}" -a \( -d "${sroot}" -o -d "${sboot}" -o -d "${sconf}" \) ]
110   then
111     yn n " ${a_yellow}*${a_normal} Workspace already exists. Delete? [y/N]" || err Aborting
112     chk rm -Rf "${sroot}" "${sboot}" "${sconf}"
113   fi
114   chk mkdir -p "${sroot}"
115   # Eliminate schg, because it interferes with hardlinks
116   chk chflags -R noschg "${root}/lib"
117   chk chflags -R noschg "${root}/libexec"
118   chk chflags -R noschg "${root}/sbin"
119   chk chflags -R noschg "${root}/usr"
120   ( cd "${root}" && find . | cpio -p --link "${sroot}" ) || chk
121 }
122
123 do_admin() {
124   log Create an emergency user admin/admin
125   # delink passwd to ensure it doesn't get patched in-place
126   chk onelink "${sroot}/etc/passwd" "${sroot}/etc/master.passwd"
127   echo '$1$2rXOWsK/$eiBHA6K7xL96DZbcY24YR0' | chk chroot "${sroot}" /usr/sbin/pw useradd admin -u 999 -g wheel -G operator -c Administrator -d /usr/home/admin -m -s /bin/csh -H 0
128 }
129
130 do_overlay() {
131   [ -d "${ovly}" ] || return
132   log Apply overlay from "${ovly##${base}/}"
133   ( cd "${ovly}" && find . | cpio -p "${sroot}" ) || chk
134 }
135
136 do_packages() {
137   [ -d "${pkgs}" ] || return
138   count="$(ls -1 "${pkgs}" | wc -l)"
139   [ "${count}" -gt 0 ] || return
140   log Install ${count} packages from "${pkgs##${base}/}"
141   chk mkdir -p "${sroot}/pkg"
142   ( cd "${pkgs}" && find . | cpio -p --link "${sroot}/pkg" ) || chk
143   chk chroot "${sroot}" /bin/sh -c 'cd /pkg; exec pkg_add -F *'
144   chk rm -Rf "${sroot}/pkg"
145 }
146
147 do_patch() {
148   [ -d "${ptch}" ] || return
149   log Apply patches from "${ptch##${base}/}"
150   for file in "${ptch}"/*
151   do
152     note "... $(basename "${file}")"
153     ( cd "${sroot}" && patch < "${file}" ) || chk
154     # Remove .orig files
155     sed -e '/^+++ /!d; s/^+++ //; s/[   ]*[0-9: .+-]*$//' "${file}" | while read orig
156     do
157       rm -f "${sroot}/${orig}.orig"
158     done || chk
159   done
160 }
161
162 do_whiteout() {
163   [ -f "${rmrf}" ] || return
164   log Whiteout files from "${rmrf##${base}/}"
165   while read entry < "${rmrf}"
166   do
167     # Strip off terminating slash
168     entry="${entry%%/}"
169     # Obtain directory name, for path resolution
170     entry_path="$(dirname "${entry}")"
171     entry_path="$(realpath "${sroot}/${entry_path}" 2>/dev/null)"
172     entry_file="$(basename "${entry}")"
173
174     # Ignore paths that do not exist
175     [ "${entry_path}" ] || continue
176
177     # Warn and ignore paths that fall outside ${sroot} after resolution
178     if [ "$(echo ${entry_path} | sed -e "s/^\(.\{${#sroot}\}\).*$/\1/")" != "${sroot}" ]
179     then
180       warn "Whiteout '${entry}' malformed: leads outside '${sroot}'"
181       continue
182     fi
183
184     # Warn and ignore paths that cannot be chdir()'d
185     if [ ! -d "${entry_path}" ]
186     then
187       warn "Whiteout '${entry}' malformed: non-directory in path"
188       continue
189     fi
190
191     ( cd "${entry_path}"
192       if [ -d "${entry}" ]
193       then
194         echo rm -Rf "${entry}"
195       else
196         echo rm -f "${entry}"
197       fi
198     )
199   done
200 }
201
202 do_prepboot() {
203   log Prepare /boot
204   chk mv "${sroot}/boot/boot" "${sroot}/boot/boot.blk"
205   chk ln -sf . "${sroot}/boot/boot"
206 }
207
208 do_preptmp() {
209   log Prepare /tmp
210   ( cd "${sroot}/tmp" && find . | cpio -p --link ../var/tmp ) || chk
211   chk rm -Rf "${sroot}/tmp"
212   chk ln -sf var/tmp "${sroot}/tmp"
213 }
214
215 do_prepetc() {
216   log Prepare /etc
217   chk mkdir -p "${sroot}/etc/local"
218   chk mkdir -p "${sroot}/usr/local/etc" # Silence warnings
219   ( cd "${sroot}/usr/local/etc" && find . | cpio -p --link ../../../etc/local ) || chk
220   chk rm -Rf "${sroot}/usr/local/etc"
221   chk ln -sf ../../etc/local "${sroot}/usr/local/etc"
222 }
223
224 do_imgboot() {
225   log Create boot imgsrc
226   chk mv "${sroot}/boot" "${tree}"
227   chk mkdir -p "${sroot}/boot"
228   chk rm -f "${sboot}/kernel"/*.symbols
229   # Gzipped kernel is okay
230   chk onelink "${sboot}/kernel/kernel"
231   chk gzip -9 "${sboot}/kernel/kernel"
232   # Compress all files in /boot/kernel
233   # Loader cannot handle gzipped modules. Decompress the required modules
234   # kldload cannot handle gzipped modules either
235   #find "${sboot}/kernel" -type f | xargs gzip -9f || chk
236   #cat "${sboot}/loader.conf" | grep '_load=' | sed -e 's/^\(.*\)_load=.*$/\1/' | while read mod
237   #do
238   #  [ -f "${sboot}/kernel/${mod}.ko.gz" ] && gunzip "${sboot}/kernel/${mod}.ko.gz" || chk
239   #done
240   #
241   # Instead: put all modules in the root image, except those needed to boot the kernel
242   chk mkdir -p "${sroot}/boot/boot"
243   chk mkdir -p "${sroot}/boot/kernel"
244   chk ln -sf "../etc/zfs" "${sroot}/boot/zfs"
245
246   # Link all modules into the root fs
247   ( cd "${sboot}/kernel" && find . -name '*.ko' -o -name 'linker.hints' | cpio -p --link "${sroot}/boot/kernel" ) || chk
248
249   # Remove all modules from the root fs that are preloaded by the loader
250   cat "${sboot}/loader.conf" | grep '_load=' | sed -e 's#^\(.*\)_load=.*$#'"${sroot}/boot/kernel/"'\1.ko#' | xargs rm -f
251
252   # Remove all modules from the boot fs that are present in the root fs
253   ( cd "${sroot}/boot/kernel" && ls -1 ) | sed -e 's#^#'"${sboot}/kernel/"'#' | xargs rm -f
254
255   # Link the preloaded modules from the boot fs to the root fs, to provide a homogenous view
256   ( cd "${sboot}/kernel" && ls -1 ) | while read mod
257   do
258     ln -sf "../boot/kernel/${mod}" "${sroot}/boot/kernel/${mod}"
259   done
260 }
261
262 do_imgconf() {
263   log Create conftree
264   chk mkdir -p "${sroot}/conf"
265   echo "ufs:/dev/ufs/conf" > "${sroot}/conf/diskless_remount" || chk
266   chk mkdir -p "${sconf}/backup"
267   chk mkdir -p "${sconf}/base"
268   chk mkdir -p "${sconf}/default"
269
270   # Create packdirs for each
271   for pack in "${cnfd}"/*.md_size
272   do
273     pack="$(basename "${pack%%.md_size}")"
274     chk mkdir -p "${sconf}/base/${pack}"
275     chk mkdir -p "${sconf}/default/${pack}"
276     ( cat "${cnfd}/${pack}.md_size" > "${sconf}/base/${pack}/md_size" ) || chk
277   done
278   chk chown -R :operator "${sconf}"
279   ( find "${sconf}" -print0 | xargs -0 chmod o-rwx ) || chk
280 }
281
282 do_imgetc() {
283   # etc requires special handling to ensure everything is properly arranged.
284   log Create etc confpack
285   chk touch "${sroot}/etc/diskless"
286   # Touch /COPYRIGHT to provide an immutable time anchor for saveconfig
287   chk touch "${sroot}/COPYRIGHT"
288   chk sleep 1 # Make sure anchor is at least one second older than everything in custom
289   chk mkdir -p "${tree}/pack"
290   chk mv "${sroot}/etc" "${tree}/pack"
291   chk mkdir -p "${sroot}/etc"
292   chk cp -p "${tree}/pack/etc/rc" "${tree}/pack/etc/rc.subr" "${tree}/pack/etc/rc.initdiskless" "${tree}/pack/etc/login.conf.db" "${tree}/pack/etc/diskless" "${sroot}/etc"
293
294   # Make sure etc confpack exists; default if necessary
295   chk mkdir -p "${sconf}/base/etc"
296   chk mkdir -p "${sconf}/default/etc"
297   [ -e "${sconf}/base/etc/md_size" ] || echo "10240" > "${sconf}/base/etc/md_size"
298
299   ( cd "${tree}/pack" && find etc | cpio -o ) | gzip -9 > "${sconf}/base/etc.cpio.gz" || chk
300   chk rm -Rf "${tree}/pack"
301 }
302
303 do_imgall() {
304   log Create remaining confpacks
305   chk mkdir -p "${tree}/pack"
306   for pack in "${cnfd}"/*.md_size
307   do
308     pack="$(basename "${pack%%.md_size}")"
309     # Ignore etc, as it was processed in do_imgetc
310     [ "${pack}" = "etc" ] && continue
311     note "... ${pack}"
312     chk mv "${sroot}/${pack}" "${tree}/pack"
313     chk mkdir -p "${sroot}/${pack}"
314     ( cd "${tree}/pack" && find "${pack}" | cpio -o ) | gzip -9 > "${sconf}/base/${pack}.cpio.gz" || chk
315     chk rm -Rf "${tree}/pack/${pack}"
316   done
317   chk rm -Rf "${tree}/pack"
318 }
319
320 do_custom() {
321   log Patch in custom config
322   ( cd "${cnfd}" && find . -name '*.md_size' -o -print | cpio -p --link "${sconf}/default" ) || chk
323   # Make sure files in default are newer than the tagfile, so they will be caught by saveconfig
324   find "${sconf}/default" -type f -print0 | xargs -0 touch
325   # If there are scavenged cpio.gz confpacks, touch their contents as well.
326   chk mkdir -p "${tree}/pack"
327   if ls -1 "${sconf}/default" | grep -q '.cpio.gz'
328   then
329     chk rm -Rf "${tree}/pack"/*
330     for file in "${sconf}/default"/*.cpio.gz
331     do
332       pack="$(basename "${file%%.cpio.gz}")"
333       zcat "${file}" | ( cd "${tree}/pack"; cpio -id ) || chk
334       find "${tree}/pack" -type f -print0 | xargs -0 touch
335       ( cd "${tree}/pack"; find . | cpio -o ) | gzip -9 > "${file}"
336       chk rm -Rf "${tree}/pack"/*
337     done
338   fi
339   chk rm -Rf "${tree}/pack"
340 }
341
342 for step in ${sequence}
343 do
344   echo "${targets}" | grep -q "${step}" || err Unrecognized target "${step}"
345   do_${step}
346 done