From 654ad4b4b223e510b630a5d39769685b030b7da0 Mon Sep 17 00:00:00 2001 From: Marie Ramlow Date: Fri, 26 May 2023 17:26:14 +0200 Subject: [PATCH 1/3] Add headless flag for unattended execution --- build/windows/finalize-install-package.sh | 2 ++ docs/technical/smapi.md | 3 +- src/SMAPI.Installer/InteractiveInstaller.cs | 36 +++++++++++++++------ src/SMAPI.Installer/Program.cs | 4 ++- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/build/windows/finalize-install-package.sh b/build/windows/finalize-install-package.sh index 117e33e5..8428cf41 100755 --- a/build/windows/finalize-install-package.sh +++ b/build/windows/finalize-install-package.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -ex + ########## ## Read config ########## diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index d115aefa..3dd66c78 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -32,6 +32,7 @@ argument | purpose `--install` | Preselects the install action, skipping the prompt asking what the user wants to do. `--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. `--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. +`--headless` | Installs or Uninstalls SMAPI without any user interaction. Should only be used in scripts, tools or if you know what you're doing. SMAPI itself recognises five arguments, but these are meant for internal use or testing, and might change without warning. **On Linux/macOS**, command-line arguments won't work; see _environment @@ -96,7 +97,7 @@ Windows](#on-windows)_ section below to create a build that retains the icon.** 2. Run `sudo apt update` in WSL to update the package list. 3. The rest of the instructions below should be run in WSL. 2. Install the required software: - 1. Install the [.NET 5 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu). + 1. Install the [.NET 5 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu). _For Ubuntu-based systems, you can run `lsb_release -a` to get the Ubuntu version number._ 2. [Install Steam](https://linuxconfig.org/how-to-install-steam-on-ubuntu-20-04-focal-fossa-linux). 3. Launch `steam` and install the game like usual. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 32a4e6e5..d07538e7 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -33,6 +33,9 @@ namespace StardewModdingApi.Installer "SMAPI.ErrorHandler" }; + /// If the installer should run headless, does not ask the user for any input when true. + private readonly bool Headless; + /// Get the absolute file or folder paths to remove when uninstalling SMAPI. /// The folder for Stardew Valley and SMAPI. /// The folder for SMAPI mods. @@ -97,10 +100,11 @@ namespace StardewModdingApi.Installer *********/ /// Construct an instance. /// The absolute path to the directory containing the files to copy into the game folder. - public InteractiveInstaller(string bundlePath) + public InteractiveInstaller(string bundlePath, bool headless) { this.BundlePath = bundlePath; this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform()); + this.Headless = headless; } /// Run the install or uninstall script. @@ -142,14 +146,14 @@ namespace StardewModdingApi.Installer if (context.IsUnix) { this.PrintError($"This is the installer for Windows. Run the 'install on {context.Platform}.{(context.Platform == Platform.Mac ? "command" : "sh")}' file instead."); - Console.ReadLine(); + this.AwaitConfirmation(); return; } #else if (context.IsWindows) { this.PrintError($"This is the installer for Linux/macOS. Run the 'install on Windows.exe' file instead."); - Console.ReadLine(); + this.AwaitConfirmation(); return; } #endif @@ -163,7 +167,12 @@ namespace StardewModdingApi.Installer if (installArg && uninstallArg) { this.PrintError("You can't specify both --install and --uninstall command-line flags."); - Console.ReadLine(); + this.AwaitConfirmation(); + return; + } + if (!installArg && !uninstallArg && Headless) + { + this.PrintError("Either --install or --uninstall is required when running with --headless."); return; } @@ -180,7 +189,7 @@ namespace StardewModdingApi.Installer ** Step 2: choose a theme (can't auto-detect on Linux/macOS) *********/ MonitorColorScheme scheme = MonitorColorScheme.AutoDetect; - if (context.IsUnix) + if (context.IsUnix && !Headless) { /**** ** print header @@ -245,7 +254,7 @@ namespace StardewModdingApi.Installer if (installDir == null) { this.PrintError("Failed finding your game path."); - Console.ReadLine(); + this.AwaitConfirmation(); return; } @@ -262,7 +271,7 @@ namespace StardewModdingApi.Installer if (!File.Exists(paths.GameDllPath)) { this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); - Console.ReadLine(); + this.AwaitConfirmation(); return; } Console.Clear(); @@ -513,7 +522,7 @@ namespace StardewModdingApi.Installer ); } - Console.ReadKey(); + this.AwaitConfirmation(); } @@ -594,7 +603,7 @@ namespace StardewModdingApi.Installer { this.PrintError($"Oops! The installer couldn't delete {path}: [{ex.GetType().Name}] {ex.Message}."); this.PrintError("Try rebooting your computer and then run the installer again. If that doesn't work, try deleting it yourself then press any key to retry."); - Console.ReadKey(); + this.AwaitConfirmation(); } } } @@ -906,5 +915,14 @@ namespace StardewModdingApi.Installer _ => true }; } + + /// Await confirmation (pressing enter) from the User. + private void AwaitConfirmation() + { + if (!this.Headless) + { + Console.ReadLine(); + } + } } } diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index dc452a46..cf38f1cf 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Reflection; using System.Threading; +using System.Linq; namespace StardewModdingApi.Installer { @@ -47,8 +48,9 @@ namespace StardewModdingApi.Installer // set up assembly resolution AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; + var headless = args.Contains("--headless"); // launch installer - var installer = new InteractiveInstaller(bundleDir.FullName); + var installer = new InteractiveInstaller(bundleDir.FullName, headless); try { From 05695be39073b1235ddc3b4d0445b8c074930080 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Jun 2023 12:36:02 -0400 Subject: [PATCH 2/3] remove unrelated `set -ex` in build script --- build/windows/finalize-install-package.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/build/windows/finalize-install-package.sh b/build/windows/finalize-install-package.sh index 8428cf41..117e33e5 100755 --- a/build/windows/finalize-install-package.sh +++ b/build/windows/finalize-install-package.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -set -ex - ########## ## Read config ########## From 5196c3bad96b212625290595ff177e2e45f9a099 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Jun 2023 12:36:02 -0400 Subject: [PATCH 3/3] rename `--headless` to `--no-prompt`, and parse with the other args --- docs/technical/smapi.md | 4 +- src/SMAPI.Installer/InteractiveInstaller.cs | 96 ++++++++++----------- src/SMAPI.Installer/Program.cs | 4 +- 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index 3dd66c78..d1591143 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -32,7 +32,7 @@ argument | purpose `--install` | Preselects the install action, skipping the prompt asking what the user wants to do. `--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. `--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. -`--headless` | Installs or Uninstalls SMAPI without any user interaction. Should only be used in scripts, tools or if you know what you're doing. +`--no-prompt` | Don't let the installer wait for user input (e.g. for cases where it's being run by a script). If the installer is unable to continue without user input, it'll fail instead. SMAPI itself recognises five arguments, but these are meant for internal use or testing, and might change without warning. **On Linux/macOS**, command-line arguments won't work; see _environment @@ -97,7 +97,7 @@ Windows](#on-windows)_ section below to create a build that retains the icon.** 2. Run `sudo apt update` in WSL to update the package list. 3. The rest of the instructions below should be run in WSL. 2. Install the required software: - 1. Install the [.NET 5 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu). + 1. Install the [.NET 5 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu). _For Ubuntu-based systems, you can run `lsb_release -a` to get the Ubuntu version number._ 2. [Install Steam](https://linuxconfig.org/how-to-install-steam-on-ubuntu-20-04-focal-fossa-linux). 3. Launch `steam` and install the game like usual. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index d07538e7..c21ccdf5 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -33,9 +33,6 @@ namespace StardewModdingApi.Installer "SMAPI.ErrorHandler" }; - /// If the installer should run headless, does not ask the user for any input when true. - private readonly bool Headless; - /// Get the absolute file or folder paths to remove when uninstalling SMAPI. /// The folder for Stardew Valley and SMAPI. /// The folder for SMAPI mods. @@ -100,11 +97,10 @@ namespace StardewModdingApi.Installer *********/ /// Construct an instance. /// The absolute path to the directory containing the files to copy into the game folder. - public InteractiveInstaller(string bundlePath, bool headless) + public InteractiveInstaller(string bundlePath) { this.BundlePath = bundlePath; this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform()); - this.Headless = headless; } /// Run the install or uninstall script. @@ -139,40 +135,24 @@ namespace StardewModdingApi.Installer Console.Title = $"SMAPI {context.GetInstallerVersion()} installer on {context.Platform} {context.PlatformName}"; Console.WriteLine(); - /**** - ** Check if correct installer - ****/ -#if SMAPI_FOR_WINDOWS - if (context.IsUnix) - { - this.PrintError($"This is the installer for Windows. Run the 'install on {context.Platform}.{(context.Platform == Platform.Mac ? "command" : "sh")}' file instead."); - this.AwaitConfirmation(); - return; - } -#else - if (context.IsWindows) - { - this.PrintError($"This is the installer for Linux/macOS. Run the 'install on Windows.exe' file instead."); - this.AwaitConfirmation(); - return; - } -#endif - /**** ** read command-line arguments ****/ - // get action from CLI + // get input mode + bool allowUserInput = !args.Contains("--no-prompt"); + + // get action bool installArg = args.Contains("--install"); bool uninstallArg = args.Contains("--uninstall"); if (installArg && uninstallArg) { this.PrintError("You can't specify both --install and --uninstall command-line flags."); - this.AwaitConfirmation(); + this.AwaitConfirmation(allowUserInput); return; } - if (!installArg && !uninstallArg && Headless) + if (!allowUserInput && !installArg && !uninstallArg) { - this.PrintError("Either --install or --uninstall is required when running with --headless."); + this.PrintError("You must specify --install or --uninstall when running with --no-prompt."); return; } @@ -184,12 +164,31 @@ namespace StardewModdingApi.Installer gamePathArg = args[pathIndex]; } + /**** + ** Check if correct installer + ****/ +#if SMAPI_FOR_WINDOWS + if (context.IsUnix) + { + this.PrintError($"This is the installer for Windows. Run the 'install on {context.Platform}.{(context.Platform == Platform.Mac ? "command" : "sh")}' file instead."); + this.AwaitConfirmation(allowUserInput); + return; + } +#else + if (context.IsWindows) + { + this.PrintError($"This is the installer for Linux/macOS. Run the 'install on Windows.exe' file instead."); + this.AwaitConfirmation(); + return; + } +#endif + /********* ** Step 2: choose a theme (can't auto-detect on Linux/macOS) *********/ MonitorColorScheme scheme = MonitorColorScheme.AutoDetect; - if (context.IsUnix && !Headless) + if (context.IsUnix && allowUserInput) { /**** ** print header @@ -254,7 +253,7 @@ namespace StardewModdingApi.Installer if (installDir == null) { this.PrintError("Failed finding your game path."); - this.AwaitConfirmation(); + this.AwaitConfirmation(allowUserInput); return; } @@ -271,7 +270,7 @@ namespace StardewModdingApi.Installer if (!File.Exists(paths.GameDllPath)) { this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); - this.AwaitConfirmation(); + this.AwaitConfirmation(allowUserInput); return; } Console.Clear(); @@ -349,7 +348,7 @@ namespace StardewModdingApi.Installer if (context.IsUnix && File.Exists(paths.BackupLaunchScriptPath)) { this.PrintDebug("Removing SMAPI launcher..."); - this.InteractivelyDelete(paths.VanillaLaunchScriptPath); + this.InteractivelyDelete(paths.VanillaLaunchScriptPath, allowUserInput); File.Move(paths.BackupLaunchScriptPath, paths.VanillaLaunchScriptPath); } @@ -361,7 +360,7 @@ namespace StardewModdingApi.Installer { this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files..."); foreach (string path in removePaths) - this.InteractivelyDelete(path); + this.InteractivelyDelete(path, allowUserInput); } // move global save data folder (changed in 3.2) @@ -373,7 +372,7 @@ namespace StardewModdingApi.Installer if (oldDir.Exists) { if (newDir.Exists) - this.InteractivelyDelete(oldDir.FullName); + this.InteractivelyDelete(oldDir.FullName, allowUserInput); else oldDir.MoveTo(newDir.FullName); } @@ -388,7 +387,7 @@ namespace StardewModdingApi.Installer this.PrintDebug("Adding SMAPI files..."); foreach (FileSystemInfo sourceEntry in paths.BundleDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) { - this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name)); + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name), allowUserInput); this.RecursiveCopy(sourceEntry, paths.GameDir); } @@ -403,7 +402,7 @@ namespace StardewModdingApi.Installer if (!File.Exists(paths.BackupLaunchScriptPath)) File.Move(paths.VanillaLaunchScriptPath, paths.BackupLaunchScriptPath); else - this.InteractivelyDelete(paths.VanillaLaunchScriptPath); + this.InteractivelyDelete(paths.VanillaLaunchScriptPath, allowUserInput); } // add new launcher @@ -473,7 +472,7 @@ namespace StardewModdingApi.Installer // remove existing folder if (targetFolder.Exists) - this.InteractivelyDelete(targetFolder.FullName); + this.InteractivelyDelete(targetFolder.FullName, allowUserInput); // copy files this.RecursiveCopy(sourceMod.Directory, paths.ModsDir, filter: this.ShouldCopy); @@ -491,7 +490,7 @@ namespace StardewModdingApi.Installer #if SMAPI_DEPRECATED // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir); + this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir, allowUserInput); #endif } } @@ -522,7 +521,7 @@ namespace StardewModdingApi.Installer ); } - this.AwaitConfirmation(); + this.AwaitConfirmation(allowUserInput); } @@ -590,7 +589,8 @@ namespace StardewModdingApi.Installer /// Interactively delete a file or folder path, and block until deletion completes. /// The file or folder path. - private void InteractivelyDelete(string path) + /// Whether the installer can ask for user input from the terminal. + private void InteractivelyDelete(string path, bool allowUserInput) { while (true) { @@ -603,7 +603,7 @@ namespace StardewModdingApi.Installer { this.PrintError($"Oops! The installer couldn't delete {path}: [{ex.GetType().Name}] {ex.Message}."); this.PrintError("Try rebooting your computer and then run the installer again. If that doesn't work, try deleting it yourself then press any key to retry."); - this.AwaitConfirmation(); + this.AwaitConfirmation(allowUserInput); } } } @@ -823,7 +823,8 @@ namespace StardewModdingApi.Installer /// Interactively move mods out of the app data directory. /// The directory which should contain all mods. /// The installer directory containing packaged mods. - private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir) + /// Whether the installer can ask for user input from the terminal. + private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir, bool allowUserInput) { // get packaged mods to delete string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray(); @@ -850,7 +851,7 @@ namespace StardewModdingApi.Installer if (isDir && packagedModNames.Contains(entry.Name, StringComparer.OrdinalIgnoreCase)) { this.PrintDebug($" Deleting {entry.Name} because it's bundled into SMAPI..."); - this.InteractivelyDelete(entry.FullName); + this.InteractivelyDelete(entry.FullName, allowUserInput); continue; } @@ -916,13 +917,12 @@ namespace StardewModdingApi.Installer }; } - /// Await confirmation (pressing enter) from the User. - private void AwaitConfirmation() + /// Wait until the user presses enter to confirm, if user input is allowed. + /// Whether the installer can ask for user input from the terminal. + private void AwaitConfirmation(bool allowUserInput) { - if (!this.Headless) - { + if (allowUserInput) Console.ReadLine(); - } } } } diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index cf38f1cf..dc452a46 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -4,7 +4,6 @@ using System.IO; using System.IO.Compression; using System.Reflection; using System.Threading; -using System.Linq; namespace StardewModdingApi.Installer { @@ -48,9 +47,8 @@ namespace StardewModdingApi.Installer // set up assembly resolution AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; - var headless = args.Contains("--headless"); // launch installer - var installer = new InteractiveInstaller(bundleDir.FullName, headless); + var installer = new InteractiveInstaller(bundleDir.FullName); try {