Introdução


No post anterior, Automatizando a instalação do meu ambiente Arch Linux, falo sobre como automatizei a instalação do meu ambiente Arch Linux por meio de um script, que será detalhado neste artigo.

Portanto, o propósito deste post é fazer um breakdown desse script, cujo objetivo é automatizar toda a instalação do Arch com as configurações que costumo utilizar — desde o ambiente Live ISO até um sistema completamente configurado, com ou sem interface gráfica.

Para refrescar a memória sem precisar reler o post anterior, podemos resgatar o fluxo do script, já que ele é a parte mais importante para este artigo.

text
Live ISO (root)
 └─ archinstall.sh
      ├─ sanity checks: root / internet
      ├─ user input: instalação minimalista ou com ambiente desktop
      ├─ user input: modo de boot BIOS ou UEFI
      ├─ detecta disco, fabricante da CPU, virtualização
      ├─ pergunta senhas + input: atualizar keyrings? + exibe o plano + confirmação
      ├─ particiona (parted) → formata → monta
      ├─ (opcional) atualiza o archlinux-keyring
      ├─ pacstrap: base + microcode + desktop + pacotes de virtualização
      ├─ genfstab
      └─ arch-chroot /mnt → chroot-setup.sh
              ├─ fuso horário, locale, layout de teclado, hostname
              ├─ habilita o NetworkManager
              ├─ define senhas, cria usuário, configura o sudo
              ├─ grub-install + grub-mkconfig
              ├─ habilita LightDM/Cinnamon (se configurado)
              └─ habilita serviços de VM guest (se detectado)
 └─ desmonta → concluído, reinicie

