package main import ( "bufio" "bytes" _ "embed" "errors" "flag" "fmt" "log" "os" "os/exec" "os/user" "path" "runtime" "strconv" "strings" "syscall" "text/template" ) const usage = `workspace - Use a containerized workspace COMMANDS dump-init-files: Dump emacs/bash init files to home directory, overwriting existing files. List of files: ~/.bash_aliases, ~/.emacs, ~/.custom.el, ~/.gitconfig, ~/.gitignore toggle-nightlight: Toggle Gnome display nightlight feature. ` //go:embed emacs/init.el var initLisp string //go:embed emacs/custom.el var customLisp string //go:embed emacs/epaper-theme.el var elispEpaperTheme string //go:embed emacs/emacs.desktop var emacsDesktop string //go:embed emacs/emacs.png var emacsIcon []byte //go:embed bash/bash_aliases var bashAliases string //go:embed git/config var gitConfig string //go:embed config/pantalaimon/pantalaimon.conf var pantalaimonConfig string //go:embed git/ignore var gitIgnore string var ErrAddUserFailExit = errors.New("useradd command returned no-zero exit code") var ErrAddGroupFailExit = errors.New("groupadd command returned no-zero exit code") func AddUser(username string, uid string) error { commandArgs := []string{ "--uid", uid, "--user-group", "--shell", "/bin/bash", username, } if os.Getenv("WORKSPACE_OS") == "darwin" { commandArgs = append(commandArgs, "--create-home") } else { commandArgs = append(commandArgs, "--no-create-home") } cmd := exec.Command("useradd", commandArgs...) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf( "Error adding user: %w. Process error: %w. Process output: %s", ErrAddUserFailExit, err, output, ) } return nil } func AddGroup(name string, gid string) error { _, err := user.LookupGroupId(gid) if err != nil { if _, ok := err.(user.UnknownGroupIdError); ok { cmd := exec.Command( "groupadd", "--gid", gid, name, ) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf( "Error adding group: %w. Process error: %s. Process output: %s", ErrAddGroupFailExit, err, output, ) } return nil } log.Println(err) } return err } const osDarwin = "darwin" const osLinux = "linux" func EntryPoint(command []string) error { hostOS := os.Getenv("WORKSPACE_OS") if hostOS != osLinux { log.Println(fmt.Sprintf("Note: Workspce OS is \"%s\"", hostOS)) } if len(command) == 0 { command = []string{"/bin/bash", "-i"} } uidString := os.Getenv("WORKSPACE_USER") uid, err := strconv.Atoi(uidString) if err != nil { return fmt.Errorf( "Failed to parse UID from WORKSPACE_USER env var with contents \"%s\". Error: %w", os.Getenv("WORKSPACE_USER"), err, ) } dockerGidString := os.Getenv("WORKSPACE_DOCKER_GID") dockerGid, err := strconv.Atoi(dockerGidString) if err != nil { return fmt.Errorf( "Failed to parse GID from WORKSPACE_DOCKER_GID env var with contents \"%s\". Error: %w", os.Getenv("WORKSPACE_DOCKER_GID"), err, ) } if uid != 0 { err = AddUser(os.Getenv("WORKSPACE_USERNAME"), uidString) if err != nil { return err } err = AddGroup("docker", dockerGidString) if err != nil { return err } err = syscall.Setgroups([]int{dockerGid}) if err != nil { return err } err = syscall.Setgid(int(uid)) if err != nil { return err } err = syscall.Setuid(int(uid)) if err != nil { return err } } SetEnvVars() path, err := exec.LookPath(command[0]) if err != nil { return err } return syscall.Exec(path, command, os.Environ()) } func Run(privileged bool, detach bool, mounts []string, command []string) error { workDir, err := os.Getwd() if err != nil { return err } curUser, err := user.Current() if err != nil { return err } home := os.Getenv("HOME") hostOS := runtime.GOOS dockerBin, err := exec.LookPath("docker") if err != nil { if _, err := os.Stat("/usr/bin/docker"); !errors.Is(err, os.ErrNotExist) { dockerBin = "/usr/bin/docker" } else { return err } } dockerCommand := []string{ dockerBin, "run", "--network=host", "--workdir=" + workDir, "--rm", "-e", "WORKSPACE_OS=" + hostOS, "-v", "workspace-nodejs:/opt/nodejs", } if hostOS != "darwin" { dockerGroup, err := user.LookupGroup("docker") if err != nil { return err } sshAuthSock := os.Getenv("SSH_AUTH_SOCK") if sshAuthSock == "" { authSockGuess := "/run/user/" + curUser.Uid + "/keyring/ssh" if FileExists(authSockGuess) { sshAuthSock = authSockGuess } } xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") waylandDisplay := os.Getenv("WAYLAND_DISPLAY") if waylandDisplay != "" { // let's be a little forceful with wayland use if it's available dockerCommand = append(dockerCommand, "-e", "GDK_BACKEND=wayland", "-e", "WAYLAND_DISPLAY="+waylandDisplay, ) } if xdgRuntimeDir != "" { dockerCommand = append(dockerCommand, "--mount", "type=bind,source="+xdgRuntimeDir+",target="+xdgRuntimeDir, "-e", "XDG_RUNTIME_DIR="+xdgRuntimeDir, ) } dockerCommand = append( dockerCommand, "-e", "WORKSPACE_DOCKER_GID="+dockerGroup.Gid, "-v", "/var/run/docker.sock:/var/run/docker.sock", "-v", "/etc/hosts:/etc/hosts:ro", "-v", "/etc/resolv.conf:/etc/resolv.conf:ro", "-v", home+":"+home, "-e", "SSH_AGENT_LAUNCHER="+os.Getenv("SSH_AGENT_LAUNCHER"), "-e", "SSH_AUTH_SOCK="+sshAuthSock, "-e", "XDG_CURRENT_DESKTOP="+os.Getenv("XDG_CURRENT_DESKTOP"), "-e", "DESKTOP_SESSION="+os.Getenv("DESKTOP_SESSION"), "-e", "PULSE_SERVER=unix:/run/user/"+curUser.Uid+"/pulse/native", "-e", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/"+curUser.Uid+"/bus", "-v", "/tmp/.X11-unix:/tmp/.X11-unix", "-v", "/dev/snd:/dev/snd", "-e", "DISPLAY="+os.Getenv("DISPLAY"), "-e", "XAUTHORITY="+os.Getenv("XAUTHORITY"), "-e", "WORKSPACE_USER="+curUser.Uid, "-e", "WORKSPACE_USERNAME="+curUser.Username, "-e", "HOME="+home, "-h", os.Getenv("HOSTNAME"), "-e", "TERM="+os.Getenv("TERM"), ) fileInfo, err := os.Lstat(home) if err != nil { return fmt.Errorf( "Failed to determine whether home directory is a symbolic link: %w", err, ) } if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { destination, err := os.Readlink(home) if err != nil { return fmt.Errorf( "Failed to determine whether home directory is a symbolic link: %w", err, ) } dockerCommand = append(dockerCommand, "-v", destination+":"+destination) } } else { dockerCommand = append( dockerCommand, "-e", "WORKSPACE_DOCKER_GID=1001", "-v", workDir+":"+workDir, "-e", "WORKSPACE_USER=1000", "-e", "WORKSPACE_USERNAME="+curUser.Username+"-workspace", "-e", "HOME=/home/"+curUser.Username+"-workspace", ) } if privileged { dockerCommand = append(dockerCommand, "--privileged") } if detach { dockerCommand = append(dockerCommand, "--detach") } if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 { dockerCommand = append(dockerCommand, "-ti") } for _, mount := range mounts { dockerCommand = append(dockerCommand, "-v", mount+":"+mount) } dockerCommand = append(dockerCommand, "git.tar.cx/hugo/workspace:latest") dockerCommand = append(dockerCommand, command...) return syscall.Exec(dockerCommand[0], dockerCommand, os.Environ()) } func SetEnvVars() { home := os.Getenv("HOME") os.Setenv("PATH", home+"/bin:"+os.Getenv("PATH")) os.Setenv("PATH", home+"/dotfiles/bin:"+os.Getenv("PATH")) // PHP os.Setenv("PATH", home+"/.config/composer/vendor/bin:"+os.Getenv("PATH")) // Node os.Setenv("PATH", home+"/.npm_packages/bin:"+os.Getenv("PATH")) os.Setenv("PATH", "/opt/nodejs/bin:"+os.Getenv("PATH")) // Perl os.Setenv("PATH", home+"/perl5/perlbrew/bin:"+os.Getenv("PATH")) // Rust os.Setenv("PATH", home+"/.cargo/bin:"+os.Getenv("PATH")) // Locally installed python packages os.Setenv("PATH", home+"/.local/bin:"+os.Getenv("PATH")) // Home for virtualenvs os.Setenv("WORKON_HOME", home+"/.local/share/virtualenvs") // GO Stuff os.Setenv("GOPATH", home+"/go") os.Setenv("PATH", home+"/go/bin:"+os.Getenv("PATH")) os.Setenv("PATH", "/usr/local/go/bin:"+os.Getenv("PATH")) os.Setenv("PATH", "/usr/local/gopkg/bin:"+os.Getenv("PATH")) os.Setenv("GOPRIVATE", "git.snorba.art,git.tar.cx") os.Setenv("EMACS_SOCKET_NAME", home+"/.cache/emacs/server") } func DumpBytesToFile(data []byte, filePath string) error { fmt.Fprintln(os.Stderr, "Dumping to "+filePath) err := EnsureDirectory(path.Dir(filePath)) if err != nil { return err } file, err := os.Create(filePath) if err != nil { return err } writer := bufio.NewWriter(file) _, err = writer.Write(data) if err != nil { return err } return writer.Flush() } func DumpStringToFile(data string, path string) error { return DumpBytesToFile([]byte(data), path) } func ToggleNightLight() error { getState := exec.Command( "gsettings", "get", "org.gnome.settings-daemon.plugins.color", "night-light-enabled", ) state, err := getState.CombinedOutput() if err != nil { return fmt.Errorf("%w: %s", err, state) } stateStr := strings.TrimSpace(string(state)) desiredState := "true" if stateStr == desiredState { desiredState = "false" } out, err := exec.Command( "gsettings", "set", "org.gnome.settings-daemon.plugins.color", "night-light-enabled", desiredState, ).CombinedOutput() if err != nil { return fmt.Errorf("%w: %s", err, out) } return nil } func FileExists(file string) bool { _, err := os.Stat(file) if err != nil { return false } return true } func EnsureDirectory(dir string) error { if _, err := os.Stat(dir); os.IsNotExist(err) { err = os.MkdirAll(dir, 0755) if err != nil { return fmt.Errorf( "Failed to create dir %v: %w", dir, err, ) } } return nil } func DumpInitFiles() error { home := os.Getenv("HOME") elispThemeDir := home + "/.workspace/elisp-themes" err := EnsureDirectory(elispThemeDir) files := map[string]string{ home + "/.emacs": initLisp, home + "/.custom.el": customLisp, home + "/.bash_aliases": bashAliases, elispThemeDir + "/epaper-theme.el": elispEpaperTheme, home + "/.gitconfig": gitConfig, home + "/.gitignore": gitIgnore, home + "/.config/pantalaimon/pantalaimon.conf": pantalaimonConfig, } for file, contents := range files { err := DumpStringToFile(contents, file) if err != nil { return err } } tmp, err := template.New("emacs.desktop").Parse(emacsDesktop) if err != nil { return err } executable, err := os.Executable() if err != nil { return err } var out bytes.Buffer tmp.Execute(&out, map[string]string{ "workspace_bin": executable, }) err = DumpBytesToFile( out.Bytes(), home+"/.local/share/applications/emacs.desktop", ) if err != nil { return err } return DumpBytesToFile( emacsIcon, home+"/.local/share/icons/hicolor/128x128/apps/emacs.png", ) } func main() { var cmd string if len(os.Args) > 1 { cmd = os.Args[1] } var detach bool run := flag.NewFlagSet("run", flag.ExitOnError) run.BoolVar( &detach, "detach", false, "Whether or not to detach from the container after running the command", ) var privileged bool run.BoolVar( &privileged, "privileged", false, "Whether or not to detach from the container after running the command", ) var mounts arrayFlag run.Var(&mounts, "mount", "Directory to mount inside the workspace container") switch cmd { case "entrypoint": err := EntryPoint(os.Args[2:]) if err != nil { fmt.Println("error executing entrypoint command:", err) os.Exit(1) } case "run": err := run.Parse(os.Args[2:]) if err != nil { fmt.Println(err) os.Exit(1) } err = Run(privileged, detach, mounts, run.Args()) if err != nil { fmt.Println("error running command in container", err) os.Exit(1) } case "dump-init-files": err := DumpInitFiles() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } case "toggle-nightlight": err := ToggleNightLight() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } default: fmt.Println(usage) fmt.Println("run COMMAND: Start workspace container and run COMMAND within it") run.Usage() } }