mkarchiso 37.2 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
# mkarchiso defaults
13
app_name="${0##*/}"
14
pkg_list=()
15
run_cmd=""
16
quiet="y"
17
18
work_dir="work"
out_dir="out"
19
img_name="${app_name}.iso"
20
sfs_mode="sfs"
21
sfs_comp="xz"
22
gpg_key=""
23
override_gpg_key=""
24

25
26
27
28
# profile defaults
profile=""
iso_name="${app_name}"
iso_label="${app_name^^}"
29
override_iso_label=""
30
iso_publisher="${app_name}"
31
override_iso_publisher=""
32
iso_application="${app_name} iso"
33
override_iso_application=""
34
35
iso_version=""
install_dir="${app_name}"
36
override_install_dir=""
37
38
arch="$(uname -m)"
pacman_conf="/etc/pacman.conf"
39
override_pacman_conf=""
40
41
42
bootmodes=()


43
44
45
46
# Show an INFO message
# $1: message string
_msg_info() {
    local _msg="${1}"
47
    [[ "${quiet}" == "y" ]] || printf '[%s] INFO: %s\n' "${app_name}" "${_msg}"
48
49
}

50
51
52
53
# Show a WARNING message
# $1: message string
_msg_warning() {
    local _msg="${1}"
54
    printf '[%s] WARNING: %s\n' "${app_name}" "${_msg}" >&2
55
56
}

57
58
59
60
61
62
# 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}
63
    printf '[%s] ERROR: %s\n' "${app_name}" "${_msg}" >&2
64
    if (( _error > 0 )); then
65
        exit "${_error}"
66
67
68
    fi
}

69
_chroot_init() {
70
    install -d -m 0755 -o 0 -g 0 -- "${airootfs_dir}"
71
    _pacman base syslinux
72
73
74
}

_chroot_run() {
75
    eval -- arch-chroot "${airootfs_dir}" "${run_cmd}"
76
77
}

78
79
_mount_airootfs() {
    trap "_umount_airootfs" EXIT HUP INT TERM
80
81
    install -d -m 0755 -- "${work_dir}/mnt/airootfs"
    _msg_info "Mounting '${airootfs_dir}.img' on '${work_dir}/mnt/airootfs'..."
82
    mount -- "${airootfs_dir}.img" "${work_dir}/mnt/airootfs"
83
    _msg_info "Done!"
84
85
}

86
_umount_airootfs() {
87
    _msg_info "Unmounting '${work_dir}/mnt/airootfs'..."
88
    umount -d -- "${work_dir}/mnt/airootfs"
89
    _msg_info "Done!"
90
    rmdir -- "${work_dir}/mnt/airootfs"
91
92
93
94
95
    trap - EXIT HUP INT TERM
}

# Show help usage, with an exit status.
# $1: exit status number.
96
_usage() {
97
    IFS='' read -r -d '' usagetext <<ENDUSAGETEXT || true
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
usage ${app_name} [options] <profile_dir or legacy_command>
  options:
     -A <application> Set an application name for the ISO
                      Default: '${iso_application}'
     -C <file>        pacman configuration file.
                      Default: '${pacman_conf}'
     -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]
     -L <label>       Set the ISO volume label
                      Default: '${iso_label}'
     -P <publisher>   Set the ISO publisher
                      Default: '${iso_publisher}'
     -c <comp_type>   Set SquashFS compression type (gzip, lzma, lzo, xz, zstd)
                      Default: '${sfs_comp}'
     -g <gpg_key>     Set the GPG key to be used for signing the sqashfs image
     -h               This message
     -o <out_dir>     Set the output directory
                      Default: '${out_dir}'
     -p PACKAGE(S)    Package(s) to install, can be used multiple times
118
     -r <run_cmd>     Set a command to be run in chroot (only relevant for legacy 'run' command)
119
120
121
122
123
124
125
126
127
128
129
130
131
                      NOTE: Deprecated, will be removed with archiso v49
     -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}'
     -v               Enable verbose output
     -w <work_dir>    Set the working directory
                      Default: '${work_dir}'

  profile_dir:        Directory of the archiso profile to build

  legacy_command:     Legacy build.sh command
                      NOTE: Deprecated, will be removed with archiso v49
132
    init
133
        initialize a chroot for building
134
    install
135
        install packages to the chroot
136
    run
137
        run a command in the chroot
138
    prepare
139
        cleanup and prepare the airootfs
140
    pkglist
141
        create a list of packages installed on the medium
142
    iso
143
        create the ISO
144
ENDUSAGETEXT
145
    printf '%s' "${usagetext}"
146
    exit "${1}"
147
148
}

149
# Shows configuration according to command mode.
150
151
# $1: build_profile | init | install | run | prepare | iso
_show_config() {
152
153
    local _mode="$1" build_date
    build_date="$(date --utc --iso-8601=seconds -d "@${SOURCE_DATE_EPOCH}")"
154
    _msg_info "${app_name} configuration settings"
155
    [[ ! -d "${command_name}" ]] && _msg_info "           Legacy Command:   ${command_name}"
156
157
158
159
    _msg_info "             Architecture:   ${arch}"
    _msg_info "        Working directory:   ${work_dir}"
    _msg_info "   Installation directory:   ${install_dir}"
    case "${_mode}" in
160
        build_profile)
161
            _msg_info "               Build date:   ${build_date}"
162
163
164
165
166
167
168
169
170
171
172
            _msg_info "         Output directory:   ${out_dir}"
            _msg_info "                  GPG key:   ${gpg_key:-None}"
            _msg_info "                  Profile:   ${profile}"
            _msg_info "Pacman configuration file:   ${pacman_conf}"
            _msg_info "          Image file name:   ${img_name}"
            _msg_info "         ISO volume label:   ${iso_label}"
            _msg_info "            ISO publisher:   ${iso_publisher}"
            _msg_info "          ISO application:   ${iso_application}"
            _msg_info "               Boot modes:   ${bootmodes[*]}"
            _msg_info "                 Packages:   ${pkg_list[*]}"
            ;;