Para não ser prolixo, já que o post anterior já aborda esses detalhes, vou resumir os requisitos para executar o script:

  • ISO do Arch Linux bootada. De preferência a ISO atual disponibilizada no site oficial archlinux.org/download.
  • Conexão com a internet
  • Ter um meio de obter os arquivos do script na live ISO (via Git é a forma mais direta, clonando meu repositório, git clone https://github.com/xyz-leo/dotfiles)
  • Conceder permissão de execução aos arquivos archinstall.sh e chroot-setup.sh (chmod +x)
  • Executar o script, responder às perguntas e aguardar a instalação ser concluída.

Agora, vamos desmembrar o código do script e entender como ele funciona.

Breakdown


Link para o código-fonte do script: dotfiles/archinstall.

Shebang, set -euo, SCRIPT_DIR, source e logs

Essas são as primeiras linhas do script e são bem importantes:

bash
#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.sh"

log()  { echo -e "\n==> $*"; }
err()  { echo "ERROR: $*" >&2; }

Esses trechos de código representam um padrão comum em scripts Bash, então vale a pena entendê-los de verdade.

TL;DR:

  • #!/usr/bin/env bash → diz ao sistema para executar o script com o Bash disponível no ambiente do usuário. É o shebang.
  • set -euo pipefail → ativa um modo seguro para o script em caso de falhas.
    • -e → encerra o script se um comando falhar.
    • -u → acusa erro ao usar uma variável não definida.
    • -o pipefail → faz uma pipeline falhar se qualquer comando dela falhar (por padrão, uma pipeline | retorna o código de saída apenas do último comando).
  • SCRIPT_DIR=... → descobre o diretório onde o próprio script está, independentemente de onde ele foi executado.
  • source "$SCRIPT_DIR/config.sh" → importa outro arquivo Bash, permitindo reutilizar variáveis e funções.
  • log() e err() → funções auxiliares para padronizar mensagens. err() envia a saída para o stderr, enquanto log() imprime mensagens comuns.

Agora, alguns dos detalhes importantes.

Primeiro, no geral, $ em Bash significa:

  • $1 → primeiro argumento do script/função.
  • $2 → segundo argumento.
  • $* → todos os argumentos.
  • $@ → todos os argumentos (preservando cada um separadamente).
  • $# → quantidade de argumentos.
  • $? → código de saída do último comando.
  • $$ → PID do processo atual.
  • $0 → nome do script.
  • ${...} → forma mais explícita de expandir variáveis (${HOME}, ${nome}, etc.).
  • Também aparece na substituição de comandos, e.g.: $(pwd) → Aqui o $ diz: "execute o comando entre parênteses e substitua isso pela saída dele".

Em resumo, o Bash expande expressões iniciadas por $, substituindo-as pelo valor correspondente antes de executar o comando.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" → essa linha é composta por várias pequenas partes.

  • BASH_SOURCE → É um array interno do Bash que contém o caminho dos scripts atualmente em execução; BASH_SOURCE[0] é o arquivo Bash atual.
  • dirname → Extrai apenas o diretório do caminho informado, e.g.: /root/dotfiles/archinstall
  • cd → Entra nesse diretório.
  • pwd → Obtém o caminho absoluto do diretório atual. Como cd não produz saída, pwd é necessário para que $(...) capture esse caminho e o atribua à variável SCRIPT_DIR.
  • $(...) → Executa o comando entre parênteses e substitui a expressão pela saída produzida.

err() { echo "ERROR: $*" >&2; } → função auxiliar para redirecionar uma mensagem para o stderr.

  • >&2 → redireciona a saída para o stderr (fluxo padrão destinado à saída de mensagens de erro de um processo).
    • > → redireciona a saída.
    • & → indica que o número seguinte representa um descritor de arquivo.
    • 2 → é o descritor de arquivo stderr.

check_root e check_internet

Uma das primeiras coisas que o script faz é verificar se ele está sendo executado como root e se há conexão com a internet. Isso é necessário porque a instalação exige privilégios administrativos para executar diversos comandos e acesso à internet para baixar os pacotes.

bash
check_root() {
  [[ $EUID -eq 0 ]] || { err "Must run as root."; exit 1; }
}

check_internet() {
  ping -c1 -W2 archlinux.org &>/dev/null || { err "No internet connection."; exit 1; }
}

check_root() → Verifica se o usuário que executou o script é root.

  • EUID → É uma variável interna do Bash (Effective User ID) que contém o identificador do usuário efetivo do processo. O usuário root sempre possui UID 0.
  • [[ $EUID -eq 0 ]] → Significa literalmente "o valor de $EUID é igual a 0?" — i.e., verifica se o processo está rodando com permissões de root.
  • || { err...}|| significa literalmente "ou" — i.e., executa o bloco à direita caso a expressão à esquerda falhe. Nesse caso, exibe a mensagem de erro e encerra o processo com código de saída 1.

check_internet() → Verifica se o script possui conexão com a internet.

  • ping → envia um pacote ICMP para testar comunicação com o host informado.
  • -c1 → envia apenas 1 pacote.
  • -W2 → espera no máximo 2 segundos por uma resposta.
  • &>/dev/null → descarta tanto a saída normal (stdout) quanto a saída de erro (stderr).

Para contextualização sobre comparação de operadores numéricos em Bash, os principais são:

  • -eq → igual (equal)
  • -ne → diferente (not equal)
  • -gt → maior (greater)
  • -ge → maior ou igual (greater or equal)
  • -lt → menor (less than)
  • -le → menor ou igual (less or equal)

part_suffix e detect_disk

Essas duas funções estão relacionadas com as operações no disco.

bash
part_suffix() {
  local disk="$1" num="$2"
  if [[ "$disk" =~ [0-9]$ ]]; then
    echo "${disk}p${num}"
  else
    echo "${disk}${num}"
  fi
}

detect_disk() {
  [[ -n "$DISK" ]] && return

  mapfile -t disks < <(lsblk -dnpo NAME,TYPE | awk '$2=="disk"{print $1}')
  if [[ ${#disks[@]} -eq 0 ]]; then
    err "No disks found."
    exit 1
  elif [[ ${#disks[@]} -eq 1 ]]; then
    DISK="${disks[0]}"
  else
    echo "Multiple disks found:"
    lsblk -dpo NAME,SIZE,MODEL
    local i=1
    for d in "${disks[@]}"; do
      echo "  $i) $d"
      ((i++))
    done
    read -rp "Select disk number: " choice
    DISK="${disks[$((choice - 1))]}"
  fi
}

part_suffix() → Responsável por montar uma string com o nome e a partição do disco corretos, e.g.: /dev/sda1 ou /dev/nvme0n1p1.

  • local disk="$1" num="$2" → Em Bash, variáveis em funções são por padrão globais, o contrário da maioria das linguagens, então usamos local. Atribuímos os dois parâmetros recebidos pela função às variáveis locais disk e num.
  • if [[ "$disk" =~ [0-9]$ ]]; then → Usa um regex (=~ [0-9]$) para verificar se o nome do disco ($disk) termina com um dígito. Nesses casos, a nomenclatura das partições exige um p antes do número (e.g. /dev/nvme0n1p1). Caso contrário, basta concatenar o número (e.g. /dev/sda1)

detect_disk() → Detecta o disco que será utilizado durante a instalação e armazena seu caminho na variável global DISK.

Eu ainda não havia falado sobre as expressões condicionais [[ ]], ela basicamente avalia se a condição é verdadeira ou falsa.

  • [[ -n "$DISK" ]] && return → A flag -n significa non-empty (não vazia). Portanto, return será executado apenas se a expressão à esquerda for verdadeira, ou seja, se DISK já possuir algum valor. Nesse caso, significa que a variável já foi definida (em config.sh) e não é necessário detectar novamente o disco que será utilizado. Esse padrão é conhecido como guard clause, i.e., sair de uma função prematuramente quando uma condição específica é atendida.
  • mapfile -t disks < <(lsblk -dnpo NAME,TYPE | awk '$2=="disk"{print $1}') → Em resumo, essa linha combina uma sequência de ferramentas para ler várias linhas de texto e armazenar cada uma delas em um elemento de um array.
    • Exemplo: se a entrada for /dev/sda e /dev/nvme0n1, após executar mapfile, o array será disks[0]="/dev/sda" e disks[1]="/dev/nvme0n1".
    • -t → Flag que remove a quebra de linha (\n) do final de cada linha lida.
    • lsblk -dnpo NAME,TYPE → Lista dispositivos de bloco (HDs, SSDs, pendrives, NVMEs...). Cada letra representa uma flag: -d mostra apenas os discos (sem as partições); -n omite o cabeçalho; -p exibe o caminho completo do dispositivo; -o define as colunas que serão exibidas (neste caso, NAME e TYPE). A saída será algo como /dev/sda disk, /dev/nvme0n1 disk e /dev/sda1 part.
    • awk '$2=="disk"{print $1}' → Quando a segunda coluna for disk, imprime a primeira coluna. Na prática, filtra a saída do lsblk, mantendo apenas os discos e descartando partições, CD-ROMs e outros dispositivos.
    • mapfile → Lê as linhas produzidas pelo comando anterior e monta o array disks.
  • if, elif e else → Depois de preencher o array disks, o script verifica quantos discos foram encontrados.
    • Se o array disks estiver vazio, encerra o script porque nenhum disco foi encontrado.
    • Se o array contiver apenas um elemento, utiliza esse disco automaticamente, já que não há outra opção.
    • Caso contrário, significa que mais de um disco foi encontrado. O script exibe a lista de discos e solicita que o usuário escolha qual deles será utilizado.
  • read -rp "Select disk number: " choiceread lê uma entrada do usuário; -r faz a leitura literalmente, sem interpretar barras invertidas (\); -p exibe um prompt antes de esperar a entrada (neste caso, "Select disk number: "); choice é a variável onde a resposta será armazenada.
  • DISK="${disks[$((choice - 1))]}" → Atribui à variável DISK o disco selecionado pelo usuário. O - 1 existe porque a lista exibida ao usuário começa em 1, enquanto os índices de um array em Bash começam em 0.

swap_size_mib, detect_microcode e detect_virt

swap_size_mib() calcula o tamanho da partição swap que será criada posteriormente; detect_microcode() e detect_virt() detectam informações sobre a CPU e o ambiente de virtualização utilizando, respectivamente, /proc/cpuinfo e systemd-detect-virt.

bash
swap_size_mib() {
  local ram_mib
  ram_mib=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
  if (( ram_mib <= 2048 )); then
    echo $(( ram_mib * 2 ))
  elif (( ram_mib <= 8192 )); then
    echo "$ram_mib"
  else
    echo 8192
  fi
}

detect_microcode() {
  local vendor
  vendor=$(awk -F: '/vendor_id/{print $2; exit}' /proc/cpuinfo | tr -d ' ')
  case "$vendor" in
    GenuineIntel) echo "intel-ucode" ;;
    AuthenticAMD) echo "amd-ucode" ;;
    *) echo "" ;;
  esac
}

detect_virt() {
  # systemd-detect-virt prints "none" on bare metal too, but still exits
  # non-zero in that case (its exit code is a separate is-virtualized
  # boolean) -- naively `|| echo "none"` on that double-prints ("none\nnone").
  local out
  out=$(systemd-detect-virt 2>/dev/null) || true
  echo "${out:-none}"
}

swap_size_mib() → A swap é uma área do disco utilizada como uma extensão da memória RAM. Como o disco é muito mais lento que a RAM, o sistema só recorre a ela quando realmente necessário. Essa função calcula o tamanho da partição swap com base na quantidade de memória RAM disponível. A lógica é simples: quanto menor a RAM de uma máquina, maior a chance de ela ser totalmente consumida durante picos de uso, fazendo com que a swap passe a ser utilizada para manter o sistema funcionando. Já em máquinas com mais memória (por exemplo, acima de 16 GiB), uma swap de 8 GiB costuma ser suficiente, pois é menos provável que toda a RAM seja utilizada.

  • ram_mib=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo) → Novamente o programa awk é utilizado para ler uma linha específica e aplicar uma ação. Nesse caso, ele lê o arquivo /proc/meminfo, procura pela linha MemTotal, que contém a quantidade total de memória RAM da máquina, divide esse valor por 1024 (já que MemTotal é fornecido em kB) e retorna o resultado em MiB, atribuindo-o à variável local ram_mib.
  • if, elif e else → Verificam o valor de ram_mib e, com base nele, definem o tamanho da partição swap em MiB.

detect_microcode() → Essa função detecta o fabricante da CPU para decidir qual pacote de microcode será instalado durante o pacstrap. Atualmente, apenas CPUs Intel e AMD são tratadas. Caso nenhum fabricante conhecido seja identificado, nenhum pacote adicional é instalado.

  • vendor=$(awk -F: '/vendor_id/{print $2; exit}' /proc/cpuinfo | tr -d ' ') → Assim como em swap_size_mib(), o awk é utilizado para ler um arquivo do sistema. Nesse caso, ele procura a linha que contém vendor_id em /proc/cpuinfo, extrai o fabricante da CPU e atribui o resultado à variável local vendor.
    • -F: → Define : como separador de colunas (Field Separator).
    • exit → Encerra o awk após encontrar a primeira ocorrência de vendor_id, já que basta ler uma única vez.
    • | tr -d ' ' → O programa tr remove todos os espaços da string retornada pelo awk.
  • case "$vendor" in ... → Verifica o valor de vendor. Se for GenuineIntel, retorna o pacote de microcode da Intel; se for AuthenticAMD, retorna o da AMD; caso contrário, retorna uma string vazia.

detect_virt() → Detecta se o sistema está sendo executado em uma máquina virtual. Essa informação é utilizada posteriormente para instalar os pacotes e habilitar os serviços apropriados para o ambiente detectado.

  • out=$(systemd-detect-virt 2>/dev/null) || true → Executa o programa systemd-detect-virt, que tenta identificar se o sistema está rodando em um ambiente virtualizado, e armazena a saída na variável out.
    • 2>/dev/null → Redireciona o stderr para /dev/null, descartando mensagens de erro.
    • || true → Se systemd-detect-virt retornar erro (código diferente de 0), executa true, fazendo com que o script continue normalmente em vez de interromper a execução. > Observação: durante os testes, systemd-detect-virt identificou meu notebook (hardware físico) como um ambiente virtualizado. Como essa função confia totalmente na saída desse programa, esse falso positivo faz com que o restante do script instale os pacotes e habilite os serviços correspondentes ao ambiente detectado. Na prática, isso não causa problemas relevantes, pois esses pacotes são pequenos, não interferem no funcionamento do sistema e apenas adicionam suporte para ambientes virtualizados.

Funções prompt, virt_packages_for, de_packages_for, update_keyring e confirm_plan

Repare que, até aqui, o script permaneceu em uma fase de reconhecimento: identificando o sistema, o ambiente em que está sendo executado, quem o executou, quais recursos estão disponíveis e tratando possíveis edge cases antes de realizar qualquer alteração. Somente depois dessa etapa é que a instalação propriamente dita começa.

Começaremos pelas funções prompt, que são as que solicitam input do usuário. Essas funções existem para coletar ou validar as últimas configurações necessárias antes da etapa destrutiva da instalação, permitindo tanto um modo interativo quanto um modo totalmente automatizado.

Funções prompt:

bash
prompt_boot_mode() {
  if [[ "$AUTO_CONFIRM" == true ]]; then
    BOOT_MODE="uefi"
    return
  fi

  # echo omitted
  ...
  read -rp "Choice [0/1] (default: 1): " choice
  choice="${choice:-1}"

  case "$choice" in
    0) BOOT_MODE="bios" ;;
    1) BOOT_MODE="uefi" ;;
    *) err "Invalid choice '$choice'."; exit 1 ;;
  esac

  if [[ "$BOOT_MODE" == "uefi" && ! -d /sys/firmware/efi/efivars ]]; then
    log "Warning: this session doesn't look like it's booted in UEFI mode (/sys/firmware/efi/efivars missing) -- grub-install may fail later."
  fi
}

