Skip to content

Instantly share code, notes, and snippets.

@josh-hemphill
Created March 18, 2025 05:35
Show Gist options
  • Save josh-hemphill/42699580b5c6d5a6c9387659e1e230bb to your computer and use it in GitHub Desktop.
Save josh-hemphill/42699580b5c6d5a6c9387659e1e230bb to your computer and use it in GitHub Desktop.
Provision raspberry pi with log2ram etc
/// <reference types="E:\\Share\\dev\\pnpm\\node_modules\\@types\\node" />
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { createInterface } from 'node:readline';
import { createWriteStream } from 'node:fs';
const DRIVE_LETTER = 'F:';
// Check for --u6143 flag to bypass prompt
const isU6143 = process.argv.includes('--u6143');
// Create readline interface for prompts
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
// Helper function to prompt user
async function prompt(question: string): Promise<boolean> {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer.toLowerCase().startsWith('y'));
});
});
}
// Check if this is for a U6143 display-equipped Pi
let needsU6143Service = isU6143;
if (!isU6143) {
needsU6143Service = await prompt('Is this for a Pi with U6143 display? (y/N): ');
}
// Close readline interface after prompt
rl.close();
async function getFile(file: string) {
let resolvedFileLocation = resolve(file);
let original = await readFile(resolvedFileLocation, 'utf8')
.catch((err) => (console.log(err), false as const));
if (original === false) {
console.error("Could not read file");
process.exit(1);
}
return original;
}
async function setFile(file: string, content: string) {
let resolvedFileLocation = resolve(file);
let written = await writeFile(resolvedFileLocation, content, 'utf8')
.catch<void | Error>((err) => err);
if (written instanceof Error) {
console.error("Could not write file", written);
process.exit(1);
}
return written;
}
let SEARCH_TEXT = `# /boot/firmware/overlays/README`
let REPLACE_TEXT = `${SEARCH_TEXT}
# Hardware disable wifi and bluetooth
dtoverlay=disable-wifi
dtoverlay=disable-bt
# Increase response times from idle
arm_freq_min=900
arm_freq_max=1700
disable_splash=1`
const configPath = "F:\\config.txt";
let configTxt = await getFile(configPath);
if (!configTxt.includes('dtoverlay=disable-wifi')) {
configTxt = configTxt.replace(SEARCH_TEXT, REPLACE_TEXT);
configTxt = configTxt.replace("#dtparam=i2c_arm=on", "dtparam=i2c_arm=on");
configTxt = configTxt.replace("dtoverlay=vc4-kms-v3d", "dtoverlay=vc4-kms-v3d,nohdmi");
configTxt = configTxt.replace("dtparam=audio=on", "dtparam=audio=off");
await setFile(configPath, configTxt);
}
// Insert tmpfs configurations and remove unneeded services via firstrun.sh
SEARCH_TEXT = `rm -f /boot/firstrun.sh`
const firstRunCmd = `
{
# Disable auto-update for man-db
rm /var/lib/man-db/auto-update
# Disable auto-update for apt (we'll do this externally)
systemctl mask apt-daily-upgrade
systemctl mask apt-daily
systemctl disable apt-daily-upgrade.timer || true
systemctl disable apt-daily.timer || true
# Disable swap
dphys-swapfile swapoff
dphys-swapfile uninstall
update-rc.d dphys-swapfile remove
apt purge dphys-swapfile -y
# Disable unneeded services
systemctl disable bluetooth.service || true
systemctl disable hciuart.service || true
systemctl disable wpa_supplicant.service || true
systemctl disable triggerhappy.service || true
# Remove unneeded packages
apt-get remove --purge avahi-daemon triggerhappy modemmanager bluez wpasupplicant -y
apt autoremove --purge
# fstab changes
echo "tmpfs /tmp tmpfs defaults,noatime,nosuid,nodev 0 0" >> /etc/fstab
sed -i '/ \/ /s/defaults/defaults,commit=900/' /etc/fstab
# Set primary partition to always check for errors
mount | grep "on / " | awk '{print $1}' | xargs -I {} tune2fs -c 1 {}
# Edit journal size
sed -i 's/#SystemMaxUse=/SystemMaxUse=25M/' /etc/systemd/journald.conf
# Edit logrotate config
sed -i 's/weekly/daily/' /etc/logrotate.conf
sed -i 's/rotate 4/rotate 1/' /etc/logrotate.conf
sed -i 's/#compress/compress/' /etc/logrotate.conf
echo "size 1M" >> /etc/logrotate.conf
sed -i 's/monthly/weekly/g' /etc/logrotate.d/apt
sed -i 's/rotate 12/rotate 4/g' /etc/logrotate.d/apt
sed -i 's/monthly/weekly/g' /etc/logrotate.d/dpkg
sed -i 's/rotate 12/rotate 4/g' /etc/logrotate.d/dpkg
# Install log2ram
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ bookworm main" | tee /etc/apt/sources.list.d/azlux.list
cp /boot/firmware/offline/key-rings/azlux-archive-keyring.gpg /usr/share/keyrings/
dpkg -i /boot/firmware/offline/aarch64/log2ram.deb
# Enable reboot after kernel panic
echo "kernel.panic = 10" >> /etc/sysctl.d/90-kernelpanic-reboot.conf
${needsU6143Service ? `
# Enable I2C
raspi-config nonint do_i2c 0
# Install U6143 display service
dpkg -i /boot/firmware/offline/aarch64/display-u6143.deb
` : ''}
} > /dev/null 2>&1
${SEARCH_TEXT}
`
const firstRunPath = "F:\\firstrun.sh";
let firstRun = await getFile(firstRunPath);
// Check if the script has already been modified
if (firstRun.includes('rm /var/lib/man-db/auto-update')) {
console.log('Firstrun script already modified, skipping...');
} else {
firstRun = firstRun.replace(SEARCH_TEXT, firstRunCmd);
await setFile(firstRunPath, firstRun);
}
const cmdlinePath = "F:\\cmdline.txt";
let cmdline = await getFile(cmdlinePath);
cmdline += " fsck.mode=skip noswap";
await setFile(cmdlinePath, cmdline);
// Download function with caching
async function downloadAndCache(url: string, cacheDir: string, targetDir: string, filename: string, version?: string): Promise<void> {
// If no version provided, try to get it from GitHub API or Azlux repository
if (!version) {
if (url.includes('github.com')) {
try {
const repoMatch = url.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (repoMatch) {
const [, owner, repo] = repoMatch;
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, {
headers: {
'User-Agent': 'Node.js',
'Accept': 'application/vnd.github.v3+json'
}
});
if (response.ok) {
const release = await response.json();
version = release.tag_name;
// Replace 'latest' in URL with actual version
url = url.replace(/\/latest\//, `/${version}/`);
}
}
} catch (err) {
console.warn('Failed to get version from GitHub:', err);
}
} else if (url.includes('packages.azlux.fr')) {
try {
const response = await fetch(url);
if (response.ok) {
const html = await response.text();
// Extract all log2ram deb files from the HTML
const debMatches = html.match(/log2ram_\d+\.\d+(?:\.\d+)?(?:b\d+)?_all\.deb/g);
if (debMatches) {
// Parse versions and find the highest
const versions = debMatches.map(match => {
const versionMatch = match.match(/log2ram_(\d+\.\d+(?:\.\d+)?(?:b\d+)?)_all\.deb/);
if (!versionMatch) return null;
// Clean version string - remove any alpha characters and everything after
const version = versionMatch[1].replace(/[a-zA-Z].*$/, '');
// Ensure it's a valid version number
return /^\d+\.\d+(?:\.\d+)?$/.test(version) ? version : null;
}).filter((v): v is string => v !== null);
if (versions.length > 0) {
// Sort versions in descending order
versions.sort((a, b) => {
const [aMajor, aMinor, aPatch = '0'] = a.split('.').map(Number);
const [bMajor, bMinor, bPatch = '0'] = b.split('.').map(Number);
if (aMajor !== bMajor) return bMajor - aMajor;
if (aMinor !== bMinor) return bMinor - aMinor;
return Number(bPatch) - Number(aPatch);
});
version = versions[0];
// Update URL with the latest version
url = url.replace(/\/$/, `/log2ram_${version}_all.deb`);
}
}
}
} catch (err) {
console.warn('Failed to get version from Azlux repository:', err);
}
}
}
// Set up paths
const cacheFilename = version ? `${filename}-${version}` : filename;
const cachedFile = resolve(cacheDir, cacheFilename);
const targetFile = resolve(targetDir, filename);
// Create directories
await mkdir(cacheDir, { recursive: true });
await mkdir(targetDir, { recursive: true });
// Check if cached version exists
let needsDownload = true;
try {
await access(cachedFile);
needsDownload = false;
} catch { }
// Download if needed
if (needsDownload) {
console.log(`Downloading ${filename}${version ? ` version ${version}` : ''}...`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download: ${response.status}`);
}
const file = createWriteStream(cachedFile);
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Failed to get response body reader');
}
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
file.write(value);
}
file.end();
await new Promise(resolve => file.on('finish', resolve));
} finally {
file.close();
reader.releaseLock();
}
}
// Copy to target
await copyFile(cachedFile, targetFile)
.catch(err => {
console.error(`Failed to copy ${filename} to target location:`, err);
process.exit(1);
});
}
if (needsU6143Service) {
// Download U6143 display deb
const debUrl = 'https://github.com/josh-hemphill/U6143_ssd1306/releases/download/latest/display-u6143.deb';
await downloadAndCache(
debUrl,
resolve('.cache'),
resolve(DRIVE_LETTER, 'offline', 'aarch64'),
'display-u6143.deb'
);
}
// Download Azlux keyring
const keyringUrl = 'https://azlux.fr/repo.gpg';
const keyringDirPath = resolve(DRIVE_LETTER, 'offline', 'key-rings');
await mkdir(keyringDirPath, { recursive: true });
await downloadAndCache(
keyringUrl,
resolve('.cache'),
keyringDirPath,
'azlux-archive-keyring.gpg'
);
// Download log2ram deb
const log2ramUrl = 'https://packages.azlux.fr/debian/pool/main/l/log2ram/';
await downloadAndCache(
log2ramUrl,
resolve('.cache'),
resolve(DRIVE_LETTER, 'offline', 'aarch64'),
'log2ram.deb'
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment