For anyone reading this in the future this works:
docker run --rm --privileged \
-v "$SYSTEM_IMAGE_DIR":/system-image:ro \
-v "$CLEO_IMAGE_DIR":/cleo-image \
-v "$CLEO_PROVISION_DIR":/cleo-provision:ro \
ubuntu:18.04 bash -c "
set -e
# Install tools
apt-get update -qq
apt-get install -y -qq android-tools-fsutils e2fsprogs >/dev/null
# Extract stock data partition contents
echo 'Extracting stock m0054-data-fs.ext4...'
mkdir -p /tmp/data_root
# Convert sparse to raw and mount
simg2img /system-image/m0054-data-fs.ext4 /tmp/stock_raw.ext4
mkdir -p /mnt/stock
mount -o loop,ro /tmp/stock_raw.ext4 /mnt/stock
# Copy all stock files
cp -a /mnt/stock/* /tmp/data_root/
umount /mnt/stock
rm /tmp/stock_raw.ext4
echo 'Stock contents copied.'
# Add cleo-provision directory
cp -a /cleo-provision /tmp/data_root/cleo-provision
echo 'Added cleo-provision directory'
# Create the ext4 image using same parameters as BitBake recipe:
# -s : sparse output
# -l SIZE : filesystem size in bytes
# -a /data : android mount point
# -b 4096 : block size
make_ext4fs -s -l $USERDATA_SIZE_EXT4 -a /data -b 4096 /cleo-image/m0054-data-fs.ext4 /tmp/data_root
echo ''
echo 'Created sparse ext4 image successfully'
"
You'll need to be on an arm64 architecture.