prompt_install_type() {
  [[ "$AUTO_CONFIRM" == true ]] && return

  local default=1
  [[ "$DESKTOP_ENV" == "none" ]] && default=0

  # echo omitted
  ...
  read -rp "Choice [0/1] (default: $default): " choice
  choice="${choice:-$default}"

  case "$choice" in
    0) DESKTOP_ENV="none" ;;
    1) DESKTOP_ENV="cinnamon" ;;
    *) err "Invalid choice '$choice'."; exit 1 ;;
  esac
}

prompt_update_keyring() {
  UPDATE_KEYRING=false
  [[ "$AUTO_CONFIRM" == true ]] && return

  read -rp "Update keyrings before installing packages? Only needed if pacstrap fails on an old ISO. [y/N]: " ans
  [[ "$ans" =~ ^[Yy] ]] && UPDATE_KEYRING=true
  return 0
}

prompt_passwords() {
  local pw1 pw2

  read -rsp "Root password: " pw1; echo
  read -rsp "Confirm root password: " pw2; echo
  [[ "$pw1" == "$pw2" ]] || { err "Passwords do not match."; exit 1; }
  ROOT_PASSWORD="$pw1"

  read -rsp "Password for user $USERNAME: " pw1; echo
  read -rsp "Confirm password for $USERNAME: " pw2; echo
  [[ "$pw1" == "$pw2" ]] || { err "Passwords do not match."; exit 1; }
  USER_PASSWORD="$pw1"
}

