@ 260802.2

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/setup-system-links/. All files

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

new file mode 100644@@ -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

deleted file mode 100644@@ -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()-    );-}