package dependency import ( "fmt" "jws/internal/logger" osinfo "jws/internal/os" "jws/pkg/download" "log/slog" "os" "os/exec" "path/filepath" "runtime" "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" "fyne.io/tools" ) var log *slog.Logger func init() { log = logger.GetChildLogger("dependency") } type Dependency struct { Name string Installed bool Icon fyne.Resource } func CheckDependencies(dependencies []Dependency) { log.Info("Checking dependencies") // Check VSCode dependencies[0].Installed = checkVSCode() // Check Docker if runtime.GOOS == "windows" { dependencies[1].Installed = checkDockerDesktop() } else { dependencies[1].Installed = checkDocker() } } func checkVSCode() bool { switch runtime.GOOS { case "windows": _, err := os.Stat(filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Microsoft VS Code", "Code.exe")) if err == nil { return true } _, err = os.Stat(filepath.Join(os.Getenv("ProgramFiles"), "Microsoft VS Code", "Code.exe")) return err == nil case "darwin": _, err := os.Stat("/Applications/Visual Studio Code.app") if err != nil { cmd := tools.CommandInShell("which", "code") return cmd.Run() == nil } return err == nil case "linux": cmd := tools.CommandInShell("which", "code") return cmd.Run() == nil default: return false } } func checkDocker() bool { cmd := tools.CommandInShell("which", "docker") return cmd.Run() == nil } func checkDockerDesktop() bool { switch runtime.GOOS { case "windows": _, err := os.Stat(filepath.Join(os.Getenv("ProgramFiles"), "Docker", "Docker", "Docker Desktop.exe")) return err == nil case "darwin": _, err := os.Stat("/Applications/Docker.app") return err == nil case "linux": cmd := tools.CommandInShell("systemctl", "is-active", "docker") output, _ := cmd.Output() return strings.TrimSpace(string(output)) == "active" default: return false } } func InstallDependency(index int, sudoPassword string, dependencies []Dependency, mainWindow fyne.Window) { depName := dependencies[index].Name var cmd *exec.Cmd var err error switch runtime.GOOS { case "windows": cmd, err = installWindowsDependencies(index, cmd, &mainWindow) if err != nil { dialog.ShowError(err, mainWindow) log.Error(err.Error()) } case "darwin": cmd, err = installDarwinDependencies(index, cmd, &mainWindow) if err != nil { dialog.ShowError(err, mainWindow) log.Error(err.Error()) } case "linux": err = installLinuxDependencies(index, sudoPassword, cmd, dependencies, &mainWindow) if err != nil { log.Error(err.Error()) dialog.ShowError(err, mainWindow) } } if cmd != nil { showInstallProgressBar(mainWindow, fmt.Sprintf("installing %v", depName), cmd, depName, dependencies) } } func installWindowsDependencies(index int, cmd *exec.Cmd, mainWindow *fyne.Window) (*exec.Cmd, error) { switch index { case 0: // VSCode cmd = tools.CommandInShell("winget", "install", "-e", "--id", "Microsoft.VisualStudioCode", "--accept-package-agreements", "--accept-source-agreements", "--silent") case 1: // Docker Desktop wslCheckCmd := tools.CommandInShell("wsl", "--status") err := wslCheckCmd.Run() if err != nil { wslInstallCmd := tools.CommandInShell("wsl", "--install", "ubuntu") dialog.ShowInformation("installing wsl", "WSL will be installed. Please wait untill installation is finished and reopen 'jws'.", *mainWindow) err = wslInstallCmd.Run() if err != nil { wslEnabled, err := checkWslFeaturesEnabled() if err != nil { return nil, fmt.Errorf("error checking WSL features: %v", err) } if !wslEnabled { dialog.ShowInformation("activate wsl", "WSL has to be activated. Please confirm to active necessary features.", *mainWindow) err = enableWslFeatures() if err != nil { return nil, fmt.Errorf("error enabling WSL features: %v", err) } dialog.ShowInformation("reboot required", "Please reboot your PC to finish wsl activation and reopen 'jws'.", *mainWindow) return nil, fmt.Errorf("reboot required") } errMsg := fmt.Errorf("error: installing WSL: %v", err) return nil, errMsg } return nil, err } cmd = tools.CommandInShell("winget", "install", "-e", "--id", "Docker.DockerDesktop", "--accept-package-agreements", "--accept-source-agreements", "--silent") } return cmd, nil } func checkWslFeaturesEnabled() (bool, error) { cmd := tools.CommandInShell("powershell", "-Command", "[bool](Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform).State -and [bool](Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux).State") output, err := cmd.CombinedOutput() if err != nil { return false, fmt.Errorf("error executing PowerShell command: %v\nOutput: %s", err, string(output)) } enabled := strings.TrimSpace(string(output)) == "True" return enabled, nil } func enableWslFeatures() error { cmd := tools.CommandInShell("powershell", "-Command", "Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart; Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart") output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("error executing PowerShell command: %v\nOutput: %s", err, string(output)) } return nil } func installDarwinDependencies(index int, cmd *exec.Cmd, mainWindow *fyne.Window) (*exec.Cmd, error) { brewCheckCmd := tools.CommandInShell("which", "brew") err := brewCheckCmd.Run() if err != nil { brewInstallCmd := tools.CommandInShell("bin/bash", "-c", "\"$(curl -fsSl https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"") err := brewInstallCmd.Run() if err != nil { errMsg := fmt.Errorf("error: installing homebrew falied: %v", err) return nil, errMsg } } switch index { case 0: // VSCode cmd = tools.CommandInShell("brew", "install", "--cask", "visual-studio-code") case 1: // Docker cmd = tools.CommandInShell("brew", "install", "docker") } return cmd, nil } func installLinuxDependencies(index int, sudoPassword string, cmd *exec.Cmd, dependencies []Dependency, mainWindow *fyne.Window) error { osInfo, err := osinfo.GetLinuxDistribution() if err != nil { errMsg := fmt.Errorf("error getting OS info: %v", err) return errMsg } var downloadURL, fileName string switch index { case 0: // VSCode switch osInfo.ID { case "debian", "ubuntu", "linuxmint": downloadURL = "https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64" fileName = "vscode.deb" case "fedora": downloadURL = "https://code.visualstudio.com/sha/download?build=stable&os=linux-rpm-x64" fileName = "vscode.rpm" default: dialog.ShowInformation("Nicht unterstützt", fmt.Sprintf("Automatische Installation für dieses OS %v nicht verfügbar", osInfo), *mainWindow) return nil } go func() { progressBar := widget.NewProgressBar() progressDialog := dialog.NewCustomWithoutButtons("Download in progress", progressBar, *mainWindow) progressDialog.Show() homeDir, _ := os.UserHomeDir() downloadDir := filepath.Join(homeDir, "Downloads") filePath := filepath.Join(downloadDir, fileName) err := download.WithProgressBar(downloadURL, filePath, progressBar) progressDialog.Hide() if err != nil { log.Error(err.Error()) dialog.ShowError(err, *mainWindow) return } var installCmd string switch osInfo.ID { case "debian", "ubuntu", "linuxmint": installCmd = fmt.Sprintf("sudo -S dpkg -i %s", filePath) case "fedora": installCmd = fmt.Sprintf("sudo -S dnf install -y %s", filePath) } cmd := exec.Command("sh", "-c", installCmd) cmd.Stdin = strings.NewReader(sudoPassword + "\n") dialog.ShowInformation("Installation started", "Installation of VSCode started.", *mainWindow) output, err := cmd.CombinedOutput() if err != nil { errMsg := fmt.Errorf("installation failed:\n%s", output) dialog.ShowError(errMsg, *mainWindow) log.Error(errMsg.Error()) return } else { dialog.ShowInformation("Success", "VSCode succesfully installed!", *mainWindow) CheckDependencies(dependencies) } }() case 1: // Docker go func() { progressBar := widget.NewProgressBar() progressDialog := dialog.NewCustomWithoutButtons("Docker installation in progress...", progressBar, *mainWindow) progressDialog.Show() osInfo, err := osinfo.GetLinuxDistribution() if err != nil { progressDialog.Hide() errMsg := fmt.Errorf("error getting os infos: %v", err) dialog.ShowError(errMsg, *mainWindow) log.Error(errMsg.Error()) return } var commands []string var cleanupCommands []string var totalSteps int switch osInfo.ID { case "ubuntu", "linuxmint", "debian": // Ubuntu/Debian Commands distroPath := "ubuntu" codeName := "$(. /etc/os-release && echo \"${UBUNTU_CODENAME:-$VERSION_CODENAME}\")" if osInfo.ID == "debian" { distroPath = "debian" codeName = "$(. /etc/os-release && echo \"$VERSION_CODENAME\")" } arch := "$(dpkg --print-architecture)" commands = []string{ "apt-get update", "apt-get install -y wget", fmt.Sprintf("wget -qO- https://download.docker.com/linux/%s/dists/%s/pool/stable/%s/ | grep -oP 'href=\"\\K[^\"]*(?=.*deb)' | xargs -I{} wget https://download.docker.com/linux/%s/dists/%s/pool/stable/%s/{}", distroPath, codeName, arch, distroPath, codeName, arch), "dpkg -i ./containerd.io*.deb docker-ce*.deb docker-ce-cli*.deb docker-buildx-plugin*.deb docker-compose-plugin*.deb", "apt-get install -f -y", "service docker start", } cleanupCommands = []string{ "rm -rf ./containerd.io*.deb ./docker-ce*.deb ./docker-ce-cli*.deb ./docker-buildx-plugin*.deb ./docker-compose-plugin*.deb", "apt-get autoremove -y", "apt-get clean", } totalSteps = len(commands) + len(cleanupCommands) case "fedora": fedoraVer := "$(rpm -E %fedora)" commands = []string{ "dnf install -y wget", fmt.Sprintf("wget https://download.docker.com/linux/fedora/%s/x86_64/stable/Packages/containerd-*.rpm docker-*.rpm docker-ce-*.rpm", fedoraVer), "dnf install -y ./*.rpm", "systemctl enable --now docker", } cleanupCommands = []string{ "rm -rf ./containerd-*.rpm ./docker-*.rpm ./docker-ce-*.rpm", "dnf autoremove -y", "dnf clean all", } totalSteps = len(commands) + len(cleanupCommands) default: progressDialog.Hide() dialog.ShowInformation("not supported", "Automatic Docker installation not supported for your OS.", *mainWindow) return } progressStep := 1.0 / float64(totalSteps) currentProgress := 0.0 for _, cmd := range commands { command := exec.Command("sudo", "-S", "sh", "-c", cmd) command.Stdin = strings.NewReader(sudoPassword + "\n") if output, err := command.CombinedOutput(); err != nil { progressDialog.Hide() errMsg := fmt.Errorf("error at %s:\n%s", cmd, output) dialog.ShowError(errMsg, *mainWindow) log.Error(errMsg.Error()) return } currentProgress += progressStep progressBar.SetValue(currentProgress) } for _, cmd := range cleanupCommands { command := exec.Command("sudo", "-S", "sh", "-c", cmd) command.Stdin = strings.NewReader(sudoPassword + "\n") if output, err := command.CombinedOutput(); err != nil { progressDialog.Hide() errMsg := fmt.Errorf("cleanup error at %s:\n%s", cmd, output) dialog.ShowError(errMsg, *mainWindow) log.Error(errMsg.Error()) return } currentProgress += progressStep progressBar.SetValue(currentProgress) } progressDialog.Hide() dialog.ShowInformation( "Installation finished", "Docker was succesfully installed! Please re-start your system to let the changes take effect.", *mainWindow, ) CheckDependencies(dependencies) }() } return nil } func showInstallProgressBar(window fyne.Window, message string, cmd *exec.Cmd, depName string, dependencies []Dependency) { progress := widget.NewProgressBarInfinite() content := container.NewVBox( widget.NewLabel(message), progress, ) popup := widget.NewModalPopUp(content, window.Canvas()) popup.Show() go func() { err := cmd.Run() popup.Hide() if err != nil { errMsg := fmt.Errorf("error during installation of %s: %v", depName, err) dialog.ShowError(errMsg, window) log.Error(errMsg.Error()) } else { dialog.ShowInformation("Installation finished", fmt.Sprintf("%s successfully installed!", depName), window) CheckDependencies(dependencies) } }() }