Dois padrões presentes no início das funções prompt a seguir são condições semelhantes a if [[ "$AUTO_CONFIRM" == true ]]; then e chamadas para read -rp. A primeira verifica se a variável AUTO_CONFIRM está habilitada; caso esteja, todas as perguntas interativas são ignoradas e a instalação utiliza os valores pré-configurados em config.sh. Já read -rp é utilizado para solicitar um input do usuário, armazenando a resposta em uma variável que será utilizada posteriormente pelo script. Outro padrão presente nessas funções é o uso de case, utilizado para tratar diferentes possibilidades de entrada do usuário. A variável choice recebe esse input e, dependendo do valor informado, o script executa uma ação específica, como definir o modo de boot (BIOS ou UEFI) ou escolher o tipo de instalação (minimal ou desktop). Caso a entrada não corresponda a nenhuma opção esperada, o script trata como uma opção inválida.

virt_packages_for() e de_packages_for():

bash
virt_packages_for() {
  case "$1" in
    oracle)    echo "virtualbox-guest-utils" ;;
    kvm|qemu)  echo "qemu-guest-agent" ;;
    *)         echo "" ;;
  esac
}

de_packages_for() {
  case "$DESKTOP_ENV" in
    cinnamon)
      echo "xorg-server xorg-xinit lightdm lightdm-gtk-greeter cinnamon gnome-terminal gvfs gvfs-smb network-manager-applet xdg-user-dirs"
      ;;
    none) echo "" ;;
    *) err "Unknown DESKTOP_ENV: $DESKTOP_ENV"; exit 1 ;;
  esac
}

Essas duas funções possuem uma lógica parecida: recebem uma configuração e retornam quais pacotes adicionais devem ser instalados durante o pacstrap.

virt_packages_for() → Responsável por retornar os pacotes necessários dependendo do ambiente virtualizado detectado.

  • case "$1" in → O $1 representa o primeiro argumento passado para a função. Nesse caso, ele recebe o valor de DETECTED_VIRT, que contém o tipo de virtualização detectada.
  • oracle) → Caso seja detectado VirtualBox, retorna o pacote virtualbox-guest-utils.
  • kvm|qemu) → Caso seja detectado KVM ou QEMU, retorna o pacote qemu-guest-agent.
  • *) → Caso nenhum dos casos anteriores seja encontrado, retorna uma string vazia, significando que nenhum pacote extra precisa ser instalado.

de_packages_for() → Possui a mesma ideia, mas relacionada ao ambiente gráfico escolhido pelo usuário.

  • case "$DESKTOP_ENV" in → Verifica o valor da variável DESKTOP_ENV, que define se a instalação terá um ambiente desktop ou será minimalista.
  • cinnamon) → Retorna a lista de pacotes necessários para instalar o ambiente Cinnamon, incluindo servidor gráfico, gerenciador de login e alguns utilitários.
  • none) → Retorna uma string vazia, pois uma instalação sem ambiente gráfico não precisa desses pacotes.
  • *) → Caso o valor de DESKTOP_ENV não seja reconhecido, exibe um erro e encerra o script.

Em resumo, essas funções apenas fazem uma ponte entre uma configuração detectada/escolhida e os pacotes que precisam ser instalados posteriormente pelo pacstrap.

