first commit

This commit is contained in:
Fabio 2025-09-11 09:59:15 +02:00
commit 45d05384f3
7 changed files with 1170 additions and 0 deletions

15
Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM debian:bookworm
# Install requirments
RUN apt update && apt install -y wget parted gzip pigz xz-utils udev e2fsprogs && apt clean
# Setup Env
ENV LANG=C.UTF-8
ENV TERM=xterm-256color
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /workdir
# Copy pishrink in
COPY pishrink.sh /usr/local/bin/pishrink
RUN chmod +x /usr/local/bin/pishrink
ENTRYPOINT [ "/usr/local/bin/pishrink" ]

23
LICENSE Normal file
View file

@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) 2016 Drew Bonasera
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

93
README.md Normal file
View file

@ -0,0 +1,93 @@
# Shrink Disk
creare immagine disco
sudo dd if=/dev/mmcblk0 of=/home/nvme/deb12.img bs=1M conv=noerror,sync status=progress
oppure
sudo dd if=/dev/mmcblk0 of=/home/nvme/deb12.img conv=noerror,sync status=progress
creare pishrink modificando pishrink.sh in modo da funzionare con OrangePi (GPT)
./newshrink
copiare in modo che siano sempre disponibili gli eseguibili
sudo cp pishrink.sh /usr/local/bin/
sudo cp pishrink /usr/local/bin/
creare il file ridotto
sudo pishrink /home/nvme/deb12.img /home/nvme/deb12r.img
sistemare la partizione GPT con gdisk
sudo gdisk /home/nvme/deb12r.img
digitare v per verificare e poi seguire le istruzioni
solitamente si va su x e poi una seconda opzione, poi si torna a verificare con v
infine comprimere con
xz <nomefile>
per decomprimere
unxz <nomefile.xz>
## Altri comandi per fare tutto in manuale
sudo dd if=/dev/mmcblk0 of=/home/nvme/deb12.img bs=128k conv=noerror,sync status=progres
sudo modprobe loop
sudo losetup -f
sudo losetup /dev/loop0 /home/nvme/deb12.img
sudo partprobe /dev/loop0
DISPLAY=:1 sudo -E gparted /dev/loop0
sudo apt update && sudo apt install -y wget parted gzip pigz xz-utils udev e2fsprogs
sudo pishrink.sh -Z /home/nvme/deb12.img /home/nvme/deb12r.img
sudo pishrink.sh /home/nvme/deb12.img /home/nvme/deb12r.img
sudo losetup /dev/loop0 /home/nvme/deb12r.img
lsblk
sudo losetup --find --show deb12r.img
sudo mkdir /mnt/a
sudo mount /dev/loop0 /mnt/a
gdisk /home/nvme/deb12r.img
v
sistemare con quanto scritto
solitamente x e poi un'altra lettera
poi v nuovamente e ricominciare fino ad aver sistemato tutto
poi w per scrivere
sudo losetup -d /dev/loop0
parted

125
README1.md Normal file
View file

