mkarchiso 32.7 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
gpg_key=""
21
override_gpg_key=""
22

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


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
usage: ${app_name} [options] <profile_dir>
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
  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}'
     -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
     -v               Enable verbose output
     -w <work_dir>    Set the working directory
                      Default: '${work_dir}'

  profile_dir:        Directory of the archiso profile to build
121
ENDUSAGETEXT
122
    printf '%s' "${usagetext}"
123
    exit "${1}"
124
125
}

126
# Shows configuration according to command mode.
127
128
# $1: build_profile | init | install | run | prepare | iso
_show_config() {
129
    local build_date
130
    build_date="$(date --utc --iso-8601=seconds -d "@${SOURCE_DATE_EPOCH}")"
131
    _msg_info "${app_name} configuration settings"
132
133
134
    _msg_info "             Architecture:   ${arch}"
    _msg_info "        Working directory:   ${work_dir}"
    _msg_info "   Installation directory:   ${install_dir}"
135
136
137
138
139
140
141
142
143
144
145
    _msg_info "               Build date:   ${build_date}"
    _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[*]}"
146
    [[ "${quiet}" == "y" ]] || printf '\n'
147
}
148

149
# Install desired packages to airootfs
150
_pacman() {
151
    _msg_info "Installing packages to '${airootfs_dir}/'..."
152
153

    if [[ "${quiet}" = "y" ]]; then
154
        pacstrap -C "${work_dir}/pacman.conf" -c -G -M -- "${airootfs_dir}" "$@" &> /dev/null
155
    else
156
        pacstrap -C "${work_dir}/pacman.conf" -c -G -M -- "${airootfs_dir}" "$@"
157
    fi
158

159
    _msg_info "Done! Packages installed successfully."
160
161
}

162
# Cleanup airootfs
163
_cleanup() {
164
    _msg_info "Cleaning up what we can on airootfs..."
165

166
    # Delete all files in /boot
167
    if [[ -d "${airootfs_dir}/boot" ]]; then
168
        find "${airootfs_dir}/boot" -mindepth 1 -delete
169
170
    fi
    # Delete pacman database sync cache files (*.tar.gz)
171
172
    if [[ -d "${airootfs_dir}/var/lib/pacman" ]]; then
        find "${airootfs_dir}/var/lib/pacman" -maxdepth 1 -type f -delete
173
174
    fi
    # Delete pacman database sync cache
175
176
    if [[ -d "${airootfs_dir}/var/lib/pacman/sync" ]]; then
        find "${airootfs_dir}/var/lib/pacman/sync" -delete
177
178
    fi
    # Delete pacman package cache
179
180
    if [[ -d "${airootfs_dir}/var/cache/pacman/pkg" ]]; then
        find "${airootfs_dir}/var/cache/pacman/pkg" -type f -delete
181
182
    fi
    # Delete all log files, keeps empty dirs.
183
184
    if [[ -d "${airootfs_dir}/var/log" ]]; then
        find "${airootfs_dir}/var/log" -type f -delete
185
186
    fi
    # Delete all temporary files and dirs
187
188
    if [[ -d "${airootfs_dir}/var/tmp" ]]; then
        find "${airootfs_dir}/var/tmp" -mindepth 1 -delete
189
    fi
190
    # Delete package pacman related files.
191
    find "${work_dir}" \( -name '*.pacnew' -o -name '*.pacsave' -o -name '*.pacorig' \) -delete
192
    # Create an empty /etc/machine-id
193
    printf '' > "${airootfs_dir}/etc/machine-id"
194
195

    _msg_info "Done!"
196
}
197