update_keyring() e confirm_plan():

bash
update_keyring() {
  log "Updating archlinux-keyring"
  pacman -Sy --noconfirm archlinux-keyring
}

confirm_plan() {
  echo
  echo "About to install Arch Linux with the following plan:"
  echo "  Disk:       $DISK (ALL DATA WILL BE ERASED)"
  echo "  Boot mode:  $BOOT_MODE"
  if [[ "$BOOT_MODE" == "uefi" ]]; then
    echo "  EFI:        ${EFI_SIZE_MIB}MiB"
  fi
  echo "  Swap mode:  $SWAP_MODE"
  echo "  Root fs:    $ROOT_FS"
  echo "  Hostname:   $HOSTNAME"
  echo "  User:       $USERNAME ($USER_GROUPS)"
  echo "  Locale:     $LOCALE_LANG (regional: $LOCALE_REGIONAL)"
  echo "  Timezone:   $TIMEZONE"
  echo "  Keymap:     $KEYMAP_CONSOLE (X11: $KEYMAP_X11_LAYOUT/$KEYMAP_X11_MODEL)"
  echo "  Desktop:    $DESKTOP_ENV"
  echo "  Keyring:    $([[ "$UPDATE_KEYRING" == true ]] && echo "update before install" || echo "skip")"
  echo "  Virt:       $DETECTED_VIRT"
  echo

  [[ "$AUTO_CONFIRM" == true ]] && return

  read -rp "Type 'yes' to continue: " ans
  [[ "$ans" == "yes" ]] || { echo "Aborted."; exit 1; }
}

update_keyring() → Atualiza o keyring do Arch Linux. Observação: a função update_keyring() só é chamada posteriormente no script.

  • pacman -Sy --noconfirm archlinux-keyring → Utiliza o gerenciador de pacotes do Arch Linux (pacman, de package manager, não o jogo, haha) para atualizar o pacote archlinux-keyring.
    • Sy-S (Sync) instala pacotes a partir dos repositórios; -y (Refresh) atualiza a lista de pacotes disponíveis.
    • --noconfirm → Não solicita confirmação do usuário, permitindo que o script execute a atualização de forma automática.

confirm_plan() → A maior parte da função consiste em chamadas para echo, utilizadas para exibir ao usuário um resumo da instalação antes que qualquer alteração seja feita no sistema. São mostradas informações como o disco que será formatado, modo de boot, sistema de arquivos, swap, hostname, usuário, layout do teclado, ambiente gráfico e outras configurações relevantes.

  • read -rp "Type 'yes' to continue: " ans → Lê o input do usuário e armazena na variável ans (answer).
  • [[ "$ans" == "yes" ]] || { echo "Aborted."; exit 1; } → Verifica se o valor de ans é "yes", a única resposta aceita para prosseguir com a instalação. Caso contrário, encerra o script.

deactivate_disk, partition_disk, format_partitions e mount_partitions

A partir daqui as funções realizam alterações no disco que será utilizado para realizar a instalação.

bash
deactivate_disk() {
  umount -R /mnt 2>/dev/null || true
  local dev
  while read -r dev; do
    [[ "$dev" == "$DISK"* ]] && swapoff "$dev" 2>/dev/null || true
  done < <(swapon --show=NAME --noheadings 2>/dev/null)
  return 0
}

partition_disk() {
  log "Partitioning $DISK"

  deactivate_disk

  wipefs -a "$DISK"

  local swap_mib
  if [[ "$SWAP_MODE" == "auto" ]]; then
    swap_mib=$(swap_size_mib)
  else
    swap_mib="$SWAP_SIZE_MIB"
  fi

  if [[ "$BOOT_MODE" == "uefi" ]]; then
    local efi_end=$(( 1 + EFI_SIZE_MIB ))
    local swap_end=$(( efi_end + swap_mib ))

    parted --script "$DISK" \
      mklabel gpt \
      mkpart ESP fat32 1MiB "${efi_end}MiB" \
      set 1 esp on \
      mkpart primary linux-swap "${efi_end}MiB" "${swap_end}MiB" \
      mkpart primary ext4 "${swap_end}MiB" 100%

    partprobe "$DISK"
    udevadm settle

    EFI_PART=$(part_suffix "$DISK" 1)
    SWAP_PART=$(part_suffix "$DISK" 2)
    ROOT_PART=$(part_suffix "$DISK" 3)
  else
    local swap_end=$(( 1 + swap_mib ))

    parted --script "$DISK" \
      mklabel msdos \
      mkpart primary linux-swap 1MiB "${swap_end}MiB" \
      mkpart primary ext4 "${swap_end}MiB" 100% \
      set 2 boot on

    partprobe "$DISK"
    udevadm settle

    SWAP_PART=$(part_suffix "$DISK" 1)
    ROOT_PART=$(part_suffix "$DISK" 2)
  fi
}

format_partitions() {
  log "Formatting partitions"
  [[ "$BOOT_MODE" == "uefi" ]] && mkfs.fat -F32 "$EFI_PART"
  mkswap "$SWAP_PART"
  swapon "$SWAP_PART"
  "mkfs.${ROOT_FS}" -F "$ROOT_PART"
}

mount_partitions() {
  log "Mounting partitions"
  mount "$ROOT_PART" /mnt
  if [[ "$BOOT_MODE" == "uefi" ]]; then
    mount --mkdir "$EFI_PART" /mnt/boot
  fi
}

deactivate_disk() → Essa função desmonta recursivamente tudo dentro de /mnt e desativa qualquer partição swap ativa que pertença ao disco que será utilizado na instalação. Ela existe como uma etapa de limpeza antes de particionar o disco novamente.

É interessante notar que essa função não fazia parte da primeira versão do script. Ela surgiu durante os testes, quando algumas tentativas de instalação falhavam porque o disco continuava sendo considerado "ocupado". Isso acontecia porque execuções anteriores deixavam estados pendentes, como partições montadas ou swap ainda ativa, impedindo que as operações seguintes fossem realizadas corretamente.

  • umount -R /mnt → Desmonta /mnt recursivamente.
    • 2>/dev/null e || true → Joga a saída para o "vazio" e continua o script mesmo o comando se falhar.
  • while read -r dev; do ... done < <(swapon --show=NAME --noheadings 2>/dev/null) → Percorre todos os dispositivos que estão sendo utilizados como swap no sistema.
    • swapon --show=NAME --noheadings → Lista as partições swap ativas, mostrando apenas o nome do dispositivo e removendo o cabeçalho, e.g.: /dev/nvme0n1p2.
    • < <(...) → É chamado de process substitution. Ele pega a saída do comando dentro dos parênteses e transforma em um input para o while. Na prática, cada linha retornada pelo swapon será lida pelo loop.
    • while read -r dev → A cada iteração, uma linha de output é armazenada na variável dev.
  • [[ "$dev" == "$DISK"* ]] && swapoff "$dev" 2>/dev/null || true → Verifica se a partição swap pertence ao disco que será formatado. O * significa "qualquer coisa depois", então se DISK="/dev/nvme0n1", uma partição como /dev/nvme0n1p2 será considerada pertencente a esse disco.
    • Se a condição for verdadeira, executa swapoff "$dev", desativando aquela partição swap antes da formatação.
    • 2>/dev/null descarta mensagens de erro caso a desativação falhe.
    • || true garante que o script continue mesmo se essa operação falhar, algo importante por causa do set -e ativado no início do script.

Em resumo, essa parte existe para garantir que o disco não esteja em uso antes de ser apagado e particionado novamente. Caso uma instalação anterior tenha deixado uma swap ativa, ela é desativada automaticamente.

partition_disk() → Limpa o disco completamente e cria a estrutura de partições que será utilizada pela instalação.

Aqui existe um ponto interessante: eu não enxergava claramente a diferença entre particionar um disco e formatar suas partições, mas são etapas separadas. Particionar pode ser entendido como "dividir o disco em partes organizadas", enquanto formatar uma partição é a etapa que cria um sistema de arquivos dentro dela, permitindo que ela armazene dados.

  • deactivate_disk() → Chama a função discutida anteriormente para garantir que o disco não esteja em uso, evitando estados pendentes de instalações anteriores que poderiam impedir as operações de particionamento.
  • wipefs -a "$DISK" → Remove assinaturas antigas de sistemas de arquivos, como ext4, NTFS e swap. Isso é necessário para evitar que o Linux encontre "restos" de instalações antigas e interprete o disco de forma incorreta.
  • if [[ "$SWAP_MODE" == "auto" ]]; then → Verifica se a variável SWAP_MODE (definida em config.sh) está configurada como auto. Nesse caso, chama a função swap_size_mib() para calcular automaticamente o tamanho da swap; caso contrário, utiliza o valor definido em SWAP_SIZE_MIB.
  • if [[ "$BOOT_MODE" == "uefi" ]]; then → Verifica se o modo de boot escolhido é UEFI. Caso seja, cria as partições seguindo o esquema necessário para esse tipo de boot.
  • parted --script "$DISK"parted é o programa utilizado para manipular a tabela de partições do disco. O parâmetro --script faz com que ele execute os comandos automaticamente, sem entrar no modo interativo ou pedir confirmações. Os comandos seguintes são instruções passadas para o parted.
    • mklabel gpt → Cria uma nova tabela de partições no formato GPT (GUID Partition Table). Esse é o padrão utilizado em instalações UEFI modernas.
    • mkpart ESP fat32 1MiB "${efi_end}MiB" → Cria uma partição chamada ESP (EFI System Partition), reservada para armazenar os arquivos de inicialização do UEFI. Ela utiliza o formato FAT32 e tem seu início e fim definidos pelos valores informados.
    • set 1 esp on → Marca a primeira partição como uma ESP. Essa flag informa ao firmware UEFI que aquela partição contém arquivos de boot.
    • mkpart primary linux-swap "${efi_end}MiB" "${swap_end}MiB" → Cria a partição destinada à swap. Nesse momento, ela apenas define a área do disco; a criação da swap propriamente acontece depois com mkswap.
    • mkpart primary ext4 "${swap_end}MiB" 100% → Cria a partição raiz do sistema, utilizando todo o espaço restante do disco. O ext4 indica o tipo esperado da partição, mas o sistema de arquivos é criado posteriormente com mkfs.ext4.
  • partprobe "$DISK" → O programa partprobe informa ao kernel que a tabela de partições do disco foi alterada. Pense nele como um "refresh" para que o sistema reconheça a nova estrutura sem precisar reiniciar.
  • udevadm settle → Aguarda o udev finalizar a criação dos dispositivos correspondentes às novas partições. Isso evita que o script tente acessar uma partição antes que ela exista em /dev (race condition).
  • else → Executa uma lógica semelhante, porém criando as partições seguindo o esquema tradicional de BIOS/MBR, que utiliza uma tabela de partições msdos em vez de GPT e não necessita de uma partição EFI separada.

format_partitions() → Como a função anterior já criou as partições, agora o próximo passo é formatá-las, ou seja, criar os sistemas de arquivos que serão utilizados pelo sistema operacional.

  • [[ "$BOOT_MODE" == "uefi" ]] && mkfs.fat -F32 "$EFI_PART" → Se BOOT_MODE for UEFI, formata a partição EFI_PART utilizando o sistema de arquivos FAT32.
  • mkswap "$SWAP_PART" e swapon "$SWAP_PART" → O primeiro comando configura a partição SWAP_PART como uma área de swap; o segundo ativa essa área para que ela possa ser utilizada pelo sistema.
  • "mkfs.${ROOT_FS}" -F "$ROOT_PART" → Formata a partição ROOT_PART utilizando o sistema de arquivos definido em config.sh. Atualmente, esse valor é ext4, mas o comando é montado dinamicamente a partir da variável ROOT_FS, permitindo trocar o sistema de arquivos sem alterar o script.

mount_partitions()mount é o comando responsável por tornar um sistema de arquivos acessível ao Linux através de um diretório. Durante a instalação do Arch, por convenção utiliza-se /mnt como ponto de montagem da nova instalação. Assim, tudo que for instalado em /mnt será, na realidade, gravado na partição root recém-criada.

  • mount "$ROOT_PART" /mnt → Monta a partição raiz em /mnt.
  • mount --mkdir "$EFI_PART" /mnt/boot → Executado apenas em instalações UEFI. Cria o diretório /mnt/boot caso ele ainda não exista e monta nele a partição ESP, permitindo que os arquivos de boot sejam gravados no local correto.

pacstrap_system e genfstab_system

Agora que tudo foi preparado, é o momento de instalar o Arch Linux!

bash
pacstrap_system() {
  log "Installing base system (pacstrap)"

  local microcode virt_pkgs de_pkgs
  microcode=$(detect_microcode)
  virt_pkgs=$(virt_packages_for "$DETECTED_VIRT")
  de_pkgs=$(de_packages_for)

  local pkgs=(base linux linux-firmware networkmanager grub sudo vim nano git base-devel)
  [[ "$BOOT_MODE" == "uefi" ]] && pkgs+=(efibootmgr)
  [[ -n "$microcode" ]] && pkgs+=("$microcode")
  [[ -n "$virt_pkgs" ]] && pkgs+=($virt_pkgs)
  [[ -n "$de_pkgs" ]] && pkgs+=($de_pkgs)

  pacstrap -K /mnt "${pkgs[@]}"
}

genfstab_system() {
  log "Generating fstab"
  genfstab -U /mnt >> /mnt/etc/fstab
}

O pacstrap responde à seguinte pergunta:

Como colocar um Arch Linux inteiro dentro dessa partição?

Ele baixa os pacotes dos repositórios do Arch Linux e os instala no diretório informado (por convenção, durante a instalação, utiliza-se /mnt). O motivo de não utilizar diretamente o pacman é que ele instala pacotes no sistema atualmente em execução, que neste caso é a Live ISO. Já o pacstrap instala os pacotes em outro diretório. Vale destacar que o pacstrap é um wrapper do pacman: ele utiliza o pacman internamente para baixar e instalar os pacotes, mas adiciona uma lógica extra para permitir a instalação de um sistema completo em um diretório diferente do sistema em execução.

pacstrap_system() → Essa função monta um array com todos os pacotes necessários para a instalação e, ao final, utiliza o pacstrap para instalar esse conjunto em /mnt, que nesse momento representa a partição raiz (root) do sistema que está sendo construindo.

  • microcode=$(detect_microcode), virt_pkgs=$(virt_packages_for "$DETECTED_VIRT") e de_pkgs=$(de_packages_for) → Chamam as funções criadas anteriormente para descobrir quais pacotes adicionais devem ser instalados e armazenam seus retornos nas variáveis microcode, virt_pkgs e de_pkgs.
  • local pkgs=(base linux linux-firmware networkmanager grub sudo vim nano git base-devel) → Inicializa o array pkgs com os pacotes que sempre serão instalados, independentemente do tipo de instalação. Eles representam a base de um sistema Arch Linux funcional.
  • [[ "$BOOT_MODE" == "uefi" ]] && pkgs+=(efibootmgr) → Se o modo de boot for UEFI, adiciona o pacote efibootmgr ao array pkgs.
  • [[ -n "$..." ]] && pkgs+=("$...") → A mesma lógica é utilizada para adicionar ao array os pacotes de microcode, virtualização e ambiente gráfico, caso existam.
  • pacstrap -K /mnt "${pkgs[@]}" → Finalmente, instala o sistema Arch Linux e todos os pacotes reunidos no array pkgs dentro de /mnt, ou seja, na partição raiz recém-criada.

genfstab_system() → Essa função gera automaticamente o arquivo fstab (File System Table), responsável por informar ao Linux quais partições devem ser montadas durante a inicialização do sistema e em quais diretórios elas devem aparecer.

  • genfstab -U /mnt >> /mnt/etc/fstab → O genfstab analisa tudo o que está montado dentro de /mnt e gera as entradas correspondentes para o arquivo fstab. A flag -U faz com que as partições sejam identificadas pelo UUID (Universally Unique Identifier), um identificador único de cada sistema de arquivos, em vez de caminhos como /dev/sda1, que podem mudar. Por fim, >> redireciona essa saída para o arquivo /mnt/etc/fstab, que será utilizado pelo sistema após a instalação.

prepare_chroot, run_chroot e cleanup_chroot_files

Agora, o Arch está instalado na máquina, o objetivo é configurar o sistema antes de reiniciá-lo. Nesta etapa, entra o arch-chroot, que é um mecanismo do Linux que muda temporariamente qual o diretório que será considerado a raiz / de um processo. Isso será útil para fazermos todas as configurações que precisamos.

