Tools for building the sdcard image
This commit is contained in:
42
scripts/create-sdcard-image.js
Executable file
42
scripts/create-sdcard-image.js
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const inquirer = require("inquirer");
|
||||||
|
const { statSync } = require("fs");
|
||||||
|
const { runCommand, initSignalHandlers, assertEffectiveRoot } = require("./util");
|
||||||
|
|
||||||
|
const IMAGE_FILENAME = "onefinity-controller.img";
|
||||||
|
|
||||||
|
initSignalHandlers();
|
||||||
|
main();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
assertEffectiveRoot();
|
||||||
|
|
||||||
|
const { uid } = statSync(".");
|
||||||
|
|
||||||
|
const devices = runCommand("df -T msdos")
|
||||||
|
.split("\n")
|
||||||
|
.map(line => {
|
||||||
|
const [ disk ] = line.split(/\s+/);
|
||||||
|
return {
|
||||||
|
name: line,
|
||||||
|
value: {
|
||||||
|
disk: disk,
|
||||||
|
device: disk.replace(/s\d$/, "")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { disk: { disk, device } } = await inquirer.prompt({
|
||||||
|
type: "list",
|
||||||
|
name: "disk",
|
||||||
|
choices: devices,
|
||||||
|
message: `Which device is the sdcard?`
|
||||||
|
});
|
||||||
|
|
||||||
|
runCommand(`diskutil unmount ${disk}`);
|
||||||
|
runCommand(`dd if=${device} of=${IMAGE_FILENAME} status=progress`, {
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
runCommand(`chown ${uid} 1f.img`);
|
||||||
|
}
|
||||||
66
scripts/prep-controller-for-imaging.js
Executable file
66
scripts/prep-controller-for-imaging.js
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const inquirer = require("inquirer");
|
||||||
|
const { runCommand, logErrorAndExit, initSignalHandlers, assertInstalled, info } = require("./util");
|
||||||
|
|
||||||
|
const PACKAGES_TO_PURGE = [
|
||||||
|
"dphys-swapfile",
|
||||||
|
"gdb",
|
||||||
|
"geoip-database",
|
||||||
|
"gnome-icon-theme",
|
||||||
|
"hostapd",
|
||||||
|
"libfreetype6-dev",
|
||||||
|
"libglib2.0-data",
|
||||||
|
"libraspberrypi-doc",
|
||||||
|
"mlocate",
|
||||||
|
"triggerhappy",
|
||||||
|
"zip",
|
||||||
|
];
|
||||||
|
|
||||||
|
let controller;
|
||||||
|
|
||||||
|
initSignalHandlers();
|
||||||
|
main();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
controller = process.argv[2] ?? "onefinity";
|
||||||
|
|
||||||
|
assertInstalled([ "sshpass", "ssh" ]);
|
||||||
|
|
||||||
|
const { password } = await inquirer.prompt({
|
||||||
|
type: "password",
|
||||||
|
name: "password",
|
||||||
|
message: `What is the password for ${controller}?`
|
||||||
|
});
|
||||||
|
|
||||||
|
process.env.SSHPASS = password;
|
||||||
|
|
||||||
|
ssh("echo sudo access confirmed", {
|
||||||
|
onError: () => {
|
||||||
|
logErrorAndExit([
|
||||||
|
"You must configure the 'bbmc` user for no-password sudo.",
|
||||||
|
"The secret is 'NOPASSWD:ALL'",
|
||||||
|
"See: https://www.cyberciti.biz/faq/linux-unix-running-sudo-command-without-a-password/"
|
||||||
|
].join("\n"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ssh("apt-get update");
|
||||||
|
ssh(`apt-get purge -y ${PACKAGES_TO_PURGE.join(" ")}`);
|
||||||
|
ssh("apt-get autoremove -y");
|
||||||
|
ssh("touch /root/.prep-controller-completed");
|
||||||
|
ssh("sed -i -E 's|NOPASSWD:ALL|ALL|' /etc/sudoers");
|
||||||
|
} catch (error) {
|
||||||
|
logErrorAndExit("An unexpected error occurred", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ssh(command, options) {
|
||||||
|
info(`Running "${command}"`);
|
||||||
|
|
||||||
|
return runCommand(`sshpass -e /usr/bin/ssh ${controller} "sudo ${command}"`, {
|
||||||
|
...options,
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const inquirer = require("inquirer");
|
||||||
const merge = require("lodash.merge");
|
const merge = require("lodash.merge");
|
||||||
const { basename, resolve } = require("path");
|
const { resolve } = require("path");
|
||||||
const { parseArgs } = require("node:util");
|
const { statSync, rmdirSync, copyFileSync, writeFileSync, readFileSync, existsSync, rmSync } = require("fs");
|
||||||
const { statSync, rmdirSync, copyFileSync, writeFileSync, readFileSync } = require("fs");
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
const { exit } = require("process");
|
const { exit } = require("process");
|
||||||
const { glob } = require("glob");
|
const { glob } = require("glob");
|
||||||
const packageJSON = require("../package.json");
|
const packageJSON = require("../package.json");
|
||||||
const config_defaults = require("../src/resources/onefinity_defaults.json");
|
const config_defaults = require("../src/resources/onefinity_defaults.json");
|
||||||
|
const { info, runCommand, logErrorAndExit, assertOS, assertEffectiveRoot, assertFileExists, assertInstalled, initSignalHandlers, registerSignalHandler } = require("./util");
|
||||||
|
|
||||||
const variant_defaults = {
|
const variant_defaults = {
|
||||||
machinist_x35: require("../src/resources/onefinity_machinist_x35_defaults.json"),
|
machinist_x35: require("../src/resources/onefinity_machinist_x35_defaults.json"),
|
||||||
@@ -17,18 +17,7 @@ const variant_defaults = {
|
|||||||
journeyman_x50: require("../src/resources/onefinity_journeyman_x50_defaults.json")
|
journeyman_x50: require("../src/resources/onefinity_journeyman_x50_defaults.json")
|
||||||
};
|
};
|
||||||
|
|
||||||
const ARGS_CONFIG = {
|
const ORIGINAL_IMAGE_FILENAME = "onefinity-controller.img";
|
||||||
options: {
|
|
||||||
input: {
|
|
||||||
type: "string",
|
|
||||||
short: "i"
|
|
||||||
},
|
|
||||||
help: {
|
|
||||||
type: "boolean",
|
|
||||||
short: "h"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const REQUIRED_TOOLS = [
|
const REQUIRED_TOOLS = [
|
||||||
"rsync",
|
"rsync",
|
||||||
@@ -43,417 +32,350 @@ const REQUIRED_TOOLS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const SYSTEM_FILES = [
|
const SYSTEM_FILES = [
|
||||||
"/var/swap",
|
|
||||||
"/tmp/*",
|
"/tmp/*",
|
||||||
"/usr/**/__pycache__",
|
"/usr/**/__pycache__",
|
||||||
"/usr/**/*.py[co]",
|
"/usr/**/*.py[co]",
|
||||||
"/usr/share/doc/*",
|
"/usr/share/doc/*",
|
||||||
"/var/@(cache|backups|log|tmp)/*",
|
"/var/@(cache|backups|log|tmp)/*",
|
||||||
"/var/lib/apt/lists/*",
|
"/var/lib/apt/lists/*",
|
||||||
"/var/lib/bbctrl/@(firmware|plans|upload)/*",
|
|
||||||
"/var/lib/bbctrl/@(config|gamepads).json",
|
"/var/lib/bbctrl/@(config|gamepads).json",
|
||||||
"/var/lib/dhcpcd5/*"
|
"/var/lib/bbctrl/@(firmware|plans|upload)/*",
|
||||||
|
"/var/lib/dhcpcd5/*",
|
||||||
|
"/var/swap",
|
||||||
];
|
];
|
||||||
|
|
||||||
const USER_FILES = [
|
const USER_FILES = [
|
||||||
".bash_history",
|
".bash_history",
|
||||||
".nano",
|
|
||||||
".cache",
|
".cache",
|
||||||
".lesshst",
|
|
||||||
".wget-hsts",
|
|
||||||
".viminfo",
|
|
||||||
".local",
|
|
||||||
".pki",
|
|
||||||
".ratpoison_history",
|
|
||||||
".Xauthority",
|
|
||||||
".config",
|
".config",
|
||||||
|
".lesshst",
|
||||||
|
".local",
|
||||||
|
".nano",
|
||||||
|
".pki",
|
||||||
|
".prep-controller-completed",
|
||||||
|
".ratpoison_history",
|
||||||
|
".viminfo",
|
||||||
|
".wget-hsts",
|
||||||
|
".Xauthority",
|
||||||
"Downloads",
|
"Downloads",
|
||||||
"splash.png"
|
"splash.png"
|
||||||
];
|
];
|
||||||
|
|
||||||
let signalHandlers = [];
|
initSignalHandlers();
|
||||||
|
|
||||||
function registerSignalHandler(cb) {
|
|
||||||
signalHandlers.push(cb);
|
|
||||||
|
|
||||||
return () => (signalHandlers = signalHandlers.filter(h => h === cb));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle(signal) {
|
|
||||||
console.log(`Received ${signal}`);
|
|
||||||
|
|
||||||
for (const handler of signalHandlers) {
|
|
||||||
handler(signal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("SIGTERM", handle);
|
|
||||||
process.on("SIGINT", handle);
|
|
||||||
process.on("SIGHUP", handle);
|
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
||||||
function main() {
|
async function main() {
|
||||||
try {
|
let meta;
|
||||||
const { values: { input, help } } = parseArgs(ARGS_CONFIG);
|
|
||||||
|
|
||||||
if (!input || help) {
|
const finallyHandler = () => {
|
||||||
displayUsageAndExit();
|
if (meta) {
|
||||||
|
if (existsSync(meta.imageFilePath)) {
|
||||||
|
rmSync(meta.imageFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(meta.compressedImageFilePath)) {
|
||||||
|
rmSync(meta.compressedImageFilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
const unregister = registerSignalHandler(finallyHandler);
|
||||||
|
|
||||||
|
try {
|
||||||
assertOS();
|
assertOS();
|
||||||
assertEffectiveRoot();
|
assertEffectiveRoot();
|
||||||
assertFileExists(input);
|
assertFileExists(ORIGINAL_IMAGE_FILENAME);
|
||||||
assertInstalled(REQUIRED_TOOLS);
|
assertInstalled(REQUIRED_TOOLS);
|
||||||
|
|
||||||
const target = createTargetFile(input);
|
const meta = prepareImage();
|
||||||
|
|
||||||
attachToLoopback(target, (loopback, meta) => {
|
await attachToLoopback(meta, "root", async (loopback) => {
|
||||||
checkAndRepair(loopback);
|
checkAndRepair(loopback);
|
||||||
prepareFilesystem(loopback);
|
await prepareFilesystem(loopback);
|
||||||
shrinkFilesystem(loopback);
|
|
||||||
shrinkPartition(target, loopback, meta);
|
|
||||||
zerofree(loopback);
|
|
||||||
truncateImage(target, meta);
|
|
||||||
configureAutoExpand(target, meta);
|
|
||||||
compress(target, meta);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await attachToLoopback(meta, "root", (loopback) => {
|
||||||
|
checkAndRepair(loopback);
|
||||||
|
shrinkFilesystem(loopback);
|
||||||
|
shrinkPartition(loopback, meta);
|
||||||
|
zerofree(loopback);
|
||||||
|
});
|
||||||
|
|
||||||
|
truncateImage(meta);
|
||||||
|
await configureAutoExpand(meta);
|
||||||
|
compress(meta);
|
||||||
|
moveImageFiles(meta);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
switch (error.code) {
|
finallyHandler();
|
||||||
case "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL":
|
unregister();
|
||||||
displayUsageAndExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertOS() {
|
function createImageFileCopy() {
|
||||||
if (process.platform !== "linux") {
|
const target = runCommand("mktemp --tmpdir --suffix=.img onefinity-raspi-XXXXXXXXXX");
|
||||||
logErrorAndExit("This script requires linux.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertEffectiveRoot() {
|
info(`Copying ${ORIGINAL_IMAGE_FILENAME} to ${target}...`);
|
||||||
if (process.geteuid() !== 0) {
|
|
||||||
logErrorAndExit("Please run this script as root");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertFileExists(file) {
|
runCommand(`rsync --times --progress ${ORIGINAL_IMAGE_FILENAME} ${target}`, {
|
||||||
const stats = statSync(file);
|
stdio: "inherit",
|
||||||
if (!stats.isFile) {
|
onError: error => {
|
||||||
logErrorAndExit(`${file} does not exist`);
|
logErrorAndExit(`Failed to copy ${ORIGINAL_IMAGE_FILENAME} to ${target}`, error);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertInstalled(tools) {
|
|
||||||
const missingTools = [];
|
|
||||||
|
|
||||||
for (const tool of tools) {
|
|
||||||
runCommand(`command -v ${tool}`, {
|
|
||||||
onError: () => missingTools.push(tool)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingTools.length) {
|
|
||||||
logErrorAndExit(`
|
|
||||||
This script requires some tools that are not installed.
|
|
||||||
|
|
||||||
Install them via:
|
|
||||||
apt-get install -y ${missingTools.join(" ")}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTargetFile(file) {
|
|
||||||
const target = `onefinity-raspi-${packageJSON.version}.img`;
|
|
||||||
|
|
||||||
return doStep(`Copying ${file} to ${target}...`, () => {
|
|
||||||
runCommand(`rsync --times --progress ${file} ${target}`, {
|
|
||||||
stdio: "inherit",
|
|
||||||
onError: error => {
|
|
||||||
logErrorAndExit(`Failed to copy ${file} to ${target}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return target;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachToLoopback(file, cb) {
|
|
||||||
const meta = gatherMetadata(file);
|
|
||||||
|
|
||||||
return doStep("Attaching the image to a loopback device...", () => {
|
|
||||||
const loopback = runCommand(`losetup -f --show -o "${meta.rootPartition.start}" "${file}"`);
|
|
||||||
|
|
||||||
const finallyHandler = () => runCommand(`losetup -d ${loopback}`);
|
|
||||||
const unregister = registerSignalHandler(finallyHandler);
|
|
||||||
|
|
||||||
try {
|
|
||||||
cb(loopback, meta);
|
|
||||||
} finally {
|
|
||||||
finallyHandler();
|
|
||||||
unregister();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gatherMetadata(file) {
|
async function attachToLoopback(meta, partition, cb) {
|
||||||
return doStep("Gathering info about the image...", () => {
|
info("Attaching the image to a loopback device...");
|
||||||
const { size: initialImageSize } = statSync(file);
|
|
||||||
|
|
||||||
const partedOutput = runCommand(`parted -s "${file}" unit B print`, {
|
const start = meta.partitions[partition].start;
|
||||||
onError: error => {
|
const loopback = runCommand(`losetup -f --show -o "${start}" "${meta.imageFilePath}"`);
|
||||||
logErrorAndExit(`
|
|
||||||
Error fetching disk image info.
|
|
||||||
|
|
||||||
'parted' failed with exitcode ${error.status}
|
const finallyHandler = () => runCommand(`losetup -d ${loopback}`);
|
||||||
|
|
||||||
Run 'parted ${file} unit B print' manually to investigate
|
|
||||||
`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ bootPartition, rootPartition ] = partedOutput
|
|
||||||
.split("\n")
|
|
||||||
.slice(-2)
|
|
||||||
.map(line => line
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.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 {
|
|
||||||
initialImageSize,
|
|
||||||
bootPartition,
|
|
||||||
rootPartition
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAndRepair(loopback) {
|
|
||||||
return doStep("Checking the filesystem...", () => {
|
|
||||||
let success = true;
|
|
||||||
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareFilesystem(loopback) {
|
|
||||||
const mountpoint = runCommand("mktemp -d");
|
|
||||||
|
|
||||||
const finallyHandler = () => {
|
|
||||||
info("Unmounting the filesystem");
|
|
||||||
runCommand(`umount "${mountpoint}"`);
|
|
||||||
rmdirSync(mountpoint);
|
|
||||||
|
|
||||||
runCommand("sync");
|
|
||||||
};
|
|
||||||
const unregister = registerSignalHandler(finallyHandler);
|
const unregister = registerSignalHandler(finallyHandler);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
doStep("Removing unnecessary files from the filesystem...", () => {
|
await cb(loopback);
|
||||||
runCommand(`mount ${loopback} ${mountpoint}`);
|
|
||||||
|
|
||||||
scrubFiles(mountpoint, SYSTEM_FILES);
|
|
||||||
scrubUserFiles(mountpoint, "/root");
|
|
||||||
scrubUserFiles(mountpoint, "/home/bbmc");
|
|
||||||
scrubUserFiles(mountpoint, "/home/pi");
|
|
||||||
});
|
|
||||||
|
|
||||||
doStep("Injecting files...", () => {
|
|
||||||
copyFileSync(
|
|
||||||
resolve(`${__dirname}/../installer/gcode/Team Onefinity.ngc`),
|
|
||||||
resolve(`${mountpoint}/var/lib/bbctrl/upload/Team Onefinity.ngc`)
|
|
||||||
);
|
|
||||||
|
|
||||||
writeFileSync(`${mountpoint}/var/lib/bbctrl/config.json`,
|
|
||||||
JSON.stringify(merge(
|
|
||||||
{},
|
|
||||||
config_defaults,
|
|
||||||
variant_defaults.woodworker_x35
|
|
||||||
), 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shrinkFilesystem(loopback) {
|
function prepareImage() {
|
||||||
return doStep(`Shrinking the root filesystem`, () => {
|
const imageFilePath = createImageFileCopy();
|
||||||
// We run the shrink step multiple times, because
|
|
||||||
// each time, resize2fs can shrink it a little more,
|
info("Gathering info about the image...");
|
||||||
// until eventually it can't.
|
const { size: initialImageSize } = statSync(imageFilePath);
|
||||||
//
|
|
||||||
// TODO: Switch to using pipes to both display the output and capture it
|
const partedOutput = runCommand(`parted -s "${imageFilePath}" unit B print`, {
|
||||||
// We can then look at the output to determine when to stop, rather than
|
onError: error => {
|
||||||
// using a fixed count for loop.
|
logErrorAndExit(`
|
||||||
// See: https://stackoverflow.com/questions/22337446/how-to-wait-for-a-child-process-to-finish-in-node-js
|
Error fetching disk image info.
|
||||||
for (let i = 0; i < 5; ++i) {
|
|
||||||
runCommand(`resize2fs -p "${loopback}" -M`, {
|
'parted' failed with exitcode ${error.status}
|
||||||
stdio: "inherit",
|
|
||||||
onError: error => {
|
Run 'parted ${imageFilePath} unit B print' manually to investigate
|
||||||
logErrorAndExit("Error while resizing", error);
|
`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ boot, root ] = partedOutput
|
||||||
|
.split("\n")
|
||||||
|
.slice(-2)
|
||||||
|
.map(line => line
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.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 {
|
||||||
|
initialImageSize,
|
||||||
|
imageFilePath,
|
||||||
|
compressedImageFilePath: getCompressedFilename(imageFilePath),
|
||||||
|
partitions: {
|
||||||
|
boot,
|
||||||
|
root
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndRepair(loopback) {
|
||||||
|
info("Checking the filesystem...");
|
||||||
|
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareFilesystem(loopback) {
|
||||||
|
await mountLoopback(loopback, async mountpoint => {
|
||||||
|
info("Removing unnecessary files from the filesystem...");
|
||||||
|
|
||||||
|
if (!existsSync(`${mountpoint}/root/.prep-controller-completed`)) {
|
||||||
|
const { proceed } = await inquirer.prompt({
|
||||||
|
type: "confirm",
|
||||||
|
name: "proceed",
|
||||||
|
message: [
|
||||||
|
"It looks like 'prep-controller-for-imaging.js' has not been run on this image.",
|
||||||
|
"Do you want to proceed anyway?"
|
||||||
|
].join("\n")
|
||||||
});
|
});
|
||||||
|
|
||||||
runCommand("sync");
|
if (!proceed) {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrubFiles(mountpoint, SYSTEM_FILES);
|
||||||
|
scrubUserFiles(mountpoint, "/root");
|
||||||
|
scrubUserFiles(mountpoint, "/home/bbmc");
|
||||||
|
scrubUserFiles(mountpoint, "/home/pi");
|
||||||
|
|
||||||
|
info("Injecting files...");
|
||||||
|
|
||||||
|
copyFileSync(
|
||||||
|
resolve(`${__dirname}/../installer/gcode/Team Onefinity.ngc`),
|
||||||
|
resolve(`${mountpoint}/var/lib/bbctrl/upload/Team Onefinity.ngc`)
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSync(`${mountpoint}/var/lib/bbctrl/config.json`,
|
||||||
|
JSON.stringify(merge(
|
||||||
|
{},
|
||||||
|
config_defaults,
|
||||||
|
variant_defaults.woodworker_x35
|
||||||
|
), 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`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function shrinkPartition(target, loopback, meta) {
|
function shrinkFilesystem(loopback) {
|
||||||
return doStep(`Shrinking the root partition`, () => {
|
info(`Shrinking the root filesystem`);
|
||||||
const tune2fsOutput = runCommand(`tune2fs -l "${loopback}"`, {
|
|
||||||
onError: error => logErrorAndExit("tune2fs failed. Unable to shrink this type of image", error)
|
// 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`, {
|
||||||
|
stdio: "inherit",
|
||||||
|
onError: error => {
|
||||||
|
logErrorAndExit("Error while resizing", error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [ , currentSize ] = tune2fsOutput.match(/^Block count:\s+(\d+)/m);
|
function shrinkPartition(loopback, meta) {
|
||||||
const [ , blockSize ] = tune2fsOutput.match(/^Block size:\s+(\d+)/m);
|
info(`Shrinking the root partition`);
|
||||||
const newSize = parseInt(currentSize) * parseInt(blockSize);
|
|
||||||
const newEnd = meta.rootPartition.start + newSize;
|
|
||||||
|
|
||||||
runCommand(`parted -s -a minimal "${target}" rm "${meta.rootPartition.number}"`, {
|
const tune2fsOutput = runCommand(`tune2fs -l "${loopback}"`, {
|
||||||
onError: error => logErrorAndExit("parted failed while deleting root partition", error)
|
onError: error => logErrorAndExit("tune2fs failed. Unable to shrink this type of image", error)
|
||||||
});
|
});
|
||||||
|
|
||||||
runCommand(`parted -s "${target}" unit B mkpart "${meta.rootPartition.type}" "${meta.rootPartition.start}" "${newEnd}"`, {
|
const root = meta.partitions.root;
|
||||||
onError: error => logErrorAndExit("parted failed while recreating the root partition", error)
|
const [ , currentSize ] = tune2fsOutput.match(/^Block count:\s+(\d+)/m);
|
||||||
});
|
const [ , blockSize ] = tune2fsOutput.match(/^Block size:\s+(\d+)/m);
|
||||||
|
const newSize = parseInt(currentSize) * parseInt(blockSize);
|
||||||
|
const newEnd = root.start + newSize;
|
||||||
|
|
||||||
runCommand("sync");
|
runCommand(`parted -s -a minimal "${meta.imageFilePath}" rm "${root.number}"`, {
|
||||||
|
onError: error => logErrorAndExit("parted failed while deleting root partition", error)
|
||||||
|
});
|
||||||
|
|
||||||
|
runCommand(`parted -s "${meta.imageFilePath}" unit B mkpart "${root.type}" "${root.start}" "${newEnd}"`, {
|
||||||
|
onError: error => logErrorAndExit("parted failed while recreating the root partition", error)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function zerofree(loopback) {
|
function zerofree(loopback) {
|
||||||
return doStep(`Setting empty blocks to zeros`, () => {
|
info(`Zeroing out empty blocks for better compression...`);
|
||||||
info("(This will take a bit - it can look like it's hung, have patience)");
|
info("(This will take a bit - it can look like it's hung, have patience)");
|
||||||
runCommand(`zerofree -v "${loopback}"`, {
|
|
||||||
stdio: "inherit"
|
|
||||||
});
|
|
||||||
|
|
||||||
runCommand("sync");
|
runCommand(`zerofree -v "${loopback}"`, {
|
||||||
|
stdio: "inherit"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncateImage(target, meta) {
|
function truncateImage(meta) {
|
||||||
return doStep(`Shrinking the image`, () => {
|
info(`Shrinking the image`);
|
||||||
const partedOutput = runCommand(`parted -sm "${target}" unit B print free`, {
|
|
||||||
onError: error => logErrorAndExit("parted failed while shrinking the image", error)
|
|
||||||
});
|
|
||||||
|
|
||||||
// The output of the parted command above will look something like this:
|
const partedOutput = runCommand(`parted -sm "${meta.imageFilePath}" unit B print free`, {
|
||||||
//
|
onError: error => logErrorAndExit("parted failed while shrinking the image", error)
|
||||||
// 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")
|
|
||||||
.at(-1)
|
|
||||||
.replace(/^([^;]+);.*$/, "$1")
|
|
||||||
.split(":")
|
|
||||||
.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("sync");
|
|
||||||
|
|
||||||
const { size: newSize } = statSync(target);
|
|
||||||
info(`Shrunk ${target} from ${meta.initialImageSize} to ${newSize}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
.at(-1)
|
||||||
|
.replace(/^([^;]+);.*$/, "$1")
|
||||||
|
.split(":")
|
||||||
|
.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}" "${meta.imageFilePath}"`);
|
||||||
|
|
||||||
|
const { size: newSize } = statSync(meta.imageFilePath);
|
||||||
|
info(`Shrank the image from ${meta.initialImageSize} to ${newSize}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureAutoExpand(target, meta) {
|
async function configureAutoExpand(meta) {
|
||||||
const mountpoint = runCommand("mktemp -d");
|
info("Configuring the root partition to autoexpand on first boot...");
|
||||||
|
|
||||||
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}`);
|
|
||||||
|
|
||||||
|
await attachToLoopback(meta, "boot", async (loopback) => {
|
||||||
|
await mountLoopback(loopback, mountpoint => {
|
||||||
let cmdline = readFileSync(`${mountpoint}/cmdline.txt`, { encoding: "utf8" });
|
let cmdline = readFileSync(`${mountpoint}/cmdline.txt`, { encoding: "utf8" });
|
||||||
if (cmdline.match(/init_resize/)) {
|
if (cmdline.match(/init_resize/)) {
|
||||||
logErrorAndExit("init_resize is already in /boot/cmdline.txt");
|
logErrorAndExit("init_resize is already in /boot/cmdline.txt");
|
||||||
@@ -461,24 +383,57 @@ function configureAutoExpand(target, meta) {
|
|||||||
|
|
||||||
cmdline = `${cmdline.trim()} init=/usr/lib/raspi-config/init_resize.sh`;
|
cmdline = `${cmdline.trim()} init=/usr/lib/raspi-config/init_resize.sh`;
|
||||||
writeFileSync(`${mountpoint}/cmdline.txt`, cmdline, { encoding: "utf8" });
|
writeFileSync(`${mountpoint}/cmdline.txt`, cmdline, { encoding: "utf8" });
|
||||||
} finally {
|
});
|
||||||
finallyHandler();
|
|
||||||
unregister();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function compress(target, meta) {
|
function compress(meta) {
|
||||||
return doStep(`Compressing the image`, () => {
|
info(`Compressing the image`);
|
||||||
runCommand(`xz -k9veT0 ${target}`, {
|
|
||||||
stdio: "inherit"
|
|
||||||
});
|
|
||||||
|
|
||||||
const compressed = `${target}.xz`;
|
runCommand(`xz -k9veT0 ${meta.imageFilePath}`, {
|
||||||
|
stdio: "inherit"
|
||||||
const { size: newSize } = statSync(compressed);
|
|
||||||
info(`Shrunk ${target} from ${meta.initialImageSize} to ${newSize} (${compressed})`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { size: oldSize } = statSync(meta.imageFilePath);
|
||||||
|
const compressed = getCompressedFilename(meta.imageFilePath);
|
||||||
|
|
||||||
|
const { size: newSize } = statSync(compressed);
|
||||||
|
info(`Compressed the image from ${oldSize} to ${newSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveImageFiles(meta) {
|
||||||
|
info("Finalizing...");
|
||||||
|
|
||||||
|
const finalImageName = `onefinity-raspi-${packageJSON.version}.img`;
|
||||||
|
const finalCompressedImageName = getCompressedFilename(finalImageName);
|
||||||
|
runCommand(`mv ${meta.imageFilePath} ${finalImageName}`);
|
||||||
|
runCommand(`mv ${meta.compressedImageFilePath} ${finalCompressedImageName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCompressedFilename(target) {
|
||||||
|
return `${target}.xz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountLoopback(loopback, cb) {
|
||||||
|
let mountpoint;
|
||||||
|
|
||||||
|
const finallyHandler = () => {
|
||||||
|
if (mountpoint) {
|
||||||
|
runCommand(`umount "${mountpoint}"`);
|
||||||
|
rmdirSync(mountpoint);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const unregister = registerSignalHandler(finallyHandler);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mountpoint = runCommand("mktemp --tmpdir -d onefinity-raspi-root-XXXXXXXXXX");
|
||||||
|
|
||||||
|
runCommand(`mount ${loopback} ${mountpoint}`);
|
||||||
|
await cb(mountpoint);
|
||||||
|
} finally {
|
||||||
|
finallyHandler();
|
||||||
|
unregister();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrubFiles(mountpoint, patterns) {
|
function scrubFiles(mountpoint, patterns) {
|
||||||
@@ -516,103 +471,3 @@ function scrubUserFiles(mountpoint, homedir) {
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function doStep(msg, cb) {
|
|
||||||
info(msg);
|
|
||||||
|
|
||||||
return cb();
|
|
||||||
}
|
|
||||||
|
|
||||||
function info(msg) {
|
|
||||||
console.log(`\n${msg}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function runCommand(command, _options) {
|
|
||||||
const options = {
|
|
||||||
encoding: "utf8",
|
|
||||||
..._options,
|
|
||||||
shell: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const { onError } = options;
|
|
||||||
delete options["onError"];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = execSync(command, options);
|
|
||||||
|
|
||||||
return (typeof result === "string")
|
|
||||||
? result.trim()
|
|
||||||
: result;
|
|
||||||
} catch (error) {
|
|
||||||
if (onError) {
|
|
||||||
onError(error);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logErrorAndExit(msg, error) {
|
|
||||||
const lines = msg.split("\n");
|
|
||||||
|
|
||||||
// Get rid of leading blank lines
|
|
||||||
while (lines.length) {
|
|
||||||
if (lines[0].trim().length == 0) {
|
|
||||||
lines.shift();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get rid of trailing blank lines
|
|
||||||
while (lines.length) {
|
|
||||||
if (lines.at(-1).trim().length == 0) {
|
|
||||||
lines.pop();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ whitespace ] = lines[0].match(/^\s*/);
|
|
||||||
|
|
||||||
console.error(lines
|
|
||||||
.map(line => line.replace(whitespace, "").trimEnd())
|
|
||||||
.join("\n")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error:", JSON.stringify({
|
|
||||||
name: error.name,
|
|
||||||
code: error.code,
|
|
||||||
status: error.status,
|
|
||||||
signal: error.signal,
|
|
||||||
error: error.error,
|
|
||||||
pid: error.pid,
|
|
||||||
output: error.output,
|
|
||||||
msg: error.msg,
|
|
||||||
message: error.message
|
|
||||||
}, null, 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayUsageAndExit() {
|
|
||||||
logErrorAndExit(`
|
|
||||||
Usage: ${basename(process.argv[1])} --input <sd-card-image-file>
|
|
||||||
|
|
||||||
This tool will:
|
|
||||||
- Check and repair the file system, if needed
|
|
||||||
- Remove unnecessary files from the root partition
|
|
||||||
- Shrink the root partition as much as possible
|
|
||||||
- Truncate the image file to be as small as possible
|
|
||||||
- Overwrite all filesystem blank space with zeros (better compression)
|
|
||||||
- Compress the output image
|
|
||||||
|
|
||||||
Two output files will be produced:
|
|
||||||
<sd-card-image-file>.shrunk.img (uncompressed)
|
|
||||||
<sd-card-image-file>.shrunk.img.xz (compressed)
|
|
||||||
`);
|
|
||||||
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|||||||
149
scripts/util.js
Normal file
149
scripts/util.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const { execSync } = require("child_process");
|
||||||
|
const { statSync } = require("fs");
|
||||||
|
const { exit } = require("process");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
info,
|
||||||
|
runCommand,
|
||||||
|
logErrorAndExit,
|
||||||
|
registerSignalHandler,
|
||||||
|
initSignalHandlers,
|
||||||
|
assertOS,
|
||||||
|
assertEffectiveRoot,
|
||||||
|
assertFileExists,
|
||||||
|
assertInstalled
|
||||||
|
};
|
||||||
|
|
||||||
|
let signalHandlers = [];
|
||||||
|
|
||||||
|
function registerSignalHandler(cb) {
|
||||||
|
signalHandlers.push(cb);
|
||||||
|
|
||||||
|
return () => (signalHandlers = signalHandlers.filter(h => h === cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSignalHandlers() {
|
||||||
|
process.on("SIGTERM", handle);
|
||||||
|
process.on("SIGINT", handle);
|
||||||
|
process.on("SIGHUP", handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle(signal) {
|
||||||
|
console.log(`Received ${signal}`);
|
||||||
|
|
||||||
|
for (const handler of signalHandlers) {
|
||||||
|
handler(signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(msg) {
|
||||||
|
console.log(`\n${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(command, _options) {
|
||||||
|
const options = {
|
||||||
|
encoding: "utf8",
|
||||||
|
..._options,
|
||||||
|
shell: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const { onError } = options;
|
||||||
|
delete options["onError"];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = execSync(command, options);
|
||||||
|
|
||||||
|
return (typeof result === "string")
|
||||||
|
? result.trim()
|
||||||
|
: result;
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logErrorAndExit(msg, error) {
|
||||||
|
const lines = msg.split("\n");
|
||||||
|
|
||||||
|
// Get rid of leading blank lines
|
||||||
|
while (lines.length) {
|
||||||
|
if (lines[0].trim().length == 0) {
|
||||||
|
lines.shift();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rid of trailing blank lines
|
||||||
|
while (lines.length) {
|
||||||
|
if (lines.at(-1).trim().length == 0) {
|
||||||
|
lines.pop();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ whitespace ] = lines[0].match(/^\s*/);
|
||||||
|
|
||||||
|
console.error(lines
|
||||||
|
.map(line => line.replace(whitespace, "").trimEnd())
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error:", JSON.stringify({
|
||||||
|
name: error.name,
|
||||||
|
code: error.code,
|
||||||
|
status: error.status,
|
||||||
|
signal: error.signal,
|
||||||
|
error: error.error,
|
||||||
|
pid: error.pid,
|
||||||
|
output: error.output,
|
||||||
|
msg: error.msg,
|
||||||
|
message: error.message
|
||||||
|
}, null, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertOS() {
|
||||||
|
if (process.platform !== "linux") {
|
||||||
|
logErrorAndExit("This script requires linux.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEffectiveRoot() {
|
||||||
|
if (process.geteuid() !== 0) {
|
||||||
|
logErrorAndExit("Please run this script as root");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFileExists(file) {
|
||||||
|
const stats = statSync(file);
|
||||||
|
if (!stats.isFile) {
|
||||||
|
logErrorAndExit(`${file} does not exist`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertInstalled(tools) {
|
||||||
|
const missingTools = [];
|
||||||
|
|
||||||
|
for (const tool of tools) {
|
||||||
|
runCommand(`command -v ${tool}`, {
|
||||||
|
onError: () => missingTools.push(tool)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingTools.length) {
|
||||||
|
logErrorAndExit(`
|
||||||
|
This script requires some tools that are not installed.
|
||||||
|
|
||||||
|
Install them via:
|
||||||
|
apt-get install -y ${missingTools.join(" ")}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user