173
        init)
174
            _msg_info "Pacman configuration file:   ${pacman_conf}"
175
176
            ;;
        install)
177
178
            _msg_info "                  GPG key:   ${gpg_key:-None}"
            _msg_info "Pacman configuration file:   ${pacman_conf}"
179
            _msg_info "                 Packages:   ${pkg_list[*]}"
180
            ;;
181
182
183
        run)
            _msg_info "              Run command:   ${run_cmd}"
            ;;
184
        prepare)
185
            _msg_info "                  GPG key:   ${gpg_key:-None}"
186
            ;;
187
188
        pkglist)
            ;;
189
        iso)
190
191
192
193
194
            _msg_info "         Output directory:   ${out_dir}"
            _msg_info "          Image file name:   ${img_name}"
            _msg_info "         ISO volume label:   ${iso_label}"
            _msg_info "            ISO publisher:   ${iso_publisher}"
            _msg_info "          ISO application:   ${iso_application}"
195
            ;;
196
    esac
197
    [[ "${quiet}" == "y" ]] || printf '\n'
198
}
199

200
# Install desired packages to airootfs
201
_pacman() {
202
    _msg_info "Installing packages to '${airootfs_dir}/'..."
203
204

    if [[ "${quiet}" = "y" ]]; then
205
        pacstrap -C "${work_dir}/pacman.conf" -c -G -M -- "${airootfs_dir}" "$@" &> /dev/null
206
    else
207
        pacstrap -C "${work_dir}/pacman.conf" -c -G -M -- "${airootfs_dir}" "$@"
208
    fi
209

210
    _msg_info "Done! Packages installed successfully."
211
212
}

213
# Cleanup airootfs
214
_cleanup() {
215
    _msg_info "Cleaning up what we can on airootfs..."
216

217
    # Delete all files in /boot
218
    if [[ -d "${airootfs_dir}/boot" ]]; then
219
        find "${airootfs_dir}/boot" -mindepth 1 -delete
220
221
    fi
    # Delete pacman database sync cache files (*.tar.gz)
222
223
    if [[ -d "${airootfs_dir}/var/lib/pacman" ]]; then
        find "${airootfs_dir}/var/lib/pacman" -maxdepth 1 -type f -delete
224
225
    fi
    # Delete pacman database sync cache
226
227
    if [[ -d "${airootfs_dir}/var/lib/pacman/sync" ]]; then
        find "${airootfs_dir}/var/lib/pacman/sync" -delete
228
229
    fi
    # Delete pacman package cache
230
231
    if [[ -d "${airootfs_dir}/var/cache/pacman/pkg" ]]; then
        find "${airootfs_dir}/var/cache/pacman/pkg" -type f -delete
232
233
    fi
    # Delete all log files, keeps empty dirs.
234
235
    if [[ -d "${airootfs_dir}/var/log" ]]; then
        find "${airootfs_dir}/var/log" -type f -delete
236
237
    fi
    # Delete all temporary files and dirs
238
239
    if [[ -d "${airootfs_dir}/var/tmp" ]]; then
        find "${airootfs_dir}/var/tmp" -mindepth 1 -delete
240
    fi
241
    # Delete package pacman related files.
242
    find "${work_dir}" \( -name '*.pacnew' -o -name '*.pacsave' -o -name '*.pacorig' \) -delete
243
    # Create an empty /etc/machine-id
244
    printf '' > "${airootfs_dir}/etc/machine-id"
245
246

    _msg_info "Done!"
247
}
248

249
# Makes a ext4 filesystem inside a SquashFS from a source directory.
250
_mkairootfs_img() {
251
252
    if [[ ! -e "${airootfs_dir}" ]]; then
        _msg_error "The path '${airootfs_dir}' does not exist" 1
253
254
    fi

255
    _msg_info "Creating ext4 image of 32 GiB..."
256
    if [[ "${quiet}" == "y" ]]; then
257
        mkfs.ext4 -q -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${airootfs_dir}.img" 32G
258
    else
259
        mkfs.ext4 -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${airootfs_dir}.img" 32G
260
    fi
261
    tune2fs -c 0 -i 0 -- "${airootfs_dir}.img" > /dev/null
262
    _msg_info "Done!"
263
    _mount_airootfs
264
265
    _msg_info "Copying '${airootfs_dir}/' to '${work_dir}/mnt/airootfs/'..."
    cp -aT -- "${airootfs_dir}/" "${work_dir}/mnt/airootfs/"
266
    chown -- 0:0 "${work_dir}/mnt/airootfs/"
267
    _msg_info "Done!"
268
    _umount_airootfs
269
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/${arch}"
270
271
    _msg_info "Creating SquashFS image, this may take some time..."
    if [[ "${quiet}" = "y" ]]; then
272
        mksquashfs "${airootfs_dir}.img" "${isofs_dir}/${install_dir}/${arch}/airootfs.sfs" -noappend \
273
            -comp "${sfs_comp}" -no-progress > /dev/null
274
    else
275
        mksquashfs "${airootfs_dir}.img" "${isofs_dir}/${install_dir}/${arch}/airootfs.sfs" -noappend \
276
            -comp "${sfs_comp}"
277
278
    fi
    _msg_info "Done!"
279
    rm -- "${airootfs_dir}.img"
280
281
}

