namespace WiiCompiled.Setup; /// /// A portable installation is a self-contained directory tree the user can move or carry on removable media: /// /// <root>\portable.txt marker; its contents are irrelevant /// <root>\Install\ the installation directory /// <root>\UserData\ runtime user state (Config.toml, NAND, Cache, Logs) /// /// The runtime finds the same root independently (runtime/include/runtime_config.h, /// PortableRootDirectory); this class must keep the same marker name, layout, and depth bound. /// internal static class PortableRoot { public const string MarkerFileName = "portable.txt"; public const string UserDataDirectoryName = "UserData"; public const string InstallDirectoryName = "Install"; /// /// Parents searched above the start directory. Kept small (installs sit 2 levels below root, /// <root>\Install\Base\game.exe) so an unrelated marker far up a drive can't capture it. /// public const int MaximumSearchDepth = 4; /// /// The portable root belongs to, or null otherwise. Callers pass the /// installation directory (not the running executable's location), since a downloaded setup runs from Downloads. /// public static string? TryFind(string startDirectory) { if (string.IsNullOrWhiteSpace(startDirectory)) return null; string current; try { // Normalize before trimming: "C:\" trimmed to "C:" is the current directory on that // drive, not the drive root. current = Normalize(startDirectory); } catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) { return null; } for (var level = 0; level <= MaximumSearchDepth; level++) { if (File.Exists(Path.Combine(current, MarkerFileName))) return current; var parent = Path.GetDirectoryName(current); if (string.IsNullOrEmpty(parent) || parent.Equals(current, StringComparison.Ordinal)) break; current = parent; } return null; } public static string UserDataDirectory(string root) => Path.Combine(root, UserDataDirectoryName); /// Whether is or lives inside it. public static bool Contains(string root, string candidate) => FileSystemUtilities.PathContains(root, candidate); /// /// Establishes the root an install is about to publish into: the marker and the user-state /// directory must exist before anything resolves a configuration path, because that resolution is /// what decides whether the installation writes portable or machine-wide settings. /// public static string Create(string root) { var full = Normalize(root); EnsureUsableRoot(full); Directory.CreateDirectory(full); Directory.CreateDirectory(UserDataDirectory(full)); var marker = Path.Combine(full, MarkerFileName); if (!File.Exists(marker)) { File.WriteAllText(marker, $"{ProductInfo.Name} portable installation." + Environment.NewLine + "This marker makes the runtime keep Config.toml, NAND, Cache, and Logs in UserData\\ " + "beside it instead of in %LOCALAPPDATA%." + Environment.NewLine + "Delete it to make this installation use per-user application data again." + Environment.NewLine); } return full; } /// /// A portable root is deleted wholesale by the user, so like an install directory it may never be a /// drive root or well-known system location; both share . /// public static void EnsureUsableRoot(string root) => FileSystemUtilities.EnsureUsableLocation(root, "A portable installation root"); private static string Normalize(string path) => FileSystemUtilities.NormalizePath(path); } /// /// A portable root can be moved or renamed between operations. Every installed-host operation that /// reads install-state.json passes through here first so exactly one place decides what a /// moved installation means, and so a non-portable installation is never touched. /// internal static class PortableInstallHealing { /// /// Reconciles a moved portable installation with its recorded location: the state file adopts the /// directory it was actually found in, and the native build tree is discarded because its /// CMake cache holds absolute paths from the old location. Returns whether anything was healed. /// public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null) { // Guard: an ordinary installation that disagrees with its state file is a real problem for // the operation to report, not something to silently rewrite. if (PortableRoot.TryFind(installation.Root) is null) return false; var state = installation.ReadInstallState(); if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false; string recorded; try { recorded = FileSystemUtilities.NormalizePath(state.InstallDir); } catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) { recorded = state.InstallDir; } if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false; var previous = state.InstallDir; state.InstallDir = installation.Root; JsonState.Write(installation.InstallStatePath, state); // The configured native build directory bakes absolute source, toolchain, and output paths // into CMakeCache.txt. After a move it is unusable and would fail the next configure rather // than being reused, so it is removed and reconfigured from scratch on the next build. var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build"); var hadNativeBuild = Directory.Exists(nativeBuild); if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild); reporter?.Diagnostic( $"This portable installation moved from {previous} to {installation.Root}. " + "The recorded location was updated" + (hadNativeBuild ? " and the location-bound native build cache was discarded." : ".")); return true; } }