mkarchiso 13.9 KB
Newer Older
1
#!/bin/bash
2
3
#
# SPDX-License-Identifier: GPL-3.0-or-later
4

5
6
set -e -u

7
8
9
10
# Control the environment
umask 0022
export LANG="C"
export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-"$(date +%s)"}"
11

12
13
app_name="${0##*/}"
arch="$(uname -m)"
14
pkg_list=()
15
run_cmd=""
16
17
quiet="y"
pacman_conf="/etc/pacman.conf"
18
iso_label="ARCH_$(date +%Y%m)"
19
20
21
iso_publisher="Arch Linux <http://www.archlinux.org>"
iso_application="Arch Linux Live/Rescue CD"
install_dir="arch"
22
23
work_dir="work"
out_dir="out"
24
sfs_mode="sfs"
25
sfs_comp="xz"
26
gpg_key=""
27
28
29
30
31

# Show an INFO message
# $1: message string
_msg_info() {
    local _msg="${1}"
32
33
    [[ "${quiet}" == "y" ]] || printf '[%s] INFO: %s\n' "${app_name}" "${_msg}"

34
35
36
37
38
39
40
41
}

# Show an ERROR message then exit with status
# $1: message string
# $2: exit code number (with 0 does not exit)
_msg_error() {
    local _msg="${1}"
    local _error=${2}
42
43
    printf '\n[%s] ERROR: %s\n\n' "${app_name}" "${_msg}" >&2
    if (( _error > 0 )); then
44
        exit "${_error}"
45
46
47
    fi
}

48
_chroot_init() {
49
    mkdir -p -- "${work_dir}/airootfs"
50
    _pacman base syslinux
51
52
53
}

_chroot_run() {
54
    eval -- arch-chroot "${work_dir}/airootfs" "${run_cmd}"
55
56
}

57
58
_mount_airootfs() {
    trap "_umount_airootfs" EXIT HUP INT TERM
59
    mkdir -p -- "${work_dir}/mnt/airootfs"
60
    _msg_info "Mounting '${work_dir}/airootfs.img' on '${work_dir}/mnt/airootfs'"
61
    mount -- "${work_dir}/airootfs.img" "${work_dir}/mnt/airootfs"
62
    _msg_info "Done!"
63
64
}

65
66
_umount_airootfs() {
    _msg_info "Unmounting '${work_dir}/mnt/airootfs'"
67
    umount -d -- "${work_dir}/mnt/airootfs"
68
    _msg_info "Done!"
69
    rmdir -- "${work_dir}/mnt/airootfs"
70
71
72
73
74
    trap - EXIT HUP INT TERM
}

# Show help usage, with an exit status.
# $1: exit status number.
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
_usage () {
    IFS='' read -r -d '' usagetext <<ENDUSAGETEXT || true
usage ${app_name} [options] command <command options>
 general options:
    -p PACKAGE(S)    Package(s) to install, can be used multiple times
    -r <command>     Run <command> inside airootfs
    -C <file>        Config file for pacman.
                     Default: '${pacman_conf}'
    -L <label>       Set a label for the disk
                     Default: '${iso_label}'
    -P <publisher>   Set a publisher for the disk
                     Default: '${iso_publisher}'
    -A <application> Set an application name for the disk
                     Default: '${iso_application}'
    -D <install_dir> Set an install_dir. All files will by located here.
                     Default: '${install_dir}'
                     NOTE: Max 8 characters, use only [a-z0-9]
    -w <work_dir>    Set the working directory
                     Default: '${work_dir}'
    -o <out_dir>     Set the output directory
                     Default: '${out_dir}'
    -s <sfs_mode>    Set SquashFS image mode (img or sfs)
                     img: prepare airootfs.sfs for dm-snapshot usage
                     sfs: prepare airootfs.sfs for overlayfs usage
                     Default: '${sfs_mode}'
    -c <comp_type>   Set SquashFS compression type (gzip, lzma, lzo, xz, zstd)
                     Default: '${sfs_comp}'
    -v               Enable verbose output
    -h               This message
 commands:
   init
      Make base layout and install base group
   install
      Install all specified packages (-p)
   run
      run command specified by -r
   prepare
      build all images
   pkglist
      make a pkglist.txt of packages installed on airootfs
   iso <image name>
      build an iso image from the working dir
ENDUSAGETEXT
    printf '%s\n' "${usagetext}"
119
    exit "${1}"
120
121
}