198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
_mkairootfs_create_image() {
    if (( $# < 1 )); then
        _msg_error "Function '${FUNCNAME[0]}' requires at least one argument" 1
    fi

    image_path="${isofs_dir}/${install_dir}/${arch}/airootfs.sfs"
    if [[ "${airootfs_image_type}" =~ .*squashfs ]] ; then
        if [[ "${quiet}" == "y" ]]; then
            mksquashfs "$@" "${image_path}" -noappend "${airootfs_image_tool_options[@]}" -no-progress > /dev/null
        else
            mksquashfs "$@" "${image_path}" -noappend "${airootfs_image_tool_options[@]}"
        fi
    else
        _msg_error "Unsupported image type: '${airootfs_image_type}'" 1
    fi
}

215
# Makes a ext4 filesystem inside a SquashFS from a source directory.
216
_mkairootfs_img() {
217
218
    if [[ ! -e "${airootfs_dir}" ]]; then
        _msg_error "The path '${airootfs_dir}' does not exist" 1
219
220
    fi

221
    _msg_info "Creating ext4 image of 32 GiB..."
222
    if [[ "${quiet}" == "y" ]]; then
223
        mkfs.ext4 -q -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${airootfs_dir}.img" 32G
224
    else
225
        mkfs.ext4 -O '^has_journal,^resize_inode' -E 'lazy_itable_init=0' -m 0 -F -- "${airootfs_dir}.img" 32G
226
    fi
227
    tune2fs -c 0 -i 0 -- "${airootfs_dir}.img" > /dev/null
228
    _msg_info "Done!"
229
    _mount_airootfs
230
231
    _msg_info "Copying '${airootfs_dir}/' to '${work_dir}/mnt/airootfs/'..."
    cp -aT -- "${airootfs_dir}/" "${work_dir}/mnt/airootfs/"
232
    chown -- 0:0 "${work_dir}/mnt/airootfs/"
233
    _msg_info "Done!"
234
    _umount_airootfs
235
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/${arch}"
236
    _msg_info "Creating SquashFS image, this may take some time..."
237
    _mkairootfs_create_image "${airootfs_dir}.img"
238
    _msg_info "Done!"
239
    rm -- "${airootfs_dir}.img"
240
241
}

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

248
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/${arch}"
249
    _msg_info "Creating SquashFS image, this may take some time..."
250
    _mkairootfs_create_image "${airootfs_dir}"
251
252
253
    _msg_info "Done!"
}

254
_mkchecksum() {
255
    _msg_info "Creating checksum file for self-test..."
256
    cd -- "${isofs_dir}/${install_dir}/${arch}"
257
    sha512sum airootfs.sfs > airootfs.sha512
258
    cd -- "${OLDPWD}"
259
    _msg_info "Done!"
260
261
}

262
263
_mksignature() {
    _msg_info "Signing SquashFS image..."
264
    cd -- "${isofs_dir}/${install_dir}/${arch}"
265
    gpg --detach-sign --default-key "${gpg_key}" airootfs.sfs
266
    cd -- "${OLDPWD}"
267
268
269
    _msg_info "Done!"
}

270
271
272
273
274
275
276
# Helper function to run functions only one time.
_run_once() {
    if [[ ! -e "${work_dir}/build.${1}" ]]; then
        "$1"
        touch "${work_dir}/build.${1}"
    fi
}
277

278
# Set up custom pacman.conf with custom cache and pacman hook directories
279
_make_pacman_conf() {
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
    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"
304
305
306
307
308
}

# Prepare working directory and copy custom airootfs files (airootfs)
_make_custom_airootfs() {
    local passwd=()
309
310
311

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

312
    if [[ -d "${profile}/airootfs" ]]; then
313
        _msg_info "Copying custom airootfs files and setting up user home directories..."
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
        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
333
        _msg_info "Done!"
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
    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=()
353

354
    if [[ -e "${profile}/airootfs/etc/passwd" ]]; then
355
        _msg_info "Copying /etc/skel/* to user homes..."
356
        while IFS=':' read -a passwd -r; do
357
            (( passwd[2] >= 1000 && passwd[2] < 60000 )) || continue
358
359
            [[ "${passwd[5]}" == '/' ]] && continue
            [[ -z "${passwd[5]}" ]] && continue
360
361
            cp -dnRT --preserve=mode,timestamps,links -- "${airootfs_dir}/etc/skel" "${airootfs_dir}${passwd[5]}"
            chmod -f 0750 -- "${airootfs_dir}${passwd[5]}"
362
363
364
            chown -hR -- "${passwd[2]}:${passwd[3]}" "${airootfs_dir}${passwd[5]}"

        done < "${profile}/airootfs/etc/passwd"
365
        _msg_info "Done!"
366
367
368
    fi

    if [[ -e "${airootfs_dir}/root/customize_airootfs.sh" ]]; then
369
        _msg_info "Running customize_airootfs.sh in '${airootfs_dir}' chroot..."
370
371
        _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"
372
        _chroot_run
373
        rm -- "${airootfs_dir}/root/customize_airootfs.sh"
374
        _msg_info "Done! customize_airootfs.sh run successfully."
375
376
377
    fi
}

378
379
380
381
382
383
384
385
386
387
388
389
# 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
}