282
# Makes a SquashFS filesystem from a source directory.
283
_mkairootfs_sfs() {
284
285
    if [[ ! -e "${airootfs_dir}" ]]; then
        _msg_error "The path '${airootfs_dir}' does not exist" 1
286
287
    fi

288
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/${arch}"
289
290
    _msg_info "Creating SquashFS image, this may take some time..."
    if [[ "${quiet}" = "y" ]]; then
291
        mksquashfs "${airootfs_dir}" "${isofs_dir}/${install_dir}/${arch}/airootfs.sfs" -noappend \
292
            -comp "${sfs_comp}" -no-progress > /dev/null
293
    else
294
        mksquashfs "${airootfs_dir}" "${isofs_dir}/${install_dir}/${arch}/airootfs.sfs" -noappend \
295
            -comp "${sfs_comp}"
296
297
298
299
    fi
    _msg_info "Done!"
}

300
_mkchecksum() {
301
    _msg_info "Creating checksum file for self-test..."
302
    cd -- "${isofs_dir}/${install_dir}/${arch}"
303
    sha512sum airootfs.sfs > airootfs.sha512
304
    cd -- "${OLDPWD}"
305
    _msg_info "Done!"
306
307
}

308
309
_mksignature() {
    _msg_info "Signing SquashFS image..."
310
    cd -- "${isofs_dir}/${install_dir}/${arch}"
311
    gpg --detach-sign --default-key "${gpg_key}" airootfs.sfs
312
    cd -- "${OLDPWD}"
313
314
315
    _msg_info "Done!"
}

316
317
318
319
320
321
322
# Helper function to run functions only one time.
_run_once() {
    if [[ ! -e "${work_dir}/build.${1}" ]]; then
        "$1"
        touch "${work_dir}/build.${1}"
    fi
}
323

324
# Set up custom pacman.conf with custom cache and pacman hook directories
325
_make_pacman_conf() {
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
    local _cache_dirs _system_cache_dirs _profile_cache_dirs
    _system_cache_dirs="$(pacman-conf CacheDir| tr '\n' ' ')"
    _profile_cache_dirs="$(pacman-conf --config "${pacman_conf}" CacheDir| tr '\n' ' ')"

    # only use the profile's CacheDir, if it is not the default and not the same as the system cache dir
    if [[ "${_profile_cache_dirs}" != "/var/cache/pacman/pkg" ]] && \
        [[ "${_system_cache_dirs}" != "${_profile_cache_dirs}" ]]; then
        _cache_dirs="${_profile_cache_dirs}"
    else
        _cache_dirs="${_system_cache_dirs}"
    fi

    _msg_info "Copying custom pacman.conf to work directory..."
    # take the profile pacman.conf and strip all settings that would break in chroot when using pacman -r
    # see `man 8 pacman` for further info
    pacman-conf --config "${pacman_conf}" | \
        sed '/CacheDir/d;/DBPath/d;/HookDir/d;/LogFile/d;/RootDir/d' > "${work_dir}/pacman.conf"

    _msg_info "Using pacman CacheDir: ${_cache_dirs}"
    # append CacheDir and HookDir to [options] section
    # HookDir is *always* set to the airootfs' override directory
    sed "/\[options\]/a CacheDir = ${_cache_dirs}
        /\[options\]/a HookDir = ${airootfs_dir}/etc/pacman.d/hooks/" \
        -i "${work_dir}/pacman.conf"
350
351
352
353
354
}

# Prepare working directory and copy custom airootfs files (airootfs)
_make_custom_airootfs() {
    local passwd=()
355
356
357

    install -d -m 0755 -o 0 -g 0 -- "${airootfs_dir}"

358
    if [[ -d "${profile}/airootfs" ]]; then
359
        _msg_info "Copying custom airootfs files and setting up user home directories..."
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
        cp -af --no-preserve=ownership -- "${profile}/airootfs/." "${airootfs_dir}"

        [[ -e "${airootfs_dir}/etc/shadow" ]] && chmod -f 0400 -- "${airootfs_dir}/etc/shadow"
        [[ -e "${airootfs_dir}/etc/gshadow" ]] && chmod -f 0400 -- "${airootfs_dir}/etc/gshadow"

        # Set up user home directories and permissions
        if [[ -e "${airootfs_dir}/etc/passwd" ]]; then
            while IFS=':' read -a passwd -r; do
                [[ "${passwd[5]}" == '/' ]] && continue
                [[ -z "${passwd[5]}" ]] && continue

                if [[ -d "${airootfs_dir}${passwd[5]}" ]]; then
                    chown -hR -- "${passwd[2]}:${passwd[3]}" "${airootfs_dir}${passwd[5]}"
                    chmod -f 0750 -- "${airootfs_dir}${passwd[5]}"
                else
                    install -d -m 0750 -o "${passwd[2]}" -g "${passwd[3]}" -- "${airootfs_dir}${passwd[5]}"
                fi
             done < "${airootfs_dir}/etc/passwd"
        fi
379
        _msg_info "Done!"
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
    fi
}

# Packages (airootfs)
_make_packages() {
    if [[ -n "${gpg_key}" ]]; then
        exec {ARCHISO_GNUPG_FD}<>"${work_dir}/pubkey.gpg"
        export ARCHISO_GNUPG_FD
    fi
    _pacman "${pkg_list[@]}"
    if [[ -n "${gpg_key}" ]]; then
        exec {ARCHISO_GNUPG_FD}<&-
        unset ARCHISO_GNUPG_FD
    fi
}

# Customize installation (airootfs)
_make_customize_airootfs() {
    local passwd=()
399

400
    if [[ -e "${profile}/airootfs/etc/passwd" ]]; then
401
        _msg_info "Copying /etc/skel/* to user homes..."
402
        while IFS=':' read -a passwd -r; do
403
            (( passwd[2] >= 1000 && passwd[2] < 60000 )) || continue
404
405
            [[ "${passwd[5]}" == '/' ]] && continue
            [[ -z "${passwd[5]}" ]] && continue
406
407
            cp -dnRT --preserve=mode,timestamps,links -- "${airootfs_dir}/etc/skel" "${airootfs_dir}${passwd[5]}"
            chmod -f 0750 -- "${airootfs_dir}${passwd[5]}"
408
409
410
            chown -hR -- "${passwd[2]}:${passwd[3]}" "${airootfs_dir}${passwd[5]}"

        done < "${profile}/airootfs/etc/passwd"
411
        _msg_info "Done!"
412
413
414
    fi

    if [[ -e "${airootfs_dir}/root/customize_airootfs.sh" ]]; then
415
        _msg_info "Running customize_airootfs.sh in '${airootfs_dir}' chroot..."
416
417
        _msg_warning "customize_airootfs.sh is deprecated! Support for it will be removed in a future archiso version."
        local run_cmd="/root/customize_airootfs.sh"
418
        _chroot_run
419
        rm -- "${airootfs_dir}/root/customize_airootfs.sh"
420
        _msg_info "Done! customize_airootfs.sh run successfully."
421
422
423
    fi
}

424
425
426
427
428
429
430
431
432
433
434
435
# Set up boot loaders
_make_bootmodes() {
    local bootmode
    for bootmode in "${bootmodes[@]}"; do
        if typeset -f "_make_boot_${bootmode}" &> /dev/null; then
            _run_once "_make_boot_${bootmode}"
        else
            _msg_error "${bootmode} is not a valid boot mode" 1
        fi
    done
}

436
# Prepare kernel/initramfs ${install_dir}/boot/
437
_make_boot_on_iso() {
438
    local ucode_image
439
440
    _msg_info "Preparing kernel and intramfs for the ISO 9660 file system..."
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/${arch}"
441
442
443
444
445
446
447
448
449
450
451
452
453
    install -m 0644 -- "${airootfs_dir}/boot/initramfs-"*".img" "${isofs_dir}/${install_dir}/boot/${arch}/"
    install -m 0644 -- "${airootfs_dir}/boot/vmlinuz-"* "${isofs_dir}/${install_dir}/boot/${arch}/"

    for ucode_image in {intel-uc.img,intel-ucode.img,amd-uc.img,amd-ucode.img,early_ucode.cpio,microcode.cpio}; do
        if [[ -e "${airootfs_dir}/boot/${ucode_image}" ]]; then
            install -m 0644 -- "${airootfs_dir}/boot/${ucode_image}" "${isofs_dir}/${install_dir}/boot/"
            if [[ -e "${airootfs_dir}/usr/share/licenses/${ucode_image%.*}/" ]]; then
                install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/licenses/${ucode_image%.*}/"
                install -m 0644 -- "${airootfs_dir}/usr/share/licenses/${ucode_image%.*}/"* \
                    "${isofs_dir}/${install_dir}/boot/licenses/${ucode_image%.*}/"
            fi
        fi
    done
454
    _msg_info "Done!"
455
456
457
}