122
# Shows configuration according to command mode.
123
# $1: init | install | run | prepare | iso
124
125
_show_config () {
    local _mode="$1"
126
    printf '\n'
127
128
129
130
131
132
    _msg_info "Configuration settings"
    _msg_info "                  Command:   ${command_name}"
    _msg_info "             Architecture:   ${arch}"
    _msg_info "        Working directory:   ${work_dir}"
    _msg_info "   Installation directory:   ${install_dir}"
    case "${_mode}" in
133
134
135
136
        init)
            _msg_info "       Pacman config file:   ${pacman_conf}"
            ;;
        install)
137
            _msg_info "       Pacman config file:   ${pacman_conf}"
138
            _msg_info "                 Packages:   ${pkg_list[*]}"
139
            ;;
140
141
142
        run)
            _msg_info "              Run command:   ${run_cmd}"
            ;;
143
144
        prepare)
            ;;
145
146
        pkglist)
            ;;
147
148
149
150
151
152
        iso)
            _msg_info "               Image name:   ${img_name}"
            _msg_info "               Disk label:   ${iso_label}"
            _msg_info "           Disk publisher:   ${iso_publisher}"
            _msg_info "         Disk application:   ${iso_application}"
            ;;
153
    esac
154
    printf '\n'
155
}
156

157
# Install desired packages to airootfs
158
_pacman () {
159
    _msg_info "Installing packages to '${work_dir}/airootfs/'..."
160
161

    if [[ "${quiet}" = "y" ]]; then
162
        pacstrap -C "${pacman_conf}" -c -G -M -- "${work_dir}/airootfs" "$@" &> /dev/null
163
    else
164
        pacstrap -C "${pacman_conf}" -c -G -M -- "${work_dir}/airootfs" "$@"
165
    fi
166

167
    _msg_info "Packages installed successfully!"
168
169
}

170
# Cleanup airootfs
171
_cleanup () {
172
    _msg_info "Cleaning up what we can on airootfs..."
173

174
    # Delete initcpio image(s)
175
176
    if [[ -d "${work_dir}/airootfs/boot" ]]; then
        find "${work_dir}/airootfs/boot" -type f -name '*.img' -delete
177
178
    fi
    # Delete kernel(s)
179
180
    if [[ -d "${work_dir}/airootfs/boot" ]]; then
        find "${work_dir}/airootfs/boot" -type f -name 'vmlinuz*' -delete
181
182
    fi
    # Delete pacman database sync cache files (*.tar.gz)
183
184
    if [[ -d "${work_dir}/airootfs/var/lib/pacman" ]]; then
        find "${work_dir}/airootfs/var/lib/pacman" -maxdepth 1 -type f -delete
185
186
    fi
    # Delete pacman database sync cache
187
188
    if [[ -d "${work_dir}/airootfs/var/lib/pacman/sync" ]]; then
        find "${work_dir}/airootfs/var/lib/pacman/sync" -delete
189
190
    fi
    # Delete pacman package cache
191
192
    if [[ -d "${work_dir}/airootfs/var/cache/pacman/pkg" ]]; then
        find "${work_dir}/airootfs/var/cache/pacman/pkg" -type f -delete
193
194
    fi
    # Delete all log files, keeps empty dirs.
195
196
    if [[ -d "${work_dir}/airootfs/var/log" ]]; then
        find "${work_dir}/airootfs/var/log" -type f -delete
197
198
    fi
    # Delete all temporary files and dirs
199
200
    if [[ -d "${work_dir}/airootfs/var/tmp" ]]; then
        find "${work_dir}/airootfs/var/tmp" -mindepth 1 -delete
201
    fi
202
    # Delete package pacman related files.
203
    find "${work_dir}" \( -name '*.pacnew' -o -name '*.pacsave' -o -name '*.pacorig' \) -delete
204
    _msg_info "Done!"
205
}
206

207
208
# Makes a ext4 filesystem inside a SquashFS from a source directory.
_mkairootfs_img () {
209
210
    if [[ ! -e "${work_dir}/airootfs" ]]; then
        _msg_error "The path '${work_dir}/airootfs' does not exist" 1
211
212
    fi

213
    _msg_info "Creating ext4 image of 32GiB..."
214
    truncate -s 32G -- "${work_dir}/airootfs.img"
215
    if [[ "${quiet}" == "y" ]]; then
216
217
218
        mkfs.ext4 -q -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${work_dir}/airootfs.img"
    else
        mkfs.ext4 -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${work_dir}/airootfs.img"
219
    fi
220
    tune2fs -c 0 -i 0 -- "${work_dir}/airootfs.img" &> /dev/null
221
    _msg_info "Done!"
222
    _mount_airootfs
223
    _msg_info "Copying '${work_dir}/airootfs/' to '${work_dir}/mnt/airootfs/'..."
224
225
    cp -aT -- "${work_dir}/airootfs/" "${work_dir}/mnt/airootfs/"
    chown root:root -- "${work_dir}/mnt/airootfs/"
226
    _msg_info "Done!"
227
    _umount_airootfs
228
    mkdir -p -- "${work_dir}/iso/${install_dir}/${arch}"
229
230
    _msg_info "Creating SquashFS image, this may take some time..."
    if [[ "${quiet}" = "y" ]]; then
231
232
        mksquashfs "${work_dir}/airootfs.img" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
            -comp "${sfs_comp}" -no-progress &> /dev/null
233
    else
234
        mksquashfs "${work_dir}/airootfs.img" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
235
            -comp "${sfs_comp}"
236
237
    fi
    _msg_info "Done!"
238
    rm -- "${work_dir}/airootfs.img"
239
240
}

