namespace YMhut.Box.Core.DevEnvironments; public static class DevEnvironmentBuildScript { public static IReadOnlyList Create(DevEnvironmentInstallPlan plan, string archivePath) { ArgumentNullException.ThrowIfNull(plan); ArgumentException.ThrowIfNullOrWhiteSpace(archivePath); var recipeCommands = plan.BuildRecipe .Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Skip(1) .ToArray(); var environmentId = SafePathPart(plan.EnvironmentId); var version = SafePathPart(plan.Version.Version); var lines = new List { "@echo off", "setlocal EnableExtensions", "chcp 65001 >nul 2>&1", "set \"SCRIPT_PATH=%~f0\"", $"set \"ARCHIVE={EscapeSetValue(archivePath)}\"", $"set \"WORKDIR=%USERPROFILE%\\YMhutBuilds\\{environmentId}-{version}\"", $"title YMhut Build - {EscapeEcho(environmentId)} {EscapeEcho(version)}", $"echo Waiting for {EscapeEcho(plan.EnvironmentName)} {EscapeEcho(plan.Version.Version)} source archive...", ":wait_download", "if not exist \"%ARCHIVE%\" (", " timeout /t 2 >nul", " goto :wait_download", ")", "if not exist \"%WORKDIR%\" mkdir \"%WORKDIR%\"", "if errorlevel 1 goto :failed", "cd /d \"%WORKDIR%\"", "if errorlevel 1 goto :failed", "echo Extracting source archive...", "tar -xf \"%ARCHIVE%\" --strip-components=1", "if errorlevel 1 goto :failed" }; foreach (var command in recipeCommands) { var executable = NormalizeRecipeCommand(command); if (string.IsNullOrWhiteSpace(executable)) { continue; } lines.Add($"echo ^> {EscapeEcho(executable)}"); lines.Add(executable); lines.Add("if errorlevel 1 goto :failed"); } lines.AddRange([ "echo.", "echo Build completed successfully.", "del \"%SCRIPT_PATH%\" >nul 2>&1", "exit /b 0", ":failed", "set \"EXIT_CODE=%ERRORLEVEL%\"", "if \"%EXIT_CODE%\"==\"0\" set \"EXIT_CODE=1\"", "echo.", "echo [ERROR] Source build failed with exit code %EXIT_CODE%.", "echo [ERROR] Working directory: %WORKDIR%", "echo [ERROR] This script was kept at: %SCRIPT_PATH%", "pause", "exit /b %EXIT_CODE%" ]); return lines; } private static string NormalizeRecipeCommand(string command) { if (command.StartsWith("tar -xf ", StringComparison.OrdinalIgnoreCase)) { return string.Empty; } if (command.StartsWith("cd ", StringComparison.OrdinalIgnoreCase)) { var target = command[3..].Trim().Trim('"'); if (target.Contains('*') || target.Contains('?')) { return string.Empty; } if (target.Equals("go\\src", StringComparison.OrdinalIgnoreCase)) { return "cd /d \"%WORKDIR%\\src\""; } return $"cd /d \"%WORKDIR%\\{target}\""; } var firstToken = command.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[0].Trim('"'); return firstToken.EndsWith(".bat", StringComparison.OrdinalIgnoreCase) || firstToken.EndsWith(".cmd", StringComparison.OrdinalIgnoreCase) ? "call " + command : command; } private static string SafePathPart(string value) => string.Concat((value ?? string.Empty).Select(character => Path.GetInvalidFileNameChars().Contains(character) ? '_' : character)); private static string EscapeSetValue(string value) => value.Replace("%", "%%", StringComparison.Ordinal); private static string EscapeEcho(string value) => value.Replace("^", "^^", StringComparison.Ordinal) .Replace("&", "^&", StringComparison.Ordinal) .Replace("|", "^|", StringComparison.Ordinal) .Replace("<", "^<", StringComparison.Ordinal) .Replace(">", "^>", StringComparison.Ordinal); }