# Prepare /${install_dir}/boot/syslinux
458
_make_boot_bios.syslinux.mbr() {
459
460
    _msg_info "Setting up SYSLINUX for BIOS booting from a disk..."
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/syslinux"
461
462
    for _cfg in "${profile}/syslinux/"*.cfg; do
        sed "s|%ARCHISO_LABEL%|${iso_label}|g;
463
464
465
             s|%INSTALL_DIR%|${install_dir}|g;
             s|%ARCH%|${arch}|g" \
             "${_cfg}" > "${isofs_dir}/${install_dir}/boot/syslinux/${_cfg##*/}"
466
    done
467
468
469
    if [[ -e "${profile}/syslinux/splash.png" ]]; then
        install -m 0644 -- "${profile}/syslinux/splash.png" "${isofs_dir}/${install_dir}/boot/syslinux/"
    fi
470
471
472
    install -m 0644 -- "${airootfs_dir}/usr/lib/syslinux/bios/"*.c32 "${isofs_dir}/${install_dir}/boot/syslinux/"
    install -m 0644 -- "${airootfs_dir}/usr/lib/syslinux/bios/lpxelinux.0" "${isofs_dir}/${install_dir}/boot/syslinux/"
    install -m 0644 -- "${airootfs_dir}/usr/lib/syslinux/bios/memdisk" "${isofs_dir}/${install_dir}/boot/syslinux/"
473

474
    _run_once _make_boot_on_iso
475

476
477
478
479
480
481
482
483
484
    if [[ -e "${isofs_dir}/${install_dir}/boot/syslinux/hdt.c32" ]]; then
        install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/syslinux/hdt"
        if [[ -e "${airootfs_dir}/usr/share/hwdata/pci.ids" ]]; then
            gzip -c -9 "${airootfs_dir}/usr/share/hwdata/pci.ids" > \
                "${isofs_dir}/${install_dir}/boot/syslinux/hdt/pciids.gz"
        fi
        find "${airootfs_dir}/usr/lib/modules" -name 'modules.alias' -print -exec gzip -c -9 '{}' ';' -quit > \
            "${isofs_dir}/${install_dir}/boot/syslinux/hdt/modalias.gz"
    fi
485
486
487
488
489

    # Add other aditional/extra files to ${install_dir}/boot/
    if [[ -e "${airootfs_dir}/boot/memtest86+/memtest.bin" ]]; then
        # rename for PXE: https://wiki.archlinux.org/index.php/Syslinux#Using_memtest
        install -m 0644 -- "${airootfs_dir}/boot/memtest86+/memtest.bin" "${isofs_dir}/${install_dir}/boot/memtest"
490
        install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/licenses/memtest86+/"
491
492
493
        install -m 0644 -- "${airootfs_dir}/usr/share/licenses/common/GPL2/license.txt" \
            "${isofs_dir}/${install_dir}/boot/licenses/memtest86+/"
    fi
494
    _msg_info "Done! SYSLINUX set up for BIOS booting from a disk successfully."
495
496
497
}

# Prepare /isolinux
498
_make_boot_bios.syslinux.eltorito() {
499
500
    _msg_info "Setting up SYSLINUX for BIOS booting from an optical disc..."
    install -d -m 0755 -- "${isofs_dir}/isolinux"
501
502
503
504
505
506
    for _cfg in "${profile}/isolinux/"*".cfg"; do
        sed "s|%ARCHISO_LABEL%|${iso_label}|g;
             s|%INSTALL_DIR%|${install_dir}|g;
             s|%ARCH%|${arch}|g" \
             "${_cfg}" > "${isofs_dir}/isolinux/${_cfg##*/}"
    done
507
508
509
    install -m 0644 -- "${airootfs_dir}/usr/lib/syslinux/bios/isolinux.bin" "${isofs_dir}/isolinux/"
    install -m 0644 -- "${airootfs_dir}/usr/lib/syslinux/bios/isohdpfx.bin" "${isofs_dir}/isolinux/"
    install -m 0644 -- "${airootfs_dir}/usr/lib/syslinux/bios/ldlinux.c32" "${isofs_dir}/isolinux/"
510
511
512

    # isolinux.cfg loads syslinux.cfg
    _run_once _make_boot_bios.syslinux.mbr
513
514

    _msg_info "Done! SYSLINUX set up for BIOS booting from an optical disc successfully."
515
516
517
518
}

# Prepare /EFI on ISO-9660
_make_efi() {
519
520
    _msg_info "Preparing an /EFI directory for the ISO 9660 file system..."
    install -d -m 0755 -- "${isofs_dir}/EFI/BOOT"
521
522
523
    install -m 0644 -- "${airootfs_dir}/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \
        "${isofs_dir}/EFI/BOOT/BOOTx64.EFI"

524
    install -d -m 0755 -- "${isofs_dir}/loader/entries"
525
526
    install -m 0644 -- "${profile}/efiboot/loader/loader.conf" "${isofs_dir}/loader/"

527
528
529
530
531
532
    for _conf in "${profile}/efiboot/loader/entries/"*".conf"; do
        sed "s|%ARCHISO_LABEL%|${iso_label}|g;
             s|%INSTALL_DIR%|${install_dir}|g;
             s|%ARCH%|${arch}|g" \
            "${_conf}" > "${isofs_dir}/loader/entries/${_conf##*/}"
    done
533
534
535

    # edk2-shell based UEFI shell
    # shellx64.efi is picked up automatically when on /
536
537
538
    if [[ -e "${airootfs_dir}/usr/share/edk2-shell/x64/Shell_Full.efi" ]]; then
        install -m 0644 -- "${airootfs_dir}/usr/share/edk2-shell/x64/Shell_Full.efi" "${isofs_dir}/shellx64.efi"
    fi
539
    _msg_info "Done!"
540
541
}

542
543
# Prepare kernel/initramfs on efiboot.img
_make_boot_on_fat() {
544
    local ucode_image all_ucode_images=()
545
    _msg_info "Preparing kernel and intramfs for the FAT file system..."
546
    mmd -i "${work_dir}/efiboot.img" \
547
        "::/${install_dir}" "::/${install_dir}/boot" "::/${install_dir}/boot/${arch}"
548
    mcopy -i "${work_dir}/efiboot.img" "${airootfs_dir}/boot/vmlinuz-"* \
549
550
551
552
553
554
555
556
557
        "${airootfs_dir}/boot/initramfs-"*".img" "::/${install_dir}/boot/${arch}/"
    for ucode_image in \
        "${airootfs_dir}/boot/"{intel-uc.img,intel-ucode.img,amd-uc.img,amd-ucode.img,early_ucode.cpio,microcode.cpio}
    do
        if [[ -e "${ucode_image}" ]]; then
            all_ucode_images+=("${ucode_image}")
        fi
    done
    if (( ${#all_ucode_images[@]} )); then
558
        mcopy -i "${work_dir}/efiboot.img" "${all_ucode_images[@]}" "::/${install_dir}/boot/"
559
    fi
560
    _msg_info "Done!"
561
562
563
564
}

# Prepare efiboot.img::/EFI for EFI boot mode
_make_boot_uefi-x64.systemd-boot.esp() {
565
    local efiboot_imgsize="0"
566
    _msg_info "Setting up systemd-boot for UEFI booting..."
567

568
569
    # the required image size in KiB (rounded up to the next full MiB with an additional MiB for reserved sectors)
    efiboot_imgsize="$(du -bc \
570
571
572
573
574
575
        "${airootfs_dir}/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \
        "${airootfs_dir}/usr/share/edk2-shell/x64/Shell_Full.efi" \
        "${profile}/efiboot/" \
        "${airootfs_dir}/boot/vmlinuz-"* \
        "${airootfs_dir}/boot/initramfs-"*".img" \
        "${airootfs_dir}/boot/"{intel-uc.img,intel-ucode.img,amd-uc.img,amd-ucode.img,early_ucode.cpio,microcode.cpio} \
576
577
578
579
580
        2>/dev/null | awk 'function ceil(x){return int(x)+(x>int(x))}
            function byte_to_kib(x){return x/1024}
            function mib_to_kib(x){return x*1024}
            END {print mib_to_kib(ceil((byte_to_kib($1)+1024)/1024))}'
        )"
581
582
    # The FAT image must be created with mkfs.fat not mformat, as some systems have issues with mformat made images:
    # https://lists.gnu.org/archive/html/grub-devel/2019-04/msg00099.html
583
    [[ -e "${work_dir}/efiboot.img" ]] && rm -f -- "${work_dir}/efiboot.img"
584
    _msg_info "Creating FAT image of size: ${efiboot_imgsize} KiB..."
585
    mkfs.fat -C -n ARCHISO_EFI "${work_dir}/efiboot.img" "$efiboot_imgsize"
586

587
588
    mmd -i "${work_dir}/efiboot.img" ::/EFI ::/EFI/BOOT
    mcopy -i "${work_dir}/efiboot.img" \
589
        "${airootfs_dir}/usr/lib/systemd/boot/efi/systemd-bootx64.efi" ::/EFI/BOOT/BOOTx64.EFI
590

591
592
    mmd -i "${work_dir}/efiboot.img" ::/loader ::/loader/entries
    mcopy -i "${work_dir}/efiboot.img" "${profile}/efiboot/loader/loader.conf" ::/loader/
593
594
595
596
    for _conf in "${profile}/efiboot/loader/entries/"*".conf"; do
        sed "s|%ARCHISO_LABEL%|${iso_label}|g;
             s|%INSTALL_DIR%|${install_dir}|g;
             s|%ARCH%|${arch}|g" \
597
            "${_conf}" | mcopy -i "${work_dir}/efiboot.img" - "::/loader/entries/${_conf##*/}"
598
    done
599
600

    # shellx64.efi is picked up automatically when on /
601
    if [[ -e "${airootfs_dir}/usr/share/edk2-shell/x64/Shell_Full.efi" ]]; then
602
        mcopy -i "${work_dir}/efiboot.img" \
603
            "${airootfs_dir}/usr/share/edk2-shell/x64/Shell_Full.efi" ::/shellx64.efi
604
605
606
    fi

    # Copy kernel and initramfs
607
    _make_boot_on_fat
608

609
    _msg_info "Done! systemd-boot set up for UEFI booting successfully."
610
611
}

612
613
614
615
616
617
618
# Prepare efiboot.img::/EFI for "El Torito" EFI boot mode
_make_boot_uefi-x64.systemd-boot.eltorito() {
    _run_once _make_boot_uefi-x64.systemd-boot.esp
    # Set up /EFI on ISO-9660
    _run_once _make_efi
}

619
620
621
# Build airootfs filesystem image
_make_prepare() {
    if [[ "${sfs_mode}" == "sfs" ]]; then
622
        _run_once _mkairootfs_sfs
623
    else
624
        _run_once _mkairootfs_img
625
626
627
628
629
630
631
632
633
    fi
    _mkchecksum
    if [[ "${gpg_key}" ]]; then
      _mksignature
    fi
}

# Build ISO
_make_iso() {
634
635
    local xorrisofs_options=()

636
637
    [[ -d "${out_dir}" ]] || install -d -- "${out_dir}"

638
639
640
    if [[ "${quiet}" == "y" ]]; then
        xorrisofs_options+=('-quiet')
    fi
641
642

    # xorrisofs options for x86 BIOS booting using SYSLINUX
643
644
    # shellcheck disable=SC2076
    if [[ " ${bootmodes[*]} " =~ ' bios.syslinux.' ]]; then
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687

        # SYSLINUX El Torito
        if [[ " ${bootmodes[*]} " =~ ' bios.syslinux.eltorito ' ]]; then
            if [[ ! -f "${isofs_dir}/isolinux/isolinux.bin" ]]; then
                _msg_error "The file '${isofs_dir}/isolinux/isolinux.bin' does not exist." 1
            fi

            # SYSLINUX MBR
            if [[ " ${bootmodes[*]} " =~ ' bios.syslinux.mbr ' ]]; then
                if [[ ! -f "${isofs_dir}/isolinux/isohdpfx.bin" ]]; then
                    _msg_error "The file '${isofs_dir}/isolinux/isohdpfx.bin' does not exist." 1
                fi

                xorrisofs_options+=(
                    # SYSLINUX MBR bootstrap code; does not work without "-eltorito-boot isolinux/isolinux.bin"
                    '-isohybrid-mbr' "${isofs_dir}/isolinux/isohdpfx.bin"
                    # When GPT is used, create an additional partition in the MBR (besides 0xEE) for sectors 0–1 (MBR
                    # bootstrap code area) and mark it as bootable
                    # This violates the UEFI specification, but may allow booting on some systems
                    # https://wiki.archlinux.org/index.php/Partitioning#Tricking_old_BIOS_into_booting_from_GPT
                    '--mbr-force-bootable'
                    # Set the ISO 9660 partition's type to "Linux filesystem data"
                    # When only MBR is present, the partition type ID will be 0x83 "Linux" as xorriso translates all
                    # GPT partition type GUIDs except for the ESP GUID to MBR type ID 0x83
                    '-iso_mbr_part_type' '0FC63DAF-8483-4772-8E79-3D69D8477DE4'
                    # Move the first partition away from the start of the ISO to match the expectations of partition
                    # editors
                    # May allow booting on some systems
                    # https://dev.lovelyhq.com/libburnia/libisoburn/src/branch/master/doc/partition_offset.wiki
                    '-partition_offset' '16'
                )
            fi

            xorrisofs_options+=(
                # El Torito boot image for x86 BIOS
                '-eltorito-boot' 'isolinux/isolinux.bin'
                # El Torito boot catalog file
                '-eltorito-catalog' 'isolinux/boot.cat'
                # Required options to boot with ISOLINUX
                '-no-emul-boot' '-boot-load-size' '4' '-boot-info-table'
            )
        else
            _msg_error "Using 'bios.syslinux.mbr' boot mode without 'bios.syslinux.eltorito' is not supported." 1
688
689
        fi
    fi
690
691

    # xorrisofs options for X64 UEFI booting using systemd-boot
692
693
    # shellcheck disable=SC2076
    if [[ " ${bootmodes[*]} " =~ ' uefi-x64.systemd-boot.' ]]; then
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
        if [[ ! -f "${work_dir}/efiboot.img" ]]; then
            _msg_error "The file '${work_dir}/efiboot.img' does not exist." 1
        fi
        [[ -e "${isofs_dir}/EFI/archiso" ]] && rm -rf -- "${isofs_dir}/EFI/archiso"

        # systemd-boot in an attached EFI system partition
        if [[ " ${bootmodes[*]} " =~ ' uefi-x64.systemd-boot.esp ' ]]; then
            # Move the first partition away from the start of the ISO, otherwise the GPT will not be valid and ISO 9660
            # partition will not be mountable
            [[ " ${xorrisofs_options[*]} " =~ ' -partition_offset ' ]] || xorrisofs_options+=('-partition_offset' '16')
            xorrisofs_options+=(
                # Attach efiboot.img as a second partition and set its partition type to "EFI system partition"
                '-append_partition' '2' 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B' "${work_dir}/efiboot.img"
                # Ensure GPT is used as some systems do not support UEFI booting without it
                '-appended_part_as_gpt'
            )

            # systemd-boot in an attached EFI system partition via El Torito
            if [[ " ${bootmodes[*]} " =~ ' uefi-x64.systemd-boot.eltorito ' ]]; then
                xorrisofs_options+=(
                    # Start a new El Torito boot entry for UEFI
                    '-eltorito-alt-boot'
                    # Set the second partition as the El Torito UEFI boot image
                    '-e' '--interval:appended_partition_2:all::'
                    # Boot image is not emulating floppy or hard disk; required for all known boot loaders
                    '-no-emul-boot'
                )
            fi
        # systemd-boot in an embedded efiboot.img via El Torito
        elif [[ " ${bootmodes[*]} " =~ ' uefi-x64.systemd-boot.eltorito ' ]]; then
            # The ISO will not contain a GPT partition table, so to be able to reference efiboot.img, place it as a
            # file inside the ISO 9660 file system
            install -d -m 0755 -- "${isofs_dir}/EFI/archiso"
            cp -a -- "${work_dir}/efiboot.img" "${isofs_dir}/EFI/archiso/efiboot.img"

            xorrisofs_options+=(
                # Start a new El Torito boot entry for UEFI
                '-eltorito-alt-boot'
                # Set efiboot.img as the El Torito UEFI boot image
                '-e' 'EFI/archiso/efiboot.img'
                # Boot image is not emulating floppy or hard disk; required for all known boot loaders
                '-no-emul-boot'
            )
        fi

        # Specify where to save the El Torito boot catalog file in case it is not already set by bios.syslinux.eltorito
        [[ " ${bootmodes[*]} " =~ ' bios.' ]] || xorrisofs_options+=('-eltorito-catalog' 'EFI/boot.cat')
741
742
743
744
745
746
    fi

    _msg_info "Creating ISO image..."
    xorriso -as mkisofs \
            -iso-level 3 \
            -full-iso9660-filenames \
nl6720's avatar
nl6720 committed
747
748
            -joliet \
            -joliet-long \
749
750
751
752
753
754
755
756
            -rational-rock \
            -volid "${iso_label}" \
            -appid "${iso_application}" \
            -publisher "${iso_publisher}" \
            -preparer "prepared by ${app_name}" \
            "${xorrisofs_options[@]}" \
            -output "${out_dir}/${img_name}" \
            "${isofs_dir}/"
757
758
    _msg_info "Done!"
    du -h -- "${out_dir}/${img_name}"
759
760
761
}

# Read profile's values from profiledef.sh
762
_read_profile() {
763
764
765
766
767
768
769
770
    if [[ -z "${profile}" ]]; then
        _msg_error "No profile specified!" 1
    fi
    if [[ ! -d "${profile}" ]]; then
        _msg_error "Profile '${profile}' does not exist!" 1
    elif [[ ! -e "${profile}/profiledef.sh" ]]; then
        _msg_error "Profile '${profile}' is missing 'profiledef.sh'!" 1
    else
771
772
        cd -- "${profile}"

773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
        # Source profile's variables
        # shellcheck source=configs/releng/profiledef.sh
        . "${profile}/profiledef.sh"

        # Resolve paths
        packages="$(realpath -- "${profile}/packages.${arch}")"
        pacman_conf="$(realpath -- "${pacman_conf}")"

        # Enumerate packages
        [[ -e "${packages}" ]] || _msg_error "File '${packages}' does not exist!" 1
        mapfile -t pkg_list < <(sed '/^[[:blank:]]*#.*/d;s/#.*//;/^[[:blank:]]*$/d' "${packages}")

        cd -- "${OLDPWD}"
    fi
}

789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
# set overrides from mkarchiso option parameters, if present
_set_overrides() {
    if [[ -n "$override_iso_label" ]]; then
        iso_label="$override_iso_label"
    fi
    if [[ -n "$override_iso_publisher" ]]; then
        iso_publisher="$override_iso_publisher"
    fi
    if [[ -n "$override_iso_application" ]]; then
        iso_application="$override_iso_application"
    fi
    if [[ -n "$override_install_dir" ]]; then
        install_dir="$override_install_dir"
    fi
    if [[ -n "$override_pacman_conf" ]]; then
        pacman_conf="$override_pacman_conf"
    fi
    if [[ -n "$override_gpg_key" ]]; then
        gpg_key="$override_gpg_key"
    fi
}


812
813
814
815
816
817
818
819
_export_gpg_publickey() {
    if [[ -n "${gpg_key}" ]]; then
        gpg --batch --output "${work_dir}/pubkey.gpg" --export "${gpg_key}"
    fi
}


_make_pkglist() {
820
    install -d -m 0755 -- "${isofs_dir}/${install_dir}"
821
    _msg_info "Creating a list of installed packages on live-enviroment..."
822
    pacman -Q --sysroot "${airootfs_dir}" > "${isofs_dir}/${install_dir}/pkglist.${arch}.txt"
823
    _msg_info "Done!"
824
}
825

826
827
command_pkglist() {
    _show_config "${FUNCNAME[0]#command_}"
828
    _make_pkglist
829
830
}

831
# Create an ISO9660 filesystem from "iso" directory.
832
command_iso() {
833
    bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito')
834

835
    # If exists, add an EFI "El Torito" boot image (FAT filesystem) to ISO-9660 image.
836
    if [[ -f "${work_dir}/efiboot.img" ]]; then
837
        bootmodes+=('uefi-x64.systemd-boot.esp' 'uefi-x64.systemd-boot.eltorito')
838
839
    fi

840
    _show_config "${FUNCNAME[0]#command_}"
841
    _make_iso
842
843
}

844
# create airootfs.sfs filesystem, and push it in "iso" directory.
845
846
command_prepare() {
    _show_config "${FUNCNAME[0]#command_}"
847
848

    _cleanup
849
    _make_prepare
850
851
}

852
# Install packages on airootfs.
853
# A basic check to avoid double execution/reinstallation is done via hashing package names.
854
command_install() {
855
856
857
858
    if [[ ! -f "${pacman_conf}" ]]; then
        _msg_error "Pacman config file '${pacman_conf}' does not exist" 1
    fi

859
    if (( ${#pkg_list[@]} == 0 )); then
860
861
862
863
        _msg_error "Packages must be specified" 0
        _usage 1
    fi

864
    _show_config "${FUNCNAME[0]#command_}"
865

866
    _make_packages
867
868
}

869
command_init() {
870
    _show_config "${FUNCNAME[0]#command_}"
871
872
873
874
    _chroot_init
}

command_run() {
875
    _show_config "${FUNCNAME[0]#command_}"
876
877
    _chroot_run
}
878

879
880
881
command_build_profile() {
    # Set up essential directory paths
    airootfs_dir="${work_dir}/${arch}/airootfs"
882
883
884
    isofs_dir="${work_dir}/iso"
    # Set ISO file name
    img_name="${iso_name}-${iso_version}-${arch}.iso"
885
886
    # Create working directory
    [[ -d "${work_dir}" ]] || install -d -- "${work_dir}"
887
888
889
890
891
892
    # Write build date to file or if the file exists, read it from there
    if [[ -e "${work_dir}/build_date" ]]; then
        SOURCE_DATE_EPOCH="$(<"${work_dir}/build_date")"
    else
        printf '%s\n' "$SOURCE_DATE_EPOCH" > "${work_dir}/build_date"
    fi
893

894
    _show_config "${FUNCNAME[0]#command_}"
895
896
897
898
899
900
    _run_once _make_pacman_conf
    _run_once _export_gpg_publickey
    _run_once _make_custom_airootfs
    _run_once _make_packages
    _run_once _make_customize_airootfs
    _run_once _make_pkglist
901
    _make_bootmodes
902
903
904
    _run_once _cleanup
    _run_once _make_prepare
    _run_once _make_iso
905
906
}

907
while getopts 'p:r:C:L:P:A:D:w:o:s:c:g:vh?' arg; do
908
    case "${arg}" in
909
910
        p)
            read -r -a opt_pkg_list <<< "${OPTARG}"
911
912
            pkg_list+=("${opt_pkg_list[@]}")
            ;;
913
        r) run_cmd="${OPTARG}" ;;
914
915
916
917
918
        C) override_pacman_conf="$(realpath -- "${OPTARG}")" ;;
        L) override_iso_label="${OPTARG}" ;;
        P) override_iso_publisher="${OPTARG}" ;;
        A) override_iso_application="${OPTARG}" ;;
        D) override_install_dir="${OPTARG}" ;;
919
920
        w) work_dir="$(realpath -- "${OPTARG}")" ;;
        o) out_dir="$(realpath -- "${OPTARG}")" ;;