241
242
243
244
245
246
# Makes a SquashFS filesystem from a source directory.
_mkairootfs_sfs () {
    if [[ ! -e "${work_dir}/airootfs" ]]; then
        _msg_error "The path '${work_dir}/airootfs' does not exist" 1
    fi

247
    mkdir -p -- "${work_dir}/iso/${install_dir}/${arch}"
248
249
    _msg_info "Creating SquashFS image, this may take some time..."
    if [[ "${quiet}" = "y" ]]; then
250
251
        mksquashfs "${work_dir}/airootfs" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
            -comp "${sfs_comp}" -no-progress &> /dev/null
252
    else
253
        mksquashfs "${work_dir}/airootfs" "${work_dir}/iso/${install_dir}/${arch}/airootfs.sfs" -noappend \
254
            -comp "${sfs_comp}"
255
256
257
258
    fi
    _msg_info "Done!"
}

259
260
_mkchecksum () {
    _msg_info "Creating checksum file for self-test..."
261
    cd -- "${work_dir}/iso/${install_dir}/${arch}"
262
    sha512sum airootfs.sfs > airootfs.sha512
263
    cd -- "${OLDPWD}"
264
    _msg_info "Done!"
265
266
}

267
268
_mksignature () {
    _msg_info "Creating signature file..."
269
    cd -- "${work_dir}/iso/${install_dir}/${arch}"
270
    gpg --detach-sign --default-key "${gpg_key}" airootfs.sfs
271
    cd -- "${OLDPWD}"
272
273
274
    _msg_info "Done!"
}

275
276
277
command_pkglist () {
    _show_config pkglist

278
    _msg_info "Creating a list of installed packages on live-enviroment..."
279
    pacman -Q --sysroot "${work_dir}/airootfs" > "${work_dir}/iso/${install_dir}/pkglist.${arch}.txt"
280
    _msg_info "Done!"
281
282
283

}

284
# Create an ISO9660 filesystem from "iso" directory.
285
command_iso () {
286
    local _iso_efi_boot_args=()
287

288
289
290
    if [[ ! -f "${work_dir}/iso/isolinux/isolinux.bin" ]]; then
         _msg_error "The file '${work_dir}/iso/isolinux/isolinux.bin' does not exist." 1
    fi
291
292
293
    if [[ ! -f "${work_dir}/iso/isolinux/isohdpfx.bin" ]]; then
         _msg_error "The file '${work_dir}/iso/isolinux/isohdpfx.bin' does not exist." 1
    fi
294

295
296
    # If exists, add an EFI "El Torito" boot image (FAT filesystem) to ISO-9660 image.
    if [[ -f "${work_dir}/iso/EFI/archiso/efiboot.img" ]]; then
297
298
299
300
301
302
        _iso_efi_boot_args+=(
            '-eltorito-alt-boot'
            '-e' 'EFI/archiso/efiboot.img'
            '-no-emul-boot'
            '-isohybrid-gpt-basdat'
        )
303
304
    fi

305
306
    _show_config iso

307
    mkdir -p -- "${out_dir}"
308
309
    _msg_info "Creating ISO image..."
    local _qflag=""
310
    if [[ "${quiet}" == "y" ]]; then
311
312
313
        xorriso -as mkisofs -quiet \
            -iso-level 3 \
            -full-iso9660-filenames \
314
            -rational-rock \
315
316
317
            -volid "${iso_label}" \
            -appid "${iso_application}" \
            -publisher "${iso_publisher}" \
318
            -preparer "prepared by ${app_name}" \
319
320
321
322
323
324
325
326
327
328
329
            -eltorito-boot isolinux/isolinux.bin \
            -eltorito-catalog isolinux/boot.cat \
            -no-emul-boot -boot-load-size 4 -boot-info-table \
            -isohybrid-mbr "${work_dir}/iso/isolinux/isohdpfx.bin" \
            "${_iso_efi_boot_args[@]}" \
            -output "${out_dir}/${img_name}" \
            "${work_dir}/iso/"
    else
        xorriso -as mkisofs \
            -iso-level 3 \
            -full-iso9660-filenames \
330
            -rational-rock \
331
332
333
            -volid "${iso_label}" \
            -appid "${iso_application}" \
            -publisher "${iso_publisher}" \
334
            -preparer "prepared by ${app_name}" \
335
336
337
338
339
340
341
            -eltorito-boot isolinux/isolinux.bin \
            -eltorito-catalog isolinux/boot.cat \
            -no-emul-boot -boot-load-size 4 -boot-info-table \
            -isohybrid-mbr "${work_dir}/iso/isolinux/isohdpfx.bin" \
            "${_iso_efi_boot_args[@]}" \
            -output "${out_dir}/${img_name}" \
            "${work_dir}/iso/"
342
    fi
343
    _msg_info "Done! | $(ls -sh -- "${out_dir}/${img_name}")"
344
345
}

