using System; using System.IO; using Bon; using GlitchyEngine; using GlitchyEngine.Scripting; using Xml_Beef; namespace GlitchyEditor; [BonTarget] class Project { [BonInclude] private String _projectName ~ delete _; [BonIgnore] private String _workspacePath ~ delete _; [BonIgnore] private String _assetsFolder ~ delete _; [BonIgnore] private String _scriptFolder ~ delete _; [BonIgnore] private ProjectUserSettings _userSettings ~ delete _; public StringView Name => _projectName; public StringView WorkspacePath => _workspacePath; public StringView AssetsFolder => _assetsFolder; public ProjectUserSettings UserSettings => _userSettings; [AllowAppend] private this(StringView workspacePath) { _workspacePath = new String(workspacePath); _assetsFolder = new String(); PathInProject(_assetsFolder, "Assets"); InitUserSettings(); } /// Writes the the absolute path of the give project-file into target. /// @param target The string that will contain the full path. /// @param relativePath The path of the file relative to the workspace folder. public void PathInProject(String target, StringView relativePath) { Path.Combine(target, WorkspacePath, relativePath); } /// Creates a new scoped string that contains the absolute path of the give project-file /// @param relativePath The path of the file relative to the workspace folder. public mixin GetScopedPath(StringView relativePath) { String target = scope:mixin String(); PathInProject(target, relativePath); target } public void GetPathToScriptSolutionFile(String outTarget) { PathInProject(outTarget, scope $"{Name}.slnx"); } public void GetPathToScriptProjectFile(String outTarget) { PathInProject(outTarget, scope $"{Name}.csproj"); } private static void GetScriptCoreProjectFilePath(String outTarget) { Directory.GetCurrentDirectory(outTarget); Path.Combine(outTarget, "../ScriptCore/ScriptCore.csproj"); Path.ToActualPath(outTarget); } /// Loads or creates the user specific settings for this project. private void InitUserSettings() { _userSettings = new ProjectUserSettings(); String userSettingsFile = scope .(); PathInProject(userSettingsFile, ProjectUserSettings.FileName); // It's not a problem if we couldn't find or deserialize the settings. We will simply restore the defaults. if (Bon.DeserializeFromFile(ref _userSettings, userSettingsFile) case .Err) { // Log anyway, just in case... Log.EngineLogger.Info($"Failed to deserialize project user settings file \"{userSettingsFile}\"."); _userSettings.RestoreDefaults(); } } public void SaveUserSettings() { if (_userSettings == null || !_userSettings.SettignsDirty) return; String userSettingsFile = scope .(); PathInProject(userSettingsFile, ProjectUserSettings.FileName); // It's not a big problem, if we fail to serialize this file. if (Bon.SerializeIntoFile(_userSettings, userSettingsFile) case .Err) { // Log anyway, just in case... Log.EngineLogger.Error($"Failed to deserialize project user settings file \"{userSettingsFile}\"."); } } /// Creates a new project with the given directory and name. public static Result CreateNew(StringView projectDirectory, StringView projectName) { Project project = new Project(projectDirectory); project._projectName = new String(projectName); String settingsFile = scope .(); project.PathInProject(settingsFile, "project.gep"); /// This is not really fatal, however the project might end up unusable if (Bon.SerializeIntoFile(project, settingsFile) case .Err) { Log.EngineLogger.Warning($"Failed to save project file \"{settingsFile}\"."); delete project; return .Err; } return project; } /// Creates a new project with the given name in the specified directory basend on the provided template. public static Result CreateNewFromTemplate(StringView projectDirectory, StringView projectName, StringView templatePath) { Project newProject = Try!(Project.CreateNew(projectDirectory, projectName)); if (Project.InitializeTemplate(newProject, templatePath) case .Err) { delete newProject; return .Err; } return newProject; } /// Initializes a project with the provided template. public static Result InitializeTemplate(Project project, StringView templatePath) { Try!(CopyTemplate(project, templatePath)); Try!(RenameAndFixupFiles(project.WorkspacePath, project)); Try!(project.FixupScriptCorePath()); return .Ok; } /// Copies the template files into the given project. private static Result CopyTemplate(Project project, StringView templatePath) { if (Directory.Copy(templatePath, project.WorkspacePath) case .Err) { Log.EngineLogger.Error($"CopyTemplate: Failed to copy template {templatePath} to {project.WorkspacePath}."); return .Err; } return .Ok; } const String ProjectNamePlaceholder = "[ProjectName]"; /// Moves files so that [ProjectName] in their paths will be replaced by the actual project name private static Result RenameAndFixupFiles(StringView directoryPath, Project project) { String fullPath = scope .(256); String modifiedFileName = scope String(256); for (FileFindEntry entry in Directory.Enumerate(scope $"{directoryPath}/*", .Files | .Directories)) { entry.GetFilePath(fullPath..Clear()); if (fullPath.Contains(ProjectNamePlaceholder)) { modifiedFileName.Set(fullPath); modifiedFileName.Replace(ProjectNamePlaceholder, project.Name); if (entry.IsDirectory) Try!(Directory.Move(fullPath, modifiedFileName)); else Try!(File.Move(fullPath, modifiedFileName)); Swap!(fullPath, modifiedFileName); } if (entry.IsDirectory) { Try!(RenameAndFixupFiles(fullPath, project)); } else { Try!(FixupFile(fullPath, project)); } } return .Ok; } // Fixups a file by replacing placeholders for e.g. the project name. private static Result FixupFile(StringView filePath, Project project) { String fileContent = scope String(); if (File.ReadAllText(filePath, fileContent, true) case .Err) { Log.EngineLogger.Error($"FixupFile: Failed to read file {filePath}."); return .Err; } if (!fileContent.Contains(ProjectNamePlaceholder)) return .Ok; fileContent.Replace(ProjectNamePlaceholder, project.Name); if (File.WriteAllText(filePath, fileContent, false) case .Err) { Log.EngineLogger.Error($"FixupFile: Failed to write file {filePath}."); return .Err; } return .Ok; } public static Result Load(StringView projectDirectory) { Project project = new Project(projectDirectory); String settingsFile = scope .(); project.PathInProject(settingsFile, "project.gep"); if (File.Exists(settingsFile)) { Result result = Bon.DeserializeFromFile(ref project, settingsFile); if (result case .Err) Log.EngineLogger.Warning($"Failed to deserialize project file \"{settingsFile}\"."); } else { Log.EngineLogger.Warning($"Project file \"{settingsFile}\" doesn't exist."); if (project.Name.IsWhiteSpace) { project._projectName ??= new String(); Path.GetFileName(project.WorkspacePath, project._projectName); /// This is not really fatal, however the project might end up unusable if (Bon.SerializeIntoFile(project, settingsFile) case .Err) { Log.EngineLogger.Warning($"Failed to save project file \"{settingsFile}\"."); delete project; return null; } } } Try!(project.FixupScriptCorePath()); return project; } /// If necessary adds or removes the reference to ScriptCore.csproj from the solution (.slnx) file. private Result FixupScriptSolutionFile(bool referenceScriptCoreProject) { String solutionPath = scope .(); GetPathToScriptSolutionFile(solutionPath); String solutionFileContent = scope String(); if (File.ReadAllText(solutionPath, solutionFileContent, true) case .Err) { Log.EngineLogger.Error($"FixupFile: Failed to read solution file {solutionPath}."); return .Err; } Xml solution = new Xml(); solution.PreserveWhitespace = true; defer delete solution; solution.LoadFromString(solutionFileContent); XmlNode scriptCoreProjectReference = null; for (XmlNode projectNode in solution.DocumentElement.EnumerateNodes("Project", recursive: true)) { if (projectNode.TryGetAttribute("Path", let includeAttribute)) { if (includeAttribute.Value.Contains("ScriptCore.csproj", true)) { scriptCoreProjectReference = projectNode; break; } } } if (referenceScriptCoreProject) { Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Solution."); if (scriptCoreProjectReference == null) { Log.EngineLogger.Warning("No reference to ScriptCore.csproj found. Adding..."); scriptCoreProjectReference = solution.DocumentElement.AddChild("Project"); } String projectFilePath = scope .(); GetScriptCoreProjectFilePath(projectFilePath); scriptCoreProjectReference["Path"] = projectFilePath; } else if (scriptCoreProjectReference != null) { Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing..."); scriptCoreProjectReference.RemoveFromParent(true); } String modifiedSolution = new String(solutionFileContent.Length * 2); defer delete modifiedSolution; solution.SaveToString(modifiedSolution); if (File.WriteAllText(solutionPath, modifiedSolution) case .Err) { Log.EngineLogger.Error($"Failed to modify {solutionPath}"); return .Err; } return .Ok; } /// If necessary adds or removes the reference to ScriptCore.dll from the project (.csproj) file. private Result FixupScriptProjectFile(bool referenceScriptCoreProject) { String csprojPath = scope .(); GetPathToScriptProjectFile(csprojPath); String projectFileContent = scope String(); if (File.ReadAllText(csprojPath, projectFileContent, true) case .Err) { Log.EngineLogger.Error($"FixupFile: Failed to read project file {csprojPath}."); return .Err; } Xml project = new Xml(); project.PreserveWhitespace = true; defer delete project; project.LoadFromString(projectFileContent); // Try to find a reference to the csproj XmlNode scriptCoreProjectReference = null; for (XmlNode projectReferenceNode in project.DocumentElement.EnumerateNodes("ProjectReference", recursive: true)) { if (projectReferenceNode.TryGetAttribute("Include", let includeAttribute)) { if (includeAttribute.Value.Contains("ScriptCore.csproj", true)) { scriptCoreProjectReference = projectReferenceNode; break; } } } // Try to find a reference to the dll XmlNode dllReferenceNode = project.DocumentElement.Find("Reference", "Include", "ScriptCore", recursive: true); if (referenceScriptCoreProject) { Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Project."); if (dllReferenceNode != null) { Log.EngineLogger.Warning("Reference to ScriptCore.dll found. Removing..."); dllReferenceNode.RemoveFromParent(true); } if (scriptCoreProjectReference == null) { Log.EngineLogger.Warning("Reference to ScriptCore.csproj missing. Adding..."); XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true); scriptCoreProjectReference = itemGroup.AddChild("ProjectReference"); String scriptCoreProjectPath = scope .(); Directory.GetCurrentDirectory(scriptCoreProjectPath); Path.Combine(scriptCoreProjectPath, "../ScriptCore/ScriptCore.csproj"); Path.ToActualPath(scriptCoreProjectPath); scriptCoreProjectReference.SetAttribute("Include", scriptCoreProjectPath); scriptCoreProjectReference.SetAttribute("OutputItemType", "Analyzer"); scriptCoreProjectReference.SetAttribute("ReferenceOutputAssembly", "true"); } } else { if (scriptCoreProjectReference != null) { Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing..."); scriptCoreProjectReference.RemoveFromParent(true); } if (dllReferenceNode == null) { Log.EngineLogger.Warning("Reference to ScriptCore.dll missing. Adding..."); XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true); dllReferenceNode = itemGroup.AddChild("Reference"); } dllReferenceNode.SetAttribute("Include", "ScriptCore"); dllReferenceNode.SetAttribute("OutputItemType", "Analyzer"); dllReferenceNode.SetAttribute("ReferenceOutputAssembly", "true"); XmlNode hintPathNode = dllReferenceNode.Find("HintPath"); if (hintPathNode == null) { hintPathNode = dllReferenceNode.AddChild("HintPath"); } String pathToScriptCoreDll = scope .(); Directory.GetCurrentDirectory(pathToScriptCoreDll); Path.Combine(pathToScriptCoreDll, ScriptEngine.ScriptCorePath); Path.ToActualPath(pathToScriptCoreDll); if (!File.Exists(pathToScriptCoreDll)) { Log.EngineLogger.Error($"Could not fild \"{pathToScriptCoreDll}\""); return .Err; } hintPathNode.SetText(pathToScriptCoreDll); } String modfiedProject = new String(projectFileContent.Length * 2); defer delete modfiedProject; project.SaveToString(modfiedProject); if (File.WriteAllText(csprojPath, modfiedProject) case .Err) { Log.EngineLogger.Error($"Failed to modify {csprojPath}"); } return .Ok; } /// Fixes the references to ScriptCore in the script-project and solution file. public Result FixupScriptCorePath() { bool referenceScriptCoreProject = false; #if DEBUG Settings settings = Application.Instance.Settings; referenceScriptCoreProject = settings.DevSettings.UseScriptCoreDll; #endif Try!(FixupScriptSolutionFile(referenceScriptCoreProject)); Try!(FixupScriptProjectFile(referenceScriptCoreProject)); return .Ok; } }