about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--tools/eaglemode/default.nix15
-rw-r--r--tools/eaglemode/wrapper.go156
2 files changed, 171 insertions, 0 deletions
diff --git a/tools/eaglemode/default.nix b/tools/eaglemode/default.nix
index 5bab3155da1e..879235b62e05 100644
--- a/tools/eaglemode/default.nix
+++ b/tools/eaglemode/default.nix
@@ -9,6 +9,13 @@ let
   mkDesc = d: lib.concatMapStringsSep "\n"
     (x: "# Descr =${x}")
     (builtins.filter (s: s != "") (lib.splitString "\n" d));
+
+  configWrapper = pkgs.runCommand "eaglemode-config-wrapper" { } ''
+    cp ${./wrapper.go} wrapper.go
+    export HOME=$PWD
+    ${pkgs.go}/bin/go build wrapper.go
+    install -Dm755 wrapper $out/bin/wrapper
+  '';
 in
 rec {
   # mkCommand creates an Eagle Mode command for the file browser.
@@ -71,4 +78,12 @@ rec {
         lib.concatMapStringsSep "\n" (s: "cp -rT ${s} $out/\nchmod -R u+rw $out/\n") ([ "${eaglemode}/etc"] ++ extraPaths)
       }
     '';
+
+  # withConfig creates an Eagle Mode wrapper that runs it with the given
+  # configuration.
+  withConfig = { eaglemode ? pkgs.eaglemode, config }: pkgs.writeShellScriptBin "eaglemode" ''
+    set -ue
+    ${configWrapper}/bin/wrapper --em-config "${config}"
+    exec ${eaglemode}/bin/eaglemode "$@"
+  '';
 }
diff --git a/tools/eaglemode/wrapper.go b/tools/eaglemode/wrapper.go
new file mode 100644
index 000000000000..841642b6d93f
--- /dev/null
+++ b/tools/eaglemode/wrapper.go
@@ -0,0 +1,156 @@
+// Eagle Mode configuration wrapper that recreates the required directory
+// structure for Eagle Mode based on the output of depot.tools.eaglemode.etcDir
+//
+// This will replace *all* symlinks in the Eagle Mode configuration directory,
+// but it will not touch actual files. Missing folders will be created.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io/fs"
+	"log"
+	"os"
+	"os/user"
+	"path"
+	"path/filepath"
+	"strings"
+)
+
+func configDir() (string, error) {
+	v := os.Getenv("EM_USER_CONFIG_DIR")
+	if v != "" {
+		return v, nil
+	}
+
+	usr, err := user.Current()
+	if err != nil {
+		return "", fmt.Errorf("failed to get current user: %w", err)
+	}
+
+	return path.Join(usr.HomeDir, ".eaglemode"), nil
+}
+
+// cleanupConfig removes *all* existing symlinks in the configuration which do
+// not point into the right Nix store path.
+func cleanupConfig(conf string, dir string) (map[string]bool, error) {
+	// In case of first launch, we might have to create the directory.
+	_ = os.MkdirAll(dir, 0755)
+	c := 0
+
+	currentFiles := map[string]bool{}
+
+	walker := func(p string, d fs.DirEntry, e error) error {
+		if e != nil {
+			return fmt.Errorf("could not walk %s in config directory: %w", p, e)
+		}
+
+		if d.Type()&fs.ModeSymlink != 0 {
+			target, err := os.Readlink(p)
+			if err != nil {
+				return fmt.Errorf("could not read link for %s: %w", p, err)
+			}
+
+			if !strings.HasPrefix(target, conf) {
+				err = os.Remove(p)
+				c++
+				if err != nil {
+					return fmt.Errorf("could not remove stale link %q: %w", p, err)
+				}
+				log.Printf("removed stale symlink %q", p)
+			} else {
+				currentFiles[p] = false
+			}
+		}
+
+		if d.Type().IsRegular() {
+			currentFiles[p] = true
+		}
+
+		return nil
+	}
+
+	err := filepath.WalkDir(dir, walker)
+	if err != nil {
+		return nil, err
+	}
+
+	if c > 0 {
+		log.Printf("removed %v stale symlinks", c)
+	}
+
+	return currentFiles, nil
+}
+
+// linkConfig traverses the given Eagle Mode configuration and links everything
+// to the expected location in the user's configuration directory.
+//
+// If the user placed actual files in the configuration directory at paths that
+// would be overwritten, they will not be touched.
+func linkConfig(conf string, dir string, existing map[string]bool) error {
+	walker := func(p string, d fs.DirEntry, e error) error {
+		if e != nil {
+			return fmt.Errorf("could not walk %s in config directory: %w", p, e)
+		}
+
+		target := path.Join(dir, strings.TrimPrefix(p, conf))
+
+		if d.Type().IsDir() {
+			err := os.MkdirAll(target, 0755)
+			if err != nil {
+				return fmt.Errorf("could not create directory %q: %w", target, err)
+			}
+
+			return nil
+		}
+
+		if shadow, exists := existing[target]; exists {
+			if shadow {
+				log.Printf("WARN: file %q already exists and shadows a file from configuration", target)
+			}
+
+			return nil
+		}
+
+		err := os.Symlink(p, target)
+		if err != nil {
+			return fmt.Errorf("failed to link %q: %w", target, err)
+		}
+
+		return nil
+	}
+
+	return filepath.WalkDir(conf, walker)
+}
+
+func main() {
+	emConfig := flag.String("em-config", "", "path to em-config dir")
+
+	flag.Parse()
+	log.Println("verifying current Eagle Mode configuration")
+
+	if *emConfig == "" {
+		log.Fatalf("Eagle Mode configuration must be given")
+	}
+
+	if !strings.HasPrefix(*emConfig, "/nix/store/") {
+		log.Fatalf("Eagle Mode configuration must be in Nix store")
+	}
+
+	dir, err := configDir()
+	if err != nil {
+		log.Fatalf("could not determine Eagle Mode config dir: %v", err)
+	}
+
+	currentFiles, err := cleanupConfig(*emConfig, dir)
+	if err != nil {
+		log.Fatalf("failed to remove stale symlinks: %v", err)
+	}
+
+	err = linkConfig(*emConfig, dir, currentFiles)
+	if err != nil {
+		log.Fatalf("failed to link new configuration: %v", err)
+	}
+
+	log.Println("Eagle Mode configuration updated")
+}