390
# Prepare kernel/initramfs ${install_dir}/boot/
391
_make_boot_on_iso() {
392
    local ucode_image
393
394
    _msg_info "Preparing kernel and intramfs for the ISO 9660 file system..."
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/${arch}"
395
396
397
398
399
400
401
402
403
404
405
406
407
    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
408
    _msg_info "Done!"
409
410
411
}

# Prepare /${install_dir}/boot/syslinux
412
_make_boot_bios.syslinux.mbr() {
413
414
    _msg_info "Setting up SYSLINUX for BIOS booting from a disk..."
    install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/syslinux"
415
416
    for _cfg in "${profile}/syslinux/"*.cfg; do
        sed "s|%ARCHISO_LABEL%|${iso_label}|g;
417
418
419
             s|%INSTALL_DIR%|${install_dir}|g;
             s|%ARCH%|${arch}|g" \
             "${_cfg}" > "${isofs_dir}/${install_dir}/boot/syslinux/${_cfg##*/}"
420
    done
421
422
423
    if [[ -e "${profile}/syslinux/splash.png" ]]; then
        install -m 0644 -- "${profile}/syslinux/splash.png" "${isofs_dir}/${install_dir}/boot/syslinux/"
    fi
424
425
426
    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/"
427

428
    _run_once _make_boot_on_iso
429

430
431
432
433
434
435
436
437
438
    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
439
440
441
442
443

    # 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"
444
        install -d -m 0755 -- "${isofs_dir}/${install_dir}/boot/licenses/memtest86+/"
445
446
447
        install -m 0644 -- "${airootfs_dir}/usr/share/licenses/common/GPL2/license.txt" \
            "${isofs_dir}/${install_dir}/boot/licenses/memtest86+/"
    fi
448
    _msg_info "Done! SYSLINUX set up for BIOS booting from a disk successfully."
449
450
451
}

# Prepare /isolinux
452
_make_boot_bios.syslinux.eltorito() {
453
454
    _msg_info "Setting up SYSLINUX for BIOS booting from an optical disc..."
    install -d -m 0755 -- "${isofs_dir}/isolinux"
455
456
457
458
459
460
    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
461
462
463
    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/"
464
465
466

    # isolinux.cfg loads syslinux.cfg
    _run_once _make_boot_bios.syslinux.mbr
467
468

    _msg_info "Done! SYSLINUX set up for BIOS booting from an optical disc successfully."
469
470
471
472
}

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

478
    install -d -m 0755 -- "${isofs_dir}/loader/entries"
479
480
    install -m 0644 -- "${profile}/efiboot/loader/loader.conf" "${isofs_dir}/loader/"

481
482
483
484
485
486
    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
487
488
489

    # edk2-shell based UEFI shell
    # shellx64.efi is picked up automatically when on /
490
491
492
    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
493
    _msg_info "Done!"
494
495
}

