Finished the prep-sd-image script

- Full scrub of .config/
- Inject the virtual keyboard extension
- Auto-expand the filesystem on first boot
- A more robust e2fsck routine
- Run "sync" at key points in the script
- Shrink the root filesystem as much as possible
- Don't fail if the input image file is already as small as possible.
This commit is contained in:
David Carley
2022-09-08 02:27:57 +00:00
parent be4110e679
commit 1079256789
3 changed files with 163 additions and 55 deletions

View File

@@ -15,8 +15,7 @@ while true; do
xrdb /home/pi/.Xresources xrdb /home/pi/.Xresources
# Start browser # Start browser
/usr/local/bin/browser --no-first-run --disable-infobars \ /usr/local/bin/browser --no-first-run --disable-infobars --noerrdialogs --disable-3d-apis http://localhost/
--noerrdialogs --disable-3d-apis http://localhost/
fi fi
sleep 1 sleep 1

Binary file not shown.

View File

@@ -3,7 +3,7 @@
const merge = require("lodash.merge"); const merge = require("lodash.merge");
const { basename, resolve } = require("path"); const { basename, resolve } = require("path");
const { parseArgs } = require("node:util"); const { parseArgs } = require("node:util");
const { statSync, rmdirSync, copyFileSync, writeFileSync } = require("fs"); const { statSync, rmdirSync, copyFileSync, writeFileSync, readFileSync } = require("fs");
const { execSync } = require("child_process"); const { execSync } = require("child_process");
const { exit } = require("process"); const { exit } = require("process");
const { glob } = require("glob"); const { glob } = require("glob");
@@ -66,16 +66,7 @@ const USER_FILES = [
".pki", ".pki",
".ratpoison_history", ".ratpoison_history",
".Xauthority", ".Xauthority",
{ ".config",
pattern: ".config/**",
ignore: [
"**/home/pi/.config",
"**/home/pi/.config/chromium",
"**/home/pi/.config/chromium/Default",
"**/home/pi/.config/chromium/Default/Extensions",
"**/home/pi/.config/chromium/Default/Extensions/*"
]
},
"Downloads", "Downloads",
"splash.png" "splash.png"
]; ];
@@ -124,6 +115,7 @@ function main() {
shrinkPartition(target, loopback, meta); shrinkPartition(target, loopback, meta);
zerofree(loopback); zerofree(loopback);
truncateImage(target, meta); truncateImage(target, meta);
configureAutoExpand(target, meta);
compress(target, meta); compress(target, meta);
}); });
} catch (error) { } catch (error) {
@@ -223,41 +215,72 @@ function gatherMetadata(file) {
} }
}); });
const [ number, start, end, size, type, filesystem, flags ] = partedOutput const [ bootPartition, rootPartition ] = partedOutput
.split("\n") .split("\n")
.at(-1) .slice(-2)
.map(line => line
.trim() .trim()
.split(/\s+/) .split(/\s+/)
.map(col => parseInt(col) || col); .map(col => parseInt(col) || col)
)
.map(columns => ({
number: columns[0],
start: columns[1],
end: columns[2],
size: columns[3],
type: columns[4],
filesystem: columns[5],
flags: columns[6]
}));
return { return {
initialImageSize, initialImageSize,
rootPartition: { bootPartition,
number, rootPartition
start,
end,
size,
type,
filesystem,
flags
}
}; };
}); });
/*
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)"
partnewsize=$(($currentsize * $blocksize))
newpartend=$(($partstart + $partnewsize))
*/
} }
function checkAndRepair(loopback) { function checkAndRepair(loopback) {
return doStep("Checking the filesystem...", () => { return doStep("Checking the filesystem...", () => {
runCommand(`e2fsck -yf ${loopback}`, { let success = true;
stdio: "inherit"
runCommand(`e2fsck -pf "${loopback}"`, {
stdio: "inherit",
onError: error => {
success = error.status < 4;
if (error.status >= 4) {
info(`First e2fsck returned '${error.status}'.`);
}
}
}); });
if (!success) {
info("Trying harder to fix the image");
runCommand(`e2fsck -y "${loopback}"`, {
stdio: "inherit",
onError: error => {
success = error.status < 4;
if (error.status >= 4) {
info(`Second e2fsck returned '${error.status}'.`);
}
}
});
}
if (!success) {
info("The filesystem must be pretty damaged. Trying again with the alternate superblock.");
runCommand(`e2fsck -yf -b 32768 "${loopback}"`, {
stdio: "inherit",
onError: error => {
if (error.status >= 4) {
logErrorAndExit(`The final e2fsck attempt returned '${error.status}'. Giving up.`, error);
}
}
});
}
runCommand("sync");
}); });
} }
@@ -265,12 +288,11 @@ function prepareFilesystem(loopback) {
const mountpoint = runCommand("mktemp -d"); const mountpoint = runCommand("mktemp -d");
const finallyHandler = () => { const finallyHandler = () => {
info("Sleeping for 10 seconds, to allow the filesystem to flush");
runCommand("sleep 10");
info("Unmounting the filesystem"); info("Unmounting the filesystem");
runCommand(`umount "${mountpoint}"`); runCommand(`umount "${mountpoint}"`);
rmdirSync(mountpoint); rmdirSync(mountpoint);
runCommand("sync");
}; };
const unregister = registerSignalHandler(finallyHandler); const unregister = registerSignalHandler(finallyHandler);
@@ -278,7 +300,7 @@ function prepareFilesystem(loopback) {
doStep("Removing unnecessary files from the filesystem...", () => { doStep("Removing unnecessary files from the filesystem...", () => {
runCommand(`mount ${loopback} ${mountpoint}`); runCommand(`mount ${loopback} ${mountpoint}`);
scrub(mountpoint, SYSTEM_FILES); scrubFiles(mountpoint, SYSTEM_FILES);
scrubUserFiles(mountpoint, "/root"); scrubUserFiles(mountpoint, "/root");
scrubUserFiles(mountpoint, "/home/bbmc"); scrubUserFiles(mountpoint, "/home/bbmc");
scrubUserFiles(mountpoint, "/home/pi"); scrubUserFiles(mountpoint, "/home/pi");
@@ -297,7 +319,15 @@ function prepareFilesystem(loopback) {
variant_defaults.woodworker_x35 variant_defaults.woodworker_x35
), null, 4) ), null, 4)
); );
const virtualKeyboardZip = resolve(`${__dirname}/../installer/linux-packages/virtualKeyboard.zip`);
const userPiHome = resolve(`${mountpoint}/home/pi`);
runCommand(`unzip "${virtualKeyboardZip}" -d "${userPiHome}"`);
runCommand(`chown -R 1000:1000 ${userPiHome}/.config`);
}); });
runCommand("sync");
} finally { } finally {
finallyHandler(); finallyHandler();
unregister(); unregister();
@@ -306,12 +336,24 @@ function prepareFilesystem(loopback) {
function shrinkFilesystem(loopback) { function shrinkFilesystem(loopback) {
return doStep(`Shrinking the root filesystem`, () => { return doStep(`Shrinking the root filesystem`, () => {
// We run the shrink step multiple times, because
// each time, resize2fs can shrink it a little more,
// until eventually it can't.
//
// TODO: Switch to using pipes to both display the output and capture it
// We can then look at the output to determine when to stop, rather than
// using a fixed count for loop.
// See: https://stackoverflow.com/questions/22337446/how-to-wait-for-a-child-process-to-finish-in-node-js
for (let i = 0; i < 5; ++i) {
runCommand(`resize2fs -p "${loopback}" -M`, { runCommand(`resize2fs -p "${loopback}" -M`, {
stdio: "inherit", stdio: "inherit",
onError: error => { onError: error => {
logErrorAndExit("Error while resizing", error); logErrorAndExit("Error while resizing", error);
} }
}); });
runCommand("sync");
}
}); });
} }
@@ -333,6 +375,8 @@ function shrinkPartition(target, loopback, meta) {
runCommand(`parted -s "${target}" unit B mkpart "${meta.rootPartition.type}" "${meta.rootPartition.start}" "${newEnd}"`, { runCommand(`parted -s "${target}" unit B mkpart "${meta.rootPartition.type}" "${meta.rootPartition.start}" "${newEnd}"`, {
onError: error => logErrorAndExit("parted failed while recreating the root partition", error) onError: error => logErrorAndExit("parted failed while recreating the root partition", error)
}); });
runCommand("sync");
}); });
} }
@@ -342,29 +386,88 @@ function zerofree(loopback) {
runCommand(`zerofree -v "${loopback}"`, { runCommand(`zerofree -v "${loopback}"`, {
stdio: "inherit" stdio: "inherit"
}); });
runCommand("sync");
}); });
} }
function truncateImage(target, meta) { function truncateImage(target, meta) {
return doStep(`Shrinking the image`, () => { return doStep(`Shrinking the image`, () => {
const partedOutput = runCommand(`parted -s "${target}" unit B print free`, { const partedOutput = runCommand(`parted -sm "${target}" unit B print free`, {
onError: error => logErrorAndExit("parted failed while shrinking the image", error) onError: error => logErrorAndExit("parted failed while shrinking the image", error)
}); });
const [ startOfFreeSpace ] = partedOutput // The output of the parted command above will look something like this:
//
// BYT;
// image.img:31914983424B:file:512:512:msdos::;
// 1:16384B:1048575B:1032192B:free;
// 1:1048576B:135266303B:134217728B:fat32::boot;
// 2:135266304B:2002096639B:1866830336B:ext4::;
// 1:2002096640B:31914983423B:29912886784B:free;
//
// The format is:
// "number":"begin":"end":"size":"filesystem-type":"partition-name":"flags-set";
//
// We're interested in the last line only, to determine
// the start of the free space in the image, after the partitions
const [ , startOfFreeSpace, , , type ] = partedOutput
.split("\n") .split("\n")
.at(-1) .at(-1)
.trim() .replace(/^([^;]+);.*$/, "$1")
.split(/\s+/) .split(":")
.map(col => parseInt(col) || col); .map(col => parseInt(col) || col);
if (type !== "free") {
info("There is no free space after the root partition, skipping image shrinking.");
return;
}
runCommand(`truncate -s "${startOfFreeSpace}" "${target}"`); runCommand(`truncate -s "${startOfFreeSpace}" "${target}"`);
runCommand("sync");
const { size: newSize } = statSync(target); const { size: newSize } = statSync(target);
info(`Shrunk ${target} from ${meta.initialImageSize} to ${newSize}`); info(`Shrunk ${target} from ${meta.initialImageSize} to ${newSize}`);
}); });
} }
function configureAutoExpand(target, meta) {
const mountpoint = runCommand("mktemp -d");
return doStep("Configuring the root partition to autoexpand on first boot...", () => {
const loopback = runCommand(`losetup -f --show -o "${meta.bootPartition.start}" "${target}"`);
const finallyHandler = () => {
info("Unmounting the filesystem");
runCommand("sync");
runCommand(`umount "${mountpoint}"`);
runCommand(`losetup -d ${loopback}`);
rmdirSync(mountpoint);
runCommand("sync");
};
const unregister = registerSignalHandler(finallyHandler);
try {
runCommand(`mount ${loopback} ${mountpoint}`);
let cmdline = readFileSync(`${mountpoint}/cmdline.txt`, { encoding: "utf8" });
if (cmdline.match(/init_resize/)) {
logErrorAndExit("init_resize is already in /boot/cmdline.txt");
}
cmdline = `${cmdline.trim()} init=/usr/lib/raspi-config/init_resize.sh`;
writeFileSync(`${mountpoint}/cmdline.txt`, cmdline, { encoding: "utf8" });
} finally {
finallyHandler();
unregister();
}
});
}
function compress(target, meta) { function compress(target, meta) {
return doStep(`Compressing the image`, () => { return doStep(`Compressing the image`, () => {
runCommand(`xz -k9veT0 ${target}`, { runCommand(`xz -k9veT0 ${target}`, {
@@ -378,7 +481,7 @@ function compress(target, meta) {
}); });
} }
function scrub(mountpoint, patterns) { function scrubFiles(mountpoint, patterns) {
for (const _pattern of patterns) { for (const _pattern of patterns) {
const { pattern, ignore } = (typeof _pattern === "string") const { pattern, ignore } = (typeof _pattern === "string")
? { pattern: _pattern } ? { pattern: _pattern }
@@ -402,7 +505,7 @@ function scrub(mountpoint, patterns) {
} }
function scrubUserFiles(mountpoint, homedir) { function scrubUserFiles(mountpoint, homedir) {
scrub(mountpoint, USER_FILES.map(item => { scrubFiles(mountpoint, USER_FILES.map(item => {
if (typeof item === "string") { if (typeof item === "string") {
return `${homedir}/${item}`; return `${homedir}/${item}`;
} }
@@ -478,11 +581,17 @@ function logErrorAndExit(msg, error) {
); );
if (error) { if (error) {
console.error("Error:", { console.error("Error:", JSON.stringify({
name: error.name, name: error.name,
code: error.code, code: error.code,
status: error.status,
signal: error.signal,
error: error.error,
pid: error.pid,
output: error.output,
msg: error.msg,
message: error.message message: error.message
}); }, null, 4));
} }
exit(1); exit(1);