From 6afbcb69f354fca5ba40d1e8eb6d054c0704b02d Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 3 Sep 2023 10:09:16 +1000 Subject: [PATCH] Improve doctor --- v3/internal/doctor/doctor.go | 97 +--------- v3/internal/doctor/doctor_darwin.go | 4 +- v3/internal/doctor/doctor_linux.go | 71 +++++++- v3/internal/doctor/doctor_windows.go | 7 +- v3/internal/doctor/packagemanager/apt.go | 97 ++++++++++ v3/internal/doctor/packagemanager/dnf.go | 130 +++++++++++++ v3/internal/doctor/packagemanager/emerge.go | 115 ++++++++++++ v3/internal/doctor/packagemanager/eopkg.go | 112 ++++++++++++ v3/internal/doctor/packagemanager/nixpkgs.go | 157 ++++++++++++++++ .../doctor/packagemanager/packagemanager.go | 171 ++++++++++++++++++ v3/internal/doctor/packagemanager/pacman.go | 112 ++++++++++++ v3/internal/doctor/packagemanager/pm.go | 64 +++++++ v3/internal/doctor/packagemanager/zypper.go | 122 +++++++++++++ 13 files changed, 1162 insertions(+), 97 deletions(-) create mode 100644 v3/internal/doctor/packagemanager/apt.go create mode 100644 v3/internal/doctor/packagemanager/dnf.go create mode 100644 v3/internal/doctor/packagemanager/emerge.go create mode 100644 v3/internal/doctor/packagemanager/eopkg.go create mode 100644 v3/internal/doctor/packagemanager/nixpkgs.go create mode 100644 v3/internal/doctor/packagemanager/packagemanager.go create mode 100644 v3/internal/doctor/packagemanager/pacman.go create mode 100644 v3/internal/doctor/packagemanager/pm.go create mode 100644 v3/internal/doctor/packagemanager/zypper.go diff --git a/v3/internal/doctor/doctor.go b/v3/internal/doctor/doctor.go index 43f8e8cbc..6f658c868 100644 --- a/v3/internal/doctor/doctor.go +++ b/v3/internal/doctor/doctor.go @@ -75,7 +75,7 @@ func Run() (err error) { } } - platformExtras := getInfo() + platformExtras, ok := getInfo() spinner.Success() @@ -140,96 +140,13 @@ func Run() (err error) { if err != nil { return err } - /* - pterm.DefaultSection.Println("Dependencies") - // Output Dependencies Status - var dependenciesMissing []string - var externalPackages []*packagemanager.Dependency - var dependenciesAvailableRequired = 0 - var dependenciesAvailableOptional = 0 + pterm.DefaultSection.Println("Diagnosis") + if !ok { + pterm.Warning.Println("There are some items above that need addressing!") + } else { + pterm.Success.Println("Your system is ready for Wails development!") + } - dependenciesTableData := pterm.TableData{ - {"Dependency", "Package Name", "Status", "Version"}, - } - - hasOptionalDependencies := false - // Loop over dependencies - for _, dependency := range info.Dependencies { - name := dependency.Name - - if dependency.Optional { - name = pterm.Gray("*") + name - hasOptionalDependencies = true - } - - packageName := "Unknown" - status := pterm.LightRed("Not Found") - - // If we found the package - if dependency.PackageName != "" { - packageName = dependency.PackageName - - // If it's installed, update the status - if dependency.Installed { - status = pterm.LightGreen("Installed") - } else { - // Generate meaningful status text - status = pterm.LightMagenta("Available") - - if dependency.Optional { - dependenciesAvailableOptional++ - } else { - dependenciesAvailableRequired++ - } - } - } else { - if !dependency.Optional { - dependenciesMissing = append(dependenciesMissing, dependency.Name) - } - - if dependency.External { - externalPackages = append(externalPackages, dependency) - } - } - - dependenciesTableData = append(dependenciesTableData, []string{name, packageName, status, dependency.Version}) - } - - dependenciesTableString, _ := pterm.DefaultTable.WithHasHeader(true).WithData(dependenciesTableData).Srender() - dependenciesBox := pterm.DefaultBox.WithTitleBottomCenter() - - if hasOptionalDependencies { - dependenciesBox = dependenciesBox.WithTitle(pterm.Gray("*") + " - Optional Dependency") - } - - dependenciesBox.Println(dependenciesTableString) - - pterm.DefaultSection.Println("Diagnosis") - - // Generate an appropriate diagnosis - - if dependenciesAvailableRequired != 0 { - pterm.Println("Required package(s) installation details: \n" + info.Dependencies.InstallAllRequiredCommand()) - } - - if dependenciesAvailableOptional != 0 { - pterm.Println("Optional package(s) installation details: \n" + info.Dependencies.InstallAllOptionalCommand()) - } - - if len(dependenciesMissing) == 0 && dependenciesAvailableRequired == 0 { - pterm.Success.Println("Your system is ready for Wails development!") - } else { - pterm.Warning.Println("Your system has missing dependencies!") - } - - if len(dependenciesMissing) != 0 { - pterm.Println("Fatal:") - pterm.Println("Required dependencies missing: " + strings.Join(dependenciesMissing, " ")) - pterm.Println("Please read this article on how to resolve this: https://wails.io/guides/resolving-missing-packages") - } - - pterm.Println() // Spacer for sponsor message - */ return nil } diff --git a/v3/internal/doctor/doctor_darwin.go b/v3/internal/doctor/doctor_darwin.go index bf2634916..14fab050c 100644 --- a/v3/internal/doctor/doctor_darwin.go +++ b/v3/internal/doctor/doctor_darwin.go @@ -2,7 +2,7 @@ package doctor -func getInfo() map[string]string { +func getInfo() (map[string]string, bool) { result := make(map[string]string) - return result + return result, true } diff --git a/v3/internal/doctor/doctor_linux.go b/v3/internal/doctor/doctor_linux.go index 54655a763..f7ca3414c 100644 --- a/v3/internal/doctor/doctor_linux.go +++ b/v3/internal/doctor/doctor_linux.go @@ -2,7 +2,74 @@ package doctor -func getInfo() map[string]string { +func getInfo() (map[string]string, bool) { result := make(map[string]string) - return result + /* + pterm.DefaultSection.Println("Dependencies") + + // Output Dependencies Status + var dependenciesMissing []string + var externalPackages []*packagemanager.Dependency + var dependenciesAvailableRequired = 0 + var dependenciesAvailableOptional = 0 + + dependenciesTableData := pterm.TableData{ + {"Dependency", "Package Name", "Status", "Version"}, + } + + hasOptionalDependencies := false + // Loop over dependencies + for _, dependency := range info.Dependencies { + name := dependency.Name + + if dependency.Optional { + name = pterm.Gray("*") + name + hasOptionalDependencies = true + } + + packageName := "Unknown" + status := pterm.LightRed("Not Found") + + // If we found the package + if dependency.PackageName != "" { + packageName = dependency.PackageName + + // If it's installed, update the status + if dependency.Installed { + status = pterm.LightGreen("Installed") + } else { + // Generate meaningful status text + status = pterm.LightMagenta("Available") + + if dependency.Optional { + dependenciesAvailableOptional++ + } else { + dependenciesAvailableRequired++ + } + } + } else { + if !dependency.Optional { + dependenciesMissing = append(dependenciesMissing, dependency.Name) + } + + if dependency.External { + externalPackages = append(externalPackages, dependency) + } + } + + dependenciesTableData = append(dependenciesTableData, []string{name, packageName, status, dependency.Version}) + } + + dependenciesTableString, _ := pterm.DefaultTable.WithHasHeader(true).WithData(dependenciesTableData).Srender() + dependenciesBox := pterm.DefaultBox.WithTitleBottomCenter() + + if hasOptionalDependencies { + dependenciesBox = dependenciesBox.WithTitle(pterm.Gray("*") + " - Optional Dependency") + } + + dependenciesBox.Println(dependenciesTableString) + + pterm.Println() // Spacer for sponsor message + */ + return result, true } diff --git a/v3/internal/doctor/doctor_windows.go b/v3/internal/doctor/doctor_windows.go index 905932011..90f5fd4db 100644 --- a/v3/internal/doctor/doctor_windows.go +++ b/v3/internal/doctor/doctor_windows.go @@ -7,14 +7,15 @@ import ( "github.com/wailsapp/go-webview2/webviewloader" ) -func getInfo() map[string]string { +func getInfo() (map[string]string, bool) { + ok := true result := make(map[string]string) result["Go WebView2Loader"] = lo.Ternary(webviewloader.UsingGoWebview2Loader, "true", "false") webviewVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString("") if err != nil { + ok = false webviewVersion = "Error:" + err.Error() } result["WebView2 Version"] = webviewVersion - - return result + return result, ok } diff --git a/v3/internal/doctor/packagemanager/apt.go b/v3/internal/doctor/packagemanager/apt.go new file mode 100644 index 000000000..17503490d --- /dev/null +++ b/v3/internal/doctor/packagemanager/apt.go @@ -0,0 +1,97 @@ +//go:build linux + +package packagemanager + +import ( + "strings" +) + +// Apt represents the Apt manager +type Apt struct { + name string + osid string +} + +// NewApt creates a new Apt instance +func NewApt(osid string) *Apt { + return &Apt{ + name: "apt", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (a *Apt) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "libgtk-3-dev", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "libwebkit2gtk-4.0-dev", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "build-essential", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker.io", SystemPackage: true, Optional: true}, + }, + "nsis": []*Package{ + {Name: "nsis", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (a *Apt) Name() string { + return a.name +} + +func (a *Apt) listPackage(name string) (string, error) { + return execCmd("apt", "list", "-qq", name) +} + +// PackageInstalled tests if the given package name is installed +func (a *Apt) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + output, err := a.listPackage(pkg.Name) + return strings.Contains(output, "[installed]"), err +} + +// PackageAvailable tests if the given package is available for installation +func (a *Apt) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + output, err := a.listPackage(pkg.Name) + // We add a space to ensure we get a full match, not partial match + output = a.removeEscapeSequences(output) + escapechars, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`) + escapechars.ReplaceAllString(output, "") + installed := strings.HasPrefix(output, pkg.Name) + a.getPackageVersion(pkg, output) + return installed, err +} + +// InstallCommand returns the package manager specific command to install a package +func (a *Apt) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[a.osid] + } + return "sudo apt install " + pkg.Name +} + +func (a *Apt) getPackageVersion(pkg *Package, output string) { + splitOutput := strings.Split(output, " ") + if len(splitOutput) > 1 { + pkg.Version = splitOutput[1] + } +} diff --git a/v3/internal/doctor/packagemanager/dnf.go b/v3/internal/doctor/packagemanager/dnf.go new file mode 100644 index 000000000..c22d3d49d --- /dev/null +++ b/v3/internal/doctor/packagemanager/dnf.go @@ -0,0 +1,130 @@ +//go:build linux + +package packagemanager + +import ( + "os/exec" + "strings" +) + +// Dnf represents the Dnf manager +type Dnf struct { + name string + osid string +} + +// NewDnf creates a new Dnf instance +func NewDnf(osid string) *Dnf { + return &Dnf{ + name: "dnf", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (y *Dnf) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "gtk3-devel", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "webkit2gtk4.0-devel", SystemPackage: true, Library: true}, + {Name: "webkit2gtk3-devel", SystemPackage: true, Library: true}, + // {Name: "webkitgtk3-devel", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc-c++", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkgconf-pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + {Name: "nodejs-npm", SystemPackage: true}, + }, + "upx": []*Package{ + {Name: "upx", SystemPackage: true, Optional: true}, + }, + "docker": []*Package{ + { + SystemPackage: false, + Optional: true, + InstallCommand: map[string]string{ + "centos": "Follow the guide: https://docs.docker.com/engine/install/centos/", + "fedora": "Follow the guide: https://docs.docker.com/engine/install/fedora/", + }, + }, + {Name: "moby-engine", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (y *Dnf) Name() string { + return y.name +} + +// PackageInstalled tests if the given package name is installed +func (y *Dnf) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, err := execCmd("dnf", "info", "installed", pkg.Name) + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + splitoutput := strings.Split(stdout, "\n") + for _, line := range splitoutput { + if strings.HasPrefix(line, "Version") { + splitline := strings.Split(line, ":") + pkg.Version = strings.TrimSpace(splitline[1]) + } + } + + return true, err +} + +// PackageAvailable tests if the given package is available for installation +func (y *Dnf) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, _, err := execCmd("dnf", "info", pkg.Name) + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + splitoutput := strings.Split(stdout, "\n") + for _, line := range splitoutput { + if strings.HasPrefix(line, "Version") { + splitline := strings.Split(line, ":") + pkg.Version = strings.TrimSpace(splitline[1]) + } + } + return true, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (y *Dnf) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[y.osid] + } + return "sudo dnf install " + pkg.Name +} + +func (y *Dnf) getPackageVersion(pkg *Package, output string) { + splitOutput := strings.Split(output, " ") + if len(splitOutput) > 0 { + pkg.Version = splitOutput[1] + } +} diff --git a/v3/internal/doctor/packagemanager/emerge.go b/v3/internal/doctor/packagemanager/emerge.go new file mode 100644 index 000000000..b8252be4b --- /dev/null +++ b/v3/internal/doctor/packagemanager/emerge.go @@ -0,0 +1,115 @@ +//go:build linux + +package packagemanager + +import ( + "os/exec" + "regexp" + "strings" +) + +// Emerge represents the Emerge package manager +type Emerge struct { + name string + osid string +} + +// NewEmerge creates a new Emerge instance +func NewEmerge(osid string) *Emerge { + return &Emerge{ + name: "emerge", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (e *Emerge) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "x11-libs/gtk+", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "net-libs/webkit-gtk", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "sys-devel/gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "dev-util/pkgconf", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "net-libs/nodejs", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "app-emulation/docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (e *Emerge) Name() string { + return e.name +} + +// PackageInstalled tests if the given package name is installed +func (e *Emerge) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, err := execCmd("emerge", "-s", pkg.Name+"$") + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + regex := `.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version installed: (.*)` + installedRegex := regexp.MustCompile(regex) + matches := installedRegex.FindStringSubmatch(stdout) + pkg.Version = "" + noOfMatches := len(matches) + installed := false + if noOfMatches > 1 && matches[1] != "[ Not Installed ]" { + installed = true + pkg.Version = strings.TrimSpace(matches[1]) + } + return installed, err +} + +// PackageAvailable tests if the given package is available for installation +func (e *Emerge) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, err := execCmd("emerge", "-s", pkg.Name+"$") + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + installedRegex := regexp.MustCompile(`.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version available: (.*)`) + matches := installedRegex.FindStringSubmatch(stdout) + pkg.Version = "" + noOfMatches := len(matches) + available := false + if noOfMatches > 1 { + available = true + pkg.Version = strings.TrimSpace(matches[1]) + } + return available, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (e *Emerge) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[e.osid] + } + return "sudo emerge " + pkg.Name +} diff --git a/v3/internal/doctor/packagemanager/eopkg.go b/v3/internal/doctor/packagemanager/eopkg.go new file mode 100644 index 000000000..e75256945 --- /dev/null +++ b/v3/internal/doctor/packagemanager/eopkg.go @@ -0,0 +1,112 @@ +//go:build linux + +package packagemanager + +import ( + "regexp" + "strings" +) + +// Eopkg represents the Eopkg manager +type Eopkg struct { + name string + osid string +} + +// NewEopkg creates a new Eopkg instance +func NewEopkg(osid string) *Eopkg { + result := &Eopkg{ + name: "eopkg", + osid: osid, + } + result.intialiseName() + return result +} + +// Packages returns the packages that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (e *Eopkg) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "libgtk-3-devel", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "libwebkit-gtk-devel", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "nodejs", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (e *Eopkg) Name() string { + return e.name +} + +// PackageInstalled tests if the given package is installed +func (e *Eopkg) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, err := execCmd("eopkg", "info", pkg.Name) + return strings.HasPrefix(stdout, "Installed"), err +} + +// PackageAvailable tests if the given package is available for installation +func (e *Eopkg) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, err := execCmd("eopkg", "info", pkg.Name) + // We add a space to ensure we get a full match, not partial match + output := e.removeEscapeSequences(stdout) + installed := strings.Contains(output, "Package found in Solus repository") + e.getPackageVersion(pkg, output) + return installed, err +} + +// InstallCommand returns the package manager specific command to install a package +func (e *Eopkg) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[e.osid] + } + return "sudo eopkg it " + pkg.Name +} + +func (e *Eopkg) removeEscapeSequences(in string) string { + escapechars, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`) + return escapechars.ReplaceAllString(in, "") +} + +func (e *Eopkg) intialiseName() { + result := "eopkg" + stdout, err := execCmd("eopkg", "--version") + if err == nil { + result = strings.TrimSpace(stdout) + } + e.name = result +} + +func (e *Eopkg) getPackageVersion(pkg *Package, output string) { + + versionRegex := regexp.MustCompile(`.*Name.*version:\s+(.*)+, release: (.*)`) + matches := versionRegex.FindStringSubmatch(output) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + pkg.Version = matches[1] + if noOfMatches > 2 { + pkg.Version += " (r" + matches[2] + ")" + } + } +} diff --git a/v3/internal/doctor/packagemanager/nixpkgs.go b/v3/internal/doctor/packagemanager/nixpkgs.go new file mode 100644 index 000000000..57b1ed023 --- /dev/null +++ b/v3/internal/doctor/packagemanager/nixpkgs.go @@ -0,0 +1,157 @@ +//go:build linux + +package packagemanager + +import ( + "encoding/json" +) + +// Nixpkgs represents the Nixpkgs manager +type Nixpkgs struct { + name string + osid string +} + +type NixPackageDetail struct { + Name string + Pname string + Version string +} + +var available map[string]NixPackageDetail + +// NewNixpkgs creates a new Nixpkgs instance +func NewNixpkgs(osid string) *Nixpkgs { + available = map[string]NixPackageDetail{} + + return &Nixpkgs{ + name: "nixpkgs", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (n *Nixpkgs) Packages() packagemap { + // Currently, only support checking the default channel. + channel := "nixpkgs" + if n.osid == "nixos" { + channel = "nixos" + } + + return packagemap{ + "libgtk-3": []*Package{ + {Name: channel + ".gtk3", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: channel + ".webkitgtk", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: channel + ".gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: channel + ".pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: channel + ".nodejs", SystemPackage: true}, + }, + "upx": []*Package{ + {Name: channel + ".upx", SystemPackage: true, Optional: true}, + }, + "docker": []*Package{ + {Name: channel + ".docker", SystemPackage: true, Optional: true}, + }, + "nsis": []*Package{ + {Name: channel + ".nsis", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (n *Nixpkgs) Name() string { + return n.name +} + +// PackageInstalled tests if the given package name is installed +func (n *Nixpkgs) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + + stdout, err := execCmd("nix-env", "--json", "-qA", pkg.Name) + if err != nil { + return false, nil + } + + var attributes map[string]NixPackageDetail + err = json.Unmarshal([]byte(stdout), &attributes) + if err != nil { + return false, err + } + + // Did we get one? + installed := false + for attribute, detail := range attributes { + if attribute == pkg.Name { + installed = true + pkg.Version = detail.Version + } + break + } + + // If on NixOS, package may be installed via system config, so check the nix store. + detail, ok := available[pkg.Name] + if !installed && n.osid == "nixos" && ok { + cmd := "nix-store --query --requisites /run/current-system | cut -d- -f2- | sort | uniq | grep '^" + detail.Pname + "'" + + if pkg.Library { + cmd += " | grep 'dev$'" + } + + stdout, err = execCmd("sh", "-c", cmd) + if err != nil { + return false, nil + } + + if len(stdout) > 0 { + installed = true + } + } + + return installed, nil +} + +// PackageAvailable tests if the given package is available for installation +func (n *Nixpkgs) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + + stdout, err := execCmd("nix-env", "--json", "-qaA", pkg.Name) + if err != nil { + return false, nil + } + + var attributes map[string]NixPackageDetail + err = json.Unmarshal([]byte(stdout), &attributes) + if err != nil { + return false, err + } + + // Grab first version. + for attribute, detail := range attributes { + pkg.Version = detail.Version + available[attribute] = detail + break + } + + return len(pkg.Version) > 0, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (n *Nixpkgs) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[n.osid] + } + return "nix-env -iA " + pkg.Name +} diff --git a/v3/internal/doctor/packagemanager/packagemanager.go b/v3/internal/doctor/packagemanager/packagemanager.go new file mode 100644 index 000000000..f318b1432 --- /dev/null +++ b/v3/internal/doctor/packagemanager/packagemanager.go @@ -0,0 +1,171 @@ +//go:build linux + +package packagemanager + +import ( + "sort" + "strings" + + "github.com/wailsapp/wails/v2/internal/shell" +) + +func execCmd(command string, args ...string) (string, error) { + cmd := exec.Command(command, args...) + var stdo, stde bytes.Buffer + cmd.Stdout = &stdo + cmd.Stderr = &stde + cmd.Env = append(os.Environ(), "LANGUAGE=en_US.utf-8") + err := cmd.Run() + return stdo.String(), err +} + +// A list of package manager commands +var pmcommands = []string{ + "eopkg", + "apt", + "dnf", + "pacman", + "emerge", + "zypper", + "nix-env", +} + +// Find will attempt to find the system package manager +func Find(osid string) PackageManager { + + // Loop over pmcommands + for _, pmname := range pmcommands { + if shell.CommandExists(pmname) { + return newPackageManager(pmname, osid) + } + } + return nil +} + +func newPackageManager(pmname string, osid string) PackageManager { + switch pmname { + case "eopkg": + return NewEopkg(osid) + case "apt": + return NewApt(osid) + case "dnf": + return NewDnf(osid) + case "pacman": + return NewPacman(osid) + case "emerge": + return NewEmerge(osid) + case "zypper": + return NewZypper(osid) + case "nix-env": + return NewNixpkgs(osid) + } + return nil +} + +// Dependencies scans the system for required dependencies +// Returns a list of dependencies search for, whether they were found +// and whether they were installed +func Dependencies(p PackageManager) (DependencyList, error) { + + var dependencies DependencyList + + for name, packages := range p.Packages() { + dependency := &Dependency{Name: name} + for _, pkg := range packages { + dependency.Optional = pkg.Optional + dependency.External = !pkg.SystemPackage + dependency.InstallCommand = p.InstallCommand(pkg) + packageavailable, err := p.PackageAvailable(pkg) + if err != nil { + return nil, err + } + if packageavailable { + dependency.Version = pkg.Version + dependency.PackageName = pkg.Name + installed, err := p.PackageInstalled(pkg) + if err != nil { + return nil, err + } + if installed { + dependency.Installed = true + dependency.Version = pkg.Version + if !pkg.SystemPackage { + dependency.Version = AppVersion(name) + } + } else { + dependency.InstallCommand = p.InstallCommand(pkg) + } + break + } + } + dependencies = append(dependencies, dependency) + } + + // Sort dependencies + sort.Slice(dependencies, func(i, j int) bool { + return dependencies[i].Name < dependencies[j].Name + }) + + return dependencies, nil +} + +// AppVersion returns the version for application related to the given package +func AppVersion(name string) string { + + if name == "gcc" { + return gccVersion() + } + + if name == "pkg-config" { + return pkgConfigVersion() + } + + if name == "npm" { + return npmVersion() + } + + if name == "docker" { + return dockerVersion() + } + + return "" + +} + +func gccVersion() string { + + var version string + var err error + + // Try "-dumpfullversion" + version, _, err = shell.RunCommand(".", "gcc", "-dumpfullversion") + if err != nil { + + // Try -dumpversion + // We ignore the error as this function is not for testing whether the + // application exists, only that we can get the version number + dumpversion, _, err := shell.RunCommand(".", "gcc", "-dumpversion") + if err == nil { + version = dumpversion + } + } + return strings.TrimSpace(version) +} + +func pkgConfigVersion() string { + version, _, _ := shell.RunCommand(".", "pkg-config", "--version") + return strings.TrimSpace(version) +} + +func npmVersion() string { + version, _, _ := shell.RunCommand(".", "npm", "--version") + return strings.TrimSpace(version) +} + +func dockerVersion() string { + version, _, _ := shell.RunCommand(".", "docker", "--version") + version = strings.TrimPrefix(version, "Docker version ") + version = strings.ReplaceAll(version, ", build ", " (") + version = strings.TrimSpace(version) + ")" + return version +} diff --git a/v3/internal/doctor/packagemanager/pacman.go b/v3/internal/doctor/packagemanager/pacman.go new file mode 100644 index 000000000..9f23514d1 --- /dev/null +++ b/v3/internal/doctor/packagemanager/pacman.go @@ -0,0 +1,112 @@ +//go:build linux + +package packagemanager + +import ( + "os/exec" + "regexp" + "strings" +) + +// Pacman represents the Pacman package manager +type Pacman struct { + name string + osid string +} + +// NewPacman creates a new Pacman instance +func NewPacman(osid string) *Pacman { + return &Pacman{ + name: "pacman", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (p *Pacman) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "gtk3", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "webkit2gtk", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkgconf", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (p *Pacman) Name() string { + return p.name +} + +// PackageInstalled tests if the given package name is installed +func (p *Pacman) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, err := execCmd("pacman", "-Q", pkg.Name) + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + splitoutput := strings.Split(stdout, "\n") + for _, line := range splitoutput { + if strings.HasPrefix(line, pkg.Name) { + splitline := strings.Split(line, " ") + pkg.Version = strings.TrimSpace(splitline[1]) + } + } + + return true, err +} + +// PackageAvailable tests if the given package is available for installation +func (p *Pacman) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + output, err := execCmd("pacman", "-Si", pkg.Name) + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + reg := regexp.MustCompile(`.*Version.*?:\s+(.*)`) + matches := reg.FindStringSubmatch(output) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + pkg.Version = strings.TrimSpace(matches[1]) + } + + return true, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (p *Pacman) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[p.osid] + } + return "sudo pacman -S " + pkg.Name +} diff --git a/v3/internal/doctor/packagemanager/pm.go b/v3/internal/doctor/packagemanager/pm.go new file mode 100644 index 000000000..3dd755a48 --- /dev/null +++ b/v3/internal/doctor/packagemanager/pm.go @@ -0,0 +1,64 @@ +//go:build linux + +package packagemanager + +// Package contains information about a system package +type Package struct { + Name string + Version string + InstallCommand map[string]string + SystemPackage bool + Library bool + Optional bool +} + +type packagemap = map[string][]*Package + +// PackageManager is a common interface across all package managers +type PackageManager interface { + Name() string + Packages() packagemap + PackageInstalled(*Package) (bool, error) + PackageAvailable(*Package) (bool, error) + InstallCommand(*Package) string +} + +// Dependency represents a system package that we require +type Dependency struct { + Name string + PackageName string + Installed bool + InstallCommand string + Version string + Optional bool + External bool +} + +// DependencyList is a list of Dependency instances +type DependencyList []*Dependency + +// InstallAllRequiredCommand returns the command you need to use to install all required dependencies +func (d DependencyList) InstallAllRequiredCommand() string { + + result := "" + for _, dependency := range d { + if !dependency.Installed && !dependency.Optional { + result += " - " + dependency.Name + ": " + dependency.InstallCommand + "\n" + } + } + + return result +} + +// InstallAllOptionalCommand returns the command you need to use to install all optional dependencies +func (d DependencyList) InstallAllOptionalCommand() string { + + result := "" + for _, dependency := range d { + if !dependency.Installed && dependency.Optional { + result += " - " + dependency.Name + ": " + dependency.InstallCommand + "\n" + } + } + + return result +} diff --git a/v3/internal/doctor/packagemanager/zypper.go b/v3/internal/doctor/packagemanager/zypper.go new file mode 100644 index 000000000..14ed8b5d2 --- /dev/null +++ b/v3/internal/doctor/packagemanager/zypper.go @@ -0,0 +1,122 @@ +//go:build linux +// +build linux + +package packagemanager + +import ( + "os/exec" + "regexp" + "strings" +) + +// Zypper represents the Zypper package manager +type Zypper struct { + name string + osid string +} + +// NewZypper creates a new Zypper instance +func NewZypper(osid string) *Zypper { + return &Zypper{ + name: "zypper", + osid: osid, + } +} + +// Packages returns the libraries that we need for Wails to compile +// They will potentially differ on different distributions or versions +func (z *Zypper) Packages() packagemap { + return packagemap{ + "libgtk-3": []*Package{ + {Name: "gtk3-devel", SystemPackage: true, Library: true}, + }, + "libwebkit": []*Package{ + {Name: "webkit2gtk3-soup2-devel", SystemPackage: true, Library: true}, + {Name: "webkit2gtk3-devel", SystemPackage: true, Library: true}, + }, + "gcc": []*Package{ + {Name: "gcc-c++", SystemPackage: true}, + }, + "pkg-config": []*Package{ + {Name: "pkg-config", SystemPackage: true}, + {Name: "pkgconf-pkg-config", SystemPackage: true}, + }, + "npm": []*Package{ + {Name: "npm10", SystemPackage: true}, + }, + "docker": []*Package{ + {Name: "docker", SystemPackage: true, Optional: true}, + }, + } +} + +// Name returns the name of the package manager +func (z *Zypper) Name() string { + return z.name +} + +// PackageInstalled tests if the given package name is installed +func (z *Zypper) PackageInstalled(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + stdout, err := cmdExec("zypper", "info", pkg.Name) + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + reg := regexp.MustCompile(`.*Installed\s*:\s*(Yes)\s*`) + matches := reg.FindStringSubmatch(stdout) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + z.getPackageVersion(pkg, stdout) + } + return noOfMatches > 1, err +} + +// PackageAvailable tests if the given package is available for installation +func (z *Zypper) PackageAvailable(pkg *Package) (bool, error) { + if pkg.SystemPackage == false { + return false, nil + } + var env []string + stdout, err := cmdExec("zypper", "info", pkg.Name) + // We add a space to ensure we get a full match, not partial match + if err != nil { + _, ok := err.(*exec.ExitError) + if ok { + return false, nil + } + return false, err + } + + available := strings.Contains(stdout, "Information for package") + if available { + z.getPackageVersion(pkg, stdout) + } + + return available, nil +} + +// InstallCommand returns the package manager specific command to install a package +func (z *Zypper) InstallCommand(pkg *Package) string { + if pkg.SystemPackage == false { + return pkg.InstallCommand[z.osid] + } + return "sudo zypper in " + pkg.Name +} + +func (z *Zypper) getPackageVersion(pkg *Package, output string) { + + reg := regexp.MustCompile(`.*Version.*:(.*)`) + matches := reg.FindStringSubmatch(output) + pkg.Version = "" + noOfMatches := len(matches) + if noOfMatches > 1 { + pkg.Version = strings.TrimSpace(matches[1]) + } +}