Created
March 18, 2025 05:35
-
-
Save josh-hemphill/42699580b5c6d5a6c9387659e1e230bb to your computer and use it in GitHub Desktop.
Provision raspberry pi with log2ram etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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