├── .gitignore ├── LICENSE ├── README.md └── ubuntu-pi-image /.gitignore: -------------------------------------------------------------------------------- 1 | *.img 2 | *.img.xz 3 | *.img.xz.sha256 4 | *.img.xz.sha256.sign 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wimpy's World 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ubuntu-pi-image 2 | 3 | **If you just want to download Ubuntu MATE images for the Raspberry Pi the go to the Ubuntu MATE website:** 4 | 5 | * **https://ubuntu-mate.org/raspberry-pi/** 6 | 7 | These "docs" are not comprehensive, but should be enough to get you started with 8 | building your own Ubuntu based images for the Raspberry Pi. 9 | 10 | 11 | ## Building Images 12 | 13 | * Clone the Retro Home project 14 | * `git clone https://github.com/wimpysworld/ubuntu-pi-image.git` 15 | 16 | It is best to run the `ubuntu-pi-image` on an Ubuntu 22.04 x86 64-bit 17 | workstation, ideally running in a VM via [Quickemu](https://github.com/quickemu-project/quickemu). 18 | If using a fresh [Quickemu](https://github.com/quickemu-project/quickemu) VM 19 | you'll need to set the `disk_size` parameter large enough to complete the build. 20 | This can be achieved by adding `disk_size="64G"` to `ubuntu-mate-jammy.conf` 21 | before running `quickemu` to create the VM. Alternatively you could mount 22 | external storage into the container for the build area. You'll also need at 23 | least to `sudo apt install git`. 24 | 25 | ## apt-cacher-ng 26 | 27 | If you `apt-get install apt-cache-ng` the `ubuntu-pi-image` script will 28 | automatically use the apt-cache-ng proxy to accelerate build performance. 29 | 30 | ## Build configuration 31 | 32 | You can tweak some variables towards the bottom of the `ubuntu-pi-image` script. 33 | 34 | ```bash 35 | FLAVOUR="ubuntu-mate" 36 | IMG_QUALITY="-beta1" 37 | IMG_VER="22.04" 38 | IMG_RELEASE="jammy" 39 | IMG_ARCH="arm64" 40 | ``` 41 | 42 | ### Usage 43 | 44 | The following incantation will build a Ubuntu MATE 22.04 arm64 image for 45 | Raspberry Pi. 46 | 47 | ```bash 48 | sudo ./ubuntu-pi-image 49 | ``` 50 | 51 | The script will create `~/Build` in your home directory that will include the 52 | caches for each stages and the image for putting on your Raspberry Pi SD card, 53 | USB stick or SSD. 54 | 55 | ## Other Desktop 56 | 57 | Modify the `stage_02_desktop` and `stage_03_snap` accordingly to adapt the script 58 | for other Ubuntu flavours/desktops. 59 | 60 | ## Bootloaders 61 | 62 | This is the bootloader configuration from Ubuntu 22.04. 63 | 64 | ### cloud-init 65 | 66 | Remove these files: 67 | 68 | * `meta-data` 69 | * `network-config` 70 | * `user-data` 71 | ### README 72 | 73 | ``` 74 | An overview of the files on the /boot/firmware partition (the 1st partition 75 | on the SD card) used by the Ubuntu boot process (roughly in order) is as 76 | follows: 77 | 78 | * bootcode.bin - this is the second stage bootloader loaded by all pis with 79 | the exception of the pi4 (where this is replaced by flash 80 | memory) 81 | * config.txt - the configuration file read by the boot process 82 | * start*.elf - the third stage bootloader, which handles device-tree 83 | modification and which loads... 84 | * vmlinuz - the Linux kernel 85 | * cmdline.txt - the Linux kernel command line 86 | * initrd.img - the initramfs 87 | * meta-data - meta-data for cloud-init; usually just contains the 88 | instance id 89 | * network-config - network configuration for cloud-init; edit this to set up 90 | wifi access points and other networking settings 91 | * user-data - user-data for cloud-init; edit this to configure initial 92 | users, SSH keys, packages, etc. 93 | ``` 94 | ### 32bit server 95 | 96 | From the armhf server image. 97 | #### cmdline.txt 98 | ``` 99 | console=serial0,115200 dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc quiet splash 100 | ``` 101 | 102 | #### config.txt 103 | ``` 104 | [all] 105 | kernel=vmlinuz 106 | cmdline=cmdline.txt 107 | initramfs initrd.img followkernel 108 | 109 | [pi4] 110 | max_framebuffers=2 111 | arm_boost=1 112 | 113 | [all] 114 | # Enable the audio output, I2C and SPI interfaces on the GPIO header. As these 115 | # parameters related to the base device-tree they must appear *before* any 116 | # other dtoverlay= specification 117 | dtparam=audio=on 118 | dtparam=i2c_arm=on 119 | dtparam=spi=on 120 | 121 | # Comment out the following line if the edges of the desktop appear outside 122 | # the edges of your display 123 | disable_overscan=1 124 | 125 | # If you have issues with audio, you may try uncommenting the following line 126 | # which forces the HDMI output into HDMI mode instead of DVI (which doesn't 127 | # support audio output) 128 | #hdmi_drive=2 129 | 130 | # Enable the serial pins 131 | enable_uart=1 132 | 133 | # Autoload overlays for any recognized cameras or displays that are attached 134 | # to the CSI/DSI ports. Please note this is for libcamera support, *not* for 135 | # the legacy camera stack 136 | camera_auto_detect=1 137 | display_auto_detect=1 138 | 139 | 140 | [cm4] 141 | # Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into 142 | # such a board) 143 | dtoverlay=dwc2,dr_mode=host 144 | 145 | [all] 146 | ``` 147 | 148 | ### 64bit server 149 | 150 | From the arm64 server image. 151 | #### cmdline.txt 152 | ``` 153 | console=serial0,115200 dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc quiet splash 154 | ``` 155 | 156 | #### config.txt 157 | ``` 158 | [all] 159 | kernel=vmlinuz 160 | cmdline=cmdline.txt 161 | initramfs initrd.img followkernel 162 | 163 | [pi4] 164 | max_framebuffers=2 165 | arm_boost=1 166 | 167 | [all] 168 | # Enable the audio output, I2C and SPI interfaces on the GPIO header. As these 169 | # parameters related to the base device-tree they must appear *before* any 170 | # other dtoverlay= specification 171 | dtparam=audio=on 172 | dtparam=i2c_arm=on 173 | dtparam=spi=on 174 | 175 | # Comment out the following line if the edges of the desktop appear outside 176 | # the edges of your display 177 | disable_overscan=1 178 | 179 | # If you have issues with audio, you may try uncommenting the following line 180 | # which forces the HDMI output into HDMI mode instead of DVI (which doesn't 181 | # support audio output) 182 | #hdmi_drive=2 183 | 184 | # Enable the serial pins 185 | enable_uart=1 186 | 187 | # Autoload overlays for any recognized cameras or displays that are attached 188 | # to the CSI/DSI ports. Please note this is for libcamera support, *not* for 189 | # the legacy camera stack 190 | camera_auto_detect=1 191 | display_auto_detect=1 192 | 193 | # Config settings specific to arm64 194 | arm_64bit=1 195 | dtoverlay=dwc2 196 | 197 | [cm4] 198 | # Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into 199 | # such a board) 200 | dtoverlay=dwc2,dr_mode=host 201 | 202 | [all] 203 | ``` 204 | 205 | ### Desktop 206 | 207 | From Ubuntu proper desktop image. 208 | 209 | #### cmdline.txt 210 | ``` 211 | zswap.enabled=1 zswap.zpool=z3fold zswap.compressor=zstd dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc quiet splash 212 | ``` 213 | ### config.txt 214 | ``` 215 | [all] 216 | kernel=vmlinuz 217 | cmdline=cmdline.txt 218 | initramfs initrd.img followkernel 219 | 220 | [pi4] 221 | max_framebuffers=2 222 | arm_boost=1 223 | 224 | [all] 225 | # Enable the audio output, I2C and SPI interfaces on the GPIO header. As these 226 | # parameters related to the base device-tree they must appear *before* any 227 | # other dtoverlay= specification 228 | dtparam=audio=on 229 | dtparam=i2c_arm=on 230 | dtparam=spi=on 231 | 232 | # Comment out the following line if the edges of the desktop appear outside 233 | # the edges of your display 234 | disable_overscan=1 235 | 236 | # If you have issues with audio, you may try uncommenting the following line 237 | # which forces the HDMI output into HDMI mode instead of DVI (which doesn't 238 | # support audio output) 239 | #hdmi_drive=2 240 | 241 | [cm4] 242 | # Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into 243 | # such a board) 244 | dtoverlay=dwc2,dr_mode=host 245 | 246 | [all] 247 | 248 | # Enable the KMS ("full" KMS) graphics overlay, leaving GPU memory as the 249 | # default (the kernel is in control of graphics memory with full KMS) 250 | dtoverlay=vc4-kms-v3d 251 | 252 | # Autoload overlays for any recognized cameras or displays that are attached 253 | # to the CSI/DSI ports. Please note this is for libcamera support, *not* for 254 | # the legacy camera stack 255 | camera_auto_detect=1 256 | display_auto_detect=1 257 | 258 | # Config settings specific to arm64 259 | arm_64bit=1 260 | dtoverlay=dwc2 261 | ``` 262 | -------------------------------------------------------------------------------- /ubuntu-pi-image: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | LC_ALL=C 3 | 4 | # Display help usage 5 | function usage () { 6 | echo 7 | echo "Usage" 8 | echo " $0" 9 | echo 10 | } 11 | 12 | function sync_from() { 13 | if [ -z "${1}" ] || [ -z "${2}" ]; then 14 | echo "ERROR! Source stages to sync were not passed." 15 | exit 1 16 | fi 17 | 18 | local B_SOURCE="${1}" 19 | local R_SOURCE="${2}" 20 | 21 | if [ -d "${B}" ] && [ -d "${R}" ]; then 22 | echo "Syncing from ${B_SOURCE}..." 23 | rsync -aHAXx --delete "${B_SOURCE}/" "${B}/" 24 | echo "Syncing from ${R_SOURCE}..." 25 | rsync -aHAXx --delete "${R_SOURCE}/" "${R}/" 26 | else 27 | echo "ERROR! Either ${B} or ${R} do not exist!" 28 | exit 1 29 | fi 30 | } 31 | 32 | function nspawn() { 33 | # Create basic resolv.conf for bind mounting inside the container 34 | echo "nameserver 1.1.1.1" > "${R_STAGE_0}/resolv.conf" 35 | 36 | if pidof apt-cacher-ng && [ -d "${R}/etc/apt/apt.conf.d" ]; then 37 | echo "Acquire::http { Proxy \"http://${APT_CACHE_IP}:3142\"; }" > "${R}/etc/apt/apt.conf.d/90cache" 38 | fi 39 | 40 | # Make sure the container has a machine-id 41 | systemd-machine-id-setup --root "${R}" --print 42 | 43 | echo "Running: ${@}" 44 | # Bind mount resolv.conf and the firmware, set the hostname and spawn 45 | systemd-nspawn \ 46 | --resolv-conf=off \ 47 | --bind-ro="${R_STAGE_0}/resolv.conf":/etc/resolv.conf \ 48 | --bind="${B}":/boot/firmware \ 49 | --machine="${FLAVOUR}" \ 50 | --directory "${R}" "${@}" 51 | 52 | if [ -e "${R}/etc/apt/apt.conf.d/90cache" ]; then 53 | rm -f "${R}/etc/apt/apt.conf.d/90cache" 54 | fi 55 | } 56 | 57 | function snap_preseed() { 58 | local SNAP_NAME="${1}" 59 | local SNAP_CHANNEL="${2}" 60 | local SNAP_CONFINEMENT="" 61 | local SNAP_FILE="" 62 | 63 | # Download a snap only once 64 | if ls -1 "${R}"/var/lib/snapd/seed/snaps/"${SNAP_NAME}"_*.snap >/dev/null 2>&1; then 65 | return 66 | fi 67 | 68 | nspawn env SNAPPY_STORE_NO_CDN=1 UBUNTU_STORE_ARCH="${IMG_ARCH}" snap download --target-directory=/var/lib/snapd/seed "${SNAP_NAME}" --channel="${SNAP_CHANNEL}" 69 | mv -v "${R}"/var/lib/snapd/seed/*.assert "${R}"/var/lib/snapd/seed/assertions/ 70 | mv -v "${R}"/var/lib/snapd/seed/*.snap "${R}"/var/lib/snapd/seed/snaps/ 71 | if [ "${SNAP_NAME}" == "snapd" ]; then 72 | touch "${R}/var/lib/snapd/seed/.snapd-explicit-install-stamp" 73 | fi 74 | 75 | # Add the snap to the seed.yaml 76 | if [ ! -e "${R}"/var/lib/snapd/seed/seed.yaml ]; then 77 | echo "snaps:" > "${R}"/var/lib/snapd/seed/seed.yaml 78 | fi 79 | 80 | cat <> "${R}"/var/lib/snapd/seed/seed.yaml 81 | - 82 | name: ${SNAP_NAME} 83 | channel: ${SNAP_CHANNEL} 84 | EOF 85 | 86 | # Process classic snaps 87 | if [ -e "${R_STAGE_0}/${SNAP_NAME}.info" ]; then 88 | SNAP_CONFINEMENT=$(grep confinement "${R_STAGE_0}/${SNAP_NAME}.info" | cut -d':' -f2 | sed 's/ //g') 89 | echo "${SNAP_CONFINEMENT}" 90 | case "${SNAP_CONFINEMENT}" in 91 | *classic*) echo " classic: true" >> "${R}"/var/lib/snapd/seed/seed.yaml;; 92 | esac 93 | fi 94 | 95 | echo -n " file: " >> "${R}"/var/lib/snapd/seed/seed.yaml 96 | SNAP_FILE=$(ls -1 "${R}"/var/lib/snapd/seed/snaps/${SNAP_NAME}_*.snap) 97 | basename "${SNAP_FILE}" >> "${R}"/var/lib/snapd/seed/seed.yaml 98 | } 99 | 100 | function stage_01_bootstrap() { 101 | local REPO="" 102 | export B="${B_STAGE_1}" 103 | export R="${R_STAGE_1}" 104 | 105 | rm -rf "${B_STAGE_1}"/* 106 | rm -rf "${R_STAGE_1}"/* 107 | 108 | # Required tools on the host 109 | apt-get -y install binfmt-support debootstrap device-tree-compiler git \ 110 | graphicsmagick-imagemagick-compat iproute2 optipng qemu-user-static rsync \ 111 | systemd-container ubuntu-keyring util-linux whois xz-utils 112 | 113 | # Bootstrap a minimal Ubuntu 114 | # Include cloud-guest-utils; prevents cloud-image-utils, and therefore qemu-system-x86, being installed later 115 | 116 | if pidof apt-cacher-ng; then 117 | REPO="http://localhost:3142/ports.ubuntu.com/" 118 | else 119 | REPO="http://ports.ubuntu.com/" 120 | fi 121 | 122 | debootstrap \ 123 | --arch="${IMG_ARCH}" \ 124 | --cache-dir="${R_STAGE_0}" \ 125 | --components=main,restricted,universe,multiverse \ 126 | --foreign \ 127 | --include=cloud-guest-utils \ 128 | "${IMG_RELEASE}" "${R}" "${REPO}" 129 | nspawn /debootstrap/debootstrap \ 130 | --second-stage 131 | 132 | cat <"${R}/etc/apt/sources.list" 133 | deb http://ports.ubuntu.com/ ${IMG_RELEASE} main restricted universe multiverse 134 | deb-src http://ports.ubuntu.com/ ${IMG_RELEASE} main restricted universe multiverse 135 | 136 | deb http://ports.ubuntu.com/ ${IMG_RELEASE}-updates main restricted universe multiverse 137 | deb-src http://ports.ubuntu.com/ ${IMG_RELEASE}-updates main restricted universe multiverse 138 | 139 | deb http://ports.ubuntu.com/ ${IMG_RELEASE}-security main restricted universe multiverse 140 | deb-src http://ports.ubuntu.com/ ${IMG_RELEASE}-security main restricted universe multiverse 141 | 142 | deb http://ports.ubuntu.com/ ${IMG_RELEASE}-backports main restricted universe multiverse 143 | deb-src http://ports.ubuntu.com/ ${IMG_RELEASE}-backports main restricted universe multiverse 144 | EOM 145 | 146 | # Set locale to C.UTF-8 by default. 147 | # https://git.launchpad.net/livecd-rootfs/tree/live-build/auto/build#n159 148 | echo "LANG=C.UTF-8" > "${R}/etc/default/locale" 149 | 150 | nspawn hostnamectl --static set-hostname "${FLAVOUR}" 151 | echo "${FLAVOUR}" > "${R}/etc/hostname" 152 | sed -i "1s|.*|127.0.0.1\tlocalhost ${FLAVOUR}|" "${R}/etc/hosts" 153 | 154 | nspawn apt-get -y update 155 | nspawn apt-get -y upgrade 156 | nspawn apt-get -y dist-upgrade 157 | 158 | # Install first boot filesystem expansion 159 | nspawn apt-get -y install --no-install-recommends cloud-guest-utils \ 160 | cloud-initramfs-growroot 161 | 162 | # Add standard Ubuntu userspace 163 | nspawn apt-get -y install standard^ 164 | 165 | # Add the Raspberry Pi specific tweaks 166 | nspawn apt-get -y install ubuntu-raspi-settings 167 | } 168 | 169 | function stage_02_desktop() { 170 | export B="${B_STAGE_2}" 171 | export R="${R_STAGE_2}" 172 | sync_from "${B_STAGE_1}" "${R_STAGE_1}" 173 | 174 | nspawn apt-get -y install ubuntu-mate-core^ 175 | nspawn apt-get -y install ubuntu-mate-desktop^ 176 | 177 | # Instruct netplan to hand all network management to NetworkManager 178 | cat < "${R}/etc/netplan/01-network-manager-all.yaml" 179 | # Let NetworkManager manage all devices on this system 180 | network: 181 | version: 2 182 | renderer: NetworkManager 183 | EOM 184 | } 185 | 186 | function stage_03_snap() { 187 | local ACCOUNT_KEY="" 188 | local BASE_SNAP="" 189 | local SEED_SNAPS="ubuntu-mate-welcome software-boutique ubuntu-mate-pi snapd-desktop-integration gtk-common-themes gnome-3-38-2004 firefox" 190 | local SNAP_CHANNEL="" 191 | local SNAP_PRESEED_FAILED=0 192 | # https://git.launchpad.net/livecd-rootfs/tree/live-build/functions#n491 193 | # https://discourse.ubuntu.com/t/seeding-a-classic-ubuntu-image/19756 194 | # https://forum.snapcraft.io/t/broken-dependency-of-content-snaps-during-seeding/11566 195 | # https://bugs.launchpad.net/ubuntu-image/+bug/1958275 196 | 197 | export B="${B_STAGE_3}" 198 | export R="${R_STAGE_3}" 199 | sync_from "${B_STAGE_2}" "${R_STAGE_2}" 200 | 201 | nspawn apt-get -y install xdelta3 202 | 203 | # Prepare assertions 204 | mkdir -p "${R}"/var/lib/snapd/seed/{assertions,snaps} 205 | snap known --remote model series=16 model=generic-classic brand-id=generic > "${R}/var/lib/snapd/seed/assertions/model" 206 | ACCOUNT_KEY=$(grep "^sign-key-sha3-384" "${R}/var/lib/snapd/seed/assertions/model" | cut -d':' -f2 | sed 's/ //g') 207 | snap known --remote account-key public-key-sha3-384="${ACCOUNT_KEY}" > "${R}/var/lib/snapd/seed/assertions/account-key" 208 | snap known --remote account account-id=generic > "${R}/var/lib/snapd/seed/assertions/account" 209 | 210 | # Download the snaps 211 | for SNAP_NAME in ${SEED_SNAPS}; do 212 | # snapd-desktop-integration is not available in stable for armhf yet 213 | if [ "${IMG_ARCH}" == "armhf" ] && [ "${SNAP_NAME}" == "snapd-desktop-integration" ]; then 214 | SNAP_CHANNEL="candidate" 215 | else 216 | case "${SNAP_NAME}" in 217 | software-boutique|ubuntu-mate-pi) SNAP_CHANNEL="stable";; 218 | *) SNAP_CHANNEL="stable/ubuntu-${IMG_VER}";; 219 | esac 220 | fi 221 | 222 | snap_preseed "${SNAP_NAME}" "${SNAP_CHANNEL}" 223 | 224 | # Download any required base snaps 225 | if snap info --verbose "${R}"/var/lib/snapd/seed/snaps/"${SNAP_NAME}"*.snap > "${R_STAGE_0}/${SNAP_NAME}.info"; then 226 | if grep -q '^base:' "${R_STAGE_0}/${SNAP_NAME}.info"; then 227 | BASE_SNAP=$(awk '/^base:/ {print $2}' "${R_STAGE_0}/${SNAP_NAME}.info") 228 | snap_preseed "${BASE_SNAP}" stable 229 | case "${BASE_SNAP}" in 230 | core[0-9]*) snap_preseed snapd stable;; 231 | esac 232 | fi 233 | fi 234 | done 235 | 236 | # Validate seed.yaml 237 | if snap debug validate-seed "${R}"/var/lib/snapd/seed/seed.yaml; then 238 | cat "${R}"/var/lib/snapd/seed/seed.yaml 239 | else 240 | echo "ERROR! seed.yaml validation failed." 241 | exit 1 242 | fi 243 | 244 | # Preseed the snaps 245 | # - NOTE! This is how livecd-rootfs runs snap-preeseed, but it fails on 246 | # - armhh and the snap preseeding does complete during oem-setup. 247 | # - Disabled for now. 248 | # snap-preseed operates from outside the image being prepared and 249 | # requires some mounts to be setup 250 | if false; then 251 | mount --rbind /dev "${R}/dev" 252 | mount proc-live -t proc "${R}/proc" 253 | mount sysfs-live -t sysfs "${R}/sys" 254 | mount securityfs -t securityfs "${R}/sys/kernel/security" 255 | 256 | /usr/lib/snapd/snap-preseed --reset "${R}" 257 | 258 | if [ "${IMG_ARCH}" != "armhf" ]; then 259 | if ! /usr/lib/snapd/snap-preseed "${R}"; then 260 | SNAP_PRESEED_FAILED=1 261 | fi 262 | fi 263 | 264 | for MOUNT in "${R}/sys/kernel/security" "${R}/sys" "${R}/proc" "${R}/dev"; do 265 | echo "unmounting: ${MOUNT}" 266 | mount --make-private "${MOUNT}" 267 | umount -l "${MOUNT}" 268 | udevadm settle 269 | sleep 5 270 | done 271 | 272 | if [ ${SNAP_PRESEED_FAILED} -eq 1 ]; then 273 | echo "ERROR! snap-preseed failed." 274 | exit 1 275 | fi 276 | 277 | nspawn apparmor_parser --skip-read-cache --write-cache --skip-kernel-load --verbose -j $(nproc) /etc/apparmor.d 278 | fi 279 | } 280 | 281 | function stage_04_kernel() { 282 | export B="${B_STAGE_4}" 283 | export R="${R_STAGE_4}" 284 | sync_from "${B_STAGE_3}" "${R_STAGE_3}" 285 | 286 | cat <<'EOM' > "${B}/README" 287 | An overview of the files on the /boot/firmware partition (the 1st partition 288 | on the SD card) used by the Ubuntu boot process (roughly in order) is as 289 | follows: 290 | 291 | * bootcode.bin - this is the second stage bootloader loaded by all pis with 292 | the exception of the pi4 (where this is replaced by flash 293 | memory) 294 | * config.txt - the configuration file read by the boot process 295 | * start*.elf - the third stage bootloader, which handles device-tree 296 | modification and which loads... 297 | * vmlinuz - the Linux kernel 298 | * cmdline.txt - the Linux kernel command line 299 | * initrd.img - the initramfs 300 | EOM 301 | 302 | if [ "${IMG_ARCH}" == "arm64" ]; then 303 | ARM64="# Config settings specific to arm64 304 | arm_64bit=1 305 | dtoverlay=dwc2" 306 | else 307 | ARM64="" 308 | fi 309 | 310 | cat < "${B}/config.txt" 311 | [all] 312 | kernel=vmlinuz 313 | cmdline=cmdline.txt 314 | initramfs initrd.img followkernel 315 | 316 | [pi4] 317 | max_framebuffers=2 318 | arm_boost=1 319 | 320 | [all] 321 | # Enable the audio output, I2C and SPI interfaces on the GPIO header. As these 322 | # parameters related to the base device-tree they must appear *before* any 323 | # other dtoverlay= specification 324 | dtparam=audio=on 325 | dtparam=i2c_arm=on 326 | dtparam=spi=on 327 | 328 | # Comment out the following line if the edges of the desktop appear outside 329 | # the edges of your display 330 | disable_overscan=1 331 | 332 | # If you have issues with audio, you may try uncommenting the following line 333 | # which forces the HDMI output into HDMI mode instead of DVI (which doesn't 334 | # support audio output) 335 | hdmi_drive=2 336 | 337 | [cm4] 338 | # Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into 339 | # such a board) 340 | dtoverlay=dwc2,dr_mode=host 341 | 342 | [all] 343 | 344 | # Enable the KMS ("full" KMS) graphics overlay, leaving GPU memory as the 345 | # default (the kernel is in control of graphics memory with full KMS) 346 | dtoverlay=vc4-kms-v3d 347 | 348 | # Autoload overlays for any recognized cameras or displays that are attached 349 | # to the CSI/DSI ports. Please note this is for libcamera support, *not* for 350 | # the legacy camera stack 351 | camera_auto_detect=1 352 | display_auto_detect=1 353 | 354 | ${ARM64} 355 | EOM 356 | 357 | echo "zswap.enabled=1 zswap.zpool=z3fold zswap.compressor=lz4 dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc quiet splash" > "${B}/cmdline.txt" 358 | cat <<'EOM' > "${R}/etc/fstab" 359 | LABEL=writable / ext4 discard,noatime,x-systemd.growfs 0 1 360 | LABEL=system-boot /boot/firmware vfat defaults 0 1 361 | EOM 362 | 363 | nspawn apt-get -y install libraspberrypi0 libraspberrypi-bin \ 364 | linux-firmware-raspi linux-image-raspi linux-modules-extra-raspi \ 365 | pi-bluetooth rpi-eeprom 366 | 367 | # Prevent triggerhappy from being installed 368 | nspawn apt-get -y install --no-install-recommends raspi-config 369 | nspawn systemctl disable raspi-config 370 | 371 | local NEW_KERNEL=$(ls -1 "${R}"/boot/vmlinuz-* | tail -n1 | awk -F/ '{print $NF}' | cut -d'-' -f2-4) 372 | if [ -z "${NEW_KERNEL}" ]; then 373 | echo "ERROR! Could not detect the new kernel version" 374 | exit 1 375 | fi 376 | echo "Kernel: ${NEW_KERNEL}" 377 | 378 | # Copy firmware, devicetree, overlays and kernel to the boot file system 379 | cp -v "${R}/lib/linux-firmware-raspi/"* "${B}/" 380 | cp -av "${R}/lib/firmware/${NEW_KERNEL}/device-tree/"* "${B}/" 381 | 382 | # Move the arm64 device-tree 383 | if [ -d "${B}/broadcom" ]; then 384 | mv -v "${B}/broadcom/"*.dtb "${B}"/ 385 | rm -rf "${B}/broadcom" 386 | fi 387 | 388 | cp -av "${R}/boot/vmlinuz-${NEW_KERNEL}" "${B}/vmlinuz" 389 | cp -av "${R}/boot/initrd.img-${NEW_KERNEL}" "${B}/initrd.img" 390 | } 391 | 392 | function stage_05_config() { 393 | export B="${B_STAGE_5}" 394 | export R="${R_STAGE_5}" 395 | sync_from "${B_STAGE_4}" "${R_STAGE_4}" 396 | 397 | # Adds lz4 and z3fold modules to initramfs. 398 | # - https://ubuntu.com/blog/how-low-can-you-go-running-ubuntu-desktop-on-a-2gb-raspberry-pi-4 399 | echo lz4 >> "${R}/etc/initramfs-tools/modules" 400 | echo z3fold >> "${R}/etc/initramfs-tools/modules" 401 | 402 | # Swap 403 | # - https://git.launchpad.net/livecd-rootfs/tree/live-build/ubuntu/hooks/099-ubuntu-image-customization.chroot 404 | mkdir -p "${R}/usr/lib/systemd/system/swap.target.wants/" 405 | cat <<'EOM' > "${R}/usr/lib/systemd/system/mkswap.service" 406 | [Unit] 407 | Description=Create the default swapfile 408 | DefaultDependencies=no 409 | Requires=local-fs.target 410 | After=local-fs.target 411 | Before=swapfile.swap 412 | ConditionPathExists=!/swapfile 413 | 414 | [Service] 415 | Type=oneshot 416 | ExecStartPre=fallocate -l 1GiB /swapfile 417 | ExecStartPre=chmod 600 /swapfile 418 | ExecStart=mkswap /swapfile 419 | 420 | [Install] 421 | WantedBy=swap.target 422 | EOM 423 | nspawn ln -s /usr/lib/systemd/system/mkswap.service /usr/lib/systemd/system/swap.target.wants/mkswap.service 424 | 425 | cat <<'EOM' > "${R}/usr/lib/systemd/system/swapfile.swap" 426 | [Unit] 427 | Description=The default swapfile 428 | 429 | [Swap] 430 | What=/swapfile 431 | EOM 432 | nspawn ln -s /usr/lib/systemd/system/swapfile.swap /usr/lib/systemd/system/swap.target.wants/swapfile.swap 433 | 434 | cat <<'EOM' > "${R}/usr/lib/systemd/system/fontconfig-regenerate-cache.service" 435 | [Unit] 436 | Description=Make sure the font cache is generated 437 | After=local-fs.target 438 | DefaultDependencies=no 439 | ConditionPathExists=!/var/cache/fontconfig/CACHEDIR.TAG 440 | 441 | [Service] 442 | Type=simple 443 | ExecStart=fc-cache -frsv 444 | 445 | [Install] 446 | WantedBy=sysinit.target 447 | EOM 448 | nspawn systemctl enable fontconfig-regenerate-cache.service 449 | 450 | # Create user and groups 451 | local DATE="" 452 | DATE=$(date +%m%H%M%S) 453 | local PASSWD="" 454 | PASSWD=$(mkpasswd -m sha-512 oem "${DATE}") 455 | nspawn addgroup --gid 29999 oem 456 | 457 | nspawn adduser --gecos "OEM Configuration (temporary user)" --add_extra_groups --disabled-password --gid 29999 --uid 29999 oem 458 | nspawn usermod -a -G adm,sudo -p "${PASSWD}" oem 459 | 460 | nspawn apt-get -y install --no-install-recommends oem-config-gtk ubiquity-frontend-gtk ubiquity-ubuntu-artwork oem-config-slideshow-ubuntu-mate 461 | # Force the slideshow to use Ubuntu MATE artwork. 462 | sed -i 's/oem-config-slideshow-ubuntu/oem-config-slideshow-ubuntu-mate/' "${R}/usr/lib/ubiquity/plugins/ubi-usersetup.py" 463 | sed -i 's/oem-config-slideshow-ubuntu/oem-config-slideshow-ubuntu-mate/' "${R}/usr/sbin/oem-config-remove-gtk" 464 | sed -i 's/ubiquity-slideshow-ubuntu/ubiquity-slideshow-ubuntu-mate/' "${R}/usr/sbin/oem-config-remove-gtk" 465 | 466 | # Create files/dirs Ubiquity requires 467 | mkdir -p "${R}/var/log/installer" 468 | touch "${R}/var/lib/oem-config/run" 469 | touch "${R}/var/log/installer/debug" 470 | touch "${R}/var/log/syslog" 471 | nspawn chown syslog:adm /var/log/syslog 472 | nspawn /usr/sbin/oem-config-prepare --quiet 473 | 474 | # Install seeded snaps during oem-setup 475 | nspawn ln -s /lib/systemd/system/snapd.seeded.service /etc/systemd/system/oem-config.target.wants/snapd.seeded.service 476 | } 477 | 478 | function stage_06_clean() { 479 | export B="${B_STAGE_6}" 480 | export R="${R_STAGE_6}" 481 | sync_from "${B_STAGE_5}" "${R_STAGE_5}" 482 | 483 | nspawn apt-get -y autoremove 484 | nspawn apt-get -y autoclean 485 | nspawn apt-get -y clean 486 | 487 | rm -f "${B}"/{*.bak,*.old} 488 | rm -f "${R}"/wget-log 489 | rm -f "${R}"/boot/{*.bak,*.old} 490 | rm -f "${R}"/etc/ssh/ssh_host_*_key* 491 | rm -f "${R}"/etc/apt/*.save 492 | rm -f "${R}"/etc/apt/apt.conf.d/90cache 493 | rm -f "${R}"/etc/apt/sources.list.d/*.save 494 | rm -f "${R}"/root/.wget-hsts 495 | rm -rf "${R}"/tmp/* 496 | rm -f "${R}"/var/log/apt/* 497 | rm -f "${R}"/var/log/alternatives.log 498 | rm -f "${R}"/var/log/bootstrap.log 499 | rm -f "${R}"/var/log/dpkg.log 500 | rm -f "${R}"/var/log/fontconfig.log 501 | rm -f "${R}"/var/cache/fontconfig/CACHEDIR.TAG 502 | rm -f "${R}"/var/crash/* 503 | rm -rf "${R}"/var/lib/apt/lists/* 504 | rm -f "${R}"/var/lib/dpkg/*-old 505 | [ -L "${R}"/var/lib/dbus/machine-id ] || rm -f "${R}"/var/lib/dbus/machine-id 506 | echo '' > "${R}"/etc/machine-id 507 | } 508 | 509 | function stage_07_image() { 510 | export B="${B_STAGE_6}" 511 | export R="${R_STAGE_6}" 512 | 513 | # Build the image file 514 | local SIZE_BOOT="256" 515 | local SIZE_ROOT=0 516 | local SIZE_IMG=0 517 | local SIZE_PAD=0 518 | # Calculate image size accounting for boot parition + 5% 519 | SIZE_ROOT=$(du -cs --block-size=MB "${R}" | tail -n1 | cut -d'M' -f1) 520 | SIZE_PAD=$(( (SIZE_ROOT / 10) / 2 )) 521 | SIZE_IMG=$((SIZE_BOOT + SIZE_ROOT + SIZE_PAD)) 522 | 523 | # Create an empty file file. 524 | rm -fv "${TMP_DIR}/${IMG_OUT}" 525 | fallocate -l "${SIZE_IMG}"M "${TMP_DIR}/${IMG_OUT}" 526 | 527 | # Initialising: msdos 528 | parted -s "${TMP_DIR}/${IMG_OUT}" mktable msdos 529 | echo "Creating /boot/firmware partition" 530 | parted -a optimal -s "${TMP_DIR}/${IMG_OUT}" mkpart primary fat32 1 "${SIZE_BOOT}MB" 531 | echo "Creating / partition" 532 | parted -a optimal -s "${TMP_DIR}/${IMG_OUT}" mkpart primary ext4 "${SIZE_BOOT}MB" 100% 533 | echo "Making partition 1 bootable" 534 | parted -s "${TMP_DIR}/${IMG_OUT}" set 1 boot on 535 | 536 | PARTED_OUT=$(parted -s "${TMP_DIR}/${IMG_OUT}" unit b print) 537 | BOOT_OFFSET=$(echo "${PARTED_OUT}" | grep -e '^ 1'| xargs echo -n \ 538 | | cut -d" " -f 2 | tr -d B) 539 | BOOT_LENGTH=$(echo "${PARTED_OUT}" | grep -e '^ 1'| xargs echo -n \ 540 | | cut -d" " -f 4 | tr -d B) 541 | 542 | ROOT_OFFSET=$(echo "${PARTED_OUT}" | grep -e '^ 2'| xargs echo -n \ 543 | | cut -d" " -f 2 | tr -d B) 544 | ROOT_LENGTH=$(echo "${PARTED_OUT}" | grep -e '^ 2'| xargs echo -n \ 545 | | cut -d" " -f 4 | tr -d B) 546 | 547 | BOOT_LOOP=$(losetup --show -f -o "${BOOT_OFFSET}" --sizelimit "${BOOT_LENGTH}" "${TMP_DIR}/${IMG_OUT}") 548 | ROOT_LOOP=$(losetup --show -f -o "${ROOT_OFFSET}" --sizelimit "${ROOT_LENGTH}" "${TMP_DIR}/${IMG_OUT}") 549 | echo "/boot/firmware: offset ${BOOT_OFFSET}, length ${BOOT_LENGTH}" 550 | echo "/: offset ${ROOT_OFFSET}, length ${ROOT_LENGTH}" 551 | 552 | mkfs.vfat -n system-boot -S 512 -s 16 -v "${BOOT_LOOP}" 553 | mkfs.ext4 -L writable -m 0 "${ROOT_LOOP}" 554 | 555 | MOUNTDIR="${TMP_DIR}/image" 556 | mkdir -p "${MOUNTDIR}" 557 | mount -v "${ROOT_LOOP}" "${MOUNTDIR}" -t ext4 558 | mkdir -p "${MOUNTDIR}/boot/firmware" 559 | mount -v "${BOOT_LOOP}" "${MOUNTDIR}/boot/firmware" -t vfat 560 | echo "Syncing root..." 561 | rsync -aHAXx --delete "${R}"/ "${MOUNTDIR}/" 562 | echo "Syncing boot..." 563 | rsync -aHAXx --delete "${B}"/ "${MOUNTDIR}/boot/firmware/" 564 | mkdir -p "${MOUNTDIR}/.disk" 565 | date +"%Y%m%d" > "${MOUNTDIR}/.disk/info" 566 | sync 567 | umount -l "${MOUNTDIR}/boot/firmware" 568 | umount -l "${MOUNTDIR}" 569 | losetup -d "${ROOT_LOOP}" 570 | losetup -d "${BOOT_LOOP}" 571 | ls -lh "${TMP_DIR}/${IMG_OUT}" 572 | rm -rf "${MOUNTDIR}" 573 | } 574 | 575 | function stage_08_compress() { 576 | export B="${B_STAGE_6}" 577 | export R="${R_STAGE_6}" 578 | 579 | # NOTE! Disabled while iterating 580 | echo "Compressing ${IMG_OUT}.xz" 581 | rm "${TMP_DIR}/${IMG_OUT}.xz" 2>/dev/null 582 | xz --keep -T 0 "${TMP_DIR}/${IMG_OUT}" 583 | ls -lh "${TMP_DIR}/${IMG_OUT}.xz" 584 | 585 | local HASH="sha256" 586 | local KEY="FFEE1E5C" 587 | 588 | local OUT_HASH="${TMP_DIR}/${IMG_OUT}.xz.${HASH}" 589 | local OUT_SIGN="${TMP_DIR}/${IMG_OUT}.xz.${HASH}.sign" 590 | 591 | rm -f "${OUT_HASH}" 592 | rm -f "${OUT_SIGN}" 593 | 594 | if [ -e "${TMP_DIR}/${IMG_OUT}.xz" ]; then 595 | echo "Hashing ${IMG_OUT}.xz" 596 | ${HASH}sum "${TMP_DIR}/${IMG_OUT}.xz" > "${OUT_HASH}" 597 | sed -i -r "s/ .*\/(.+)/ \1/g" "${OUT_HASH}" 598 | gpg --default-key ${KEY} --armor --output "${OUT_SIGN}" --detach-sig "${OUT_HASH}" 599 | else 600 | echo "WARNING! Didn't find ${TMP_DIR}/${IMG_OUT} to hash." 601 | fi 602 | } 603 | 604 | if [ -z "${SUDO_USER}" ]; then 605 | echo "ERROR! You must use sudo to run this script: sudo ./$(basename ${0})" 606 | exit 1 607 | else 608 | SUDO_HOME=$(getent passwd "${SUDO_USER}" | cut -d: -f6) 609 | fi 610 | 611 | # Install apt-cacher-ng on the host and this script will use it. 612 | APT_CACHE_IP=$(ip route get 1.1.1.1 | head -n 1 | cut -d' ' -f 7) 613 | FLAVOUR="ubuntu-mate" 614 | IMG_QUALITY="" # Or something like "-beta1" for testing images 615 | IMG_VER="22.04" 616 | IMG_RELEASE="jammy" 617 | IMG_ARCH="arm64" 618 | IMG_OUT="${FLAVOUR}-${IMG_VER}${IMG_QUALITY}-desktop-${IMG_ARCH}+raspi.img" 619 | TMP_DIR="${SUDO_HOME}/Builds" 620 | 621 | # Create caches 622 | for LOOP in 0 1 2 3 4 5 6 7 8 9; do 623 | case ${LOOP} in 624 | 0) 625 | export R_STAGE_${LOOP}="${TMP_DIR}/${IMG_ARCH}/${FLAVOUR}/${IMG_VER}/${LOOP}_cache" 626 | mkdir -p "${TMP_DIR}/${IMG_ARCH}/${FLAVOUR}/${IMG_VER}/${LOOP}_cache" 2>/dev/null 627 | ;; 628 | *) 629 | export B_STAGE_${LOOP}="${TMP_DIR}/${IMG_ARCH}/${FLAVOUR}/${IMG_VER}/${LOOP}_boot" 630 | export R_STAGE_${LOOP}="${TMP_DIR}/${IMG_ARCH}/${FLAVOUR}/${IMG_VER}/${LOOP}_root" 631 | mkdir -p "${TMP_DIR}/${IMG_ARCH}/${FLAVOUR}/${IMG_VER}/${LOOP}_boot" 2>/dev/null 632 | mkdir -p "${TMP_DIR}/${IMG_ARCH}/${FLAVOUR}/${IMG_VER}/${LOOP}_root/boot/firmware" 2>/dev/null 633 | ;; 634 | esac 635 | done 636 | 637 | stage_01_bootstrap 638 | stage_02_desktop 639 | stage_03_snap 640 | stage_04_kernel 641 | stage_05_config 642 | stage_06_clean 643 | stage_07_image 644 | stage_08_compress 645 | --------------------------------------------------------------------------------