@ -0,0 +1,125 @@
# PiShrink #
PiShrink is a bash script that automatically shrink a pi image that will then resize to the max size of the SD card on boot. This will make putting the image back onto the SD card faster and the shrunk images will compress better.
In addition the shrunk image can be compressed with gzip and xz to create an even smaller image. Parallel compression of the image
using multiple cores is supported.
## Usage ##
```text
Usage: pishrink.sh [-adhnrsvzZ] imagefile.img [newimagefile.img]
-s Don't expand filesystem when image is booted the first time
-v Be verbose
-n Disable automatic update checking
-r Use advanced filesystem repair option if the normal one fails
-z Compress image after shrinking with gzip
-Z Compress image after shrinking with xz
-a Compress image in parallel using multiple cores
-d Write debug messages in a debug log file
```
If you specify the `newimagefile.img` parameter, the script will make a copy of `imagefile.img` and work off that. You will need enough space to make a full copy of the image to use that option.
* `-s` prevents automatic filesystem expansion on the images next boot
* `-v` enables more verbose output
* `-n` disables the script from checking Github for a new PiShrink release
* `-r` will attempt to repair the filesystem using additional options if the normal repair fails
* `-z` will compress the image after shrinking using gzip. `.gz` extension will be added to the filename.
* `-Z` will compress the image after shrinking using xz. `.xz` extension will be added to the filename.
* `-a` will use option -f9 for pigz and option -T0 for xz and compress in parallel.
* `-d` will create a logfile `pishrink.log` which may help for problem analysis.
Default options for compressors can be overwritten by defining PISHRINK_GZIP or PSHRINK_XZ environment variables for gzip and xz.
## Prerequisites ##
If you are running PiShrink in VirtualBox you will likely encounter an error if you
attempt to use VirtualBox's "Shared Folder" feature. You can copy the image you wish to
shrink on to the VM from a Shared Folder, but shrinking directly from the Shared Folder
is know to cause issues.
If using Ubuntu, you will likely see an error about `e2fsck` being out of date and `metadata_csum`. The simplest fix for this is to use Ubuntu 16.10 and up, as it will save you a lot of hassle in the long run.
PiShrink will shrink the last partition of your image. If that partition is not ext2, ext3, or ext4 it will not be able to shrink your image.
If the last partition is not the root filesystem partition, auto resizing will not run on boot.
If you want to use auto resizing on a distro using Systemd, you should ensure you [Enabled /etc/rc.local Compatibility](https://www.linuxbabe.com/linux-server/how-to-enable-etcrc-local-with-systemd).
## Installation ##
### Linux Instructions ###
If you are on Debian/Ubuntu you can install all the packages you would need by running: `sudo apt update && sudo apt install -y wget parted gzip pigz xz-utils udev e2fsprogs`
Run the block below to install PiShrink onto your system.
```bash
wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod +x pishrink.sh
sudo mv pishrink.sh /usr/local/bin
```
### Windows Instructions ###
PiShrink can be ran on Windows using [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/about) (WSL 2).
1. In an Administrator command prompt run `wsl --install -d Debian`. You will likely need to reboot after. Please check [Microsoft's documentation](https://learn.microsoft.com/en-us/windows/wsl/install) if you run into issues.
2. Open the `Debian` app from your start menu.
3. Run `sudo apt update && sudo apt install -y wget parted gzip pigz xz-utils udev e2fsprogs`
4. Go to the Linux Instructions section above, do that and you're good to go! Your C:\ drive is mounted at /mnt/c/
### MacOS Instructions ###
> [!NOTE]
> These instructions were sourced from the community and should work on Intel and M1 Macs.
1. [Installer Docker](https://docs.docker.com/docker-for-mac/install/).
2. Clone this repo and cd into the pishrink directory:
```bash
git clone https://github.com/Drewsif/PiShrink && cd PiShrink
```
4. Build the container by running:
```bash
docker build -t pishrink .
```
6. Create an alias to run PiShrink:
```bash
echo "alias pishrink='docker run -it --rm --privileged=true -v $(pwd):/workdir pishrink'" >> ~/.bashrc && source ~/.bashrc
```
You can now run the `pishrink` command as normal to shrink your images.
> [!WARNING]
> You MUST change directory into the images folder for this command to work. The command mounts your current working directory into the container so absolute file paths will not work. Relative paths should work just fine as long as they are below your current directory.
## Example ##
```bash
[user@localhost PiShrink]$ sudo pishrink.sh pi.img
e2fsck 1.42.9 (28-Dec-2013)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/loop1: 88262/1929536 files (0.2% non-contiguous), 842728/7717632 blocks
resize2fs 1.42.9 (28-Dec-2013)
resize2fs 1.42.9 (28-Dec-2013)
Resizing the filesystem on /dev/loop1 to 773603 (4k) blocks.
Begin pass 2 (max = 100387)
Relocating blocks XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 3 (max = 236)
Scanning inode table XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 4 (max = 7348)
Updating inode references XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The filesystem on /dev/loop1 is now 773603 blocks long.
Shrunk pi.img from 30G to 3.1G
```
## Contributing ##
If you find a bug please create an issue for it. If you would like a new feature added, you can create an issue for it but I can't promise that I will get to it.
Pull requests for new features and bug fixes are more than welcome!

3
newshrink Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
sudo sed '/endresult=$(tail /a \ \ endresult=$((endresult+2048*512))' pishrink.sh > pishrink
sudo chmod a+x pishrink

456
pishrink Executable file
View file

@ -0,0 +1,456 @@
#!/usr/bin/env bash
# Project: PiShrink
# Description: PiShrink is a bash script that automatically shrink a pi image that will then resize to the max size of the SD card on boot.
# Link: https://github.com/Drewsif/PiShrink
version="v24.10.23"
CURRENT_DIR="$(pwd)"
SCRIPTNAME="${0##*/}"
MYNAME="${SCRIPTNAME%.*}"
LOGFILE="${CURRENT_DIR}/${SCRIPTNAME%.*}.log"
REQUIRED_TOOLS="parted losetup tune2fs md5sum e2fsck resize2fs"
ZIPTOOLS=("gzip xz")
declare -A ZIP_PARALLEL_TOOL=( [gzip]="pigz" [xz]="xz" ) # parallel zip tool to use in parallel mode
declare -A ZIP_PARALLEL_OPTIONS=( [gzip]="-f9" [xz]="-T0" ) # options for zip tools in parallel mode
declare -A ZIPEXTENSIONS=( [gzip]="gz" [xz]="xz" ) # extensions of zipped files
function info() {
echo "$SCRIPTNAME: $1"
}
function error() {
echo -n "$SCRIPTNAME: ERROR occurred in line $1: "
shift
echo "$@"
}
function cleanup() {
if losetup "$loopback" &>/dev/null; then
losetup -d "$loopback"
fi
if [ "$debug" = true ]; then
local old_owner=$(stat -c %u:%g "$src")
chown "$old_owner" "$LOGFILE"
fi
}
function logVariables() {
if [ "$debug" = true ]; then
echo "Line $1" >> "$LOGFILE"
shift
local v var
for var in "$@"; do
eval "v=\$$var"
echo "$var: $v" >> "$LOGFILE"
done
fi
}
function checkFilesystem() {
info "Checking filesystem"
e2fsck -pf "$loopback"
(( $? < 4 )) && return
info "Filesystem error detected!"
info "Trying to recover corrupted filesystem"
e2fsck -y "$loopback"
(( $? < 4 )) && return
if [[ $repair == true ]]; then
info "Trying to recover corrupted filesystem - Phase 2"
e2fsck -fy -b 32768 "$loopback"
(( $? < 4 )) && return
fi
error $LINENO "Filesystem recoveries failed. Giving up..."
exit 9
}
function set_autoexpand() {
#Make pi expand rootfs on next boot
mountdir=$(mktemp -d)
partprobe "$loopback"
sleep 3
umount "$loopback" > /dev/null 2>&1
mount "$loopback" "$mountdir" -o rw
if (( $? != 0 )); then
info "Unable to mount loopback, autoexpand will not be enabled"
return
fi
if [ ! -d "$mountdir/etc" ]; then
info "/etc not found, autoexpand will not be enabled"
umount "$mountdir"
return
fi
if [[ ! -f "$mountdir/etc/rc.local" ]]; then
info "An existing /etc/rc.local was not found, autoexpand may fail..."
fi
if ! grep -q "## PiShrink https://github.com/Drewsif/PiShrink ##" "$mountdir/etc/rc.local"; then
echo "Creating new /etc/rc.local"
if [ -f "$mountdir/etc/rc.local" ]; then
mv "$mountdir/etc/rc.local" "$mountdir/etc/rc.local.bak"
fi
cat <<'EOFRC' > "$mountdir/etc/rc.local"
#!/bin/bash
## PiShrink https://github.com/Drewsif/PiShrink ##
do_expand_rootfs() {
ROOT_PART=$(mount | sed -n 's|^/dev/\(.*\) on / .*|\1|p')
PART_NUM=${ROOT_PART#mmcblk0p}
if [ "$PART_NUM" = "$ROOT_PART" ]; then
echo "$ROOT_PART is not an SD card. Don't know how to expand"
return 0
fi
# Get the starting offset of the root partition
PART_START=$(parted /dev/mmcblk0 -ms unit s p | grep "^${PART_NUM}" | cut -f 2 -d: | sed 's/[^0-9]//g')
[ "$PART_START" ] || return 1
# Return value will likely be error for fdisk as it fails to reload the
# partition table because the root fs is mounted
fdisk /dev/mmcblk0 <<EOF
p
d
$PART_NUM
n
p
$PART_NUM
$PART_START
p
w
EOF
cat <<EOF > /etc/rc.local &&
#!/bin/sh
echo "Expanding /dev/$ROOT_PART"
resize2fs /dev/$ROOT_PART
rm -f /etc/rc.local; cp -fp /etc/rc.local.bak /etc/rc.local && /etc/rc.local
EOF
reboot
exit
}
raspi_config_expand() {
/usr/bin/env raspi-config --expand-rootfs
if [[ $? != 0 ]]; then
return -1
else
rm -f /etc/rc.local; cp -fp /etc/rc.local.bak /etc/rc.local && /etc/rc.local
reboot
exit
fi
}
raspi_config_expand
echo "WARNING: Using backup expand..."
sleep 5
do_expand_rootfs
echo "ERROR: Expanding failed..."
sleep 5
if [[ -f /etc/rc.local.bak ]]; then
cp -fp /etc/rc.local.bak /etc/rc.local
/etc/rc.local
fi
exit 0
EOFRC
chmod +x "$mountdir/etc/rc.local"
fi
umount "$mountdir"
}
help() {
local help
read -r -d '' help << EOM
Usage: $0 [-adhnrsvzZ] imagefile.img [newimagefile.img]
-s Don't expand filesystem when image is booted the first time
-v Be verbose
-n Disable automatic update checking
-r Use advanced filesystem repair option if the normal one fails
-z Compress image after shrinking with gzip
-Z Compress image after shrinking with xz
-a Compress image in parallel using multiple cores
-d Write debug messages in a debug log file
EOM
echo "$help"
exit 1
}
should_skip_autoexpand=false
debug=false
update_check=true
repair=false
parallel=false
verbose=false
ziptool=""
while getopts ":adnhrsvzZ" opt; do
case "${opt}" in
a) parallel=true;;
d) debug=true;;
n) update_check=false;;
h) help;;
r) repair=true;;
s) should_skip_autoexpand=true ;;
v) verbose=true;;
z) ziptool="gzip";;
Z) ziptool="xz";;
*) help;;
esac
done
shift $((OPTIND-1))
if [ "$debug" = true ]; then
info "Creating log file $LOGFILE"
rm "$LOGFILE" &>/dev/null
exec 1> >(stdbuf -i0 -o0 -e0 tee -a "$LOGFILE" >&1)
exec 2> >(stdbuf -i0 -o0 -e0 tee -a "$LOGFILE" >&2)
fi
echo -e "PiShrink $version - https://github.com/Drewsif/PiShrink\n"
# Try and check for updates
if $update_check; then
latest_release=$(curl -m 5 https://api.github.com/repos/Drewsif/PiShrink/releases/latest 2>/dev/null | grep -i "tag_name" 2>/dev/null | awk -F '"' '{print $4}' 2>/dev/null)
if [[ $? ]] && [ "$latest_release" \> "$version" ]; then
echo "WARNING: You do not appear to be running the latest version of PiShrink. Head on over to https://github.com/Drewsif/PiShrink to grab $latest_release"
echo ""
fi
fi
#Args
src="$1"
img="$1"
#Usage checks
if [[ -z "$img" ]]; then
help
fi
if [[ ! -f "$img" ]]; then
error $LINENO "$img is not a file..."
exit 2
fi
if (( EUID != 0 )); then
error $LINENO "You need to be running as root."
exit 3
fi
# set locale to POSIX(English) temporarily
# these locale settings only affect the script and its sub processes
export LANGUAGE=POSIX
export LC_ALL=POSIX
export LANG=POSIX
# check selected compression tool is supported and installed
if [[ -n $ziptool ]]; then
if [[ ! " ${ZIPTOOLS[@]} " =~ $ziptool ]]; then
error $LINENO "$ziptool is an unsupported ziptool."
exit 17
else
if [[ $parallel == true && $ziptool == "gzip" ]]; then
REQUIRED_TOOLS="$REQUIRED_TOOLS pigz"
else
REQUIRED_TOOLS="$REQUIRED_TOOLS $ziptool"
fi
fi
fi
#Check that what we need is installed
for command in $REQUIRED_TOOLS; do
command -v $command >/dev/null 2>&1
if (( $? != 0 )); then
error $LINENO "$command is not installed."
exit 4
fi
done
#Copy to new file if requested
if [ -n "$2" ]; then
f="$2"
if [[ -n $ziptool && "${f##*.}" == "${ZIPEXTENSIONS[$ziptool]}" ]]; then # remove zip extension if zip requested because zip tool will complain about extension
f="${f%.*}"
fi
info "Copying $1 to $f..."
cp --reflink=auto --sparse=always "$1" "$f"
if (( $? != 0 )); then
error $LINENO "Could not copy file..."
exit 5
fi
old_owner=$(stat -c %u:%g "$1")
chown "$old_owner" "$f"
img="$f"
fi
# cleanup at script exit
trap cleanup EXIT
#Gather info
info "Gathering data"
beforesize="$(ls -lh "$img" | cut -d ' ' -f 5)"
parted_output="$(parted -ms "$img" unit B print)"
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
info "Possibly invalid image. Run 'parted $img unit B print' manually to investigate"
exit 6
fi
partnum="$(echo "$parted_output" | tail -n 1 | cut -d ':' -f 1)"
partstart="$(echo "$parted_output" | tail -n 1 | cut -d ':' -f 2 | tr -d 'B')"
if [ -z "$(parted -s "$img" unit B print | grep "$partstart" | grep logical)" ]; then
parttype="primary"
else
parttype="logical"
fi
loopback="$(losetup -f --show -o "$partstart" "$img")"
tune2fs_output="$(tune2fs -l "$loopback")"
rc=$?
if (( $rc )); then
echo "$tune2fs_output"
error $LINENO "tune2fs failed. Unable to shrink this type of image"
exit 7
fi
currentsize="$(echo "$tune2fs_output" | grep '^Block count:' | tr -d ' ' | cut -d ':' -f 2)"
blocksize="$(echo "$tune2fs_output" | grep '^Block size:' | tr -d ' ' | cut -d ':' -f 2)"
logVariables $LINENO beforesize parted_output partnum partstart parttype tune2fs_output currentsize blocksize
#Check if we should make pi expand rootfs on next boot
if [ "$parttype" == "logical" ]; then
echo "WARNING: PiShrink does not yet support autoexpanding of this type of image"
elif [ "$should_skip_autoexpand" = false ]; then
set_autoexpand
else
echo "Skipping autoexpanding process..."
fi
#Make sure filesystem is ok
checkFilesystem
if ! minsize=$(resize2fs -P "$loopback"); then
rc=$?
error $LINENO "resize2fs failed with rc $rc"
exit 10
fi
minsize=$(cut -d ':' -f 2 <<< "$minsize" | tr -d ' ')
logVariables $LINENO currentsize minsize
if [[ $currentsize -eq $minsize ]]; then
info "Filesystem already shrunk to smallest size. Skipping filesystem shrinking"
else
#Add some free space to the end of the filesystem
extra_space=$(($currentsize - $minsize))
logVariables $LINENO extra_space
for space in 5000 1000 100; do
if [[ $extra_space -gt $space ]]; then
minsize=$(($minsize + $space))
break
fi
done
logVariables $LINENO minsize
#Shrink filesystem
info "Shrinking filesystem"
if [ -z "$mountdir" ]; then
mountdir=$(mktemp -d)
fi
resize2fs -p "$loopback" $minsize
rc=$?
if (( $rc )); then
error $LINENO "resize2fs failed with rc $rc"
mount "$loopback" "$mountdir"
mv "$mountdir/etc/rc.local.bak" "$mountdir/etc/rc.local"
umount "$mountdir"
losetup -d "$loopback"
exit 12
else
info "Zeroing any free space left"
mount "$loopback" "$mountdir"
cat /dev/zero > "$mountdir/PiShrink_zero_file" 2>/dev/null
info "Zeroed $(ls -lh "$mountdir/PiShrink_zero_file" | cut -d ' ' -f 5)"
rm -f "$mountdir/PiShrink_zero_file"
umount "$mountdir"
fi
sleep 1
#Shrink partition
info "Shrinking partition"
partnewsize=$(($minsize * $blocksize))
newpartend=$(($partstart + $partnewsize))
logVariables $LINENO partnewsize newpartend
parted -s -a minimal "$img" rm "$partnum"
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
exit 13
fi
parted -s "$img" unit B mkpart "$parttype" "$partstart" "$newpartend"
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
exit 14
fi
#Truncate the file
info "Truncating image"
endresult=$(parted -ms "$img" unit B print free)
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
exit 15
fi
endresult=$(tail -1 <<< "$endresult" | cut -d ':' -f 2 | tr -d 'B')
endresult=$((endresult+2048*512))
logVariables $LINENO endresult
truncate -s "$endresult" "$img"
rc=$?
if (( $rc )); then
error $LINENO "truncate failed with rc $rc"
exit 16
fi
fi
# handle compression
if [[ -n $ziptool ]]; then
options=""
envVarname="${MYNAME^^}_${ziptool^^}" # PISHRINK_GZIP or PISHRINK_XZ environment variables allow to override all options for gzip or xz
[[ $parallel == true ]] && options="${ZIP_PARALLEL_OPTIONS[$ziptool]}"
[[ -v $envVarname ]] && options="${!envVarname}" # if environment variable defined use these options
[[ $verbose == true ]] && options="$options -v" # add verbose flag if requested
if [[ $parallel == true ]]; then
parallel_tool="${ZIP_PARALLEL_TOOL[$ziptool]}"
info "Using $parallel_tool on the shrunk image"
if ! $parallel_tool ${options} "$img"; then
rc=$?
error $LINENO "$parallel_tool failed with rc $rc"
exit 18
fi
else # sequential
info "Using $ziptool on the shrunk image"
if ! $ziptool ${options} "$img"; then
rc=$?
error $LINENO "$ziptool failed with rc $rc"
exit 19
fi
fi
img=$img.${ZIPEXTENSIONS[$ziptool]}
fi
aftersize=$(ls -lh "$img" | cut -d ' ' -f 5)
logVariables $LINENO aftersize
info "Shrunk $img from $beforesize to $aftersize"

455
pishrink.sh Executable file
View file

@ -0,0 +1,455 @@
#!/usr/bin/env bash
# Project: PiShrink
# Description: PiShrink is a bash script that automatically shrink a pi image that will then resize to the max size of the SD card on boot.
# Link: https://github.com/Drewsif/PiShrink
version="v24.10.23"
CURRENT_DIR="$(pwd)"
SCRIPTNAME="${0##*/}"
MYNAME="${SCRIPTNAME%.*}"
LOGFILE="${CURRENT_DIR}/${SCRIPTNAME%.*}.log"
REQUIRED_TOOLS="parted losetup tune2fs md5sum e2fsck resize2fs"
ZIPTOOLS=("gzip xz")
declare -A ZIP_PARALLEL_TOOL=( [gzip]="pigz" [xz]="xz" ) # parallel zip tool to use in parallel mode
declare -A ZIP_PARALLEL_OPTIONS=( [gzip]="-f9" [xz]="-T0" ) # options for zip tools in parallel mode
declare -A ZIPEXTENSIONS=( [gzip]="gz" [xz]="xz" ) # extensions of zipped files
function info() {
echo "$SCRIPTNAME: $1"
}
function error() {
echo -n "$SCRIPTNAME: ERROR occurred in line $1: "
shift
echo "$@"
}
function cleanup() {
if losetup "$loopback" &>/dev/null; then
losetup -d "$loopback"
fi
if [ "$debug" = true ]; then
local old_owner=$(stat -c %u:%g "$src")
chown "$old_owner" "$LOGFILE"
fi
}
function logVariables() {
if [ "$debug" = true ]; then
echo "Line $1" >> "$LOGFILE"
shift
local v var
for var in "$@"; do
eval "v=\$$var"
echo "$var: $v" >> "$LOGFILE"
done
fi
}
function checkFilesystem() {
info "Checking filesystem"
e2fsck -pf "$loopback"
(( $? < 4 )) && return
info "Filesystem error detected!"
info "Trying to recover corrupted filesystem"
e2fsck -y "$loopback"
(( $? < 4 )) && return
if [[ $repair == true ]]; then
info "Trying to recover corrupted filesystem - Phase 2"
e2fsck -fy -b 32768 "$loopback"
(( $? < 4 )) && return
fi
error $LINENO "Filesystem recoveries failed. Giving up..."
exit 9
}
function set_autoexpand() {
#Make pi expand rootfs on next boot
mountdir=$(mktemp -d)
partprobe "$loopback"
sleep 3
umount "$loopback" > /dev/null 2>&1
mount "$loopback" "$mountdir" -o rw
if (( $? != 0 )); then
info "Unable to mount loopback, autoexpand will not be enabled"
return
fi
if [ ! -d "$mountdir/etc" ]; then
info "/etc not found, autoexpand will not be enabled"
umount "$mountdir"
return
fi
if [[ ! -f "$mountdir/etc/rc.local" ]]; then
info "An existing /etc/rc.local was not found, autoexpand may fail..."
fi
if ! grep -q "## PiShrink https://github.com/Drewsif/PiShrink ##" "$mountdir/etc/rc.local"; then
echo "Creating new /etc/rc.local"
if [ -f "$mountdir/etc/rc.local" ]; then
mv "$mountdir/etc/rc.local" "$mountdir/etc/rc.local.bak"
fi
cat <<'EOFRC' > "$mountdir/etc/rc.local"
#!/bin/bash
## PiShrink https://github.com/Drewsif/PiShrink ##
do_expand_rootfs() {
ROOT_PART=$(mount | sed -n 's|^/dev/\(.*\) on / .*|\1|p')
PART_NUM=${ROOT_PART#mmcblk0p}
if [ "$PART_NUM" = "$ROOT_PART" ]; then
echo "$ROOT_PART is not an SD card. Don't know how to expand"
return 0
fi
# Get the starting offset of the root partition
PART_START=$(parted /dev/mmcblk0 -ms unit s p | grep "^${PART_NUM}" | cut -f 2 -d: | sed 's/[^0-9]//g')
[ "$PART_START" ] || return 1
# Return value will likely be error for fdisk as it fails to reload the
# partition table because the root fs is mounted
fdisk /dev/mmcblk0 <<EOF
p
d
$PART_NUM
n
p
$PART_NUM
$PART_START
p
w
EOF
cat <<EOF > /etc/rc.local &&
#!/bin/sh
echo "Expanding /dev/$ROOT_PART"
resize2fs /dev/$ROOT_PART
rm -f /etc/rc.local; cp -fp /etc/rc.local.bak /etc/rc.local && /etc/rc.local
EOF
reboot
exit
}
raspi_config_expand() {
/usr/bin/env raspi-config --expand-rootfs
if [[ $? != 0 ]]; then
return -1
else
rm -f /etc/rc.local; cp -fp /etc/rc.local.bak /etc/rc.local && /etc/rc.local
reboot
exit
fi
}
raspi_config_expand
echo "WARNING: Using backup expand..."
sleep 5
do_expand_rootfs
echo "ERROR: Expanding failed..."
sleep 5
if [[ -f /etc/rc.local.bak ]]; then
cp -fp /etc/rc.local.bak /etc/rc.local
/etc/rc.local
fi
exit 0
EOFRC
chmod +x "$mountdir/etc/rc.local"
fi
umount "$mountdir"
}
help() {
local help
read -r -d '' help << EOM
Usage: $0 [-adhnrsvzZ] imagefile.img [newimagefile.img]
-s Don't expand filesystem when image is booted the first time
-v Be verbose
-n Disable automatic update checking
-r Use advanced filesystem repair option if the normal one fails
-z Compress image after shrinking with gzip
-Z Compress image after shrinking with xz
-a Compress image in parallel using multiple cores
-d Write debug messages in a debug log file
EOM
echo "$help"
exit 1
}
should_skip_autoexpand=false
debug=false
update_check=true
repair=false
parallel=false
verbose=false
ziptool=""
while getopts ":adnhrsvzZ" opt; do
case "${opt}" in
a) parallel=true;;
d) debug=true;;
n) update_check=false;;
h) help;;
r) repair=true;;
s) should_skip_autoexpand=true ;;
v) verbose=true;;
z) ziptool="gzip";;
Z) ziptool="xz";;
*) help;;
esac
done
shift $((OPTIND-1))
if [ "$debug" = true ]; then
info "Creating log file $LOGFILE"
rm "$LOGFILE" &>/dev/null
exec 1> >(stdbuf -i0 -o0 -e0 tee -a "$LOGFILE" >&1)
exec 2> >(stdbuf -i0 -o0 -e0 tee -a "$LOGFILE" >&2)
fi
echo -e "PiShrink $version - https://github.com/Drewsif/PiShrink\n"
# Try and check for updates
if $update_check; then
latest_release=$(curl -m 5 https://api.github.com/repos/Drewsif/PiShrink/releases/latest 2>/dev/null | grep -i "tag_name" 2>/dev/null | awk -F '"' '{print $4}' 2>/dev/null)
if [[ $? ]] && [ "$latest_release" \> "$version" ]; then
echo "WARNING: You do not appear to be running the latest version of PiShrink. Head on over to https://github.com/Drewsif/PiShrink to grab $latest_release"
echo ""
fi
fi
#Args
src="$1"
img="$1"
#Usage checks
if [[ -z "$img" ]]; then
help
fi
if [[ ! -f "$img" ]]; then
error $LINENO "$img is not a file..."
exit 2
fi
if (( EUID != 0 )); then
error $LINENO "You need to be running as root."
exit 3
fi
# set locale to POSIX(English) temporarily
# these locale settings only affect the script and its sub processes
export LANGUAGE=POSIX
export LC_ALL=POSIX
export LANG=POSIX
# check selected compression tool is supported and installed
if [[ -n $ziptool ]]; then
if [[ ! " ${ZIPTOOLS[@]} " =~ $ziptool ]]; then
error $LINENO "$ziptool is an unsupported ziptool."
exit 17
else
if [[ $parallel == true && $ziptool == "gzip" ]]; then
REQUIRED_TOOLS="$REQUIRED_TOOLS pigz"
else
REQUIRED_TOOLS="$REQUIRED_TOOLS $ziptool"
fi
fi
fi
#Check that what we need is installed
for command in $REQUIRED_TOOLS; do
command -v $command >/dev/null 2>&1
if (( $? != 0 )); then
error $LINENO "$command is not installed."
exit 4
fi
done
#Copy to new file if requested
if [ -n "$2" ]; then
f="$2"
if [[ -n $ziptool && "${f##*.}" == "${ZIPEXTENSIONS[$ziptool]}" ]]; then # remove zip extension if zip requested because zip tool will complain about extension
f="${f%.*}"
fi
info "Copying $1 to $f..."
cp --reflink=auto --sparse=always "$1" "$f"
if (( $? != 0 )); then
error $LINENO "Could not copy file..."
exit 5
fi
old_owner=$(stat -c %u:%g "$1")
chown "$old_owner" "$f"
img="$f"
fi
# cleanup at script exit
trap cleanup EXIT
#Gather info
info "Gathering data"
beforesize="$(ls -lh "$img" | cut -d ' ' -f 5)"
parted_output="$(parted -ms "$img" unit B print)"
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
info "Possibly invalid image. Run 'parted $img unit B print' manually to investigate"
exit 6
fi
partnum="$(echo "$parted_output" | tail -n 1 | cut -d ':' -f 1)"
partstart="$(echo "$parted_output" | tail -n 1 | cut -d ':' -f 2 | tr -d 'B')"
if [ -z "$(parted -s "$img" unit B print | grep "$partstart" | grep logical)" ]; then
parttype="primary"
else
parttype="logical"
fi
loopback="$(losetup -f --show -o "$partstart" "$img")"
tune2fs_output="$(tune2fs -l "$loopback")"
rc=$?
if (( $rc )); then
echo "$tune2fs_output"
error $LINENO "tune2fs failed. Unable to shrink this type of image"
exit 7
fi
currentsize="$(echo "$tune2fs_output" | grep '^Block count:' | tr -d ' ' | cut -d ':' -f 2)"
blocksize="$(echo "$tune2fs_output" | grep '^Block size:' | tr -d ' ' | cut -d ':' -f 2)"
logVariables $LINENO beforesize parted_output partnum partstart parttype tune2fs_output currentsize blocksize
#Check if we should make pi expand rootfs on next boot
if [ "$parttype" == "logical" ]; then
echo "WARNING: PiShrink does not yet support autoexpanding of this type of image"
elif [ "$should_skip_autoexpand" = false ]; then
set_autoexpand
else
echo "Skipping autoexpanding process..."
fi
#Make sure filesystem is ok
checkFilesystem
if ! minsize=$(resize2fs -P "$loopback"); then
rc=$?
error $LINENO "resize2fs failed with rc $rc"
exit 10
fi
minsize=$(cut -d ':' -f 2 <<< "$minsize" | tr -d ' ')
logVariables $LINENO currentsize minsize
if [[ $currentsize -eq $minsize ]]; then
info "Filesystem already shrunk to smallest size. Skipping filesystem shrinking"
else
#Add some free space to the end of the filesystem
extra_space=$(($currentsize - $minsize))
logVariables $LINENO extra_space
for space in 5000 1000 100; do
if [[ $extra_space -gt $space ]]; then
minsize=$(($minsize + $space))
break
fi
done
logVariables $LINENO minsize
#Shrink filesystem
info "Shrinking filesystem"
if [ -z "$mountdir" ]; then
mountdir=$(mktemp -d)
fi
resize2fs -p "$loopback" $minsize
rc=$?
if (( $rc )); then
error $LINENO "resize2fs failed with rc $rc"
mount "$loopback" "$mountdir"
mv "$mountdir/etc/rc.local.bak" "$mountdir/etc/rc.local"
umount "$mountdir"
losetup -d "$loopback"
exit 12
else
info "Zeroing any free space left"
mount "$loopback" "$mountdir"
cat /dev/zero > "$mountdir/PiShrink_zero_file" 2>/dev/null
info "Zeroed $(ls -lh "$mountdir/PiShrink_zero_file" | cut -d ' ' -f 5)"
rm -f "$mountdir/PiShrink_zero_file"
umount "$mountdir"
fi
sleep 1
#Shrink partition
info "Shrinking partition"
partnewsize=$(($minsize * $blocksize))
newpartend=$(($partstart + $partnewsize))
logVariables $LINENO partnewsize newpartend
parted -s -a minimal "$img" rm "$partnum"
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
exit 13
fi
parted -s "$img" unit B mkpart "$parttype" "$partstart" "$newpartend"
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
exit 14
fi
#Truncate the file
info "Truncating image"
endresult=$(parted -ms "$img" unit B print free)
rc=$?
if (( $rc )); then
error $LINENO "parted failed with rc $rc"
exit 15
fi
endresult=$(tail -1 <<< "$endresult" | cut -d ':' -f 2 | tr -d 'B')
logVariables $LINENO endresult
truncate -s "$endresult" "$img"
rc=$?
if (( $rc )); then
error $LINENO "truncate failed with rc $rc"
exit 16
fi
fi
# handle compression
if [[ -n $ziptool ]]; then
options=""
envVarname="${MYNAME^^}_${ziptool^^}" # PISHRINK_GZIP or PISHRINK_XZ environment variables allow to override all options for gzip or xz
[[ $parallel == true ]] && options="${ZIP_PARALLEL_OPTIONS[$ziptool]}"
[[ -v $envVarname ]] && options="${!envVarname}" # if environment variable defined use these options
[[ $verbose == true ]] && options="$options -v" # add verbose flag if requested
if [[ $parallel == true ]]; then
parallel_tool="${ZIP_PARALLEL_TOOL[$ziptool]}"
info "Using $parallel_tool on the shrunk image"
if ! $parallel_tool ${options} "$img"; then
rc=$?
error $LINENO "$parallel_tool failed with rc $rc"
exit 18
fi
else # sequential
info "Using $ziptool on the shrunk image"
if ! $ziptool ${options} "$img"; then
rc=$?
error $LINENO "$ziptool failed with rc $rc"
exit 19
fi
fi
img=$img.${ZIPEXTENSIONS[$ziptool]}
fi
aftersize=$(ls -lh "$img" | cut -d ' ' -f 5)
logVariables $LINENO aftersize
info "Shrunk $img from $beforesize to $aftersize"