496
497
# Prepare kernel/initramfs on efiboot.img
_make_boot_on_fat() {
498
    local ucode_image all_ucode_images=()
499
    _msg_info "Preparing kernel and intramfs for the FAT file system..."
500
    mmd -i "${work_dir}/efiboot.img" \
501
        "::/${install_dir}" "::/${install_dir}/boot" "::/${install_dir}/boot/${arch}"
502
    mcopy -i "${work_dir}/efiboot.img" "${airootfs_dir}/boot/vmlinuz-"* \
503
504
505
506
507
508
509
510
511
        "${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
512
        mcopy -i "${work_dir}/efiboot.img" "${all_ucode_images[@]}" "::/${install_dir}/boot/"
513
    fi
514
    _msg_info "Done!"
515
516
517
518
}

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

522
523
    # the required image size in KiB (rounded up to the next full MiB with an additional MiB for reserved sectors)
    efiboot_imgsize="$(du -bc \
524
525
526
527
528
529
        "${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} \
530
531
532
533
534
        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))}'
        )"
535
536
    # 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
537
    [[ -e "${work_dir}/efiboot.img" ]] && rm -f -- "${work_dir}/efiboot.img"
538
    _msg_info "Creating FAT image of size: ${efiboot_imgsize} KiB..."
539
    mkfs.fat -C -n ARCHISO_EFI "${work_dir}/efiboot.img" "$efiboot_imgsize"
540

541
542
    mmd -i "${work_dir}/efiboot.img" ::/EFI ::/EFI/BOOT
    mcopy -i "${work_dir}/efiboot.img" \
543
        "${airootfs_dir}/usr/lib/systemd/boot/efi/systemd-bootx64.efi" ::/EFI/BOOT/BOOTx64.EFI
544

545
546
    mmd -i "${work_dir}/efiboot.img" ::/loader ::/loader/entries
    mcopy -i "${work_dir}/efiboot.img" "${profile}/efiboot/loader/loader.conf" ::/loader/
547
548
549
550
    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" \
551
            "${_conf}" | mcopy -i "${work_dir}/efiboot.img" - "::/loader/entries/${_conf##*/}"
552
    done
553
554

    # shellx64.efi is picked up automatically when on /
555
    if [[ -e "${airootfs_dir}/usr/share/edk2-shell/x64/Shell_Full.efi" ]]; then
556
        mcopy -i "${work_dir}/efiboot.img" \
557
            "${airootfs_dir}/usr/share/edk2-shell/x64/Shell_Full.efi" ::/shellx64.efi
558
559
560
    fi

    # Copy kernel and initramfs
561
    _make_boot_on_fat
562

563
    _msg_info "Done! systemd-boot set up for UEFI booting successfully."
564
565
}

566
567
568
569
570
571
572
# 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
}

573
574
# Build airootfs filesystem image
_make_prepare() {
575
    if [[ "${airootfs_image_type}" == "squashfs" ]]; then # prepare airootfs.sfs for overlayfs usage (default)
576
        _run_once _mkairootfs_sfs
577
    elif [[ "${airootfs_image_type}" == "ext4+squashfs" ]]; then # prepare airootfs.sfs for dm-snapshot usage
578
        _run_once _mkairootfs_img
579
580
    else
        _msg_error "Unsupported image type: '${airootfs_image_type}'" 1
581
582
583
584
585
586
587
588
589
    fi
    _mkchecksum
    if [[ "${gpg_key}" ]]; then
      _mksignature
    fi
}

