forked from msrd0/mkalpiimg
initial commit
This commit is contained in:
commit
ddc990b522
13 changed files with 1679 additions and 0 deletions
23
src/main.rs
Normal file
23
src/main.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
mod setup;
|
||||
mod steps;
|
||||
|
||||
use crate::steps::{install_autostart, set_passwords};
|
||||
use setup::Setup;
|
||||
use steps::{init_first_boot, init_network, init_os, install_packages, prepare_img};
|
||||
|
||||
const IMAGE_SIZE_GB: u64 = 2;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let setup = Setup::load()?;
|
||||
|
||||
let img = prepare_img()?;
|
||||
init_os(&setup, &img)?;
|
||||
init_network(&img, &setup)?;
|
||||
install_packages(&img, &setup)?;
|
||||
install_autostart(&img, &setup)?;
|
||||
init_first_boot(&img)?;
|
||||
set_passwords(&img, &setup)?;
|
||||
|
||||
eprintln!("SUCCESS");
|
||||
Ok(())
|
||||
}
|
47
src/steps/init_first_boot.rs
Normal file
47
src/steps/init_first_boot.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use super::{run_chroot, Image};
|
||||
use std::fs;
|
||||
|
||||
pub fn init_first_boot(img: &Image) -> anyhow::Result<()> {
|
||||
eprintln!("Preparing first boot script ...");
|
||||
|
||||
fs::write(
|
||||
img.root.0.join("usr").join("local").join("bin").join("first-boot"),
|
||||
r#"#!/bin/sh
|
||||
set -xe
|
||||
|
||||
cat <<PARTED | sudo parted ---pretend-input-tty /dev/mmcblk0
|
||||
unit %
|
||||
resizepart 2
|
||||
Yes
|
||||
100%
|
||||
PARTED
|
||||
|
||||
partprobe
|
||||
resize2fs /dev/mmcblk0p2
|
||||
|
||||
rc-update add zram-init boot
|
||||
|
||||
rc-update del first-boot
|
||||
rm /etc/init.d/first-boot /usr/local/bin/first-boot
|
||||
|
||||
reboot
|
||||
"#
|
||||
)?;
|
||||
fs::write(
|
||||
img.root.0.join("etc").join("init.d").join("first-boot"),
|
||||
r#"#!/sbin/openrc-run
|
||||
command="/usr/local/bin/first-boot"
|
||||
command_background=false
|
||||
depend() {
|
||||
after modules
|
||||
need localmount
|
||||
}"#
|
||||
)?;
|
||||
run_chroot(
|
||||
img,
|
||||
"root",
|
||||
"chmod +x /etc/init.d/first-boot /usr/local/bin/first-boot; rc-update add first-boot"
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
111
src/steps/init_network.rs
Normal file
111
src/steps/init_network.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use super::{run_chroot, Image};
|
||||
use crate::setup::{network::WifiSecurity, Setup};
|
||||
use anyhow::Context as _;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write as _
|
||||
};
|
||||
|
||||
pub fn init_network(img: &Image, setup: &Setup) -> anyhow::Result<()> {
|
||||
eprintln!("Preparing network ...");
|
||||
|
||||
let mut interfaces = File::create(img.root.0.join("etc").join("network").join("interfaces"))?;
|
||||
for (name, ty) in [("lo", "loopback"), ("eth0", "dhcp")] {
|
||||
writeln!(interfaces, "auto {name}")?;
|
||||
writeln!(interfaces, "iface {name} inet {ty}")?;
|
||||
writeln!(interfaces)?;
|
||||
}
|
||||
writeln!(interfaces, "hostname {}", setup.os.host.hostname)?;
|
||||
fs::write(
|
||||
img.root.0.join("etc").join("resolv.conf"),
|
||||
r#"nameserver 1.0.0.1
|
||||
nameserver 1.1.1.1
|
||||
nameserver 2606:4700:4700::1001
|
||||
nameserver 2606:4700:4700::1111
|
||||
"#
|
||||
)?;
|
||||
run_chroot(img, "root", ["rc-update", "add", "networking", "default"])?;
|
||||
|
||||
if setup.network.tailscale {
|
||||
run_chroot(img, "root", "apk add tailscale; rc-update add tailscale default")?;
|
||||
fs::write(
|
||||
img.root.0.join("usr").join("local").join("bin").join("tailscale-up"),
|
||||
r#"#!/bin/sh
|
||||
for i in 0 1 2 3 4; do
|
||||
ping -c 2 -W 1 1.1.1.1 && break
|
||||
if [ "$i" == "4" ]; then exit 1; fi
|
||||
echo "Waiting for network ..."
|
||||
sleep 2s
|
||||
done
|
||||
|
||||
set -e
|
||||
|
||||
echo "Connecting to tailscale ..."
|
||||
timeout -v 2m tailscale up
|
||||
"#
|
||||
)?;
|
||||
fs::write(
|
||||
img.root.0.join("etc").join("init.d").join("tailscale-up"),
|
||||
r#"#!/sbin/openrc-run
|
||||
command="/usr/local/bin/tailscale-up"
|
||||
command_background=false
|
||||
depend() {
|
||||
after ntpd
|
||||
need tailscale
|
||||
}"#
|
||||
)?;
|
||||
run_chroot(
|
||||
img,
|
||||
"root",
|
||||
"chmod +x /usr/local/bin/tailscale-up /etc/init.d/tailscale-up; rc-update add tailscale-up default"
|
||||
)?;
|
||||
}
|
||||
|
||||
if !setup.network.wifi.is_empty() {
|
||||
run_chroot(img, "root", "echo 'brcmfmac' >> /etc/modules")?;
|
||||
run_chroot(img, "root", ["apk", "add", "iw", "iwd", "wireless-tools", "wireless-regdb"])?;
|
||||
run_chroot(img, "root", ["rc-update", "add", "iwd", "default"])?;
|
||||
|
||||
let iwd = img.root.0.join("etc").join("iwd");
|
||||
fs::create_dir_all(&iwd).context("Unable to create /etc/iwd directory")?;
|
||||
fs::write(
|
||||
iwd.join("main.conf"),
|
||||
r#"# main iwd configuration file
|
||||
|
||||
[General]
|
||||
EnableNetworkConfiguration=true
|
||||
|
||||
[Network]
|
||||
EnableIPv6=true
|
||||
NameResolvingService=none
|
||||
"#
|
||||
)
|
||||
.context("Unable to write /etc/iwd/main.conf")?;
|
||||
|
||||
let iwd = img.root.0.join("var").join("lib").join("iwd");
|
||||
fs::create_dir_all(&iwd).context("Unable to create /var/lib/iwd directory")?;
|
||||
for wifi in &setup.network.wifi {
|
||||
let ssid = &wifi.ssid;
|
||||
match &wifi.security {
|
||||
WifiSecurity::None => {
|
||||
fs::write(iwd.join(format!("{ssid}.open")), "\n")?;
|
||||
},
|
||||
WifiSecurity::WpaPsk { password } => {
|
||||
fs::write(iwd.join(format!("{ssid}.psk")), format!("[Security]\nPassphrase={password}\n"))?;
|
||||
},
|
||||
WifiSecurity::WpaEap { identity, password } => {
|
||||
let mut file = File::create(iwd.join(format!("{ssid}.8021x")))?;
|
||||
writeln!(file, "[Security]")?;
|
||||
writeln!(file, "EAP-Method=PWD")?;
|
||||
writeln!(file, "EAP-Identity={identity}")?;
|
||||
writeln!(file, "EAP-Password={password}")?;
|
||||
writeln!(file)?;
|
||||
writeln!(file, "[Settings]")?;
|
||||
writeln!(file, "AutoConnect=true")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
189
src/steps/init_os.rs
Normal file
189
src/steps/init_os.rs
Normal file
|
@ -0,0 +1,189 @@
|
|||
use super::{download, run, run_chroot, tempfile, Image};
|
||||
use crate::setup::Setup;
|
||||
use anyhow::Context as _;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write as _,
|
||||
path::Path
|
||||
};
|
||||
|
||||
pub fn init_os(setup: &Setup, img: &Image) -> anyhow::Result<()> {
|
||||
eprintln!("Preparing operating system ...");
|
||||
|
||||
// write raspberry pi config
|
||||
fs::write(img.boot.0.join("cmdline.txt"), &setup.os.rpi.cmdline)?;
|
||||
let mut config = File::create(img.boot.0.join("config.txt"))?;
|
||||
writeln!(
|
||||
config,
|
||||
r#"# Do not modify this file. Create and/or modify usercfg.txt instead.
|
||||
[pi3]
|
||||
kernel=vmlinuz-rpi
|
||||
initramfs initramfs-rpi
|
||||
[pi3+]
|
||||
kernel=vmlinuz-rpi
|
||||
initramfs initramfs-rpi
|
||||
[pi4]
|
||||
enable_gic=1
|
||||
kernel=vmlinuz-rpi4
|
||||
initramfs initramfs-rpi4
|
||||
[all]"#
|
||||
)?;
|
||||
if setup.os.alpine.arch.is64bit() {
|
||||
writeln!(config, "arm_64bit=1")?;
|
||||
}
|
||||
writeln!(config, "include usercfg.txt")?;
|
||||
drop(config);
|
||||
fs::write(img.boot.0.join("usercfg.txt"), &setup.os.rpi.usercfg)?;
|
||||
|
||||
// write initial apk config
|
||||
let etc = img.root.0.join("etc");
|
||||
let etc_apk = etc.join("apk");
|
||||
let etc_apk_keys = etc_apk.join("keys");
|
||||
fs::create_dir_all(&etc_apk_keys).context("Failed to create /etc/apk/keys directory")?;
|
||||
let mut repositories = File::create(etc_apk.join("repositories"))?;
|
||||
for repo in ["main", "community"] {
|
||||
writeln!(repositories, "{}/{}/{repo}", setup.os.alpine.mirror, setup.os.alpine.branch)?;
|
||||
}
|
||||
for repo in &setup.os.alpine.extra_repos {
|
||||
writeln!(repositories, "{repo}")?;
|
||||
}
|
||||
drop(repositories);
|
||||
|
||||
// download apk keys
|
||||
let keys = tempfile()?;
|
||||
download(
|
||||
keys.path(),
|
||||
&format!(
|
||||
"https://gitlab.alpinelinux.org/alpine/aports/-/archive/{branch}/aports-{branch}.tar.bz2?path=main/alpine-keys",
|
||||
branch = setup.os.alpine.branch.git_branch()
|
||||
)
|
||||
)?;
|
||||
run(&[
|
||||
"tar",
|
||||
"xfj",
|
||||
keys.path().to_str().unwrap(),
|
||||
"-C",
|
||||
etc_apk_keys.to_str().unwrap(),
|
||||
"--strip-components=3",
|
||||
"--wildcards",
|
||||
"*/alpine-devel@lists.alpinelinux.org-*.rsa.pub"
|
||||
])?;
|
||||
drop(keys);
|
||||
|
||||
// copy additional keys
|
||||
for key in &setup.os.alpine.extra_keys {
|
||||
let key = Path::new(key);
|
||||
let name = key.file_name().unwrap();
|
||||
fs::copy(Path::new("setup").join(key), etc_apk_keys.join(name))?;
|
||||
}
|
||||
|
||||
// bootstrap alpine
|
||||
run(&[
|
||||
"apk",
|
||||
"add",
|
||||
"--root",
|
||||
&img.root,
|
||||
"--update-cache",
|
||||
"--initdb",
|
||||
"--arch",
|
||||
setup.os.alpine.arch.to_str(),
|
||||
"agetty",
|
||||
"alpine-base",
|
||||
"alpine-sdk",
|
||||
"ca-certificates",
|
||||
"elogind",
|
||||
"eudev",
|
||||
"haveged",
|
||||
"linux-rpi",
|
||||
"linux-rpi4",
|
||||
"openssh",
|
||||
"parted",
|
||||
"raspberrypi-bootloader",
|
||||
"sudo",
|
||||
"tzdata",
|
||||
"udev-init-scripts",
|
||||
"util-linux-login",
|
||||
"zram-init"
|
||||
])?;
|
||||
run(&[
|
||||
"sed",
|
||||
"-E",
|
||||
"-e",
|
||||
"s,getty(.*)$,agetty --noclear\\1 linux,",
|
||||
"-i",
|
||||
etc.join("inittab").to_str().unwrap()
|
||||
])?;
|
||||
for service in ["udev", "udev-postmount", "udev-settle", "udev-trigger"] {
|
||||
run_chroot(img, "root", ["rc-update", "add", service, "sysinit"])?;
|
||||
}
|
||||
for service in ["modules", "syslog", "swclock"] {
|
||||
run_chroot(img, "root", ["rc-update", "add", service, "boot"])?;
|
||||
}
|
||||
for service in ["elogind", "haveged", "ntpd", "sshd"] {
|
||||
run_chroot(img, "root", ["rc-update", "add", service, "default"])?;
|
||||
}
|
||||
|
||||
// configure fstab
|
||||
fs::write(
|
||||
etc.join("fstab"),
|
||||
r#"# <device> <dir> <type> <options> <dump> <fsck>
|
||||
/dev/mmcblk0p1 /boot vfat defaults 0 2
|
||||
/dev/mmcblk0p2 / ext4 defaults,noatime 0 1
|
||||
"#
|
||||
)?;
|
||||
|
||||
// something is broken with abuild's functions.sh, so let's set CBUILD ourselves
|
||||
let profile_d = etc.join("profile.d");
|
||||
fs::create_dir_all(&profile_d)?;
|
||||
fs::write(
|
||||
profile_d.join("cbuild.sh"),
|
||||
&format!("export CBUILD={}", setup.os.alpine.arch.cbuild())
|
||||
)?;
|
||||
|
||||
// set up the locale
|
||||
run_chroot(img, "root", ["setup-hostname", &setup.os.host.hostname])?;
|
||||
run_chroot(img, "root", ["setup-timezone", "-z", &setup.os.host.timezone])?;
|
||||
if let Some(variant) = setup.os.host.keymap_variant.as_deref() {
|
||||
run_chroot(img, "root", ["setup-keymap", &setup.os.host.keymap, variant])?;
|
||||
} else {
|
||||
run_chroot(img, "root", ["setup-keymap", &setup.os.host.keymap])?;
|
||||
}
|
||||
|
||||
// set up a user that we can use to install packages
|
||||
run_chroot(img, "root", ["adduser", "-D", "mkalpiimg"])?;
|
||||
run_chroot(img, "root", ["adduser", "mkalpiimg", "abuild"])?;
|
||||
run_chroot(img, "root", "echo mkalpiimg:mkalpiimg | chpasswd")?;
|
||||
fs::write(etc.join("sudoers.d").join("mkalpiimg"), "%abuild ALL=(ALL) NOPASSWD: ALL")?;
|
||||
fs::create_dir_all(img.root.0.join("var").join("cache").join("distfiles"))?;
|
||||
run_chroot(img, "root", "chgrp abuild /var/cache/distfiles; chmod 775 /var/cache/distfiles")?;
|
||||
run_chroot(img, "mkalpiimg", ["abuild-keygen", "-a", "-n", "-b", "4096"])?;
|
||||
run_chroot(img, "root", "cp /home/mkalpiimg/.abuild/*.rsa.pub /etc/apk/keys/")?;
|
||||
|
||||
// avoid a bug with cargo in qemu: https://github.com/rust-lang/cargo/issues/10583
|
||||
let cargo = img.root.0.join("home").join("mkalpiimg").join(".cargo");
|
||||
fs::create_dir_all(&cargo)?;
|
||||
fs::write(cargo.join("config.toml"), "[net]\ngit-fetch-with-cli = true\n")?;
|
||||
|
||||
// set up the other users as requested
|
||||
fs::write(etc.join("sudoers.d").join("wheel"), "%wheel ALL=(ALL:ALL) ALL")?;
|
||||
for user in &setup.os.user {
|
||||
run_chroot(img, "root", ["adduser", "-s", "/bin/ash", "-D", &user.name])?;
|
||||
if user.sudo {
|
||||
run_chroot(img, "root", ["adduser", &user.name, "wheel"])?;
|
||||
}
|
||||
for grp in &user.groups {
|
||||
run_chroot(img, "root", ["adduser", &user.name, grp])?;
|
||||
}
|
||||
let ssh = img.root.0.join("home").join(&user.name).join(".ssh");
|
||||
fs::create_dir_all(&ssh)?;
|
||||
let mut keys = File::create(ssh.join("authorized_keys"))?;
|
||||
for key in &user.authorized_keys {
|
||||
writeln!(keys, "{key}")?;
|
||||
}
|
||||
drop(keys);
|
||||
run_chroot(img, "root", ["chown", "-R", &user.name, &format!("/home/{}/.ssh", user.name)])?;
|
||||
run_chroot(img, &user.name, "chmod 700 ~/.ssh; chmod 600 ~/.ssh/*")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
52
src/steps/install_packages.rs
Normal file
52
src/steps/install_packages.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use super::{run_chroot, Image};
|
||||
use crate::setup::Setup;
|
||||
|
||||
pub fn install_packages(img: &Image, setup: &Setup) -> anyhow::Result<()> {
|
||||
eprintln!("Installing packages ...");
|
||||
|
||||
for pkg in &setup.packages.install {
|
||||
run_chroot(img, "root", ["apk", "add", pkg])?;
|
||||
}
|
||||
|
||||
for service in &setup.packages.autostart {
|
||||
run_chroot(img, "root", ["rc-update", "add", service, "default"])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_autostart(img: &Image, setup: &Setup) -> anyhow::Result<()> {
|
||||
// TODO make the kiosk optional maybe?
|
||||
let kiosk = &setup.autostart.kiosk;
|
||||
eprintln!("Installing kiosk ...");
|
||||
run_chroot(img, "root", [
|
||||
"apk",
|
||||
"add",
|
||||
"cage",
|
||||
"firefox-esr",
|
||||
"font-noto",
|
||||
"mesa-dri-gallium",
|
||||
"polkit",
|
||||
"polkit-elogind"
|
||||
])?;
|
||||
run_chroot(img, "root", [
|
||||
"sed",
|
||||
"-E",
|
||||
"-i",
|
||||
"-e",
|
||||
&format!("/tty1/s,agetty,agetty --autologin {},", kiosk.user),
|
||||
"/etc/inittab"
|
||||
])?;
|
||||
run_chroot(img, &kiosk.user, "[ -e ~/.profile ] || echo '#!/bin/busybox ash' >~/.profile")?;
|
||||
run_chroot(
|
||||
img,
|
||||
&kiosk.user,
|
||||
format!(
|
||||
r#"echo 'if [ "$(tty)" == "/dev/tty1" ]; then dbus-run-session cage firefox-esr --kiosk "{}"; fi' >>~/.profile"#,
|
||||
kiosk.page
|
||||
)
|
||||
.as_str()
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
183
src/steps/mod.rs
Normal file
183
src/steps/mod.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use anyhow::{anyhow, bail, Context as _};
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
fs::File,
|
||||
io::{self, BufWriter},
|
||||
ops::Deref,
|
||||
os::unix::process::ExitStatusExt as _,
|
||||
path::{Path, PathBuf},
|
||||
process::Command
|
||||
};
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
|
||||
fn tempfile() -> io::Result<NamedTempFile> {
|
||||
NamedTempFile::new()
|
||||
}
|
||||
|
||||
fn run(cmd: &[&str]) -> anyhow::Result<()> {
|
||||
let display = format!("`\"{}\"`", cmd.join("\" \""));
|
||||
eprintln!(" -> Running {display} ...");
|
||||
|
||||
let status = match Command::new(cmd[0]).args(cmd.iter().skip(1)).status() {
|
||||
Ok(status) => status,
|
||||
Err(err) => bail!("Failed to run `\"{}\"`: {err}", cmd.join("\" \""))
|
||||
};
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"Failed to run `\"{}\"`: Process returned exit code {}",
|
||||
cmd.join("\" \""),
|
||||
status.into_raw()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_output(cmd: &[&str]) -> anyhow::Result<String> {
|
||||
let display = format!("`\"{}\"`", cmd.join("\" \""));
|
||||
eprintln!(" -> Running {display} ...");
|
||||
|
||||
let output = match Command::new(cmd[0]).args(cmd.iter().skip(1)).output() {
|
||||
Ok(output) => output,
|
||||
Err(err) => bail!("Failed to run {display}: {err}")
|
||||
};
|
||||
if !output.status.success() {
|
||||
bail!("Failed to run {display}: Process returned exit code {}", output.status.into_raw());
|
||||
}
|
||||
String::from_utf8(output.stdout).context("The output of {display} contained invalid characters")
|
||||
}
|
||||
|
||||
macro_rules! run_parted {
|
||||
($path:expr, $($arg:expr),+) => {
|
||||
run(&["parted", "-s", $path.to_str().unwrap(), $($arg,)+])
|
||||
};
|
||||
}
|
||||
|
||||
trait ChrootCmd {
|
||||
fn into_shell_cmd(self) -> String;
|
||||
}
|
||||
|
||||
impl<const N: usize> ChrootCmd for [&str; N] {
|
||||
fn into_shell_cmd(self) -> String {
|
||||
(&self[..]).into_shell_cmd()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChrootCmd for &[&str] {
|
||||
fn into_shell_cmd(self) -> String {
|
||||
let mut cmd = String::new();
|
||||
for arg in self {
|
||||
write!(cmd, "'{}' ", arg.replace('\'', "'\"'\"'")).unwrap();
|
||||
}
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
impl ChrootCmd for &str {
|
||||
fn into_shell_cmd(self) -> String {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run_chroot(img: &Image, user: &str, cmd: impl ChrootCmd) -> anyhow::Result<()> {
|
||||
let cmd = format!(". /etc/profile; set -euo pipefail; {}", cmd.into_shell_cmd());
|
||||
run(&[
|
||||
"chroot",
|
||||
&img.root,
|
||||
"/usr/bin/env",
|
||||
"-i",
|
||||
"sudo",
|
||||
"-u",
|
||||
user,
|
||||
"-i",
|
||||
"-n",
|
||||
"ash",
|
||||
"-c",
|
||||
&cmd
|
||||
])
|
||||
}
|
||||
|
||||
fn download(out: &Path, url: &str) -> anyhow::Result<()> {
|
||||
eprintln!(" -> Downloading {url} ...");
|
||||
attohttpc::get(url).send()?.write_to(BufWriter::new(File::create(out)?))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Image {
|
||||
path: PathBuf,
|
||||
boot: Mount,
|
||||
dev: Mount,
|
||||
root: Mount,
|
||||
device: LoopDevice
|
||||
}
|
||||
|
||||
impl Drop for Image {
|
||||
fn drop(&mut self) {
|
||||
eprintln!("Cleaning up ...");
|
||||
}
|
||||
}
|
||||
|
||||
struct LoopDevice(String);
|
||||
|
||||
impl LoopDevice {
|
||||
fn part(&self, num: usize) -> String {
|
||||
format!("{}p{num}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for LoopDevice {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LoopDevice {
|
||||
fn drop(&mut self) {
|
||||
let res = run(&["udisksctl", "loop-delete", "--no-user-interaction", "-b", self]);
|
||||
if let Err(err) = res {
|
||||
eprintln!("Failed to free loop device {}: {err}", self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Mount(PathBuf);
|
||||
|
||||
impl Mount {
|
||||
fn create(device: &str) -> anyhow::Result<Self> {
|
||||
let path = TempDir::new()?.into_path();
|
||||
run(&["mount", device, &path.to_string_lossy()])?;
|
||||
Ok(Self(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Mount {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.to_str().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Mount {
|
||||
fn drop(&mut self) {
|
||||
let res = run(&["umount", self]);
|
||||
if let Err(err) = res {
|
||||
eprintln!("Failed to unmount {}: {err}", self.0.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod init_first_boot;
|
||||
mod init_network;
|
||||
mod init_os;
|
||||
mod install_packages;
|
||||
mod prepare_img;
|
||||
mod set_passwords;
|
||||
|
||||
pub use init_first_boot::init_first_boot;
|
||||
pub use init_network::init_network;
|
||||
pub use init_os::init_os;
|
||||
pub use install_packages::{install_autostart, install_packages};
|
||||
pub use prepare_img::prepare_img;
|
||||
pub use set_passwords::set_passwords;
|
57
src/steps/prepare_img.rs
Normal file
57
src/steps/prepare_img.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use super::{run, run_output, Image, LoopDevice, Mount};
|
||||
use crate::IMAGE_SIZE_GB;
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::PathBuf
|
||||
};
|
||||
|
||||
pub fn prepare_img() -> anyhow::Result<Image> {
|
||||
eprintln!("Preparing empty image ...");
|
||||
|
||||
// let's create a file with the correct size
|
||||
let path = PathBuf::from("alpi.img");
|
||||
let file = File::create(&path).context("Failed to create alpi.img")?;
|
||||
file.set_len(IMAGE_SIZE_GB * 1024 * 1024 * 1024)?;
|
||||
drop(file);
|
||||
|
||||
// lets get the thing partitioned
|
||||
let size = format!("{IMAGE_SIZE_GB}G");
|
||||
run_parted!(path, "mktable", "msdos")?;
|
||||
run_parted!(path, "mkpart", "primary", "fat32", "0G", "128M")?;
|
||||
run_parted!(path, "mkpart", "primary", "ext4", "128M", &size)?;
|
||||
|
||||
// create a loopback device
|
||||
let output = run_output(&["udisksctl", "loop-setup", "--no-user-interaction", "-f", path.to_str().unwrap()])?;
|
||||
print!("{output}");
|
||||
let idx = output.find("/dev/loop").ok_or_else(|| anyhow!("Unable to identify loop device"))?;
|
||||
let mut end = idx + "/dev/loop".len();
|
||||
while (output.as_bytes()[end] as char).is_ascii_digit() {
|
||||
end += 1;
|
||||
}
|
||||
let device = LoopDevice(output[idx .. end].to_owned());
|
||||
|
||||
// TODO: Somehow create the file systems without root
|
||||
run(&["mkfs.fat", "-n", "ALPI-BOOT", "-F", "32", &device.part(1)])?;
|
||||
run(&["mkfs.ext4", "-q", "-F", "-L", "ALPI", "-O", "^has_journal", &device.part(2)])?;
|
||||
run_parted!(path, "p")?;
|
||||
|
||||
// mount everything
|
||||
let root = Mount::create(&device.part(2))?;
|
||||
let boot = root.0.join("boot");
|
||||
fs::create_dir_all(&boot)?;
|
||||
run(&["mount", &device.part(1), boot.to_str().unwrap()])?;
|
||||
let boot = Mount(boot);
|
||||
let dev = root.0.join("dev");
|
||||
fs::create_dir_all(&dev)?;
|
||||
run(&["mount", "udev", dev.to_str().unwrap(), "-t", "devtmpfs", "-o", "mode=0755,nosuid"])?;
|
||||
let dev = Mount(dev);
|
||||
|
||||
Ok(Image {
|
||||
path,
|
||||
device,
|
||||
boot,
|
||||
dev,
|
||||
root
|
||||
})
|
||||
}
|
21
src/steps/set_passwords.rs
Normal file
21
src/steps/set_passwords.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use super::{run_chroot, Image};
|
||||
use crate::setup::Setup;
|
||||
use anyhow::bail;
|
||||
|
||||
pub fn set_passwords(img: &Image, setup: &Setup) -> anyhow::Result<()> {
|
||||
eprintln!("Setting user passwords ...");
|
||||
|
||||
// the password is insecure by design, so we'll lock it for security
|
||||
run_chroot(img, "root", ["passwd", "-l", "mkalpiimg"])?;
|
||||
|
||||
for user in &setup.os.user {
|
||||
if let Some(password) = &user.password {
|
||||
if password.contains('\'') {
|
||||
bail!("Sorry I won't set passwords containing `'`");
|
||||
}
|
||||
run_chroot(img, "root", format!("echo '{}:{password}' | chpasswd", user.name).as_str())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue