260615.3 · 98792b1dab553d0ac9a206e40c10643d28cc57f5
*: a near complete overhaul of some fundamental components
Particularly, the `video` script and how Wingo boots. We consolidate much here and avoid separate init scripts for each machine. They were a huge pain to maintain and frequently got out of sync from one another.
We do still have some case analysis, which is annoying. But a foolish consistency is the hobgoblin of little minds.
reading declarations
Changes to bin/rust/. All files
@@ -10,10 +10,7 @@ Usage: See `freq --help` for more detail. */ -#![allow(warnings)]- use std::{- borrow::Cow, io::Write, path::{Path, PathBuf}, };@@ -368,8 +365,6 @@ impl FreqsOtherMany { fn into_table(self, config: &Config) -> Table { let mut table = Table::default(); for (units, freq) in self.map {- use std::fmt::Write;- table.total += freq; if !units.iter().all(|unit| config.include_unit(&unit)) { continue;
bin/rust/setup-system-links/Cargo.lock
@@ -1,39 +1,35 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing.+version = 4+ [[package]] name = "anyhow"-version = "1.0.32"+version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags"-version = "1.2.1"+version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr"-version = "0.2.13"+version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931"+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ "lazy_static", "memchr", "regex-automata", ] -[[package]]-name = "byteorder"-version = "1.3.4"-source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"- [[package]] name = "clap"-version = "2.33.3"+version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", "textwrap",@@ -42,24 +38,21 @@ dependencies = [ [[package]] name = "lazy_static"-version = "1.4.0"+version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "memchr"-version = "2.3.3"+version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "regex-automata"-version = "0.1.9"+version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"-dependencies = [- "byteorder",-]+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "same-file"@@ -92,48 +85,40 @@ dependencies = [ [[package]] name = "unicode-width"-version = "0.1.8"+version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "walkdir"-version = "2.3.1"+version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file",- "winapi", "winapi-util", ] [[package]]-name = "winapi"-version = "0.3.9"+name = "winapi-util"+version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [- "winapi-i686-pc-windows-gnu",- "winapi-x86_64-pc-windows-gnu",+ "windows-sys", ] [[package]]-name = "winapi-i686-pc-windows-gnu"-version = "0.4.0"+name = "windows-link"+version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]]-name = "winapi-util"-version = "0.1.5"+name = "windows-sys"+version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [- "winapi",+ "windows-link", ]--[[package]]-name = "winapi-x86_64-pc-windows-gnu"-version = "0.4.0"-source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
bin/rust/setup-system-links/Cargo.toml
@@ -4,6 +4,10 @@ version = "0.0.1" authors = ["Andrew Gallant <jamslam@gmail.com>"] edition = "2018" +[[bin]]+name = "setup-system-links"+path = "main.rs"+ [dependencies] anyhow = "1.0.32" bstr = "0.2.13"
bin/rust/setup-system-links/main.rs
@@ -0,0 +1,446 @@+// Add a convention that lets us avoid doing symlinks. e.g., For PAM.++#[cfg(not(unix))]+compile_error!("setup-system-links only works on Unix systems");++use std::collections::{BTreeMap, BTreeSet};+use std::env;+use std::ffi::{OsStr, OsString};+use std::io::{self, Write};+#[cfg(unix)]+use std::os::unix;+use std::path::{Path, PathBuf};++use anyhow::Context;+use bstr::{BString, ByteSlice, ByteVec};+use walkdir::WalkDir;++fn main() -> anyhow::Result<()> {+ let args = Args::parse()?;+ let mut links = Links::discover(&args.system, &args.hostname)?;+ if let Some(ref backup_dir) = args.backup {+ links.backup(backup_dir.to_owned());+ }+ if args.dry_run {+ links.dry_run(std::io::stdout())?;+ } else {+ links.run(std::io::stdout())?;+ }+ Ok(())+}++/// Represents a collection of links derived from this machine's ~/system+/// directory.+#[derive(Debug)]+struct Links {+ links: Vec<Link>,+ backup: Option<PathBuf>,+}++impl Links {+ /// Determine the full set of system config links for this machine given+ /// the system configuration directory and this machine's hostname.+ fn discover(system: &Path, hostname: &str) -> anyhow::Result<Links> {+ // Collect all leaf files in ~/system, creating a map, e.g.,+ // etc/default/grub |--> {default, duff, frink, ...}+ // Note that the key lacks a leading slash. We add that below.+ #[derive(Debug)]+ struct Leaf {+ machine_config_name: String,+ copy: bool,+ }++ impl Eq for Leaf {}++ impl PartialEq for Leaf {+ fn eq(&self, rhs: &Leaf) -> bool {+ self.machine_config_name == rhs.machine_config_name+ }+ }++ impl Ord for Leaf {+ fn cmp(&self, rhs: &Leaf) -> std::cmp::Ordering {+ self.machine_config_name.cmp(&rhs.machine_config_name)+ }+ }++ impl PartialOrd for Leaf {+ fn partial_cmp(&self, rhs: &Leaf) -> Option<std::cmp::Ordering> {+ Some(self.cmp(rhs))+ }+ }++ let mut all: BTreeMap<PathBuf, BTreeSet<Leaf>> = BTreeMap::new();+ for result in WalkDir::new(system) {+ let dent = result.with_context(|| {+ format!("{}: failed to walk directory", system.display())+ })?;+ if dent.file_type().is_dir() {+ continue;+ }++ // Should never fail since 'system' is our root and is guaranteed+ // to be non-empty.+ let dst = dent+ .path()+ .strip_prefix(system)+ .unwrap()+ .parent()+ .unwrap()+ .to_path_buf();+ if dst.as_os_str().is_empty() {+ // These are files directly in ~/system/ like+ // ~/system/README.md and are not part of our config.+ continue;+ }++ let machine_config_name =+ dent.file_name().to_str().with_context(|| {+ format!("{} is not valid UTF-8", dent.path().display())+ })?;+ let mut leaf = Leaf {+ machine_config_name: machine_config_name.to_string(),+ copy: false,+ };+ if let Some(hostname) = machine_config_name.strip_prefix("copy:") {+ leaf.machine_config_name = hostname.to_string();+ leaf.copy = true;+ }+ all.entry(dst).or_insert_with(|| BTreeSet::new()).insert(leaf);+ }+ // Now organize them into links by choosing the appropriate specific+ // config for this machine.+ let mut links = vec![];+ let attempts = [+ Leaf { machine_config_name: hostname.to_string(), copy: false },+ Leaf { machine_config_name: "default".to_string(), copy: false },+ ];+ for (relative_dst, configs) in all {+ let dst = Path::new("/").join(&relative_dst);+ for attempt in &attempts {+ let Some(config) = configs.get(attempt) else { continue };+ let src = system.join(relative_dst).join(if !config.copy {+ attempt.machine_config_name.to_string()+ } else {+ format!("copy:{}", attempt.machine_config_name)+ });+ links.push(Link { src, dst, copy: config.copy });+ break;+ }+ }+ Ok(Links { links, backup: None })+ }++ /// Indicates that the destination of each link should be backed up+ /// first.+ fn backup(&mut self, dir: PathBuf) {+ self.backup = Some(dir);+ }++ /// Create the links that do not yet exist.+ ///+ /// When backup is enabled (the default), then all config files will be+ /// backed up before attempting any link. If any backup fails, then no+ /// link will be created.+ ///+ /// The equivalent Unix command to each operation will be written to the+ /// given writer (like during a dry run).+ fn run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {+ let mut to_link = vec![];+ for link in &self.links {+ if link.exists()? {+ continue;+ }+ to_link.push(link);+ }+ if let Some(ref backup_dir) = self.backup {+ for link in &to_link {+ link.backup(&mut wtr, backup_dir)?;+ }+ }+ for link in to_link {+ link.install(&mut wtr)?;+ }+ Ok(())+ }++ /// Print the equivalent Unix commands that this would execute if we were+ /// running for real, but make no changes to the file system.+ fn dry_run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {+ let mut to_link = vec![];+ for link in &self.links {+ if link.exists()? {+ continue;+ }+ to_link.push(link);+ }+ if let Some(ref backup_dir) = self.backup {+ for link in &to_link {+ link.dry_backup(&mut wtr, backup_dir)?;+ }+ }+ for link in to_link {+ link.dry_install(&mut wtr)?;+ }+ Ok(())+ }+}++/// Represents a link between a file in ~/system and an actual system+/// configuration path on the system. e.g., ~/system/etc/default/grub/krusty+/// will link to /etc/default/grub on my 'krusty' laptop.+#[derive(Debug)]+struct Link {+ /// The ~/system/ path to this machine's config.+ src: PathBuf,+ /// The path to where this config lives on the system.+ dst: PathBuf,+ /// When true, we create a completely new file that is+ /// an identical copy of the source. This is needed in+ /// some cases (like for `/etc/pam.d/system-auth`) where+ /// symlinks are tricky because of permissions. Namely,+ /// the permission of the source file are relevant.+ ///+ /// We can't use hard-links either, because for PAM, files+ /// in `/etc/pam.d` need to be owned by root. But we don't+ /// want this for `~/system`, where files should be owned by+ /// the user. Hardlinks don't allow it.+ ///+ /// So... we copy.+ copy: bool,+}++impl Link {+ /// Write the equivalent Unix command we would execute if we were running+ /// for real.+ fn dry_install<W: Write>(&self, mut wtr: W) -> io::Result<()> {+ let (prog, flags) = if self.copy { ("cp", "") } else { ("ln", " -s") };+ writeln!(+ wtr,+ "{prog}{flags} \"{}\" \"{}\"",+ self.src.display(),+ self.dst.display()+ )+ }++ /// Write the equivalent Unix command we would execute to backup the config+ /// file if we were running for real.+ fn dry_backup<W: Write>(&self, mut wtr: W, dir: &Path) -> io::Result<()> {+ if !self.dst.exists() {+ return Ok(());+ }+ let backup_dst = dir.join(self.relative_dst());+ // OK since self.dst is guaranteed to be non-empty.+ let parent = backup_dst.parent().unwrap();+ writeln!(wtr, "mkdir -p \"{}\"", parent.display(),)?;+ writeln!(+ wtr,+ "cp \"{}\" \"{}\"",+ self.dst.display(),+ backup_dst.display()+ )+ }++ /// Link (or install) the source to the destination, overwriting the+ /// destination if it exists. Callers should ensure that the destination is+ /// backed up before executing.+ fn install<W: Write>(&self, wtr: W) -> anyhow::Result<()> {+ if self.dst.exists() {+ std::fs::remove_file(&self.dst).with_context(|| {+ format!(+ "failed to delete {} before creating symlink from {}",+ self.dst.display(),+ self.src.display()+ )+ })?;+ }+ if self.copy {+ std::fs::copy(&self.src, &self.dst).with_context(|| {+ format!(+ "failed to copy {} to {}",+ self.src.display(),+ self.dst.display()+ )+ })?;+ } else {+ unix::fs::symlink(&self.src, &self.dst).with_context(|| {+ format!(+ "failed to link {} to {}",+ self.src.display(),+ self.dst.display()+ )+ })?;+ }+ self.dry_install(wtr)?;+ Ok(())+ }++ /// Backup the destination to the directory given.+ fn backup<W: Write>(&self, wtr: W, dir: &Path) -> anyhow::Result<()> {+ if !self.dst.exists() {+ return Ok(());+ }++ let backup_dst = dir.join(self.relative_dst());+ // OK since self.dst is guaranteed to be non-empty.+ let parent = backup_dst.parent().unwrap();+ std::fs::create_dir_all(parent).with_context(|| {+ format!(+ "failed to create parent directory {} for backup",+ parent.display()+ )+ })?;+ std::fs::copy(&self.dst, &backup_dst).with_context(|| {+ format!(+ "failed to backup {} to {}",+ self.dst.display(),+ backup_dst.display()+ )+ })?;+ self.dry_backup(wtr, dir)?;+ Ok(())+ }++ /// Returns true if and only if this link already exists in the file+ /// system. That is, when the src and dst refer to the same underlying+ /// file.+ fn exists(&self) -> anyhow::Result<bool> {+ if !self.dst.exists() {+ return Ok(false);+ }+ same_file::is_same_file(&self.src, &self.dst).with_context(|| {+ format!(+ "failed to determine if {} == {}",+ self.src.display(),+ self.dst.display(),+ )+ })+ }++ /// Returns the destination path without its leading /.+ fn relative_dst(&self) -> &Path {+ // OK because dst is always absolute, by construction.+ self.dst.strip_prefix("/").unwrap()+ }+}++#[derive(Debug)]+struct Args {+ /// The directory in which to copy the contents of system configuration+ /// files before removing them and putting a symlink in its place.+ backup: Option<PathBuf>,+ /// When true, make no changes to the file system and instead print what+ /// we would do instead.+ dry_run: bool,+ /// The hostname of this machine (possibly derived from ~/.myhostname).+ hostname: String,+ /// The path to the directory containing this machine's system+ /// configuration. This is currently hard-coded to ~/system.+ system: PathBuf,+}++impl Args {+ fn parse() -> anyhow::Result<Args> {+ use clap::{crate_authors, crate_version, App, Arg};++ let parsed = App::new("Install system configuration symlinks.")+ .author(crate_authors!())+ .version(crate_version!())+ .max_term_width(80)+ .arg(Arg::with_name("no-backup").long("no-backup"))+ .arg(Arg::with_name("dry-run").long("dry-run"))+ .get_matches();++ let home = match env::var_os("HOME") {+ None => anyhow::bail!("HOME environment variable not set"),+ Some(home) => PathBuf::from(home),+ };+ let system = home.join("system");+ if !system.is_dir() {+ anyhow::bail!(+ "{}: system directory does not exist",+ system.display()+ );+ }++ let dry_run = parsed.is_present("dry-run");+ let hostname = runcmd(&["myhostname"])?;+ let backup = if parsed.is_present("no-backup") {+ None+ } else if dry_run {+ let parent = home.join("tmp").join("setup-system-links-backup");+ Some(parent.join("backup-{timestamp}"))+ } else {+ let parent = home.join("tmp").join("setup-system-links-backup");+ Some(+ create_unique_dir_in(parent, "backup")+ .context("failed to create backup directory")?,+ )+ };+ Ok(Args { backup, dry_run, hostname, system })+ }+}++fn runcmd<A: AsRef<OsStr>>(+ argv: impl IntoIterator<Item = A>,+) -> anyhow::Result<String> {+ let args: Vec<OsString> =+ argv.into_iter().map(|a| a.as_ref().to_os_string()).collect();+ let output = std::process::Command::new(&args[0])+ .args(&args[1..])+ .output()+ .context("failed to run 'myhostname'")?;+ if !output.status.success() {+ let stderr = BString::from(output.stderr.trim().to_vec());+ let args: Vec<Vec<u8>> = args+ .into_iter()+ .map(|a| Vec::from_os_str_lossy(&a).into_owned())+ .collect();+ let mut command = BString::from(bstr::join(" ", args));+ if let Some(code) = output.status.code() {+ command =+ BString::from(format!("`{}` (exit code {})", command, code));+ } else {+ command = BString::from(format!("`{}`", command));+ }+ anyhow::bail!("failed to run {}, stderr:\n\n{}", command, stderr)+ }++ let stdout = output+ .stdout+ .to_str()+ .context("hostname is not valid UTF-8")?+ .trim()+ .to_string();+ Ok(stdout)+}++fn create_unique_dir_in<P: AsRef<Path>, S: AsRef<str>>(+ parent: P,+ prefix: S,+) -> anyhow::Result<PathBuf> {+ use std::time::{Duration, SystemTime};++ let parent = parent.as_ref();+ let prefix = prefix.as_ref();+ std::fs::create_dir_all(&parent).with_context(|| {+ format!("{}: failed to create parent directory", parent.display())+ })?;+ for _ in 0..100 {+ let ts = SystemTime::now()+ .duration_since(SystemTime::UNIX_EPOCH)+ .context("system time is borked")?;+ let name =+ format!("{}-{}-{}", prefix, ts.as_secs(), ts.subsec_nanos());+ let dir = parent.join(name);+ if std::fs::create_dir(&dir).is_err() {+ std::thread::sleep(Duration::from_secs(1));+ continue;+ }+ return Ok(dir);+ }+ anyhow::bail!(+ "failed to create unique directory inside of {}",+ parent.display()+ );+}
bin/rust/setup-system-links/src/main.rs
@@ -1,446 +0,0 @@-// Add a convention that lets us avoid doing symlinks. e.g., For PAM.--#[cfg(not(unix))]-compile_error!("setup-system-links only works on Unix systems");--use std::collections::{BTreeMap, BTreeSet};-use std::env;-use std::ffi::{OsStr, OsString};-use std::io::{self, Write};-#[cfg(unix)]-use std::os::unix;-use std::path::{Path, PathBuf};--use anyhow::Context;-use bstr::{BString, ByteSlice, ByteVec};-use walkdir::WalkDir;--fn main() -> anyhow::Result<()> {- let args = Args::parse()?;- let mut links = Links::discover(&args.system, &args.hostname)?;- if let Some(ref backup_dir) = args.backup {- links.backup(backup_dir.to_owned());- }- if args.dry_run {- links.dry_run(std::io::stdout())?;- } else {- links.run(std::io::stdout())?;- }- Ok(())-}--/// Represents a collection of links derived from this machine's ~/system-/// directory.-#[derive(Debug)]-struct Links {- links: Vec<Link>,- backup: Option<PathBuf>,-}--impl Links {- /// Determine the full set of system config links for this machine given- /// the system configuration directory and this machine's hostname.- fn discover(system: &Path, hostname: &str) -> anyhow::Result<Links> {- // Collect all leaf files in ~/system, creating a map, e.g.,- // etc/default/grub |--> {default, duff, frink, ...}- // Note that the key lacks a leading slash. We add that below.- #[derive(Debug)]- struct Leaf {- machine_config_name: String,- copy: bool,- }-- impl Eq for Leaf {}-- impl PartialEq for Leaf {- fn eq(&self, rhs: &Leaf) -> bool {- self.machine_config_name == rhs.machine_config_name- }- }-- impl Ord for Leaf {- fn cmp(&self, rhs: &Leaf) -> std::cmp::Ordering {- self.machine_config_name.cmp(&rhs.machine_config_name)- }- }-- impl PartialOrd for Leaf {- fn partial_cmp(&self, rhs: &Leaf) -> Option<std::cmp::Ordering> {- Some(self.cmp(rhs))- }- }-- let mut all: BTreeMap<PathBuf, BTreeSet<Leaf>> = BTreeMap::new();- for result in WalkDir::new(system) {- let dent = result.with_context(|| {- format!("{}: failed to walk directory", system.display())- })?;- if dent.file_type().is_dir() {- continue;- }-- // Should never fail since 'system' is our root and is guaranteed- // to be non-empty.- let dst = dent- .path()- .strip_prefix(system)- .unwrap()- .parent()- .unwrap()- .to_path_buf();- if dst.as_os_str().is_empty() {- // These are files directly in ~/system/ like- // ~/system/README.md and are not part of our config.- continue;- }-- let machine_config_name =- dent.file_name().to_str().with_context(|| {- format!("{} is not valid UTF-8", dent.path().display())- })?;- let mut leaf = Leaf {- machine_config_name: machine_config_name.to_string(),- copy: false,- };- if let Some(hostname) = machine_config_name.strip_prefix("copy:") {- leaf.machine_config_name = hostname.to_string();- leaf.copy = true;- }- all.entry(dst).or_insert_with(|| BTreeSet::new()).insert(leaf);- }- // Now organize them into links by choosing the appropriate specific- // config for this machine.- let mut links = vec![];- let attempts = [- Leaf { machine_config_name: hostname.to_string(), copy: false },- Leaf { machine_config_name: "default".to_string(), copy: false },- ];- for (relative_dst, configs) in all {- let dst = Path::new("/").join(&relative_dst);- for attempt in &attempts {- let Some(config) = configs.get(attempt) else { continue };- let src = system.join(relative_dst).join(if !config.copy {- attempt.machine_config_name.to_string()- } else {- format!("copy:{}", attempt.machine_config_name)- });- links.push(Link { src, dst, copy: config.copy });- break;- }- }- Ok(Links { links, backup: None })- }-- /// Indicates that the destination of each link should be backed up- /// first.- fn backup(&mut self, dir: PathBuf) {- self.backup = Some(dir);- }-- /// Create the links that do not yet exist.- ///- /// When backup is enabled (the default), then all config files will be- /// backed up before attempting any link. If any backup fails, then no- /// link will be created.- ///- /// The equivalent Unix command to each operation will be written to the- /// given writer (like during a dry run).- fn run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {- let mut to_link = vec![];- for link in &self.links {- if link.exists()? {- continue;- }- to_link.push(link);- }- if let Some(ref backup_dir) = self.backup {- for link in &to_link {- link.backup(&mut wtr, backup_dir)?;- }- }- for link in to_link {- link.install(&mut wtr)?;- }- Ok(())- }-- /// Print the equivalent Unix commands that this would execute if we were- /// running for real, but make no changes to the file system.- fn dry_run<W: Write>(&self, mut wtr: W) -> anyhow::Result<()> {- let mut to_link = vec![];- for link in &self.links {- if link.exists()? {- continue;- }- to_link.push(link);- }- if let Some(ref backup_dir) = self.backup {- for link in &to_link {- link.dry_backup(&mut wtr, backup_dir)?;- }- }- for link in to_link {- link.dry_install(&mut wtr)?;- }- Ok(())- }-}--/// Represents a link between a file in ~/system and an actual system-/// configuration path on the system. e.g., ~/system/etc/default/grub/krusty-/// will link to /etc/default/grub on my 'krusty' laptop.-#[derive(Debug)]-struct Link {- /// The ~/system/ path to this machine's config.- src: PathBuf,- /// The path to where this config lives on the system.- dst: PathBuf,- /// When true, we create a completely new file that is- /// an identical copy of the source. This is needed in- /// some cases (like for `/etc/pam.d/system-auth`) where- /// symlinks are tricky because of permissions. Namely,- /// the permission of the source file are relevant.- ///- /// We can't use hard-links either, because for PAM, files- /// in `/etc/pam.d` need to be owned by root. But we don't- /// want this for `~/system`, where files should be owned by- /// the user. Hardlinks don't allow it.- ///- /// So... we copy.- copy: bool,-}--impl Link {- /// Write the equivalent Unix command we would execute if we were running- /// for real.- fn dry_install<W: Write>(&self, mut wtr: W) -> io::Result<()> {- let (prog, flags) = if self.copy { ("cp", "") } else { ("ln", " -s") };- writeln!(- wtr,- "{prog}{flags} \"{}\" \"{}\"",- self.src.display(),- self.dst.display()- )- }-- /// Write the equivalent Unix command we would execute to backup the config- /// file if we were running for real.- fn dry_backup<W: Write>(&self, mut wtr: W, dir: &Path) -> io::Result<()> {- if !self.dst.exists() {- return Ok(());- }- let backup_dst = dir.join(self.relative_dst());- // OK since self.dst is guaranteed to be non-empty.- let parent = backup_dst.parent().unwrap();- writeln!(wtr, "mkdir -p \"{}\"", parent.display(),)?;- writeln!(- wtr,- "cp \"{}\" \"{}\"",- self.dst.display(),- backup_dst.display()- )- }-- /// Link (or install) the source to the destination, overwriting the- /// destination if it exists. Callers should ensure that the destination is- /// backed up before executing.- fn install<W: Write>(&self, wtr: W) -> anyhow::Result<()> {- if self.dst.exists() {- std::fs::remove_file(&self.dst).with_context(|| {- format!(- "failed to delete {} before creating symlink from {}",- self.dst.display(),- self.src.display()- )- })?;- }- if self.copy {- std::fs::copy(&self.src, &self.dst).with_context(|| {- format!(- "failed to copy {} to {}",- self.src.display(),- self.dst.display()- )- })?;- } else {- unix::fs::symlink(&self.src, &self.dst).with_context(|| {- format!(- "failed to link {} to {}",- self.src.display(),- self.dst.display()- )- })?;- }- self.dry_install(wtr)?;- Ok(())- }-- /// Backup the destination to the directory given.- fn backup<W: Write>(&self, wtr: W, dir: &Path) -> anyhow::Result<()> {- if !self.dst.exists() {- return Ok(());- }-- let backup_dst = dir.join(self.relative_dst());- // OK since self.dst is guaranteed to be non-empty.- let parent = backup_dst.parent().unwrap();- std::fs::create_dir_all(parent).with_context(|| {- format!(- "failed to create parent directory {} for backup",- parent.display()- )- })?;- std::fs::copy(&self.dst, &backup_dst).with_context(|| {- format!(- "failed to backup {} to {}",- self.dst.display(),- backup_dst.display()- )- })?;- self.dry_backup(wtr, dir)?;- Ok(())- }-- /// Returns true if and only if this link already exists in the file- /// system. That is, when the src and dst refer to the same underlying- /// file.- fn exists(&self) -> anyhow::Result<bool> {- if !self.dst.exists() {- return Ok(false);- }- same_file::is_same_file(&self.src, &self.dst).with_context(|| {- format!(- "failed to determine if {} == {}",- self.src.display(),- self.dst.display(),- )- })- }-- /// Returns the destination path without its leading /.- fn relative_dst(&self) -> &Path {- // OK because dst is always absolute, by construction.- self.dst.strip_prefix("/").unwrap()- }-}--#[derive(Debug)]-struct Args {- /// The directory in which to copy the contents of system configuration- /// files before removing them and putting a symlink in its place.- backup: Option<PathBuf>,- /// When true, make no changes to the file system and instead print what- /// we would do instead.- dry_run: bool,- /// The hostname of this machine (possibly derived from ~/.myhostname).- hostname: String,- /// The path to the directory containing this machine's system- /// configuration. This is currently hard-coded to ~/system.- system: PathBuf,-}--impl Args {- fn parse() -> anyhow::Result<Args> {- use clap::{crate_authors, crate_version, App, Arg};-- let parsed = App::new("Install system configuration symlinks.")- .author(crate_authors!())- .version(crate_version!())- .max_term_width(80)- .arg(Arg::with_name("no-backup").long("no-backup"))- .arg(Arg::with_name("dry-run").long("dry-run"))- .get_matches();-- let home = match env::var_os("HOME") {- None => anyhow::bail!("HOME environment variable not set"),- Some(home) => PathBuf::from(home),- };- let system = home.join("system");- if !system.is_dir() {- anyhow::bail!(- "{}: system directory does not exist",- system.display()- );- }-- let dry_run = parsed.is_present("dry-run");- let hostname = runcmd(&["myhostname"])?;- let backup = if parsed.is_present("no-backup") {- None- } else if dry_run {- let parent = home.join("tmp").join("setup-system-links-backup");- Some(parent.join("backup-{timestamp}"))- } else {- let parent = home.join("tmp").join("setup-system-links-backup");- Some(- create_unique_dir_in(parent, "backup")- .context("failed to create backup directory")?,- )- };- Ok(Args { backup, dry_run, hostname, system })- }-}--fn runcmd<A: AsRef<OsStr>>(- argv: impl IntoIterator<Item = A>,-) -> anyhow::Result<String> {- let args: Vec<OsString> =- argv.into_iter().map(|a| a.as_ref().to_os_string()).collect();- let output = std::process::Command::new(&args[0])- .args(&args[1..])- .output()- .context("failed to run 'myhostname'")?;- if !output.status.success() {- let stderr = BString::from(output.stderr.trim().to_vec());- let args: Vec<Vec<u8>> = args- .into_iter()- .map(|a| Vec::from_os_str_lossy(&a).into_owned())- .collect();- let mut command = BString::from(bstr::join(" ", args));- if let Some(code) = output.status.code() {- command =- BString::from(format!("`{}` (exit code {})", command, code));- } else {- command = BString::from(format!("`{}`", command));- }- anyhow::bail!("failed to run {}, stderr:\n\n{}", command, stderr)- }-- let stdout = output- .stdout- .to_str()- .context("hostname is not valid UTF-8")?- .trim()- .to_string();- Ok(stdout)-}--fn create_unique_dir_in<P: AsRef<Path>, S: AsRef<str>>(- parent: P,- prefix: S,-) -> anyhow::Result<PathBuf> {- use std::time::{Duration, SystemTime};-- let parent = parent.as_ref();- let prefix = prefix.as_ref();- std::fs::create_dir_all(&parent).with_context(|| {- format!("{}: failed to create parent directory", parent.display())- })?;- for _ in 0..100 {- let ts = SystemTime::now()- .duration_since(SystemTime::UNIX_EPOCH)- .context("system time is borked")?;- let name =- format!("{}-{}-{}", prefix, ts.as_secs(), ts.subsec_nanos());- let dir = parent.join(name);- if std::fs::create_dir(&dir).is_err() {- std::thread::sleep(Duration::from_secs(1));- continue;- }- return Ok(dir);- }- anyhow::bail!(- "failed to create unique directory inside of {}",- parent.display()- );-}