about summary refs log tree commit diff
path: root/tools/eaglemode/wrapper.go
// 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")
}