@ 260802.2

260520.9 · 6c6f0cff28f6357e224f2b954cd88424a7ca021c

system: add convention for explicitly copying files

See the comment in the patch, but essentially, we occasionally need (or so I think) to install configuration files in `/etc` in a finicky way that really wants a file wholly distinct from one that is linked (symbolic or hard) to a file in `~/system`.

reading declarations

bin/rust/setup-system-links/src/main.rs

@@ -1,3 +1,5 @@+// 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"); @@ -41,8 +43,34 @@ impl Links {     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 lack the leading slash. We add that below.-        let mut all: BTreeMap<PathBuf, BTreeSet<String>> = BTreeMap::new();+        // 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())@@ -51,13 +79,6 @@ impl Links {                 continue;             } -            let machine_config_name = dent-                .file_name()-                .to_str()-                .with_context(|| {-                    format!("{} is not valid UTF-8", dent.path().display())-                })?-                .to_string();             // Should never fail since 'system' is our root and is guaranteed             // to be non-empty.             let dst = dent@@ -72,21 +93,39 @@ impl Links {                 // ~/system/README.md and are not part of our config.                 continue;             }-            all.entry(dst)-                .or_insert_with(|| BTreeSet::new())-                .insert(machine_config_name);++            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 &[hostname, "default"] {-                if configs.contains(attempt) {-                    let src = system.join(relative_dst).join(attempt);-                    links.push(Link { src, dst });-                    break;-                }+            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 })@@ -120,7 +159,7 @@ impl Links {             }         }         for link in to_link {-            link.link(&mut wtr)?;+            link.install(&mut wtr)?;         }         Ok(())     }@@ -141,7 +180,7 @@ impl Links {             }         }         for link in to_link {-            link.dry_link(&mut wtr)?;+            link.dry_install(&mut wtr)?;         }         Ok(())     }@@ -156,15 +195,29 @@ struct Link {     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_link<W: Write>(&self, mut wtr: W) -> io::Result<()> {+    fn dry_install<W: Write>(&self, mut wtr: W) -> io::Result<()> {+        let (prog, flags) = if self.copy { ("cp", "") } else { ("ln", " -s") };         writeln!(             wtr,-            "ln \"{}\" \"{}\"",+            "{prog}{flags} \"{}\" \"{}\"",             self.src.display(),             self.dst.display()         )@@ -188,10 +241,10 @@ impl Link {         )     } -    /// Link the source to the destination, overwriting the destination if it-    /// exists. Callers should ensure that the destination is backed up before-    /// executing.-    fn link<W: Write>(&self, wtr: W) -> anyhow::Result<()> {+    /// 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!(@@ -201,14 +254,24 @@ impl Link {                 )             })?;         }-        unix::fs::symlink(&self.src, &self.dst).with_context(|| {-            format!(-                "failed to link {} to {}",-                self.src.display(),-                self.dst.display()-            )-        })?;-        self.dry_link(wtr)?;+        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(())     } 

system/etc/pam.d/system-auth

deleted file mode 100644@@ -1,28 +0,0 @@-#%PAM-1.0--auth       required                    pam_faillock.so      preauth-# Optionally use requisite above if you do not want to prompt for the password-# on locked accounts.--auth      [success=3 default=ignore]  pam_systemd_home.so-auth       [success=2 default=ignore]  pam_fprintd.so-auth       [success=1 default=bad]     pam_unix.so          try_first_pass nullok-auth       [default=die]               pam_faillock.so      authfail-auth       optional                    pam_permit.so-auth       required                    pam_env.so-auth       required                    pam_faillock.so      authsucc-# If you drop the above call to pam_faillock.so the lock will be done also-# on non-consecutive authentication failures.---account   [success=1 default=ignore]  pam_systemd_home.so-account    required                    pam_unix.so-account    optional                    pam_permit.so-account    required                    pam_time.so---password  [success=1 default=ignore]  pam_systemd_home.so-password   required                    pam_unix.so          try_first_pass nullok shadow-password   optional                    pam_permit.so---session   optional                    pam_systemd_home.so-session    required                    pam_limits.so-session    required                    pam_unix.so-session    optional                    pam_permit.so

system/etc/pam.d/system-auth/copy:bart

new file mode 100644@@ -0,0 +1,28 @@+#%PAM-1.0++auth       required                    pam_faillock.so      preauth+# Optionally use requisite above if you do not want to prompt for the password+# on locked accounts.+-auth      [success=3 default=ignore]  pam_systemd_home.so+auth       [success=2 default=ignore]  pam_fprintd.so+auth       [success=1 default=bad]     pam_unix.so          try_first_pass nullok+auth       [default=die]               pam_faillock.so      authfail+auth       optional                    pam_permit.so+auth       required                    pam_env.so+auth       required                    pam_faillock.so      authsucc+# If you drop the above call to pam_faillock.so the lock will be done also+# on non-consecutive authentication failures.++-account   [success=1 default=ignore]  pam_systemd_home.so+account    required                    pam_unix.so+account    optional                    pam_permit.so+account    required                    pam_time.so++-password  [success=1 default=ignore]  pam_systemd_home.so+password   required                    pam_unix.so          try_first_pass nullok shadow+password   optional                    pam_permit.so++-session   optional                    pam_systemd_home.so+session    required                    pam_limits.so+session    required                    pam_unix.so+session    optional                    pam_permit.so