// 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;
}
}
}