mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
switch hto monorepo structure
This commit is contained in:
659
backend/internal/distros/base.go
Normal file
659
backend/internal/distros/base.go
Normal file
@@ -0,0 +1,659 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/version"
|
||||
)
|
||||
|
||||
const forceQuickshellGit = false
|
||||
const forceDMSGit = false
|
||||
|
||||
// BaseDistribution provides common functionality for all distributions
|
||||
type BaseDistribution struct {
|
||||
logChan chan<- string
|
||||
}
|
||||
|
||||
// NewBaseDistribution creates a new base distribution
|
||||
func NewBaseDistribution(logChan chan<- string) *BaseDistribution {
|
||||
return &BaseDistribution{
|
||||
logChan: logChan,
|
||||
}
|
||||
}
|
||||
|
||||
// Common helper methods
|
||||
func (b *BaseDistribution) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) CommandExists(cmd string) bool {
|
||||
return b.commandExists(cmd)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) log(message string) {
|
||||
if b.logChan != nil {
|
||||
b.logChan <- message
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) logError(message string, err error) {
|
||||
errorMsg := fmt.Sprintf("ERROR: %s: %v", message, err)
|
||||
b.log(errorMsg)
|
||||
}
|
||||
|
||||
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
||||
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
||||
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
||||
func escapeSingleQuotes(s string) string {
|
||||
return strings.ReplaceAll(s, "'", "'\\''")
|
||||
}
|
||||
|
||||
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
||||
// This helper escapes special characters in the password to prevent shell injection
|
||||
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
||||
func MakeSudoCommand(sudoPassword string, command string) string {
|
||||
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
||||
}
|
||||
|
||||
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
||||
// The password is properly escaped to prevent shell injection and syntax errors.
|
||||
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||
cmdStr := MakeSudoCommand(sudoPassword, command)
|
||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||
}
|
||||
|
||||
// Common dependency detection methods
|
||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("git") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "git",
|
||||
Status: status,
|
||||
Description: "Version control system",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("matugen") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "matugen",
|
||||
Status: status,
|
||||
Description: "Material Design color generation tool",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("dgop") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "dgop",
|
||||
Status: status,
|
||||
Description: "Desktop portal management tool",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||
|
||||
status := deps.StatusMissing
|
||||
currentVersion := ""
|
||||
|
||||
if _, err := os.Stat(dmsPath); err == nil {
|
||||
status = deps.StatusInstalled
|
||||
|
||||
// Only get current version, don't check for updates (lazy loading)
|
||||
current, err := version.GetCurrentDMSVersion()
|
||||
if err == nil {
|
||||
currentVersion = current
|
||||
}
|
||||
}
|
||||
|
||||
dep := deps.Dependency{
|
||||
Name: "dms (DankMaterialShell)",
|
||||
Status: status,
|
||||
Description: "Desktop Management System configuration",
|
||||
Required: true,
|
||||
CanToggle: true,
|
||||
}
|
||||
|
||||
if currentVersion != "" {
|
||||
dep.Version = currentVersion
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.Dependency {
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("ghostty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "ghostty",
|
||||
Status: status,
|
||||
Description: "A fast, native terminal emulator built in Zig.",
|
||||
Required: true,
|
||||
}
|
||||
case deps.TerminalKitty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("kitty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "kitty",
|
||||
Status: status,
|
||||
Description: "A feature-rich, customizable terminal emulator.",
|
||||
Required: true,
|
||||
}
|
||||
case deps.TerminalAlacritty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("alacritty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "alacritty",
|
||||
Status: status,
|
||||
Description: "A simple terminal emulator. (No dynamic theming)",
|
||||
Required: true,
|
||||
}
|
||||
default:
|
||||
return b.detectSpecificTerminal(deps.TerminalGhostty)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
cliphist := deps.StatusMissing
|
||||
if b.commandExists("cliphist") {
|
||||
cliphist = deps.StatusInstalled
|
||||
}
|
||||
|
||||
wlClipboard := deps.StatusMissing
|
||||
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
||||
wlClipboard = deps.StatusInstalled
|
||||
}
|
||||
|
||||
dependencies = append(dependencies,
|
||||
deps.Dependency{
|
||||
Name: "cliphist",
|
||||
Status: cliphist,
|
||||
Description: "Wayland clipboard manager",
|
||||
Required: true,
|
||||
},
|
||||
deps.Dependency{
|
||||
Name: "wl-clipboard",
|
||||
Status: wlClipboard,
|
||||
Description: "Wayland clipboard utilities",
|
||||
Required: true,
|
||||
},
|
||||
)
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("hyprpicker") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "hyprpicker",
|
||||
Status: status,
|
||||
Description: "Color picker for Wayland",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
tools := []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"grim", "Screenshot utility for Wayland"},
|
||||
{"slurp", "Region selection utility for Wayland"},
|
||||
{"hyprctl", "Hyprland control utility"},
|
||||
{"grimblast", "Screenshot script for Hyprland"},
|
||||
{"jq", "JSON processor"},
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists(tool.name) {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, deps.Dependency{
|
||||
Name: tool.name,
|
||||
Status: status,
|
||||
Description: tool.description,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectQuickshell() deps.Dependency {
|
||||
if !b.commandExists("qs") {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "QtQuick based desktop shell toolkit",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("qs", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsReinstall,
|
||||
Description: "QtQuick based desktop shell toolkit (version check failed)",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
versionStr := string(output)
|
||||
versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||
matches := versionRegex.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) < 2 {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsReinstall,
|
||||
Description: "QtQuick based desktop shell toolkit (unknown version)",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
version := matches[1]
|
||||
variant := deps.VariantStable
|
||||
if strings.Contains(versionStr, "git") || strings.Contains(versionStr, "+") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
|
||||
if b.versionCompare(version, "0.2.0") >= 0 {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusInstalled,
|
||||
Version: version,
|
||||
Description: "QtQuick based desktop shell toolkit",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsUpdate,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
Version: version,
|
||||
Description: "QtQuick based desktop shell toolkit (needs 0.2.0+)",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
status := deps.StatusMissing
|
||||
variant := deps.VariantStable
|
||||
version := ""
|
||||
|
||||
if b.commandExists("hyprland") || b.commandExists("Hyprland") {
|
||||
status = deps.StatusInstalled
|
||||
cmd := exec.Command("hyprctl", "version")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
outStr := string(output)
|
||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
if versionRegex := regexp.MustCompile(`v(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||
matches := versionRegex.FindStringSubmatch(outStr)
|
||||
if len(matches) > 1 {
|
||||
version = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "hyprland",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "Dynamic tiling Wayland compositor",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
case deps.WindowManagerNiri:
|
||||
status := deps.StatusMissing
|
||||
variant := deps.VariantStable
|
||||
version := ""
|
||||
|
||||
if b.commandExists("niri") {
|
||||
status = deps.StatusInstalled
|
||||
cmd := exec.Command("niri", "--version")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
outStr := string(output)
|
||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "+") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
if versionRegex := regexp.MustCompile(`niri (\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||
matches := versionRegex.FindStringSubmatch(outStr)
|
||||
if len(matches) > 1 {
|
||||
version = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "niri",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "Scrollable-tiling Wayland compositor",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
default:
|
||||
return deps.Dependency{
|
||||
Name: "unknown-wm",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "Unknown window manager",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version comparison helper
|
||||
func (b *BaseDistribution) versionCompare(v1, v2 string) int {
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
||||
if parts1[i] < parts2[i] {
|
||||
return -1
|
||||
}
|
||||
if parts1[i] > parts2[i] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts1) < len(parts2) {
|
||||
return -1
|
||||
}
|
||||
if len(parts1) > len(parts2) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Common installation helper
|
||||
func (b *BaseDistribution) runWithProgress(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64) error {
|
||||
return b.runWithProgressTimeout(cmd, progressChan, phase, startProgress, endProgress, 20*time.Minute)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, timeout time.Duration) error {
|
||||
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, "Installing...", timeout)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressStep(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string) error {
|
||||
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, stepMessage, 20*time.Minute)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string, timeoutDuration time.Duration) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputChan := make(chan string, 100)
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
b.log(line)
|
||||
outputChan <- line
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
b.log(line)
|
||||
outputChan <- line
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
close(outputChan)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
progress := startProgress
|
||||
progressStep := (endProgress - startProgress) / 50
|
||||
lastOutput := ""
|
||||
|
||||
var timeout *time.Timer
|
||||
var timeoutChan <-chan time.Time
|
||||
if timeoutDuration > 0 {
|
||||
timeout = time.NewTimer(timeoutDuration)
|
||||
defer timeout.Stop()
|
||||
timeoutChan = timeout.C
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
b.logError("Command execution failed", err)
|
||||
b.log(fmt.Sprintf("Last output before failure: %s", lastOutput))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: startProgress,
|
||||
Step: "Command failed",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
Error: err,
|
||||
}
|
||||
return err
|
||||
}
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: endProgress,
|
||||
Step: "Installation step complete",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
return nil
|
||||
case output, ok := <-outputChan:
|
||||
if ok {
|
||||
lastOutput = output
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: progress,
|
||||
Step: stepMessage,
|
||||
IsComplete: false,
|
||||
LogOutput: output,
|
||||
}
|
||||
if timeout != nil {
|
||||
timeout.Reset(timeoutDuration)
|
||||
}
|
||||
}
|
||||
case <-timeoutChan:
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
err := fmt.Errorf("installation timed out after %v", timeoutDuration)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: startProgress,
|
||||
Step: "Installation timed out",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
Error: err,
|
||||
}
|
||||
return err
|
||||
case <-ticker.C:
|
||||
if progress < endProgress-0.01 {
|
||||
progress += progressStep
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: progress,
|
||||
Step: "Installing...",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// installDMSBinary installs the DMS binary from GitHub releases
|
||||
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
b.log("Installing/updating DMS binary...")
|
||||
|
||||
// Detect architecture
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
case "amd64":
|
||||
case "arm64":
|
||||
default:
|
||||
return fmt.Errorf("unsupported architecture for DMS: %s", arch)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.80,
|
||||
Step: "Downloading DMS binary...",
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch),
|
||||
}
|
||||
|
||||
// Get latest release version
|
||||
latestVersionCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
`curl -s https://api.github.com/repos/AvengeMedia/danklinux/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`)
|
||||
versionOutput, err := latestVersionCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest DMS version: %w", err)
|
||||
}
|
||||
version := strings.TrimSpace(string(versionOutput))
|
||||
if version == "" {
|
||||
return fmt.Errorf("could not determine latest DMS version")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Download the gzipped binary
|
||||
downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/backend/releases/download/%s/dms-%s.gz", version, arch)
|
||||
gzPath := filepath.Join(tmpDir, "dms.gz")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to download DMS binary: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.85,
|
||||
Step: "Extracting DMS binary...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "gunzip dms.gz",
|
||||
}
|
||||
|
||||
// Extract the binary
|
||||
extractCmd := exec.CommandContext(ctx, "gunzip", gzPath)
|
||||
if err := extractCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to extract DMS binary: %w", err)
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(tmpDir, "dms")
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", binaryPath)
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make DMS binary executable: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.88,
|
||||
Step: "Installing DMS to /usr/local/bin...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cp dms /usr/local/bin/",
|
||||
}
|
||||
|
||||
// Install to /usr/local/bin
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||
}
|
||||
|
||||
b.log("DMS binary installed successfully")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user