# Build ISO
_make_iso() {
590
591
    local xorrisofs_options=()

592
593
    [[ -d "${out_dir}" ]] || install -d -- "${out_dir}"

594
595
596
    if [[ "${quiet}" == "y" ]]; then
        xorrisofs_options+=('-quiet')
    fi
597
598

    # xorrisofs options for x86 BIOS booting using SYSLINUX
599
600
    # shellcheck disable=SC2076
    if [[ " ${bootmodes[*]} " =~ ' bios.syslinux.' ]]; then
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643

        # 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
644
645
        fi
    fi
646
647

    # xorrisofs options for X64 UEFI booting using systemd-boot
648
649
    # shellcheck disable=SC2076
    if [[ " ${bootmodes[*]} " =~ ' uefi-x64.systemd-boot.' ]]; then
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
688
689
690
691
692
693
694
695
696
        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')
697
698
699
700
701
702
    fi

    _msg_info "Creating ISO image..."
    xorriso -as mkisofs \
            -iso-level 3 \
            -full-iso9660-filenames \
nl6720's avatar
nl6720 committed
703
704
            -joliet \
            -joliet-long \
705
706
707
708
709
710
711
712
            -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}/"
713
714
    _msg_info "Done!"
    du -h -- "${out_dir}/${img_name}"
715
716
717
}

# Read profile's values from profiledef.sh
718
_read_profile() {
719
720
721
722
723
724
725
726
    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
727
728
        cd -- "${profile}"

729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
        # 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
}

745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# 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
}


768
769
770
771
772
773
774
775
_export_gpg_publickey() {
    if [[ -n "${gpg_key}" ]]; then
        gpg --batch --output "${work_dir}/pubkey.gpg" --export "${gpg_key}"
    fi
}


_make_pkglist() {
776
    install -d -m 0755 -- "${isofs_dir}/${install_dir}"
777
    _msg_info "Creating a list of installed packages on live-enviroment..."
778
    pacman -Q --sysroot "${airootfs_dir}" > "${isofs_dir}/${install_dir}/pkglist.${arch}.txt"
779
    _msg_info "Done!"
780
}
781

782
_build_profile() {
783
784
    # Set up essential directory paths
    airootfs_dir="${work_dir}/${arch}/airootfs"
785
786
787
    isofs_dir="${work_dir}/iso"
    # Set ISO file name
    img_name="${iso_name}-${iso_version}-${arch}.iso"
788
789
    # Create working directory
    [[ -d "${work_dir}" ]] || install -d -- "${work_dir}"
790
791
792
793
794
795
    # 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
796

797
    _show_config
798
799
800
801
802
803
    _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
804
    _make_bootmodes
805
806
807
    _run_once _cleanup
    _run_once _make_prepare
    _run_once _make_iso
808
809
}

810
while getopts 'p:r:C:L:P:A:D:w:o:g:vh?' arg; do
811
    case "${arg}" in
812
813
        p)
            read -r -a opt_pkg_list <<< "${OPTARG}"
814
815
            pkg_list+=("${opt_pkg_list[@]}")
            ;;
816
        r) run_cmd="${OPTARG}" ;;
817
818
819
820
821
        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}" ;;
822
823
        w) work_dir="$(realpath -- "${OPTARG}")" ;;
        o) out_dir="$(realpath -- "${OPTARG}")" ;;
824
        g) override_gpg_key="${OPTARG}" ;;
825
826
827
828
829
830
831
832
833
        v) quiet="n" ;;
        h|?) _usage 0 ;;
        *)
            _msg_error "Invalid argument '${arg}'" 0
            _usage 1
            ;;
    esac
done

834
835
836
shift $((OPTIND - 1))

if (( $# < 1 )); then
837
    _msg_error "No profile specified" 0
838
839
840
    _usage 1
fi

841
842
843
844
if (( EUID != 0 )); then
    _msg_error "${app_name} must be run as root." 1
fi

845
846
# get the absolute path representation of the first non-option argument
profile="$(realpath -- "${1}")"
847

848
# Set directory path defaults for legacy commands
849
airootfs_dir="${work_dir}/airootfs"
850
isofs_dir="${work_dir}/iso"
851
852
853
_read_profile
_set_overrides
_build_profile
854
855

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