diff --git a/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs new file mode 100644 index 00000000..2de5223f --- /dev/null +++ b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs @@ -0,0 +1,84 @@ +using Godot; +using System.Diagnostics; +using System.Threading.Tasks; +using System.IO; // For Path.GetFullPath + +namespace SharpIDE.Godot.Features.SlnPicker; + +public partial class GitCommandLine : Node +{ + /// + /// Initializes a Git repository in the specified base path by running 'git init'. + /// Runs asynchronously to avoid blocking the Godot main thread. + /// + /// The directory path where the repo should be created. + /// A task that completes when the command finishes, with success indicator. + public async Task InitializeGitRepoAsync(string basePath) + { + // Normalize and resolve absolute path for cross-platform reliability + basePath = Path.GetFullPath(basePath); + // First, check if Git is installed + bool gitInstalled = await CheckGitInstalledAsync(); + if (!gitInstalled) + { + GD.Print("Git is not installed or not found in PATH. Cannot initialize repo."); + return false; + } + + // Run 'git init' asynchronously + (string output, string error, int exitCode) = await RunGitCommandAsync("init", basePath); + + if (exitCode == 0) + { + GD.Print($"Git repo initialized successfully in {basePath}. Output: {output}"); + return true; + } + else + { + GD.Print($"Failed to initialize Git repo. Error: {error}"); + return false; + } + } + + /// + /// Checks if Git is installed by running 'git --version'. + /// + private async Task CheckGitInstalledAsync() + { + (string output, string error, int exitCode) = await RunGitCommandAsync("--version", Directory.GetCurrentDirectory()); + return exitCode == 0 && !string.IsNullOrEmpty(output); + } + + /// + /// Helper to run any Git command asynchronously in a specified working directory. + /// Captures output and errors. + /// + private async Task<(string Output, string Error, int ExitCode)> RunGitCommandAsync(string arguments, string workingDirectory) + { + return await Task.Run(() => + { + var process = new Process(); + process.StartInfo.FileName = "git"; + process.StartInfo.Arguments = arguments; + process.StartInfo.WorkingDirectory = workingDirectory; + process.StartInfo.UseShellExecute = false; // Required for redirection + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; // Hide console window + + try + { + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + return (output, error, process.ExitCode); + } + catch (Exception ex) + { + GD.Print($"Exception running Git command: {ex.Message}"); + return (string.Empty, ex.Message, -1); + } + }); + } +} \ No newline at end of file diff --git a/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs.uid b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs.uid new file mode 100644 index 00000000..48c98ded --- /dev/null +++ b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs.uid @@ -0,0 +1 @@ +uid://btg7j07v7s3ni diff --git a/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs new file mode 100644 index 00000000..29fbc58f --- /dev/null +++ b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs @@ -0,0 +1,215 @@ +using System.Diagnostics; +using Godot; +using Directory = System.IO.Directory; +using Exception = System.Exception; +using GD = Godot.GD; +using Path = System.IO.Path; +using System.Text.RegularExpressions; // For parsing SDK list +using System.Collections.Generic; // For lists +using System.Linq; // For sorting and selecting +using File = System.IO.File; // For File.Exists + +namespace SharpIDE.Godot.Features.SlnPicker; + +public partial class ProjectCreator : Node +{ + // Creates a new project with the specified parameters. + public void CreateProject(string basePath, string solutionName, string projectName, string template, + string language, string sdkVersion, string extraArgs = "") + { + //"blazor" for Blazor, "webapi" for API, etc. + + // Normalize and resolve absolute paths for cross-platform reliability + basePath = Path.GetFullPath(basePath); + string solutionDir = Path.Combine(basePath, solutionName); + string projectDir = Path.Combine(solutionDir, projectName); + string framework = "net" + sdkVersion.Split('.')[0] + ".0"; + + + try + { + string dotnetPath = GetDotnetPath(); + GD.Print($"Using dotnet CLI path: {dotnetPath}"); + + // Check if dotnet is available at the resolved path + if (!IsDotnetAvailable(dotnetPath)) + { + throw new Exception($"dotnet CLI not found at {dotnetPath}. Ensure .NET SDK is installed and update GetDotnetPath() if needed."); + } + + // Require sdkVersion to be specified + if (string.IsNullOrEmpty(sdkVersion)) + { + throw new Exception("SDK version must be specified (e.g., '8.0.411')."); + } + + // Validate the specified SDK is installed using the system CLI + string installedSdks = GetInstalledSdks(dotnetPath); + if (!Regex.IsMatch(installedSdks, $@"\b{Regex.Escape(sdkVersion)}\b")) + { + OS.Alert($"Specified SDK {sdkVersion} not installed. Available: {installedSdks}. Install from https://dotnet.microsoft.com.","Error"); + } + + Directory.CreateDirectory(solutionDir); + // Pin dotnet conf for solution creation + PinGlobalConfig(solutionDir, sdkVersion); + + // Create solution file + ExecuteDotnetCommand(dotnetPath, solutionDir, $"new sln -n {solutionName}"); + + // Create project subdirectory + Directory.CreateDirectory(projectDir); + + // Create project with generic template + string projectArgs = $"new {template} -n {projectName} --framework {framework} --language {language} {extraArgs}"; + ExecuteDotnetCommand(dotnetPath, solutionDir, projectArgs); + + // Delete dotnet conf + PinGlobalConfig(solutionDir, sdkVersion,true); + // Add project to solution + var solutionArgs = $"sln add {projectName}/{projectName}.csproj"; + ExecuteDotnetCommand(dotnetPath, solutionDir, solutionArgs); + + GD.Print($"Project ({template}) created successfully at {solutionDir}!"); + + } + catch (Exception ex) + { + OS.Alert($"Error creating project: {ex.Message}. Folder may be partially created at {solutionDir}. If access denied to template cache, try running 'sudo chown -R $(whoami) ~/.templateengine' or 'rm -rf ~/.templateengine/dotnetcli/' in terminal.","Error"); + } + } + + // Get platform-specific path to system dotnet CLI + public string GetDotnetPath() + { + string osName = OS.GetName(); + string dotnetPath = "dotnet"; // Fallback + + if (osName == "macOS") + { + dotnetPath = "/usr/local/share/dotnet/dotnet"; + } + else if (osName == "Linux") + { + dotnetPath = "/usr/share/dotnet/dotnet"; + } + else if (osName == "Windows") + { + dotnetPath = @"C:\Program Files\dotnet\dotnet.exe"; + } + + if (File.Exists(dotnetPath)) + { + return dotnetPath; + } + else + { + OS.Alert($"System dotnet not found at {dotnetPath}; falling back to 'dotnet'. Check your install.","Warning"); + return "dotnet"; + } + } + + // Get list of installed SDKs using specified dotnetPath + public string GetInstalledSdks(string dotnetPath) + { + var processInfo = new ProcessStartInfo + { + FileName = dotnetPath, + Arguments = "--list-sdks", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(processInfo)) + { + process?.WaitForExit(); + string output = process?.StandardOutput.ReadToEnd() ?? ""; + // Parse versions (e.g., lines like "8.0.411 [/path]") with Multiline option + var versions = Regex.Matches(output, @"^(\d+\.\d+\.\d+)", RegexOptions.Multiline); + return string.Join(", ", versions.Select(m => m.Groups[1].Value)); + } + } + + // Check if dotnet is available using specified dotnetPath + private bool IsDotnetAvailable(string dotnetPath) + { + try + { + var processInfo = new ProcessStartInfo + { + FileName = dotnetPath, + Arguments = "--version", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + using (var process = Process.Start(processInfo)) + { + process?.WaitForExit(); + return process?.ExitCode == 0; + } + } + catch + { + return false; + } + } + + // Execute dotnet commands using specified dotnetPath + private void ExecuteDotnetCommand(string dotnetPath, string workingDir, string arguments) + { + workingDir = Path.GetFullPath(workingDir); // Ensure absolute for OS consistency + var processInfo = new ProcessStartInfo + { + FileName = dotnetPath, + Arguments = arguments, + WorkingDirectory = workingDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + processInfo.EnvironmentVariables["DOTNET_MULTILEVEL_LOOKUP"] = "0"; + + using (var process = new Process { StartInfo = processInfo }) + { + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) OS.Alert($"dotnet command failed in {workingDir}: {error}", "Error"); + // if (!string.IsNullOrEmpty(error)) OS.Alert($"Command Error: {error}", "Error");; + + GD.Print($"dotnet output in {workingDir}: {output}"); + } + } + + private void PinGlobalConfig(string solutionDir, string sdkVersion, bool deleteExistingGlobalJson = false) + { + if (!deleteExistingGlobalJson) + { + // Create global.json to pin the specified SDK + string globalJsonPath = Path.Combine(solutionDir, "global.json"); + string globalJsonContent = $@"{{ +""sdk"": {{ + ""version"": ""{sdkVersion}"" + }} +}}"; + File.WriteAllText(globalJsonPath, globalJsonContent); + GD.Print($"Pinned SDK to {sdkVersion} via global.json in {solutionDir}"); + } + else + { + string globalJsonPath = Path.Combine(solutionDir, "global.json"); + if (File.Exists(globalJsonPath)) + { + File.Delete(globalJsonPath); + GD.Print($"Deleted global.json from {solutionDir}"); + } + } + + } +} diff --git a/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs.uid b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs.uid new file mode 100644 index 00000000..9cc4cbf4 --- /dev/null +++ b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs.uid @@ -0,0 +1 @@ +uid://b07ijdcta2ixt diff --git a/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs b/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs index 9966335d..e3418e59 100644 --- a/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs +++ b/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs @@ -7,62 +7,91 @@ namespace SharpIDE.Godot.Features.SlnPicker; // This is a bit of a mess intertwined with the optional popup window public partial class SlnPicker : Control { - private FileDialog _fileDialog = null!; - private Button _openSlnButton = null!; - private VBoxContainer _previousSlnsVBoxContainer = null!; - private Label _versionLabel = null!; - private static NuGetVersion? _version; + private FileDialog _fileDialog = null!; + private AcceptDialog _solutionDialog = null!; + private Button _newSolutionButton = null!; + private Button _openSlnButton = null!; + private VBoxContainer _previousSlnsVBoxContainer = null!; + private Label _versionLabel = null!; + private static NuGetVersion? _version; - private PackedScene _previousSlnEntryScene = ResourceLoader.Load("res://Features/SlnPicker/PreviousSlnEntry.tscn"); + private PackedScene _previousSlnEntryScene = ResourceLoader.Load("res://Features/SlnPicker/PreviousSlnEntry.tscn"); - private readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + - public override void _ExitTree() - { - if (!_tcs.Task.IsCompleted) _tcs.SetResult(null); - } - public override void _Ready() - { - _previousSlnsVBoxContainer = GetNode("%PreviousSlnsVBoxContainer"); - _versionLabel = GetNode