346
# create airootfs.sfs filesystem, and push it in "iso" directory.
347
348
349
350
command_prepare () {
    _show_config prepare

    _cleanup
351
    if [[ "${sfs_mode}" == "sfs" ]]; then
352
353
354
355
        _mkairootfs_sfs
    else
        _mkairootfs_img
    fi
356
    _mkchecksum
357
    if [[ "${gpg_key}" ]]; then
Gerardo Exequiel Pozzi's avatar
Gerardo Exequiel Pozzi committed
358
359
      _mksignature
    fi
360
361
}

362
# Install packages on airootfs.
363
# A basic check to avoid double execution/reinstallation is done via hashing package names.
364
command_install () {
365
366
367
368
    if [[ ! -f "${pacman_conf}" ]]; then
        _msg_error "Pacman config file '${pacman_conf}' does not exist" 1
    fi

369
    if (( ${#pkg_list[@]} == 0 )); then
370
371
372
373
        _msg_error "Packages must be specified" 0
        _usage 1
    fi

374
    _show_config install
375

376
    _pacman "${pkg_list[@]}"
377
378
}

379
380
381
382
383
384
385
386
387
command_init() {
    _show_config init
    _chroot_init
}

command_run() {
    _show_config run
    _chroot_run
}
388

389
while getopts 'p:r:C:L:P:A:D:w:o:s:c:g:vh' arg; do
390
    case "${arg}" in
391
392
        p)
            read -r -a opt_pkg_list <<< "${OPTARG}"
393
394
            pkg_list+=("${opt_pkg_list[@]}")
            ;;
395
        r) run_cmd="${OPTARG}" ;;
396
        C) pacman_conf="$(realpath -- "${OPTARG}")" ;;
397
398
399
400
        L) iso_label="${OPTARG}" ;;
        P) iso_publisher="${OPTARG}" ;;
        A) iso_application="${OPTARG}" ;;
        D) install_dir="${OPTARG}" ;;
401
402
        w) work_dir="$(realpath -- "${OPTARG}")" ;;
        o) out_dir="$(realpath -- "${OPTARG}")" ;;
403
        s) sfs_mode="${OPTARG}" ;;
404
        c) sfs_comp="${OPTARG}" ;;
405
        g) gpg_key="${OPTARG}" ;;
406
407
408
409
410
411
412
413
414
        v) quiet="n" ;;
        h|?) _usage 0 ;;
        *)
            _msg_error "Invalid argument '${arg}'" 0
            _usage 1
            ;;
    esac
done

415
416
417
418
if (( EUID != 0 )); then
    _msg_error "${app_name} must be run as root." 1
fi

419
420
shift $((OPTIND - 1))

421
if (( $# < 1 )); then
422
423
    _msg_error "No command specified" 0
    _usage 1
424
fi
425
426
427
command_name="${1}"

case "${command_name}" in
428
429
430
431
432
433
434
435
    init)
        command_init
        ;;
    install)
        command_install
        ;;
    run)
        command_run
436
437
438
439
        ;;
    prepare)
        command_prepare
        ;;
440
441
442
    pkglist)
        command_pkglist
        ;;
443
    iso)
444
        if (( $# < 2 )); then
445
446
447
            _msg_error "No image specified" 0
            _usage 1
        fi
448
        img_name="${2}"
449
450
451
452
453
454
455
        command_iso
        ;;
    *)
        _msg_error "Invalid command name '${command_name}'" 0
        _usage 1
        ;;
esac
456
457

# vim:ts=4:sw=4:et: