// Copyright (c) Ubisoft. All Rights Reserved. // Licensed under the Apache 2.0 License. See LICENSE.md in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; namespace Sharpmake.Generators.VisualStudio { internal class LaunchSettingsJson { private const string FileNameWithoutExtension = "launchSettings"; private const string Extension = ".json"; private const string FileName = FileNameWithoutExtension + Extension; /// /// Generate a launchSettings.json in a Properties subfolder of the csproj from the conf.CsprojUserFile /// /// The builder to use /// The project the conf belong to /// The path of the csproj /// The list of configurations to lookup for CsprojUserFile /// Files written by the method /// Files already up-to-date and skipped /// The full path of the launchSettings.json public static string Generate( Builder builder, Project project, string projectPath, IEnumerable configurations, IList generatedFiles, IList skipFiles ) { bool overwriteFile; var launchSettingsProfiles = GetLaunchSettingsFromCsprojUserFile(project, configurations, out overwriteFile); if (launchSettingsProfiles == null || !launchSettingsProfiles.Any()) return null; var memoryStream = new MemoryStream(); var writer = new StreamWriter(memoryStream); var root = new JsonRoot { profiles = launchSettingsProfiles }; // Write the list of files. writer.Write(JsonSerializer.Serialize(root, GetJsonSerializerOptions())); writer.Flush(); //Skip overwriting user file if it exists already so he can keep his setup // unless the UserProjSettings specifies to overwrite var userFileInfo = new FileInfo(Path.Combine(projectPath, "Properties", FileName)); bool shouldWrite = !userFileInfo.Exists || overwriteFile; if (shouldWrite && builder.Context.WriteGeneratedFile(typeof(LaunchSettingsJson), userFileInfo, memoryStream)) generatedFiles.Add(userFileInfo.FullName); else skipFiles.Add(userFileInfo.FullName); return userFileInfo.FullName; } /// /// Get the formatting properties of the json /// private static JsonSerializerOptions GetJsonSerializerOptions() { return new JsonSerializerOptions() { // shouldn't matter AllowTrailingCommas = true, // we want enums to be written in plain text, so we need a converter Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }, // we only write the values that are non default DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault, // write properties as they are named in the Profile class PropertyNamingPolicy = null, // the file is read by humans as well as machine, so indent it WriteIndented = true, }; } /// /// Represents the json root node /// private class JsonRoot { /// /// The list of profiles, the key is the profile name /// public Dictionary profiles { get; set; } } /// /// Represents an individual profile in the json /// private class Profile { public enum Command { Invalid, // keep this as default below so we force the other values to be written in the json Project, Executable } /// /// Identifies the debug target to run. /// /// /// Mandatory argument. /// public Command commandName { get; set; } = Command.Invalid; /// /// The arguments to pass to the target being run. /// public string commandLineArgs { get; set; } /// /// An absolute or relative path to the executable. /// public string executablePath { get; set; } /// /// Sets the working directory of the command. /// public string workingDirectory { get; set; } /// /// Set to true to enable native code debugging. /// /// /// Default: false /// public bool nativeDebugging { get; set; } public override bool Equals(object obj) { return obj is Profile profile && commandName == profile.commandName && commandLineArgs == profile.commandLineArgs && executablePath == profile.executablePath && workingDirectory == profile.workingDirectory && nativeDebugging == profile.nativeDebugging; } public override int GetHashCode() { int hashCode = -451875935; hashCode = hashCode * -1521134295 + commandName.GetHashCode(); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(commandLineArgs); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(executablePath); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(workingDirectory); hashCode = hashCode * -1521134295 + nativeDebugging.GetHashCode(); return hashCode; } } /// /// Convert a CsprojUserFileSettings.StartActionSetting to a Profile.Command /// /// The setting to convert /// The converted setting /// Some values are not supported, so we'll throw if that's the case private static Profile.Command GetCommandFromStartAction(Project.Configuration.CsprojUserFileSettings.StartActionSetting startActionSetting) { switch (startActionSetting) { case Project.Configuration.CsprojUserFileSettings.StartActionSetting.Project: return Profile.Command.Project; case Project.Configuration.CsprojUserFileSettings.StartActionSetting.Program: return Profile.Command.Executable; case Project.Configuration.CsprojUserFileSettings.StartActionSetting.URL: default: throw new NotSupportedException($"{startActionSetting} is not supported in {FileName}"); } } /// /// Helper method to convert the value of a string from CsprojUserFileSettings to the expected format of Profile /// /// The value read from CsprojUserFileSettings /// The converted value, or null if it was unset or empty private static string GetStringOrNullIfRemoveLineTag(string value) { if (string.IsNullOrEmpty(value) || value == FileGeneratorUtilities.RemoveLineTag) return null; return value; } /// /// Converts a CsprojUserFileSettings to a Profile /// /// The CsprojUserFileSettings to convert /// The converted Profile /// Throws in case an unsupported setting is passed private static Profile GetProfileFromCsprojUserFileConf(Project.Configuration.CsprojUserFileSettings csprojUserFileSettings) { if (GetStringOrNullIfRemoveLineTag(csprojUserFileSettings.StartURL) != null) throw new NotImplementedException($"Don't know how to convert CsprojUserFileSettings.StartURL in {FileName}"); return new Profile { commandName = GetCommandFromStartAction(csprojUserFileSettings.StartAction), commandLineArgs = csprojUserFileSettings.StartArguments, executablePath = GetStringOrNullIfRemoveLineTag(csprojUserFileSettings.StartProgram), workingDirectory = GetStringOrNullIfRemoveLineTag(csprojUserFileSettings.WorkingDirectory), nativeDebugging = csprojUserFileSettings.EnableUnmanagedDebug }; } /// /// Construct the list of profiles from the conf.CsprojUserFile from all configurations /// /// The project that the configurations belong to /// The list of configurations to lookup /// Will be set to true if the file is allowed to be overwritten /// The list of profiles private static Dictionary GetLaunchSettingsFromCsprojUserFile( Project project, IEnumerable configurations, out bool overwriteFile ) { var csprojUserFileConfs = configurations.Where(conf => conf.CsprojUserFile != null); overwriteFile = !csprojUserFileConfs.Any(conf => !conf.CsprojUserFile.OverwriteExistingFile); var profiles = csprojUserFileConfs .Select(conf => GetProfileFromCsprojUserFileConf(conf.CsprojUserFile)) .Distinct() .ToList(); if (!profiles.Any()) return null; // in case we have only one profile, use the project name without any suffix if (profiles.Count == 1) return profiles.ToDictionary(profile => project.Name, profile => profile); // in case we have more, we'll suffix the project name with a decimal number starting from 1 var dict = new Dictionary(); for (int i = 0; i < profiles.Count; i++) dict.Add($"{project.Name} {i + 1}", profiles[i]); return dict; } } }