diff --git a/backend/app/debug_info.go b/backend/app/debug_info.go index 1a4d5bc0..bef87e94 100644 --- a/backend/app/debug_info.go +++ b/backend/app/debug_info.go @@ -2,11 +2,13 @@ package app import ( "archive/zip" + "crypto/sha256" + "encoding/hex" + "errors" "fmt" "log/slog" "os" "path/filepath" - "runtime" "strings" "time" @@ -25,6 +27,7 @@ type MetadataInstallation struct { LaunchPath string `json:"launchPath"` Name string `json:"name"` Profile string `json:"profile"` + Log string `json:"log"` } type Metadata struct { @@ -38,22 +41,60 @@ type Metadata struct { ModsEnabled bool `json:"modsEnabled"` } -func addFactoryGameLog(writer *zip.Writer) error { - if runtime.GOOS == "windows" { - cacheDir, err := os.UserCacheDir() +func addFactoryGameLogs(writer *zip.Writer) error { + cacheDir, err := os.UserCacheDir() + if err != nil { + return fmt.Errorf("failed to get user cache dir: %w", err) + } + err = utils.AddFileToZip(writer, filepath.Join(cacheDir, "FactoryGame", "Saved", "Logs", "FactoryGame.log"), "FactoryGame.log") + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to add file to zip: %w", err) + } + } + for _, meta := range ficsitcli.FicsitCLI.GetInstallationsMetadata() { + if meta.Info == nil { + continue + } + + logPath := filepath.Join(meta.Info.SavedPath, "Logs", "FactoryGame.log") + d, err := ficsitcli.FicsitCLI.GetInstallation(meta.Info.Path).GetDisk() if err != nil { - return fmt.Errorf("failed to get user cache dir: %w", err) + slog.Warn("failed to get disk for installation", slog.String("path", meta.Info.Path), slog.Any("error", err)) + continue } - err = utils.AddFileToZip(writer, filepath.Join(cacheDir, "FactoryGame", "Saved", "Logs", "FactoryGame.log"), "FactoryGame.log") + logExists, err := d.Exists(logPath) if err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("failed to add file to zip: %w", err) - } + slog.Warn("failed to check if log exists", slog.String("path", logPath), slog.Any("error", err)) + continue + } + if !logExists { + continue + } + bytes, err := d.Read(logPath) + if err != nil { + slog.Warn("failed to read log file", slog.String("path", logPath), slog.Any("error", err)) + continue + } + logFile, err := writer.Create(getLogNameForInstall(meta.Info)) + if err != nil { + slog.Warn("failed to create log file in zip", slog.Any("error", err)) + continue + } + _, err = logFile.Write(bytes) + if err != nil { + slog.Warn("failed to write log file to zip", slog.Any("error", err)) + continue } } return nil } +func getLogNameForInstall(install *common.Installation) string { + hash := sha256.Sum256([]byte(install.Path)) + return fmt.Sprintf("FactoryGame_%s.log", hex.EncodeToString(hash[:])[:8]) +} + func addMetadata(writer *zip.Writer) error { installs := ficsitcli.FicsitCLI.GetInstallations() selectedInstallInstance := ficsitcli.FicsitCLI.GetSelectedInstall() @@ -67,8 +108,9 @@ func addMetadata(writer *zip.Writer) error { } i := &MetadataInstallation{ Installation: metadata.Info, - Name: fmt.Sprintf("Satisfactory %s (%s)", metadata.Info.Branch, metadata.Info.Branch), + Name: fmt.Sprintf("Satisfactory %s (%s)", metadata.Info.Branch, metadata.Info.Launcher), Profile: ficsitcli.FicsitCLI.GetInstallation(install).Profile, + Log: getLogNameForInstall(metadata.Info), } i.Path = utils.RedactPath(i.Path) i.LaunchPath = strings.Join(i.Installation.LaunchPath, " ") @@ -143,7 +185,7 @@ func (a *app) generateAndSaveDebugInfo(filename string) error { writer := zip.NewWriter(file) defer writer.Close() - err = addFactoryGameLog(writer) + err = addFactoryGameLogs(writer) if err != nil { return fmt.Errorf("failed to add FactoryGame.log to debuginfo zip: %w", err) } @@ -153,6 +195,7 @@ func (a *app) generateAndSaveDebugInfo(filename string) error { return fmt.Errorf("failed to add metadata to debuginfo zip: %w", err) } + // Add SMM log last, as it may list errors from previous steps err = utils.AddFileToZip(writer, viper.GetString("log-file"), "SatisfactoryModManager.log") if err != nil { return fmt.Errorf("failed to add SatisfactoryModManager.log to debuginfo zip: %w", err) diff --git a/backend/ficsitcli/metadata.go b/backend/ficsitcli/metadata.go index b6ef9679..3eecc09b 100644 --- a/backend/ficsitcli/metadata.go +++ b/backend/ficsitcli/metadata.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log/slog" + "path" "strconv" "strings" "sync" @@ -161,12 +162,13 @@ func (f *ficsitCLI) getRemoteServerMetadata(installation *cli.Installation) (*co branch := common.BranchEarlyAccess // TODO: Do we have a way to detect this for remote installs? return &common.Installation{ - Path: installation.Path, - Type: installType, - Location: common.LocationTypeRemote, - Branch: branch, - Version: gameVersion, - Launcher: f.getNextRemoteLauncherName(), + Path: installation.Path, + Type: installType, + Location: common.LocationTypeRemote, + Branch: branch, + Version: gameVersion, + Launcher: f.getNextRemoteLauncherName(), + SavedPath: path.Join(installation.BasePath(), "FactoryGame", "Saved"), }, nil } diff --git a/backend/installfinders/common/gameinfo.go b/backend/installfinders/common/gameinfo.go index 30a359c7..3fdae109 100644 --- a/backend/installfinders/common/gameinfo.go +++ b/backend/installfinders/common/gameinfo.go @@ -55,7 +55,7 @@ type GameVersionFile struct { BuildID string `json:"BuildId"` } -func GetGameInfo(path string) (InstallType, int, error) { +func GetGameInfo(path string, platform Platform) (InstallType, int, string, error) { for _, info := range gameInfo { executablePath := filepath.Join(path, info.executable) if _, err := os.Stat(executablePath); os.IsNotExist(err) { @@ -65,20 +65,33 @@ func GetGameInfo(path string) (InstallType, int, error) { versionFilePath := filepath.Join(path, info.versionPath) if _, err := os.Stat(versionFilePath); os.IsNotExist(err) { - return InstallTypeWindowsClient, 0, fmt.Errorf("failed to get game info") + return InstallTypeWindowsClient, 0, "", fmt.Errorf("failed to get game info") } versionFile, err := os.ReadFile(versionFilePath) if err != nil { - return InstallTypeWindowsClient, 0, fmt.Errorf("failed to read version file %s: %w", versionFilePath, err) + return InstallTypeWindowsClient, 0, "", fmt.Errorf("failed to read version file %s: %w", versionFilePath, err) } var versionData GameVersionFile if err := json.Unmarshal(versionFile, &versionData); err != nil { - return InstallTypeWindowsClient, 0, fmt.Errorf("failed to parse version file %s: %w", versionFilePath, err) + return InstallTypeWindowsClient, 0, "", fmt.Errorf("failed to parse version file %s: %w", versionFilePath, err) } - return info.installType, versionData.Changelist, nil + return info.installType, versionData.Changelist, getGameSavedDir(path, info.installType, platform), nil } - return InstallTypeWindowsClient, 0, fmt.Errorf("failed to get game info") + return InstallTypeWindowsClient, 0, "", fmt.Errorf("failed to get game info") +} + +func getGameSavedDir(gamePath string, install InstallType, platform Platform) string { + if install == InstallTypeWindowsClient { + cacheDir, err := platform.CacheDir() + if err != nil { + slog.Error("failed to get cache dir", slog.Any("error", err)) + return "" + } + + return filepath.Join(cacheDir, "FactoryGame", "Saved") + } + return filepath.Join(gamePath, "FactoryGame", "Saved") } diff --git a/backend/installfinders/common/launcherplatform.go b/backend/installfinders/common/launcherplatform.go index 4f2546d0..13925c6f 100644 --- a/backend/installfinders/common/launcherplatform.go +++ b/backend/installfinders/common/launcherplatform.go @@ -2,6 +2,7 @@ package common type Platform interface { ProcessPath(path string) string + CacheDir() (string, error) Os() string } diff --git a/backend/installfinders/common/launcherplatform_native.go b/backend/installfinders/common/launcherplatform_native.go index 03a5f4f1..423f094e 100644 --- a/backend/installfinders/common/launcherplatform_native.go +++ b/backend/installfinders/common/launcherplatform_native.go @@ -1,6 +1,7 @@ package common import ( + "os" "runtime" ) @@ -14,6 +15,10 @@ func (p nativePlatform) ProcessPath(path string) string { return path } +func (p nativePlatform) CacheDir() (string, error) { + return os.UserCacheDir() //nolint:wrapcheck +} + func (p nativePlatform) Os() string { return runtime.GOOS } diff --git a/backend/installfinders/common/launcherplatform_unix.go b/backend/installfinders/common/launcherplatform_unix.go deleted file mode 100644 index 88d682ad..00000000 --- a/backend/installfinders/common/launcherplatform_unix.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build unix - -package common - -import ( - "path/filepath" - "strings" -) - -type winePlatform struct { - winePrefix string -} - -func WineLauncherPlatform(winePrefix string) Platform { - return winePlatform{winePrefix: winePrefix} -} - -func (p winePlatform) ProcessPath(path string) string { - return filepath.Join(p.winePrefix, "dosdevices", strings.ToLower(path[0:1])+strings.ReplaceAll(path[1:], "\\", "/")) -} - -func (p winePlatform) Os() string { - return "windows" -} diff --git a/backend/installfinders/common/launcherplatform_wine.go b/backend/installfinders/common/launcherplatform_wine.go new file mode 100644 index 00000000..8f62994d --- /dev/null +++ b/backend/installfinders/common/launcherplatform_wine.go @@ -0,0 +1,75 @@ +package common + +import ( + "errors" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "gopkg.in/ini.v1" +) + +type winePlatform struct { + winePrefix string +} + +func WineLauncherPlatform(winePrefix string) Platform { + return winePlatform{winePrefix: winePrefix} +} + +func (p winePlatform) ProcessPath(path string) string { + return filepath.Join(p.winePrefix, "dosdevices", strings.ToLower(path[0:1])+strings.ReplaceAll(path[1:], "\\", "/")) +} + +func (p winePlatform) CacheDir() (string, error) { + regCacheDir, err := p.getRegCacheDir() + if err == nil { + return regCacheDir, nil + } + if errors.Is(err, os.ErrNotExist) { + return p.getDefaultCacheDir() + } + return "", err +} + +func (p winePlatform) Os() string { + return "windows" +} + +func (p winePlatform) getRegCacheDir() (string, error) { + userRegPath := filepath.Join(p.winePrefix, "user.reg") + userRegBytes, err := os.ReadFile(userRegPath) + if err != nil { + return "", fmt.Errorf("failed to read user.reg: %w", err) + } + userRegText := string(userRegBytes) + if strings.HasPrefix(userRegText, "WINE REGISTRY") { + newLineIndex := strings.Index(userRegText, "\n") + userRegText = userRegText[newLineIndex+1:] + } + userReg, err := ini.Load(strings.NewReader(userRegText)) + if err != nil { + return "", fmt.Errorf("failed to load user.reg: %w", err) + } + return p.ProcessPath(userReg.Section(`Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders`).Key("Local AppData").String()), nil +} + +func (p winePlatform) getDefaultCacheDir() (string, error) { + // Default can be either + // modern: C:\Users\\AppData\Local + // legacy: C:\Users\\Local Settings\Application Data + currentUser, err := user.Current() + if err != nil { + return "", fmt.Errorf("failed to get current user: %w", err) + } + modernPath := p.ProcessPath(fmt.Sprintf("C:\\Users\\%s\\AppData\\Local", currentUser.Name)) + legacyPath := p.ProcessPath(fmt.Sprintf("C:\\Users\\%s\\Local Settings\\Application Data", currentUser.Name)) + + if _, err := os.Stat(modernPath); err == nil { + return modernPath, nil + } + + return legacyPath, nil +} diff --git a/backend/installfinders/common/types.go b/backend/installfinders/common/types.go index 00d30f8b..9cccb530 100644 --- a/backend/installfinders/common/types.go +++ b/backend/installfinders/common/types.go @@ -30,6 +30,7 @@ type Installation struct { Branch GameBranch `json:"branch"` Launcher string `json:"launcher"` LaunchPath []string `json:"launchPath"` + SavedPath string `json:"-"` } type InstallFindError struct { diff --git a/backend/installfinders/launchers/epic/epic.go b/backend/installfinders/launchers/epic/epic.go index 818480f6..93adc05a 100644 --- a/backend/installfinders/launchers/epic/epic.go +++ b/backend/installfinders/launchers/epic/epic.go @@ -122,7 +122,10 @@ func FindInstallationsEpic(epicManifestsPath string, launcher string, platform c continue } - installType, version, err := common.GetGameInfo(installLocation) + // Epic can only launch games of the same platform + gamePlatform := platform.Platform + + installType, version, savedPath, err := common.GetGameInfo(installLocation, gamePlatform) if err != nil { findErrors = append(findErrors, common.InstallFindError{ Path: installLocation, @@ -148,6 +151,7 @@ func FindInstallationsEpic(epicManifestsPath string, launcher string, platform c Branch: branch, Launcher: launcher, LaunchPath: platform.LauncherCommand(epicManifest.MainGameAppName), + SavedPath: savedPath, }) } diff --git a/backend/installfinders/launchers/heroic/heroic.go b/backend/installfinders/launchers/heroic/heroic.go index 793ae736..afc8c919 100644 --- a/backend/installfinders/launchers/heroic/heroic.go +++ b/backend/installfinders/launchers/heroic/heroic.go @@ -1,9 +1,12 @@ package heroic import ( + "encoding/json" "fmt" + "log/slog" "os" "path/filepath" + "strings" "github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders/common" "github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders/launchers/legendary" @@ -15,7 +18,68 @@ func findInstallationsHeroic(snap bool, xdgConfigHomeEnv string, launcher string return nil, []error{fmt.Errorf("failed to get heroic legendary config paths: %w", err)} } - return legendary.FindInstallationsIn(legendaryDataPath, launcher, common.MakeLauncherPlatform(common.NativePlatform(), nil)) + knownPrefixes, err := getHeroicKnownWinePrefixes(xdgConfigHomeEnv) + if err != nil { + return nil, []error{fmt.Errorf("failed to get heroic known wine prefixes: %w", err)} + } + + return legendary.FindInstallationsIn(legendaryDataPath, launcher, knownPrefixes, common.MakeLauncherPlatform(common.NativePlatform(), nil)) +} + +func getHeroicKnownWinePrefixes(xdgConfigHomeEnv string) (map[string]string, error) { + configPath := xdgConfigHomeEnv + if configPath == "" { + var err error + configPath, err = os.UserConfigDir() + if err != nil { + return nil, fmt.Errorf("failed to get user config dir: %w", err) + } + } + + heroicGamesConfigPath := filepath.Join(configPath, "heroic", "GamesConfig") + + knownPrefixes := make(map[string]string) + + items, err := os.ReadDir(heroicGamesConfigPath) + if err != nil { + if os.IsNotExist(err) { + return knownPrefixes, nil + } + return nil, fmt.Errorf("failed to read heroic games config path: %w", err) + } + + for _, item := range items { + if item.IsDir() || !strings.HasSuffix(item.Name(), ".json") { + continue + } + + gameID := strings.TrimSuffix(item.Name(), ".json") + + bytes, err := os.ReadFile(filepath.Join(heroicGamesConfigPath, item.Name())) + if err != nil { + slog.Warn("failed to read heroic game config", slog.String("path", item.Name()), slog.Any("error", err)) + continue + } + + var game map[string]interface{} + if err = json.Unmarshal(bytes, &game); err != nil { + slog.Warn("failed to parse heroic game config", slog.String("path", item.Name()), slog.Any("error", err)) + continue + } + + if gameEntry := game[gameID]; gameEntry != nil { + gameData := gameEntry.(map[string]interface{}) + + prefix, ok := gameData["winePrefix"].(string) + if !ok { + continue + } + + knownPrefixes[gameID] = prefix + } + } + + return knownPrefixes, nil } func getHeroicLegendaryConfigPath(snap bool, xdgConfigHomeEnv string) (string, error) { diff --git a/backend/installfinders/launchers/legendary/legendary.go b/backend/installfinders/launchers/legendary/legendary.go index aa18a955..b40bab24 100644 --- a/backend/installfinders/launchers/legendary/legendary.go +++ b/backend/installfinders/launchers/legendary/legendary.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" + "gopkg.in/ini.v1" + "github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders/common" "github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders/launchers/epic" ) @@ -30,7 +32,7 @@ type Game struct { type Data = map[string]Game -func FindInstallationsIn(legendaryDataPath string, launcher string, platform common.LauncherPlatform) ([]*common.Installation, []error) { +func FindInstallationsIn(legendaryDataPath string, launcher string, knownPrefixes map[string]string, platform common.LauncherPlatform) ([]*common.Installation, []error) { legendaryInstalledPath := filepath.Join(legendaryDataPath, "installed.json") if _, err := os.Stat(legendaryInstalledPath); os.IsNotExist(err) { return nil, []error{fmt.Errorf("%s not installed", launcher)} @@ -51,7 +53,24 @@ func FindInstallationsIn(legendaryDataPath string, launcher string, platform com for _, legendaryGame := range legendaryData { installLocation := filepath.Clean(legendaryGame.InstallPath) - installType, version, err := common.GetGameInfo(installLocation) + gamePlatform := platform.Platform + + if platform.Os() != "windows" { + if knownPrefix, found := knownPrefixes[legendaryGame.AppName]; found { + gamePlatform = common.WineLauncherPlatform(knownPrefix) + } else { + prefix, err := getLegendaryWinePrefix(legendaryDataPath, legendaryGame.AppName, platform) + if err != nil { + findErrors = append(findErrors, fmt.Errorf("failed to get wine prefix for %s: %w", legendaryGame.AppName, err)) + continue + } + if prefix != "" { + gamePlatform = common.WineLauncherPlatform(prefix) + } + } + } + + installType, version, savedPath, err := common.GetGameInfo(installLocation, gamePlatform) if err != nil { findErrors = append(findErrors, common.InstallFindError{ Path: installLocation, @@ -77,6 +96,7 @@ func FindInstallationsIn(legendaryDataPath string, launcher string, platform com Branch: branch, Launcher: launcher, LaunchPath: platform.LauncherCommand(legendaryGame.AppName), + SavedPath: savedPath, }) } return installs, findErrors @@ -98,3 +118,47 @@ func getGlobalLegendaryDataPath(xdgConfigHomeEnv string) (string, error) { } return filepath.Join(homeDir, ".config", "legendary"), nil } + +func getLegendaryWinePrefix(legendaryDataPath string, appName string, platform common.Platform) (string, error) { + // Should be kept in sync with + // https://github.com/derrod/legendary/blob/master/legendary/core.py#L591 + + config, err := ini.Load(filepath.Join(legendaryDataPath, "config.ini")) + if err != nil { + return "", fmt.Errorf("failed to load legendary config.ini: %w", err) + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home dir: %w", err) + } + + prefix := "" + + prefix = stringOrFallback(config.Section("default.env").Key("WINEPREFIX").String(), prefix) + prefix = stringOrFallback(config.Section(fmt.Sprintf("%s.env", appName)).Key("WINEPREFIX").String(), prefix) + + if platform.Os() == "darwin" { + cxBottle := "Legendary" + cxBottle = stringOrFallback(config.Section("default").Key("crossover_bottle").String(), cxBottle) + cxBottle = stringOrFallback(config.Section(appName).Key("crossover_bottle").String(), cxBottle) + + bottlePath := filepath.Join(homeDir, "Library", "Application Support", "CrossOver", "Bottles", cxBottle) + if _, err := os.Stat(bottlePath); err == nil { + prefix = stringOrFallback(bottlePath, prefix) + } + } + + prefix = stringOrFallback(config.Section(appName).Key("wine_prefix").String(), prefix) + + prefix = stringOrFallback(filepath.Join(homeDir, ".wine"), prefix) + + return prefix, nil +} + +func stringOrFallback(a, fallback string) string { + if a == "" { + return fallback + } + return a +} diff --git a/backend/installfinders/launchers/legendary/legendary_all.go b/backend/installfinders/launchers/legendary/legendary_all.go index beb51074..53c19765 100644 --- a/backend/installfinders/launchers/legendary/legendary_all.go +++ b/backend/installfinders/launchers/legendary/legendary_all.go @@ -21,6 +21,7 @@ func init() { return FindInstallationsIn( legendaryDataPath, "Legendary", + nil, common.MakeLauncherPlatform( common.NativePlatform(), func(appName string) []string { diff --git a/backend/installfinders/launchers/lutris/lutris_linux.go b/backend/installfinders/launchers/lutris/lutris_linux.go index 36ee23fa..38beba20 100644 --- a/backend/installfinders/launchers/lutris/lutris_linux.go +++ b/backend/installfinders/launchers/lutris/lutris_linux.go @@ -30,6 +30,7 @@ func init() { func findInstallations(lutrisCmd []string, launcher string) ([]*common.Installation, []error) { lutrisLjCmd := makeLutrisCmd(lutrisCmd, "-lj") lutrisLj := exec.Command(lutrisLjCmd[0], lutrisLjCmd[1:]...) + lutrisLj.Env = append(lutrisLj.Env, "LUTRIS_SKIP_INIT=1") outputBytes, err := lutrisLj.Output() if err != nil { return nil, []error{ diff --git a/backend/installfinders/launchers/steam/steam.go b/backend/installfinders/launchers/steam/steam.go index 65c2e928..5d099b6e 100644 --- a/backend/installfinders/launchers/steam/steam.go +++ b/backend/installfinders/launchers/steam/steam.go @@ -91,9 +91,27 @@ func FindInstallationsSteam(steamPath string, launcher string, platform common.L continue } - fullInstallationPath := processPath(filepath.Join(libraryFolder, "steamapps", "common", manifest["AppState"].(map[string]interface{})["installdir"].(string))) + appState := manifest["AppState"].(map[string]interface{}) + + fullInstallationPath := platform.ProcessPath(filepath.Join(libraryFolder, "steamapps", "common", appState["installdir"].(string))) + + gamePlatform := platform.Platform + if platform.Os() != "windows" { + // The game might be running under Proton + // There's no appmanifest field that would specify it, but if the proton prefix exists, + // the game is most likely running under Proton. + gameProtonPrefix := platform.ProcessPath(filepath.Join(steamPath, "steamapps", "compatdata", appState["appid"].(string), "pfx")) + _, err := os.Stat(gameProtonPrefix) + if err != nil && !os.IsNotExist(err) { + findErrors = append(findErrors, fmt.Errorf("failed to find proton prefix for game %s: %w", appState["appid"].(string), err)) + continue + } + if err == nil { + gamePlatform = common.WineLauncherPlatform(gameProtonPrefix) + } + } - installType, version, err := common.GetGameInfo(fullInstallationPath) + installType, version, savedPath, err := common.GetGameInfo(fullInstallationPath, gamePlatform) if err != nil { findErrors = append(findErrors, common.InstallFindError{ Path: fullInstallationPath, @@ -123,6 +141,8 @@ func FindInstallationsSteam(steamPath string, launcher string, platform common.L Branch: branch, Launcher: launcher, LaunchPath: platform.LauncherCommand(`steam://rungameid/526870`), + // pass wine platform if necessary, as platform here is going to be native + SavedPath: savedPath, }) } } diff --git a/go.mod b/go.mod index 4ab93ec1..1ce313c7 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 golang.org/x/sync v0.6.0 golang.org/x/sys v0.18.0 + gopkg.in/ini.v1 v1.67.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 howett.net/plist v1.0.1 ) @@ -92,6 +93,5 @@ require ( golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/text v0.14.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect )