#!/usr/bin/env bash # A Linux program to create bootable Windows USB stick from a real Windows DVD or an image # # This file is part of WoeUSB. # https://github.com/WoeUSB/WoeUSB # # Copyright © 2013 Colin GILLE / congelli501 # Copyright © 2020 slacka et. al. # Copyright © 2021 WoeUSB project contributors # SPDX-License-Identifier: GPL-3.0-or-later # # Compatibility notes: # * realpath: Use `--strip` instead of `--no-symlinks` for realpath(1) for compatibility of Ubuntu 14.04(EoL: April 2019) # * mktemp: Use short options(-d, -t) for Slackware 14.2 (EoL: Unknown) # # lint: We use indirections and primitive variables, which is false positive of this rule # shellcheck disable=SC2034 # lint: Required for I18N # shellcheck disable=SC2059 RUFUS_UEFI_NTFS_VERSION="${RUFUS_UEFI_NTFS_VERSION:-b30e3b387a3ca7a5e2fddebcc2c8f9538a89b868}" DD_BLOCK_SIZE="${DD_BLOCK_SIZE:-$((4 * 1024 * 1024))}" # 4MiB # Entry point of the main code init(){ check_function_parameters_quantity 5 "${#}" local -r program_name="${1}"; shift local -n flag_source_filesystem_mounted_ref="${1}"; shift local -n flag_target_filesystem_mounted_ref="${1}"; shift local -r rufus_uefi_ntfs_version="${1}"; shift local -r dd_block_size="${1}"; shift local -r application_name="$(gettext 'WoeUSB')" local -r application_version='@@WOEUSB_VERSION@@' local \ flag_print_help=false \ flag_print_version=false \ flag_print_about=false local -r \ application_site_url=https://github.com/WoeUSB/WoeUSB \ application_copyright_declaration="Copyright © Colin GILLE / congelli501 2013\\nCopyright © slacka et.al. 2020" \ application_copyright_notice="${application_name} is free software licensed under the GNU General Public License version 3(or any later version of your preference) that gives you THE 4 ESSENTIAL FREEDOMS\\nhttps://www.gnu.org/philosophy/" local install_mode # source_media may be an optical disk drive or a disk image # target_media may be an entire usb storage device or just a partition local \ source_media \ target_media local target_partition local workaround_bios_boot_flag=false local \ target_filesystem_type=FS_FAT \ target_fs_label='Windows USB' # Parameters that needs to be determined in runtime # due to different names in distributions local \ command_mkdosfs \ command_mkntfs \ command_grubinstall \ name_grub_prefix if ! check_runtime_dependencies \ "${application_name}" \ command_mkdosfs \ command_mkntfs \ command_grubinstall \ name_grub_prefix; then exit 1 fi if ! \ process_commandline_parameters \ application_name \ flag_print_help \ flag_print_version \ flag_print_about \ install_mode \ source_media \ target_media \ target_fs_label \ workaround_bios_boot_flag \ target_filesystem_type; then print_help \ "${application_name}" \ "${application_version}" \ "${program_name}" exit 1 fi process_miscellaneous_requests \ "${flag_print_help}" \ "${flag_print_version}" \ "${flag_print_about}" \ "${application_name}" \ "${application_version}" \ "${application_site_url}" \ "${application_copyright_declaration}" \ "${application_copyright_notice}" \ "${program_name}" printf -- \ '%s\n%s\n' \ "${application_name} v${application_version}" \ '==============================' check_superuser \ "${application_name}" if ! check_runtime_parameters \ install_mode \ source_media \ target_media \ "${target_fs_label}"; then print_help \ "${application_name}" \ "${application_version}" \ "${program_name}" exit 1 fi determine_target_parameters \ "${install_mode}" \ "${target_media}" \ target_device \ target_partition \ target_filesystem_type check_source_and_target_not_busy \ "${install_mode}" \ "${source_media}" \ "${target_device}" \ "${target_partition}" source_fs_mountpoint="$( mktemp \ -d \ -t \ woeusb-source-"$(generate_timestamp)".XXXXXX )" target_fs_mountpoint="$( mktemp \ -d \ -t \ woeusb-target-"$(generate_timestamp)".XXXXXX )" if ! mount_source_filesystem \ "${source_media}" \ "${source_fs_mountpoint}"; then print_error \ "$(gettext 'Unable to mount source filesystem in %s\n')" \ "${source_media}" exit 1 fi flag_source_filesystem_mounted_ref=true if [ "${install_mode}" = device ]; then wipe_existing_partition_table_and_filesystem_signatures \ "${target_device}" create_target_partition_table \ "${target_device}" \ legacy create_target_partition \ "${target_partition}" \ "${target_filesystem_type}" \ "${target_fs_label}" \ "${command_mkdosfs}" \ "${command_mkntfs}" if [ "${target_filesystem_type}" == FS_NTFS ]; then create_uefi_ntfs_support_partition \ "${target_device}" install_uefi_ntfs_support_partition \ "${rufus_uefi_ntfs_version}" \ "${target_device}2" \ "${temp_dir}" \ "${target_device}" fi fi if [ "${install_mode}" = partition ]; then check_target_partition \ "${target_partition}" \ "${install_mode}" \ "${target_device}" fi flag_target_filesystem_mounted_ref=true if ! mount_target_filesystem \ "${target_partition}" \ "${target_fs_mountpoint}" \ "${target_filesystem_type}"; then print_error \ "$(gettext 'Unable to mount target filesystem in %s\n')" \ "${target_partition}" exit 1 fi check_target_filesystem_free_space \ "${target_fs_mountpoint}" \ "${source_fs_mountpoint}" \ || exit 1 copy_filesystem_files \ "${source_fs_mountpoint}" \ "${target_fs_mountpoint}" \ "${target_filesystem_type}" \ "${dd_block_size}" workaround_support_windows_7_uefi_boot \ "${source_fs_mountpoint}" \ "${target_fs_mountpoint}" install_legacy_pc_bootloader_grub \ "${target_fs_mountpoint}" \ "${target_device}" \ "${command_grubinstall}" install_legacy_pc_bootloader_grub_config \ "${target_fs_mountpoint}" \ "${name_grub_prefix}" if [ "${workaround_bios_boot_flag}" == true ]; then workaround_buggy_motherboards_that_ignore_disks_without_boot_flag_toggled \ "${target_device}" fi print_info \ "$(gettext 'Done :)\n')" print_info \ "$(gettext 'The target device should be bootable now\n')" exit 0 } # FIXME: Doesn't support only printing LHS of the ATX style heading print_markdown_heading(){ local text="${1}"; shift local -i level="${1}"; shift # [atx|setext] local style="${1}"; shift local setext_separator_max_length if test "${#}" -ne 0; then setext_separator_max_length="${1}"; shift fi case "${style}" in atx) if test -v setext_separator_max_length; then printf \ "$(gettext '%s: Error: The setext_separator_max_length parameter can only be used with the Setext heading style.\n')" \ "${FUNCNAME[0]}" \ 1>&2 return 1 fi if test "${level}" -gt 6; then printf \ "$(gettext '%s: Error: Atx heading style only support at most level 6, got %s\n')" \ "${FUNCNAME[0]}" \ "${level}" \ 1>&2 return 1 fi local -i i local heading_marker= for((i=0; i&2 return 1 fi if test -v setext_separator_max_length \ && [[ ! "${setext_separator_max_length}" =~ ([[:digit:]]+|terminal) ]]; then printf \ "$(gettext "%s: Error: The setext_separator_max_length parameter only accepts numbers and 'terminal' as argument, but got \"%s\".\\n")" \ 1>&2 \ "${FUNCNAME[0]}" \ "${setext_separator_max_length}" return 1 fi local -i separator_length local -i terminal_columns terminal_columns="$( stty size \ | awk '{print $2}' )" local -i text_length="${#text}" if ! test -v setext_separator_max_length; then if test "${terminal_columns}" -gt "${text_length}"; then separator_length="${text_length}" else separator_length="${terminal_columns}" fi elif test "${setext_separator_max_length}" == terminal; then separator_length="${terminal_columns}" else # Use terminal width if setext_separator_max_length will # cause line wrap if test "${terminal_columns}" -gt "${setext_separator_max_length}"; then separator_length="${setext_separator_max_length}" else separator_length="${terminal_columns}" fi fi local -i i local separator= for((i=0; i&2 return 1 ;; esac # Additional linebreak to separate heading and context printf '\n' } print_markdown_paragraph(){ local text="${1}"; shift printf \ -- \ '%s\n' \ "${text}" printf \ '\n' } print_markdown_codeblock(){ local text="${1}"; shift if test "${#}" -ne 0; then local syntax_hint="${1}"; shift fi if ! test -v syntax_hint; then printf '```\n' else printf '```%s\n' "${syntax_hint}" fi printf '%s\n' "${text}" printf '```\n' # Separate with following content printf '\n' } print_help(){ check_function_parameters_quantity 3 "${#}" local -r application_name="${1}"; shift local -r application_version="${1}"; shift local -r program_name="${1}"; shift set +o xtrace print_markdown_heading \ "$( printf \ "$(gettext '%s %s Help Information')" \ "${application_name}" \ "${application_version}" )" \ 1 \ setext print_markdown_paragraph \ "$( printf \ "$(gettext '%s can create a bootable Microsoft Windows(R) USB storage device from an existing Windows optical disk or an ISO disk image.')" \ "${application_name}" )" print_markdown_heading \ "$(gettext 'Supported installation drive preparation modes')" \ 2 \ setext print_markdown_paragraph \ "$(gettext 'Currently two creation methods are supported:')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--device`, `-d`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Completely WIPE the entire USB storage device, then build a bootable Windows USB device from scratch.')" print_markdown_paragraph \ "$(gettext 'WARNING: All previous data on the device will be gone!')" print_markdown_codeblock \ "$( printf \ '$ %s --device \n' \ "${program_name}" )" \ synopsis print_markdown_codeblock \ "$( printf \ -- \ '%s --device Windows7_x64.iso /dev/sdX' \ "${program_name}" printf '\n' printf \ -- \ '%s --device /dev/sr0 /dev/sdX' \ "${program_name}" )" \ example # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--partition`, `-p`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Copy Windows files to an existing partition of a USB storage device and make it bootable. This allows files to coexist as long as no filename conflict exists.')" print_markdown_paragraph \ "$(gettext 'WARNING: All files that has the same name will be overwritten!')" print_markdown_codeblock \ "$( printf \ '%s --partition ' \ "${program_name}" )" \ synopsis print_markdown_codeblock \ "$( printf \ -- \ '%s --partition Windows7_x64.iso /dev/sdX1' \ "${program_name}" printf '\n' printf \ -- \ '%s --partition /dev/sr0 /dev/sdX1' \ "${program_name}" )" \ example print_markdown_heading \ "$(gettext 'Command-line Options')" \ 2 \ setext # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--verbose`, `-v`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Verbose mode')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--help`, `-h`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Show this help message and exit')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--version`, `-V`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Print application version')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--about`, `-ab`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Show info about this application')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--no-color`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Disable message coloring')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--debug`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Enable script debugging')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--label`, `-l `' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Specify label for the newly created file system in --device creation method')" print_markdown_paragraph \ "$(gettext 'Note that the label is not verified for validity and may be illegal for the filesystem')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--workaround-bios-boot-flag`' \ 3 \ atx print_markdown_paragraph \ "$(gettext "Workaround BIOS bug that won't include the device in boot menu if non of the partition's boot flag is toggled")" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--debugging-internal-function-call (function_argument)...`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'Development option for developers to test certain function without running the entire build\n')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--target-filesystem`, `--tgt-fs `' \ 3 \ atx print_markdown_paragraph \ "$(gettext "Specify the filesystem to use as the target partition's filesystem.")" print_markdown_paragraph \ "$(gettext 'Currently supported: FAT(default)/NTFS')" # The backtick pairs are Markdown's markup, not a shell # command substitution # shellcheck disable=SC2016 print_markdown_heading \ '`--for-gui`' \ 3 \ atx print_markdown_paragraph \ "$(gettext 'No longer supported, reserved for compatibility with the wrapper programs')" } print_application_info(){ check_function_parameters_quantity 5 "${#}" local -r application_name="${1}"; shift local -r application_version="${1}"; shift local -r application_site_url="${1}"; shift local -r application_copyright_declaration="${1}"; shift local -r application_copyright_notice="${1}"; shift echo "${application_name} ${application_version}" echo -e "${application_site_url}" echo -e "${application_copyright_declaration}" echo -e "${application_copyright_notice}" } process_commandline_parameters(){ check_function_parameters_quantity 10 "${#}" local -r application_name="${1}"; shift local -n flag_print_help_ref="${1}"; shift local -n flag_print_version_ref="${1}"; shift local -n flag_print_about_ref="${1}"; shift local -n install_mode_ref="${1}"; shift local -n source_media_ref="${1}"; shift local -n target_media_ref="${1}"; shift local -n target_fs_label_ref="${1}"; shift local -n workaround_bios_boot_flag_ref="${1}"; shift local -n target_filesystem_type_ref="${1}"; shift if [ "${#program_args[@]}" -eq 0 ]; then flag_print_help_ref=true return 0 fi local -a parameters=("${program_args[@]}") local \ enable_debug=false \ enable_device=false \ enable_partition=false \ enable_label=false \ enable_workaround_bios_boot_flag=false \ enable_debugging_internal_function_call=false \ enable_target_filesystem=false # Inputs that requires sanitation local target_filesystem_type_input while [ "${#parameters[@]}" -ne 0 ]; do case "${parameters[0]}" in --help \ |-h) flag_print_help_ref=true return 0 ;; --version \ |-V) flag_print_version_ref=true return 0 ;; --about \ |-ab) flag_print_about_ref=true return 0 ;; --debug) enable_debug=true ;; --partition \ |-p) enable_partition=true install_mode_ref=partition shift_array parameters if [ "${#parameters[@]}" -lt 2 ]; then print_error \ "$(gettext -- '--partition option requires 2 arguments!\n')" return 1 fi source_media_ref="${parameters[0]}" shift_array parameters target_media_ref="${parameters[0]}" ;; --device \ |-d) enable_device=true # Limitation of ShellCheck to detect usage of indirection variables # https://github.com/koalaman/shellcheck/wiki/SC2034 # shellcheck disable=SC2034 install_mode_ref=device shift_array parameters if [ "${#parameters[@]}" -lt 2 ]; then print_error \ "$(gettext -- '--device option requires 2 arguments!\n')" return 1 fi source_media_ref="${parameters[0]}" shift_array parameters target_media_ref="${parameters[0]}" ;; --verbose|-v) flag_verbose=true ;; --for-gui) flag_no_color=true ;; --no-color) flag_no_color=true ;; --label|-l) enable_label=true shift_array parameters if [ ${#parameters[@]} -lt 1 ]; then print_error \ "$(gettext -- '--label option requires 1 argument.\n')" return 1 fi target_fs_label_ref="${parameters[0]}" ;; --workaround-bios-boot-flag) enable_workaround_bios_boot_flag=true workaround_bios_boot_flag_ref=true ;; --debugging-internal-function-call) enable_debugging_internal_function_call=true shift_array parameters if [ ${#parameters[@]} -lt 1 ]; then print_error \ "$(gettext -- '--debugging-internal-function-call option requires at least 1 argument.\n')" return 1 fi "${parameters[@]}" exit ${?} ;; --target-filesystem \ |--tgt-fs) enable_target_filesystem=true shift_array parameters if [ ${#parameters[@]} -lt 1 ]; then print_error \ "$(gettext -- '--target-filesystem option requires 1 argument.\n')" return 1 fi target_filesystem_type_input="$( # Normalize input to uppercase tr \ '[:lower:]' \ '[:upper:]' \ <<< "${parameters[0]}" )" ;; *) print_error \ "$(gettext 'Unknown command-line argument "%s"\n')" \ "${parameters[0]}" return 1 ;; esac shift_array parameters done # None useful action is specified if \ [ "${enable_device}" == false ] \ && [ "${enable_partition}" == false ] \ && [ "${enable_debugging_internal_function_call}" == false ]; then # intentionally don't mention about the internal function call option print_error \ "$(gettext 'No creation method specified!\n')" return 1 elif \ [ "${enable_device}" == true ] \ && [ "${enable_partition}" == true ]; then print_error \ "$(gettext -- '--device and --partition creation method are mutual-exclusive.\n')" return 1 fi # --label option is no use with --partition creation method if \ [ "${enable_partition}" == true ] \ && [ "${enable_label}" == true ]; then print_error \ "$(gettext -- '--label option only can be used with --device creation method\n')" return 1 fi # --target-filesystem option is no use with --partition creation method if \ [ "${enable_target_filesystem}" == true ] \ && [ "${enable_partition}" == true ]; then print_error \ "$(gettext -- '--target-filesystem option only can be used with --device creation method\n')" return 1 fi # --target-filesystem should be supported if \ [ "${enable_target_filesystem}" = true ]; then case "${target_filesystem_type_input}" in FAT) target_filesystem_type_ref=FS_FAT ;; NTFS) target_filesystem_type_ref=FS_NTFS ;; *) \ "$(gettext 'Target filesystem not supported.\n')" return 1 ;; esac fi if [ "${flag_verbose}" = true ] && [ "${enable_debug}" != true ]; then trap 'trap_return "${FUNCNAME[0]}"' RETURN fi # Always be the last condition so that less debug message from this function will be printed if [ "${enable_debug}" = true ]; then set -o xtrace fi return 0 } process_miscellaneous_requests(){ check_function_parameters_quantity 9 "${#}" local -r flag_print_help="${1}"; shift local -r flag_print_version="${1}"; shift local -r flag_print_about="${1}"; shift local -r application_name="${1}"; shift local -r application_version="${1}"; shift local -r application_site_url="${1}"; shift local -r application_copyright_declaration="${1}"; shift local -r application_copyright_notice="${1}"; shift local -r program_name="${1}"; shift if [ "${flag_print_help}" == true ]; then trap - EXIT print_help \ "${application_name}" \ "${application_version}" \ "${program_name}" exit 0 fi if [ "${flag_print_version}" == true ]; then trap - EXIT printf -- \ '%s\n' \ "${application_version}" exit 0 fi if [ "${flag_print_about}" == true ]; then trap - EXIT print_application_info \ "${application_name}" \ "${application_version}" \ "${application_site_url}" \ "${application_copyright_declaration}" \ "${application_copyright_notice}" exit 0 fi } check_runtime_dependencies(){ check_function_parameters_quantity 5 "${#}" local -r application_name="${1}"; shift local -n command_mkdosfs_ref="${1}"; shift local -n command_mkntfs_ref="${1}"; shift local -n command_grubinstall_ref="${1}"; shift local -n name_grub_prefix_ref="${1}"; shift local result=unknown for required_command in \ awk \ blockdev \ cut \ dd \ df \ du \ find \ grep \ id \ lsblk \ mkdir \ mount \ parted \ partprobe \ readlink \ rm \ stat \ stty \ wget \ wimlib-imagex \ wipefs do if ! command -v "${required_command}" >/dev/null; then print_error \ "$(gettext -- '%s requires %s command in the executable search path, but it is not found.\n')" \ "${application_name}" \ "${required_command}" result=failed fi done; unset required_command if command -v mkdosfs &> /dev/null; then command_mkdosfs_ref=mkdosfs elif command -v mkfs.msdos &> /dev/null; then command_mkdosfs_ref=mkfs.msdos elif command -v mkfs.vfat &>/dev/null; then command_mkdosfs_ref=mkfs.vfat elif command -v mkfs.fat &>/dev/null; then command_mkdosfs_ref=mkfs.fat else print_error \ "$(gettext 'mkdosfs/mkfs.msdos/mkfs.vfat/mkfs.fat command not found!\n')" print_error \ "$(gettext 'Please make sure that dosfstools is properly installed!\n')" result='failed' fi if command -v mkntfs &>/dev/null; then command_mkntfs_ref=mkntfs else print_error \ "$(gettext 'mkntfs command not found!\n')" print_error \ "$(gettext 'Please make sure that ntfs-3g is properly installed!\n')" result=failed fi if command -v grub-install &> /dev/null; then command_grubinstall_ref=grub-install name_grub_prefix_ref=grub elif command -v grub2-install &> /dev/null; then command_grubinstall_ref=grub2-install name_grub_prefix_ref=grub2 else print_error \ "$(gettext 'grub-install or grub2-install command not found!\n')" print_error \ "$(gettext 'Please make sure that GNU GRUB is properly installed!\n')" result=failed fi if [ "${result}" == failed ]; then return 1 else return 0 fi } check_superuser(){ check_function_parameters_quantity 1 "${#}" local -r application_name="${1}"; shift if [ ! "$(id --user)" = 0 ]; then print_warning \ "$(gettext 'You are not running %s as the superuser(root)!\n')" \ "${application_name}" print_warning \ "$(gettext 'This might be the reason of the following failure.\n')" fi return 0 } check_runtime_parameters(){ check_function_parameters_quantity 4 "${#}" local -n install_mode_ref="${1}"; shift local -n source_media_ref="${1}"; shift local -n target_media_ref="${1}"; shift if [ ! -f "${source_media_ref}" ] \ && [ ! -b "${source_media_ref}" ]; then print_error \ "$(gettext 'source media "%s" not found or not a regular file or a block device file!\n')" \ "${source_media_ref}" return 1 fi if ! [ -b "${target_media_ref}" ]; then print_error \ "$(gettext 'Target media "%s" is not a block device file!\n')" \ "${target_media_ref}" return 1 fi if [ "${install_mode_ref}" = device ] \ && [[ "${target_media}" =~ .*[0-9] ]] then print_error \ "$(gettext 'Target media "%s" is not an entire storage device!\n')" \ "${target_media_ref}" return 1 fi if [ "${install_mode_ref}" = partition ] \ && ! [[ "${target_media}" =~ .*[0-9] ]] then print_error \ "$(gettext 'Target media "%s" is not an partition!\n')" \ "${target_media_ref}" return 1 fi return 0 } determine_target_parameters(){ check_function_parameters_quantity 5 "${#}" local install_mode="${1}"; shift local target_media="${1}"; shift local -n target_device_ref="${1}"; shift local -n target_partition_ref="${1}"; shift local -n target_filesystem_type_ref="${1}"; shift if [ "${install_mode}" = partition ]; then local target_filesystem_type_libblkid target_partition_ref="${target_media}" # BASHDOC: Basic Shell Features » Shell Expansions » Shell Parameter Expansion(`${PARAMETER/PATTERN/STRING}') target_device_ref="${target_media/%[0-9]/}" # Detect target filesystem target_filesystem_type_libblkid="$( lsblk \ --noheadings \ --output FSTYPE \ "${target_partition_ref}" )" case "${target_filesystem_type_libblkid}" in vfat) target_filesystem_type_ref=FS_FAT ;; ntfs) target_filesystem_type_ref=FS_NTFS ;; *) print_error \ "$(gettext 'Unsupported target filesystem "%s", currently supported target filesystems: %s, %s\n')" \ "${target_filesystem_type_libblkid}" \ "${ENUM_SUPPORTED_FILESYSTEMS[FS_FAT]}" \ "${ENUM_SUPPORTED_FILESYSTEMS[FS_NTFS]}" return 1 ;; esac unset target_filesystem_type_libblkid else # install_mode = device target_device_ref="${target_media}" target_partition_ref="${target_device}1" fi if [ "${flag_verbose}" = true ]; then print_info \ "$(gettext "Target device is '%s'.\\n")" \ "${target_device_ref}" print_info \ "$(gettext "Target partition is '%s'.\\n")" \ "${target_partition_ref}" if [ "${install_mode}" = partition ]; then print_info \ "$(gettext "Target filesystem is '%s'.\\n")" \ "${ENUM_SUPPORTED_FILESYSTEMS[$target_filesystem_type]}" fi fi return 0 } is_target_busy(){ check_function_parameters_quantity 1 "${#}" local device="${1}"; shift if [ "$(mount \ | grep \ --count \ --fixed-strings\ "${device}" )" -ne 0 ]; then return 0 else return 1 fi } check_source_and_target_not_busy(){ check_function_parameters_quantity 4 "${#}" local install_mode="${1}"; shift local source_media="${1}"; shift local target_device="${1}"; shift local target_partition="${1}"; shift if [ "$(mount \ | grep \ --count \ --fixed-strings \ "${source_media}" )" != 0 ]; then print_error \ "$(gettext 'Source media is currently mounted, unmount the partition then try again\n')" exit 1 fi if [ "${install_mode}" = partition ]; then if [ "$(mount \ | grep \ --count \ --fixed-strings \ "${target_partition}" )" != 0 ]; then print_error \ "$(gettext 'Target partition is currently mounted, unmount the partition then try again\n')" exit 1 fi else # When install_mode = device, all target partitions needs to by unmounted if is_target_busy "${target_device}"; then print_error \ "$(gettext 'Target device is currently busy, unmount all mounted partitions in target device then try again\n')" exit 1 fi fi } wipe_existing_partition_table_and_filesystem_signatures(){ check_function_parameters_quantity 1 "${#}" local -r target_device="${1}"; shift print_info \ "$(gettext 'Wiping all existing partition table and filesystem signatures in %s...\n')" \ "${target_device}" wipefs --all "${target_device}" check_if_the_drive_is_really_wiped \ "${target_device}" return $? } # Some broken locked-down flash drive will appears to be successfully wiped but actually nothing is written into it and will shown previous partition scheme afterwards. This is the detection of the case and will bail out if such things happened # target_device: The target device file to be checked check_if_the_drive_is_really_wiped(){ check_function_parameters_quantity 1 "${#}" local -r target_device="${1}"; shift print_info \ "$(gettext 'Ensure that %s is really wiped...\n')" \ "${target_device}" if [ "$( env LANG=C \ lsblk \ --noheadings \ --output TYPE \ "${target_device}" \ | grep \ --count \ --fixed-strings \ part )" -ne 0 ]; then print_error \ "$(gettext 'Partition is still detected after wiping all signatures, this indicates that the drive might be locked into readonly mode due to end of lifespan.\n')" return 1 else return 0 fi } create_target_partition_table(){ check_function_parameters_quantity 2 "${#}" local -r target_device="${1}"; shift local -r partition_table_type="${1}"; shift print_info \ "$(gettext 'Creating new partition table on %s...\n')" \ "${target_device}" local parted_partiton_table_argument case "${partition_table_type}" in legacy|msdos|mbr|pc) parted_partiton_table_argument=msdos ;; gpt|guid) parted_partiton_table_argument=gpt print_error \ "$(gettext 'Currently GUID partition table is not supported.\n')" return 2 ;; *) print_error \ "$(gettext 'Partition table not supported.\n')" return 2 ;; esac # Create partition table(and overwrite the old one, whatever it was) parted --script \ "${target_device}" \ mklabel \ "${parted_partiton_table_argument}" } # NOTE: This really should be done automatically by GNU Parted after every operation workaround_make_system_realize_partition_table_changed(){ check_function_parameters_quantity 1 "${#}" local -r target_device="${1}"; shift print_info \ "$(gettext 'Making system realize that partition table has changed...\n')" blockdev \ --rereadpt \ "${target_device}" \ || true print_info \ "$(gettext 'Wait 3 seconds for block device nodes to populate...\n')" sleep 3 } create_target_partition(){ check_function_parameters_quantity 5 "${#}" local -r target_partition="${1}"; shift # ENUM_SUPPORTED_FILESYSTEMS local -r filesystem_type="${1}"; shift local -r filesystem_label="${1}"; shift local -r command_mkdosfs="${1}"; shift local -r command_mkntfs="${1}"; shift # Refer: GNU Parted's (info) manual: Using Parted » Command explanations » mkpart # Refer: sudo parted --script /dev/sda help mkpart local parted_mkpart_fs_type case "${filesystem_type}" in FS_FAT) parted_mkpart_fs_type=fat32 ;; FS_NTFS) parted_mkpart_fs_type=ntfs ;; *) print_error \ "$(gettext 'Filesystem not supported\n')" return 2 ;; esac print_info \ "$(gettext 'Creating target partition...\n')" # Create target partition # We start at 4MiB for grub (it needs a post-mbr gap for its code) and alignment of flash memery block erase segment in general, for details see http://www.gnu.org/software/grub/manual/grub.html#BIOS-installation and http://lwn.net/Articles/428584/ # If NTFS filesystem is used we leave a 512KiB partition at the end for installing UEFI:NTFS partition for NTFS support case "${parted_mkpart_fs_type}" in fat32) parted --script \ "${target_device}" \ mkpart \ primary \ "${parted_mkpart_fs_type}" \ 4MiB \ -- -1s # last sector of the disk ;; ntfs) # Major partition for storing user files # NOTE: Microsoft Windows has a bug that only recognize the first partition for removable storage devices, that's why this partition should always be the first one parted --script \ "${target_device}" \ mkpart \ primary \ "${parted_mkpart_fs_type}" \ 4MiB \ -- -2049s # Leave 1024KiB==2048sector in traditional 512bytes/sector disk, disks with sector with more than 512bytes only result in partition size greater than 1024KiB and is intentionally let-it-be. # FIXME: Leave exact 1024KiB in all circumstances is better, but the algorithm to do so is quite brainkilling. ;; *) print_fatal \ "$(gettext 'Illegal parted_mkpart_fs_type, please report bug.\n')" ;; esac unset parted_mkpart_fs_type workaround_make_system_realize_partition_table_changed \ "${target_device}" # Format target partition's filesystem case "${filesystem_type}" in FS_FAT) "${command_mkdosfs}" \ -F 32 \ -n "${filesystem_label}" \ "${target_partition}" ;; FS_NTFS) "${command_mkntfs}" \ --quick \ --label "${filesystem_label}" \ "${target_partition}" ;; *) print_fatal \ "Shouldn't be here\\n" exit 1 ;; esac } # Create UEFI:NTFS partition to support booting UEFI bootloader in NTFS filesystem where some UEFI firmwares are not able to do so # https://github.com/pbatard/uefi-ntfs # This routine assumes that there's only one partition on the disk, and the trailing 1024KiB space is not partitioned # This routine should be run after create_target_partition and only on target partition's filesystem is NTFS # target_device: The target device's entire deice file create_uefi_ntfs_support_partition(){ check_function_parameters_quantity 1 "${#}" local -r target_device="${1}"; shift # FIXME: The partition type should be `fat12` but `fat12` isn't recognized by Parted... # NOTE: The --align is set to none because this partition is indeed misaligned, but ignored due to it's small size parted \ --align none \ --script \ "${target_device}" \ mkpart \ primary \ fat16 \ -- \ -2048s \ -1s return "$?" } # Install UEFI:NTFS partition by writing the partition image into the created partition # FIXME: Currently this requires internet access to download the image from GitHub directly, it should be replaced by including the image in our datadir # uefi_ntfs_partition: The previously allocated partition for installing UEFI:NTFS, requires at least 512KiB # download_directory: The temporary directory for downloading UEFI:NTFS image from GitHub # target_device: For workaround_make_system_realize_partition_table_changed install_uefi_ntfs_support_partition(){ check_function_parameters_quantity 4 "${#}" local -r rufus_uefi_ntfs_version="${1}"; shift local -r uefi_ntfs_partition="${1}"; shift local -r download_directory="${1}"; shift local -r target_device="${1}"; shift if ! wget \ --directory-prefix="${download_directory}" \ "https://cdn.jsdelivr.net/gh/pbatard/rufus@${rufus_uefi_ntfs_version}/res/uefi/uefi-ntfs.img"; then print_warning \ "Unable to download UEFI:NTFS partition image from GitHub, installation skipped. Target device might not be bootable if the UEFI firmware doesn't support NTFS filesystem.\\n" return 0 fi # Write partition image to partition dd \ if="${download_directory}/uefi-ntfs.img" \ of="${uefi_ntfs_partition}" } # Some buggy BIOSes won't put detected device with valid MBR but no partitions with boot flag toggled into the boot menu, workaround this by setting the first partition's boot flag(which partition doesn't matter as GNU GRUB doesn't depend on it anyway workaround_buggy_motherboards_that_ignore_disks_without_boot_flag_toggled(){ check_function_parameters_quantity 1 "${#}" local -r target_device="${1}"; shift print_warning \ "$(gettext 'Applying workaround for buggy motherboards that will ignore disks with no partitions with the boot flag toggled\n')" parted --script \ "${target_device}" \ set 1 boot on return $? } # Check target partition for potential problems before mounting them for --partition creation mode as we don't know about the existing partition # target_partition: The target partition to check # install_mode: The usb storage creation method to be used # target_device: The parent device of the target partition, this is passed in to check UEFI:NTFS filesystem's existence on check_uefi_ntfs_support_partition check_target_partition(){ check_function_parameters_quantity 3 "${#}" local target_partition="${1}"; shift local install_mode="${1}"; shift local target_device="${1}"; shift local target_filesystem target_filesystem="$( lsblk \ --output FSTYPE \ --noheadings \ "${target_partition}" )" case "${target_filesystem}" in vfat) : # supported ;; ntfs) check_uefi_ntfs_support_partition \ "${target_device}" ;; *) print_error \ "$(gettext 'Target filesystem not supported, currently supported filesystem: FAT, NTFS.\n')" return 1 ;; esac return 0 } # Check if the UEFI:NTFS support partition exists # Currently it depends on the fact that this partition has a label of "UEFI_NTFS" # target_device: The UEFI:NTFS partition residing entier device file check_uefi_ntfs_support_partition(){ check_function_parameters_quantity 1 "${#}" local -r target_device="${1}"; shift if \ ! \ lsblk \ --output LABEL \ --noheadings \ "${target_device}" \ | grep \ --fixed-strings \ --silent \ 'UEFI_NTFS'; then print_warning \ "$(gettext "Your device doesn't seems to have an UEFI:NTFS partition, UEFI booting will fail if the motherboard firmware itself doesn't support NTFS filesystem!\\n")" print_info \ "$(gettext 'You may recreate disk with an UEFI:NTFS partition by using the --device creation method\n')" fi } mount_source_filesystem(){ check_function_parameters_quantity 2 "${#}" local source_media="${1}"; shift local source_fs_mountpoint="${1}"; shift print_info \ "$(gettext 'Mounting source filesystem...\n')" mkdir \ --parents \ "${source_fs_mountpoint}" if [ -f "${source_media}" ]; then # ${source_media} is an ISO image mount \ --options loop,ro \ --types udf,iso9660 \ "${source_media}" \ "${source_fs_mountpoint}" else # ${source_media} is a real optical disk drive (block device) mount \ --options ro \ "${source_media}" \ "${source_fs_mountpoint}" fi } # Mount target filesystem to existing path as mountpoint # target_partition: The partition device file target filesystem resides, for example /dev/sdX1 # target_fs_mountpoint: The existing directory used as the target filesystem's mountpoint, for example /mnt/target_filesystem # target_fs_type: The filesystem of the target filesystem currently supports: FAT, NTFS mount_target_filesystem(){ check_function_parameters_quantity 3 "${#}" local target_partition="${1}"; shift local target_fs_mountpoint="${1}"; shift # ENUM_SUPPORTED_FILESYSTEMS local target_fs_type="${1}" print_info \ "$(gettext 'Mounting target filesystem...\n')" mkdir \ --parents \ "${target_fs_mountpoint}" # Determine proper mount options according to filesystem type local mount_options='defaults' local fstype_options='' case "${target_fs_type}" in FS_FAT) mount_options+=',utf8=1' fstype_options="-t vfat" ;; FS_NTFS) fstype_options="-t ntfs-3g" ;; *) print_fatal \ "$(gettext 'Unsupported target_fs_type, please report bug.\n')" exit 1 ;; esac # false-positive: The fstype_options are expected to be expanded verbatim # shellcheck disable=SC2086 mount \ --options "${mount_options}" \ ${fstype_options} \ "${target_partition}" \ "${target_fs_mountpoint}" } check_target_filesystem_free_space(){ check_function_parameters_quantity 2 "${#}" local -r target_fs_mountpoint="${1}"; shift local -r source_fs_mountpoint="${1}"; shift free_space=$( df \ --block-size=1 \ "${target_fs_mountpoint}" \ | grep \ --fixed-strings \ "${target_fs_mountpoint}" \ | awk '{print $4}' ) free_space_human_readable=$( df \ --human-readable \ "${target_fs_mountpoint}" \ | grep \ --fixed-strings \ "${target_fs_mountpoint}" \ | awk '{print $4}' ) needed_space=$( du \ --summarize \ --bytes \ "${source_fs_mountpoint}" \ | awk '{print $1}' ) needed_space_human_readable=$( du \ --summarize \ --human-readable \ "${source_fs_mountpoint}" \ | awk '{print $1}' ) additional_space_required_for_grub_installation="$((1000 * 1000 * 10))" # 10MiB ((needed_space = needed_space + additional_space_required_for_grub_installation)) if [ "${needed_space}" -gt "${free_space}" ]; then print_error \ "$(gettext 'Not enough free space on target partition!\n')" print_error \ "$(gettext "We required %s(%u bytes) but '%s' only has %s(%u bytes).\n")" \ "${needed_space_human_readable}" \ "${needed_space}" \ "${target_partition}" \ "${free_space_human_readable}" \ "${free_space}" return 1 fi } # Copying all files from one filesystem to another, with progress reporting copy_filesystem_files(){ check_function_parameters_quantity 4 "${#}" local source_fs_mountpoint="${1}"; shift local target_fs_mountpoint="${1}"; shift local target_filesystem_type="${1}"; shift local dd_block_size="${1}"; shift local -ir fat32_filesize_limit=4294967295 # 2^32 - 1 local -i total_size total_size=$( du \ --summarize \ --bytes \ "${source_fs_mountpoint}" \ | awk '{print $1}' ) print_info \ "$(gettext 'Copying files from source media...\n')" pushd "${source_fs_mountpoint}" >/dev/null local -i copied_size=0 percentage; while IFS='' read -r -d '' source_file; do dest_file="${target_fs_mountpoint}/${source_file}" source_file_size=$( stat \ --format=%s \ "${source_file}" ) if [ -d "${source_file}" ]; then mkdir --parents "${dest_file}" elif [ -f "${source_file}" ]; then if [ "${flag_verbose}" = true ]; then print_info \ "$(gettext 'Copying "%s"...\n')" \ "${source_file}" fi if [ "${source_file_size}" -lt "${dd_block_size}" ]; then cp "${source_file}" "${dest_file}" else # FS_FAT is not a parameter expansion # shellcheck disable=SC2050 if [ "${target_filesystem_type}" == FS_FAT ] \ && [ "${source_file_size}" -gt "${fat32_filesize_limit}" ]; then if [[ ! "${source_file}" =~ \.wim$ ]]; then print_warning \ "$(gettext 'Unsupported oversized file "%s" detected, this file will NOT be transferred.\n')" \ "${source_file}" else copy_oversized_wim_file \ "${source_file}" \ "${dest_file}" \ "${copied_size}" \ "${total_size}" fi else copy_large_file \ "${source_file}" \ "${dest_file}" \ "${copied_size}" \ "${total_size}" \ "${dd_block_size}" fi fi else print_error \ "$(gettext "Unknown type of '%s'!\\n")" \ "${source_file}" exit 1 fi # Calculate and report progress # BASHDOC: Basic Shell Features » Shell Expansions » Arithmetic Expansion # BASHDOC: Bash Features » Shell Arithmetic ((copied_size = copied_size + source_file_size)) || true ((percentage = (copied_size * 100) / total_size)) || true echo -en "${percentage}%\\r" done < <( \ find \ . \ -not -path "." \ -print0 \ ); unset source_file dest_file source_file_size copied_size percentage popd >/dev/null return 0 } # Companion function of copy_filesystem_files for copying large files # Copy source_file to dest_file, overwrite file if dest_file exists # Also report copy_filesystem_files progress during operation copy_large_file(){ check_function_parameters_quantity 5 "${#}" local -r source_file="${1}"; shift local -r dest_file="${1}"; shift local -ir caller_copied_size="${1}"; shift local -ir caller_total_size="${1}"; shift local -ir dd_block_size="${1}"; shift local -i source_file_size source_file_size=$( stat \ --format=%s \ "${source_file}" ) # block count of the source file local -i block_number ((block_number = source_file_size / dd_block_size + 1)) unset source_file_size if [ -f "${dest_file}" ]; then rm "${dest_file}" fi # Copy file block by block local -i i=0 copied_size_total percentage; while [ "${i}" -lt "${block_number}" ]; do dd \ if="${source_file}" \ bs="${dd_block_size}" \ skip="${i}" \ seek="${i}" \ of="${dest_file}" \ count=1 \ 2> /dev/null ((i = i + 1)) # Calculate and report progress # BASHDOC: Basic Shell Features » Shell Expansions » Arithmetic Expansion # BASHDOC: Bash Features » Shell Arithmetic ((copied_size_total = caller_copied_size + dd_block_size * i)) || true ((percentage = (copied_size_total * 100) / caller_total_size)) || true echo -en "${percentage}%\\r" done; unset i copied_size_total percentage return 0 } # Copy WIM files that is over 4GiB to a FAT32 filesystem # Using wimlib to split file into smaller parts copy_oversized_wim_file(){ check_function_parameters_quantity 4 "${#}" local source_file="${1}"; shift local dest_file="${1}"; shift local -ir caller_copied_size="${1}"; shift local -ir caller_total_size="${1}"; shift local dest_file_basename="${dest_file%.wim}" # Split WIM image using wimtools to fit FAT32 restriction # Optimal split file size: 4GiB - 1MiB = 4095MiB wimlib-imagex split \ "${source_file}" \ "${dest_file_basename}.swm" \ 4095 \ & # We simulate progress report via sum-up the destination files local -i \ splitted_files_size_total=0 \ splitted_file_size=0 \ copied_size_current="${caller_copied_size}" \ percentage_current=0 wimlib_imagex_pid="${!}" while kill -0 "${wimlib_imagex_pid}" 2>/dev/null; do splitted_files_size_total=0 splitted_file_size=0 copied_size_current="${caller_copied_size}" is_dest_file_available=false # Ensure swm file is created by wimlib before accounting sizes while test "${is_dest_file_available}" == false && \ ! test -e "${dest_file_basename}".swm; do sleep 1 done is_dest_file_available=true for splitted_file in "${dest_file_basename}"*.swm; do splitted_file_size="$( stat \ --format=%s \ "${splitted_file}" )" (( splitted_files_size_total += splitted_file_size )) done (( copied_size_current += splitted_files_size_total )) (( percentage = (copied_size_current * 100) / caller_total_size)) echo -en "${percentage}%\\r" # Use less resource during accounting sizes sleep 0.2s done # Let EXIT trap know the process ended so that it won't try to # kill random processes wimlib_imagex_pid= } # As Windows 7's installation media doesn't place the required EFI # bootloaders in the right location, we extract them from the # system image manually # TODO: Functionize Windows 7 checking workaround_support_windows_7_uefi_boot(){ check_function_parameters_quantity 2 "${#}" local source_fs_mountpoint="${1}"; shift local target_fs_mountpoint="${1}"; shift # Apply workaround only if the source is based on Windows 7, and EFI version of bootmgr is in place if ! grep \ --extended-regexp \ --quiet \ '^MinServer=7[0-9]{3}\.[0-9]' \ "${source_fs_mountpoint}/sources/cversion.ini" \ || ! [ -f "${source_fs_mountpoint}/bootmgr.efi" ]; then return 0 fi print_warning \ "$(gettext 'Source media seems to be Windows 7-based with EFI support, applying workaround to make it support UEFI booting\n')" if ! command -v '7z' >/dev/null 2>&1; then print_warning \ "$(gettext "'7z' utility not found, workaround is not applied.\\n")" return 0 fi # Detect **case-insensitive** existing efi directories according to UEFI spec local \ test_efi_directory \ efi_directory test_efi_directory="$( find \ "${target_fs_mountpoint}" \ -ipath "${target_fs_mountpoint}/efi" )" if [ -z "${test_efi_directory}" ]; then efi_directory="${target_fs_mountpoint}/efi" if [ "${flag_verbose}" = true ]; then print_debug \ "Can't find efi directory, use %s.\\n" \ "${efi_directory}" fi else # efi directory(case don't care) exists efi_directory="${test_efi_directory}" if [ "${flag_verbose}" = true ]; then print_debug \ "$(gettext '%s detected.\n')" \ "${efi_directory}" fi fi unset test_efi_directory local \ test_efi_boot_directory \ efi_boot_directory test_efi_boot_directory="$( find \ "${target_fs_mountpoint}" \ -ipath "${efi_directory}/boot" )" if [ -z "${test_efi_boot_directory}" ]; then efi_boot_directory="${efi_directory}/boot" if [ "${flag_verbose}" = true ]; then print_debug \ "$(gettext "Can't find efi/boot directory, use %s.\\n")" \ "${efi_boot_directory}" fi else # boot directory(case don't care) exists efi_boot_directory="${test_efi_boot_directory}" if [ "${flag_verbose}" = true ]; then print_debug \ "$(gettext '%s detected.\n')" \ "${efi_boot_directory}" fi fi unset \ efi_directory \ test_efi_boot_directory # If there's already an EFI bootloader existed, skip the workaround local test_efi_bootloader test_efi_bootloader="$( find \ "${target_fs_mountpoint}" \ -ipath "${target_fs_mountpoint}/efi/boot/boot*.efi" )" if [ -n "${test_efi_bootloader}" ]; then print_info \ "$(gettext 'Detected existing EFI bootloader, workaround skipped.\n')" return 0 fi mkdir \ --parents \ "${efi_boot_directory}" # Skip workaround if EFI bootloader already exist in efi_boot_directory 7z \ e \ -so \ "${source_fs_mountpoint}/sources/install.wim" \ "Windows/Boot/EFI/bootmgfw.efi" \ > "${efi_boot_directory}/bootx64.efi" } install_legacy_pc_bootloader_grub(){ check_function_parameters_quantity 3 "${#}" local -r target_fs_mountpoint="${1}"; shift local -r target_device="${1}"; shift local -r command_grubinstall="${1}"; shift print_info \ "$(gettext 'Installing GRUB bootloader for legacy PC booting support...\n')" "${command_grubinstall}" \ --target=i386-pc \ --boot-directory="${target_fs_mountpoint}" \ --force "${target_device}" } # Install a GRUB config file to chainload Microsoft Windows's bootloader in Legacy PC bootmode # target_fs_mountpoint: Target filesystem's mountpoint(where GRUB is installed) # name_grub_prefix: May be different between distributions, so need to be specified (grub/grub2) install_legacy_pc_bootloader_grub_config(){ check_function_parameters_quantity 2 "${#}" local -r target_fs_mountpoint="${1}"; shift local -r name_grub_prefix="${1}"; shift print_info \ "$(gettext 'Installing custom GRUB config for legacy PC booting...\n')" local -r grub_cfg="${target_fs_mountpoint}/${name_grub_prefix}/grub.cfg" local target_fs_uuid; target_fs_uuid=$( lsblk \ --noheadings \ --raw \ --output UUID,MOUNTPOINT \ | grep \ --extended-regexp \ "${target_fs_mountpoint}"$ \ | cut \ --fields=1 \ --delimiter=' ' ) mkdir --parents "$(dirname "${grub_cfg}")" { # SC1040 workaround # https://github.com/koalaman/shellcheck/wiki/SC1040 cat <<- END_OF_FILE ntldr /bootmgr boot END_OF_FILE } > "${grub_cfg}"; unset target_fs_uuid } # Unmount mounted filesystem and clean-up mountpoint before exiting program # return_value: 1 - Failed to unmount / 2 - Failed to remove mountpoint cleanup_mountpoint(){ check_function_parameters_quantity 1 "${#}" local -r fs_mountpoint="${1}"; shift # In copy_filesystem_files, we use `pushd` to changed the working # directory into source_fs_mountpoint in order to get proper source # and target file path, proactively `popd` to ensure we are not in # source_fs_mountpoint and preventing source filesystem to unmount popd &>/dev/null \ || true if [ -e "${fs_mountpoint}" ]; then print_info \ "$(gettext 'Unmounting and removing "%s"...\n')" \ "${fs_mountpoint}" if ! umount "${fs_mountpoint}"; then print_warning \ "$(gettext 'Unable to unmount "%s".\n')" \ "${fs_mountpoint}" return 1 fi if ! rmdir "${fs_mountpoint}"; then print_warning \ "$(gettext 'Unable to remove "%s".\n')" \ "${fs_mountpoint}" return 2 fi fi return 0 } # Traps: Functions that are triggered when certain condition occurred # Shell Builtin Commands » Bourne Shell Builtins » trap trap_errexit(){ print_error \ "$(gettext 'The command "%s" failed with exit status "%u", program is prematurely aborted\n')"\ "${BASH_COMMAND}" \ "${?}" return 0 } trap_exit(){ # Mountpoints aren't successfully removed local flag_unclean=false # Target filesystem failed to unmount local flag_unsafe=false if [ -n "${wimlib_imagex_pid}" ]; then kill -s SIGKILL "${wimlib_imagex_pid}" || true # Ensure fs is inactive before unmount sleep 1 fi if test "${flag_source_filesystem_mounted}" == true; then if ! cleanup_mountpoint \ "${source_fs_mountpoint}"; then flag_unclean=true fi fi if test "${flag_target_filesystem_mounted}" == true; then if ! cleanup_mountpoint \ "${target_fs_mountpoint}"; then local -i return_value="${?}" flag_unclean=true if [ "${return_value}" = 1 ]; then flag_unsafe=true fi fi fi if [ "${flag_unclean}" = true ]; then print_warning \ "$(gettext 'Some mountpoints are not unmount/cleaned successfully and must be done manually\n')" fi if [ "${flag_unsafe}" = true ]; then print_warning \ "$(gettext 'We unable to unmount target filesystem for you, please make sure target filesystem is unmounted before detaching to prevent data corruption\n')" fi unset \ flag_unclean \ flag_unsafe if \ is_var_set_and_not_empty\ target_device; then if is_target_busy "${target_device}"; then print_warning \ "$(gettext 'Target device is busy, please make sure you unmount all filesystems on target device or shutdown the computer before detaching it.\n')" else print_info \ "$(gettext 'You may now safely detach the target device\n')" fi fi rm \ --recursive \ "${temp_dir}" return 0 } trap_interrupt(){ printf '\n' # Separate message with previous output print_warning \ "$(gettext 'Recieved SIGINT, program is interrupted.\n')" return 1 } trap_return(){ check_function_parameters_quantity 1 "${#}" local returning_function="${1}"; shift for ignored_function in \ check_function_parameters_quantity \ is_var_set_and_not_empty \ switch_terminal_text_color \ printf_with_color \ print_info \ print_warning \ print_error \ print_fatal \ print_debug; do if [ "${returning_function}" == "${ignored_function}" ]; then return 0 fi done print_debug \ "$(gettext 'returning from %s\n')" \ "${returning_function}" } # An utility function for inhibiting command call output and # only show them to user when error occurred util_call_external_command(){ local -ar command=("${@}") local command_output local -i command_exit_status if command_output="$( "${command[@]}" 2>&1 )"; then command_exit_status=0 else command_exit_status="$?" fi if [ "${command_exit_status}" -ne 0 ]; then print_error \ "$(gettext 'Error occurred while running command "%s" (exit status: %u)!\n')" \ "${command[*]}" \ "${command_exit_status}" local -r read_prompt="Read command output (Y/n)?" printf '%s' "${read_prompt}" local answer=y while true; do read -r answer if [ "${answer}" == y ] || [ "${answer}" == Y ]; then echo "${command_output}" break elif [ "${answer}" == n ] || [ "${answer}" == N ]; then break else printf '%s' "${read_prompt}" fi done print_error \ "$(gettext 'Press ENTER to continue\n')" read -r # catch enter key fi return "${command_exit_status}" } # Configure the terminal to print future messages with certain color # Parameters: # - color: color of the next message, or `none` to reset to default color switch_terminal_text_color(){ check_function_parameters_quantity 1 "${#}" local -r color="${1}"; shift case "${color}" in black) echo -en '\033[0;30m' ;; red) echo -en '\033[0;31m' ;; green) echo -en '\033[0;32m' ;; yellow) echo -en '\033[0;33m' ;; blue) echo -en '\033[0;34m' ;; white) echo -en '\033[0;37m' ;; none) tput sgr0 ;; *) print_fatal \ "$(gettext 'Illegal parameter, please report bug.\n')" exit 1 ;; esac } # Print formatted message with color printf_with_color(){ if [ "${#}" -lt 2 ]; then print_fatal \ "$(gettext 'Parameter quantity illegal, please report bug.\n')" exit 1 fi local -r color="${1}"; shift local -ar printf_parameters=("${@}") if [ "${flag_no_color}" == true ]; then # False positive: not format string(ShellCheck #1028) # shellcheck disable=SC2059 printf -- \ "${printf_parameters[@]}" else # flag_no_color = false switch_terminal_text_color "${color}" # False positive: not format string(ShellCheck #1028) # shellcheck disable=SC2059 printf -- \ "${printf_parameters[@]}" switch_terminal_text_color none fi } print_fatal(){ local -a printf_arguments=("${@}") if test "${#printf_arguments[@]}" -eq 0; then printf \ "$(gettext '%s: FATAL: This function should have at least 1 argument.\n')" \ "${FUNCNAME[0]}" \ 1>&2 exit 1 fi printf \ "$(gettext '%s: FATAL: ')" \ "${FUNCNAME[1]}" \ 1>&2 # False positive: not format string(ShellCheck #1028) # shellcheck disable=SC2059 printf \ "${printf_arguments[@]}" \ 1>&2 } print_info(){ local -a printf_arguments=("${@}") if test "${#printf_arguments[@]}" -eq 0; then print_fatal "$(gettext 'This function should have at least 1 argument.\n')" exit 1 fi printf_with_color \ green \ "$(gettext 'Info: ')" # False positive: not format string(ShellCheck #1028) # shellcheck disable=SC2059 printf_with_color \ green \ "${printf_arguments[@]}" } print_warning(){ local -a printf_arguments=("${@}") if test "${#printf_arguments[@]}" -eq 0; then print_fatal "$(gettext 'This function should have at least 1 argument.\n')" exit 1 fi printf_with_color \ yellow \ "$(gettext 'Warning: ')" \ 1>&2 # False positive: not format string(ShellCheck #1028) # shellcheck disable=SC2059 printf_with_color \ yellow \ "${printf_arguments[@]}" \ 1>&2 } print_error(){ local -a printf_arguments=("${@}") if test "${#printf_arguments[@]}" -eq 0; then print_fatal "$(gettext 'This function should have at least 1 argument.\n')" exit 1 fi printf_with_color \ red \ "$(gettext 'ERROR: ')" \ 1>&2 # False positive: not format string(ShellCheck #1028) # shellcheck disable=SC2059 printf_with_color \ red \ "${printf_arguments[@]}" \ 1>&2 } print_debug(){ local -a printf_arguments=("${@}") if test "${#printf_arguments[@]}" -eq 0; then print_fatal "$(gettext 'This function should have at least 1 argument.\n')" exit 1 fi printf \ "$(gettext '%s: DEBUG: ')" \ "${FUNCNAME[1]}" \ 1>&2 # False positive: not format string(ShellCheck #1028) # shellcheck disable=SC2059 printf \ "${printf_arguments[@]}" \ 1>&2 } shift_array(){ check_function_parameters_quantity 1 "${#}" local -n array_ref="${1}"; shift if [ "${#array_ref[@]}" -eq 0 ]; then print_fatal \ "$(gettext 'array is empty!\n')" exit 1 fi # Unset the 1st element unset 'array_ref[0]' # Repack array if element still available in array if [ "${#array_ref[@]}" -ne 0 ]; then array_ref=("${array_ref[@]}") fi return 0 } is_var_set_and_not_empty(){ check_function_parameters_quantity 1 "${#}" local parameter_name="${1}"; shift if [ ! -v "${parameter_name}" ]; then return 1 else local -n parameter_ref parameter_ref="${parameter_name}" if [ -z "${parameter_ref}" ]; then return 1 else return 0 fi fi } # Utility function to check if function parameters quantity is legal # NOTE: non-static function parameter quantity(e.g. either 2 or 3) is not supported check_function_parameters_quantity(){ if [ "${#}" -ne 2 ]; then print_fatal \ "$(gettext 'Function %s requires %u parameters, but %u is given\n')" \ "${FUNCNAME[0]}" \ 2 \ "${#}" exit 1 fi # The expected given quantity local -i expected_parameter_quantity="${1}"; shift # The actual given parameter quantity, simply pass "${#}" will do local -i given_parameter_quantity="${1}" local failed=false if ! [[ $expected_parameter_quantity =~ ^[[:digit:]]+$ ]]; then local test_expression="${expected_parameter_quantity}" # test_expression is supposed to be word splitted here # shellcheck disable=SC2086 if ! test "${given_parameter_quantity}" ${test_expression}; then failed=true fi else if [ "${given_parameter_quantity}" -ne "${expected_parameter_quantity}" ]; then failed=true fi fi if test "${failed}" == true; then print_fatal \ "$(gettext 'Function %s requires %u parameters, but %u is given\n')" \ "${FUNCNAME[1]}" \ "${expected_parameter_quantity}" \ "${given_parameter_quantity}" # FATAL error exit 1 else return 0 fi } # Generate reliable and user-friendly timestamps generate_timestamp(){ printf \ %s \ "$( LC_TIME=C.UTF-8 \ date \ +%Y%m%d-%H%M%S-%A )" } # Makes debuggers' life easier - Unofficial Bash Strict Mode # BASHDOC: Shell Builtin Commands - Modifying Shell Behavior - The Set Builtin set \ -o errexit \ -o errtrace \ -o nounset \ -o pipefail # Check unsupported Bash version if test "${BASH_VERSINFO[0]}" -lt 4 \ || { test "${BASH_VERSINFO[0]}" -eq 4 \ && test "${BASH_VERSINFO[1]}" -lt 3;}; then print_error \ "$(gettext 'This program requires Bash >= 4.3 to function properly, refer <%s> for more info.\n')" \ https://github.com/WoeUSB/WoeUSB#dependencies exit 1 fi # Check critical dependencies for WoeUSB for critical_command in \ basename \ dirname \ mktemp \ realpath; do if ! command -v "${critical_command}" &> /dev/null; then print_fatal \ "$(gettext 'This program requires GNU Coreutils in the executable search path')" exit 1 fi done # Non-overridable Primitive Variables # BASHDOC: Shell Variables » Bash Variables # BASHDOC: Basic Shell Features » Shell Parameters » Special Parameters if [ -v 'BASH_SOURCE[0]' ]; then script_path="$(realpath --strip "${BASH_SOURCE[0]}")" script_filename="${script_path##*/}" script_name="${script_filename%%.*}" script_dir="${script_path%/*}" program_basecommand="${0}" # Keep these partially unused variables declared # shellcheck disable=SC2034 declare -r \ script_path \ script_filename \ script_name \ script_dir \ program_basecommand else # sane defaults script_name=_script_ program_basecommand=_script_ script_filename=_script_ script_dir= script_path= fi declare -ar program_args=("${@}") # Global Variables # # Only set variables global when there's no other way around(like passing # in function as a function argument), usually when the variable is directly # or indirectly referenced by traps # # Even when the parameter is set global, you should pass it in as function # argument whenever it's possible for better code reusability. # Increase verboseness, provide more information when required declare flag_verbose=false # Disable message coloring when set to `true`, set by --no-color declare flag_no_color=false # Need to be consumed by traps for clean-up declare flag_source_filesystem_mounted=false declare flag_target_filesystem_mounted=false # NOTE: Need to pass to traps, so need to be global declare \ source_fs_mountpoint \ target_fs_mountpoint \ target_device # I18N if ! command -v gettext >/dev/null; then # Dummy gettext utility to fall back to no I18N gettext(){ local text="${1}" printf '%s' "${text}" } else # NOTE: We don't really use the library functions as of now # External scope # shellcheck source=/dev/null #. gettext.sh TEXTDOMAIN="${script_name}" TEXTDOMAINDIR="${script_dir}"/../share/locale export \ TEXTDOMAIN \ TEXTDOMAINDIR fi # Supported filesystems # A associated array with supported filesystem's identifier as key and its description as value declare -A ENUM_SUPPORTED_FILESYSTEMS=( [FS_FAT]="$(gettext 'File Allocation Table(FAT)')" [FS_NTFS]="$(gettext 'New Technology File System(NTFS)')" ) trap trap_exit EXIT trap trap_errexit ERR trap trap_interrupt INT declare temp_dir; temp_dir=$( mktemp \ -d \ -t \ WoeUSB.tempdir.XXXXXX ); declare -r temp_dir # wimlib's process ID, needed to be killed in EXIT trap declare wimlib_imagex_pid= init \ "${script_name}" \ flag_source_filesystem_mounted \ flag_target_filesystem_mounted \ "${RUFUS_UEFI_NTFS_VERSION}" \ "${DD_BLOCK_SIZE}"