bash
prepare_chroot() {
  log "Preparing chroot setup"
  cp "$SCRIPT_DIR/config.sh" /mnt/root/config.sh
  cp "$SCRIPT_DIR/chroot-setup.sh" /mnt/root/chroot-setup.sh
  chmod +x /mnt/root/chroot-setup.sh

  install -m 600 /dev/null /mnt/root/.archinstall_secrets
  cat > /mnt/root/.archinstall_secrets <<EOF
ROOT_PASSWORD='${ROOT_PASSWORD}'
USER_PASSWORD='${USER_PASSWORD}'
DETECTED_VIRT='${DETECTED_VIRT}'
BOOT_MODE='${BOOT_MODE}'
DISK='${DISK}'
DESKTOP_ENV='${DESKTOP_ENV}'
EOF
}

run_chroot() {
  log "Entering chroot for system configuration"
  arch-chroot /mnt /root/chroot-setup.sh
}

cleanup_chroot_files() {
  rm -f /mnt/root/.archinstall_secrets /mnt/root/config.sh /mnt/root/chroot-setup.sh
}

prepare_chroot() → Prepara o ambiente chroot, copiando os arquivos necessários para a continuidade da configuração do sistema.

  • cp "$SCRIPT_DIR/config.sh" /mnt/root/config.sh e cp "$SCRIPT_DIR/chroot-setup.sh" /mnt/root/chroot-setup.sh → Copia os arquivos config.sh e chroot-setup.sh para /mnt/root/, permitindo que eles sejam executados após entrar no ambiente chroot.
  • install -m 600 /dev/null /mnt/root/.archinstall_secrets → Cria o arquivo .archinstall_secrets com permissões restritas (600), permitindo leitura e escrita apenas pelo usuário root.
  • cat > /mnt/root/.archinstall_secrets <<EOF ... EOF → Escreve nesse arquivo informações necessárias para a próxima etapa da instalação, como senhas, modo de boot, tipo de instalação e demais variáveis que serão utilizadas pelo chroot-setup.sh.

run_chroot() → Entra no ambiente chroot e executa o script responsável pela configuração do sistema recém-instalado.

  • arch-chroot /mnt /root/chroot-setup.sh → Entra no ambiente chroot, fazendo com que /mnt passe a ser tratado como a raiz (/) do sistema de arquivos, e executa o script chroot-setup.sh.

cleanup_chroot_files() → Remove do sistema os arquivos utilizados apenas durante a instalação, incluindo o .archinstall_secrets, que contém informações sensíveis, como as senhas.

  • rm -f /mnt/root/.archinstall_secrets /mnt/root/config.sh /mnt/root/chroot-setup.sh → Remove os arquivos temporários utilizados durante a instalação. A flag -f (force) evita erros caso algum deles já não exista.

main

A função main funciona como um orquestrador. Ela praticamente não contém lógica própria; sua responsabilidade é apenas chamar as funções na ordem correta, organizando todo o fluxo da instalação.

Esse padrão torna o script mais legível e facilita a manutenção, já que cada função possui uma única responsabilidade, enquanto a main apenas descreve a sequência das etapas.

bash
main() {
  check_root
  check_internet

  prompt_install_type
  prompt_boot_mode

  detect_disk
  DETECTED_VIRT=$(detect_virt)

  prompt_passwords
  prompt_update_keyring
  confirm_plan

  partition_disk
  format_partitions
  mount_partitions
  [[ "$UPDATE_KEYRING" == true ]] && update_keyring
  pacstrap_system
  genfstab_system

  prepare_chroot
  trap cleanup_chroot_files EXIT
  run_chroot

  log "Installation finished. Unmounting."
  umount -R /mnt
  swapoff "$SWAP_PART" || true

  echo
  echo "All done. Run 'reboot' when ready."
}

main "$@"

É possível dividir seu fluxo em quatro grandes fases:

  1. Validação e coleta de informações → Verifica permissões, conexão com a internet, coleta informações sobre o sistema e solicita os dados necessários ao usuário.
  2. Preparação do disco → Particiona, formata e monta as partições que receberão o novo sistema.
  3. Instalação do sistema base → Instala o Arch Linux com pacstrap, gera o fstab e prepara o ambiente para entrar no chroot.
  4. Configuração final → Executa o script chroot-setup.sh, desmonta as partições, desativa a swap e encerra a instalação.

Alguns detalhes merecem destaque:

  • [[ "$UPDATE_KEYRING" == true ]] && update_keyring → Atualiza o archlinux-keyring apenas se o usuário solicitou essa etapa.

  • trap cleanup_chroot_files EXIT → Registra a função cleanup_chroot_files para ser executada automaticamente quando o script terminar, independentemente do motivo (fim da execução ou erro). Isso garante que arquivos temporários, como .archinstall_secrets, não permaneçam no sistema após a instalação.

  • main "$@" → Inicia a execução do script chamando a função main. O "$@" representa todos os argumentos passados pela linha de comando e os encaminha para a função, embora neste script eles não sejam utilizados.

Finalmente acabou?


Com certeza este foi um post enorme, cheio de detalhes e cansativo tanto de escrever quanto de ler. Ainda assim, espero que ele tenha mostrado que a instalação de um sistema operacional envolve muito mais do que parece; também que quando quebramos o processo em pequenas etapas, cada uma passa a fazer bastante sentido.

Entretanto, ainda não acabou. No próximo post, veremos o que acontece durante a configuração do sistema dentro do chroot, antes da instalação ser concluída. Embora faça parte do mesmo processo, essa etapa merece um artigo próprio, pois possui responsabilidades bem diferentes do archinstall.sh. Felizmente, ele será consideravelmente menor e menos denso do que este.