921
        s) sfs_mode="${OPTARG}" ;;
922
        c) sfs_comp="${OPTARG}" ;;
923
        g) override_gpg_key="${OPTARG}" ;;
924
925
926
927
928
929
930
931
932
        v) quiet="n" ;;
        h|?) _usage 0 ;;
        *)
            _msg_error "Invalid argument '${arg}'" 0
            _usage 1
            ;;
    esac
done

933
934
935
shift $((OPTIND - 1))

if (( $# < 1 )); then
936
937
938
939
    _msg_error "No command specified" 0
    _usage 1
fi

940
941
942
943
if (( EUID != 0 )); then
    _msg_error "${app_name} must be run as root." 1
fi

944
945
command_name="${1}"

946
# Set directory path defaults for legacy commands
947
airootfs_dir="${work_dir}/airootfs"
948
isofs_dir="${work_dir}/iso"
949

950
case "${command_name}" in
951
    init)
952
953
        _msg_warning "The '${command_name}' command is deprecated! It will be removed with archiso v49."
        _set_overrides
954
955
956
        command_init
        ;;
    install)
957
958
        _msg_warning "The '${command_name}' command is deprecated! It will be removed with archiso v49."
        _set_overrides
959
960
961
        command_install
        ;;
    run)
962
        _msg_warning "The '${command_name}' command is deprecated! It will be removed with archiso v49."
963
        command_run
964
965
        ;;
    prepare)
966
967
        _msg_warning "The '${command_name}' command is deprecated! It will be removed with archiso v49."
        _set_overrides
968
969
        command_prepare
        ;;
970
    pkglist)
971
        _msg_warning "The '${command_name}' command is deprecated! It will be removed with archiso v49."
972
973
        command_pkglist
        ;;
974
    iso)
975
        _msg_warning "The '${command_name}' command is deprecated! It will be removed with archiso v49."
976
        if (( $# < 2 )); then
977
978
979
            _msg_error "No image specified" 0
            _usage 1
        fi
980
        img_name="${2}"
981
        _set_overrides
982
983
984
        command_iso
        ;;
    *)
985
986
987
988
989
990
991
        # NOTE: we call read_profile here, assuming that the first non-option parameter is a profile directory
        # This way we can retain backwards compatibility with legacy build.sh scripts until we deprecate the old way of
        # calling mkarchiso with named parameters in v49
        profile="$(realpath -- "${command_name}")"
        _read_profile
        _set_overrides
        command_build_profile
992
993
        ;;
esac
994
995

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