Initial commit: R0Installer source, patch generator, API backend and build scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace R0PatchGenerator
|
||||
{
|
||||
public class PatchGenerator
|
||||
{
|
||||
private string hdiffzPath;
|
||||
|
||||
public PatchGenerator()
|
||||
{
|
||||
// 查找 hdiffz.exe
|
||||
string exeDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
hdiffzPath = Path.Combine(exeDir, "hdiffz.exe");
|
||||
|
||||
if (!File.Exists(hdiffzPath))
|
||||
{
|
||||
// 尝试在当前目录查找
|
||||
hdiffzPath = "hdiffz.exe";
|
||||
}
|
||||
}
|
||||
|
||||
public bool GeneratePatch(string oldPath, string newPath, string outputZip, string fromVersion, string toVersion)
|
||||
{
|
||||
// 创建临时目录
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), "r0patch_" + Guid.NewGuid().ToString("N"));
|
||||
string patchesDir = Path.Combine(tempDir, "patches");
|
||||
string newFilesDir = Path.Combine(tempDir, "new");
|
||||
|
||||
Directory.CreateDirectory(tempDir);
|
||||
Directory.CreateDirectory(patchesDir);
|
||||
Directory.CreateDirectory(newFilesDir);
|
||||
|
||||
try
|
||||
{
|
||||
// 获取所有文件
|
||||
Dictionary<string, FileInfo> oldFiles = GetAllFiles(oldPath);
|
||||
Dictionary<string, FileInfo> newFiles = GetAllFiles(newPath);
|
||||
|
||||
Console.WriteLine("旧版本文件数: " + oldFiles.Count);
|
||||
Console.WriteLine("新版本文件数: " + newFiles.Count);
|
||||
Console.WriteLine();
|
||||
|
||||
// 创建清单
|
||||
PatchManifest manifest = new PatchManifest();
|
||||
manifest.from_version = fromVersion;
|
||||
manifest.to_version = toVersion;
|
||||
manifest.created_at = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
|
||||
int processedCount = 0;
|
||||
int totalFiles = newFiles.Count;
|
||||
|
||||
foreach (var kvp in newFiles)
|
||||
{
|
||||
string relativePath = kvp.Key;
|
||||
FileInfo newFile = kvp.Value;
|
||||
processedCount++;
|
||||
|
||||
Console.Write("[" + processedCount + "/" + totalFiles + "] " + relativePath + " ... ");
|
||||
|
||||
if (oldFiles.ContainsKey(relativePath))
|
||||
{
|
||||
// 文件存在于两个版本
|
||||
FileInfo oldFile = oldFiles[relativePath];
|
||||
string oldHash = CalculateMD5(oldFile.FullName);
|
||||
string newHash = CalculateMD5(newFile.FullName);
|
||||
|
||||
if (oldHash == newHash)
|
||||
{
|
||||
// 文件未变化
|
||||
Console.WriteLine("未变化");
|
||||
manifest.unchanged.Add(relativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 文件已修改,生成差异
|
||||
string patchFileName = relativePath.Replace("\\", "_").Replace("/", "_") + ".hdiff";
|
||||
string patchFilePath = Path.Combine(patchesDir, patchFileName);
|
||||
|
||||
// 确保目录存在
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(patchFilePath));
|
||||
|
||||
bool patchSuccess = CreateHDiff(oldFile.FullName, newFile.FullName, patchFilePath);
|
||||
|
||||
if (patchSuccess && File.Exists(patchFilePath))
|
||||
{
|
||||
FileInfo patchInfo = new FileInfo(patchFilePath);
|
||||
FileInfo originalInfo = new FileInfo(newFile.FullName);
|
||||
|
||||
// 如果差异文件比原文件还大,直接用新文件
|
||||
if (patchInfo.Length >= originalInfo.Length * 0.9)
|
||||
{
|
||||
Console.WriteLine("替换 (差异过大)");
|
||||
File.Delete(patchFilePath);
|
||||
|
||||
// 作为新文件处理
|
||||
string newFilePath = Path.Combine(newFilesDir, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
||||
File.Copy(newFile.FullName, newFilePath, true);
|
||||
|
||||
PatchFileInfo pfi = new PatchFileInfo();
|
||||
pfi.path = relativePath;
|
||||
pfi.hash = newHash;
|
||||
pfi.size = newFile.Length;
|
||||
manifest.new_files.Add(pfi);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("差异 (" + FormatFileSize(patchInfo.Length) + ")");
|
||||
|
||||
PatchDiffInfo pdi = new PatchDiffInfo();
|
||||
pdi.path = relativePath;
|
||||
pdi.patch_file = "patches/" + patchFileName;
|
||||
pdi.old_hash = oldHash;
|
||||
pdi.new_hash = newHash;
|
||||
pdi.new_size = newFile.Length;
|
||||
manifest.modified.Add(pdi);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 差异生成失败,使用完整文件
|
||||
Console.WriteLine("替换 (差异失败)");
|
||||
|
||||
string newFilePath = Path.Combine(newFilesDir, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
||||
File.Copy(newFile.FullName, newFilePath, true);
|
||||
|
||||
PatchFileInfo pfi = new PatchFileInfo();
|
||||
pfi.path = relativePath;
|
||||
pfi.hash = newHash;
|
||||
pfi.size = newFile.Length;
|
||||
manifest.new_files.Add(pfi);
|
||||
}
|
||||
}
|
||||
|
||||
// 从旧文件列表移除
|
||||
oldFiles.Remove(relativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新增文件
|
||||
Console.WriteLine("新增");
|
||||
|
||||
string newFilePath = Path.Combine(newFilesDir, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
||||
File.Copy(newFile.FullName, newFilePath, true);
|
||||
|
||||
PatchFileInfo pfi = new PatchFileInfo();
|
||||
pfi.path = relativePath;
|
||||
pfi.hash = CalculateMD5(newFile.FullName);
|
||||
pfi.size = newFile.Length;
|
||||
manifest.new_files.Add(pfi);
|
||||
}
|
||||
}
|
||||
|
||||
// 剩余的旧文件需要删除
|
||||
foreach (var kvp in oldFiles)
|
||||
{
|
||||
Console.WriteLine("删除: " + kvp.Key);
|
||||
manifest.deleted.Add(kvp.Key);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("统计:");
|
||||
Console.WriteLine(" - 未变化: " + manifest.unchanged.Count);
|
||||
Console.WriteLine(" - 已修改: " + manifest.modified.Count);
|
||||
Console.WriteLine(" - 新增: " + manifest.new_files.Count);
|
||||
Console.WriteLine(" - 删除: " + manifest.deleted.Count);
|
||||
|
||||
// 生成清单JSON
|
||||
string manifestJson = GenerateManifestJson(manifest);
|
||||
File.WriteAllText(Path.Combine(tempDir, "patch.json"), manifestJson, Encoding.UTF8);
|
||||
|
||||
// 打包成ZIP
|
||||
if (File.Exists(outputZip))
|
||||
File.Delete(outputZip);
|
||||
|
||||
ZipFile.CreateFromDirectory(tempDir, outputZip, CompressionLevel.Optimal, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理临时目录
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, FileInfo> GetAllFiles(string rootPath)
|
||||
{
|
||||
Dictionary<string, FileInfo> files = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (string filePath in Directory.GetFiles(rootPath, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
string relativePath = filePath.Substring(rootPath.Length).TrimStart('\\', '/');
|
||||
files[relativePath] = new FileInfo(filePath);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private string CalculateMD5(string filePath)
|
||||
{
|
||||
using (var md5 = MD5.Create())
|
||||
using (var stream = File.OpenRead(filePath))
|
||||
{
|
||||
byte[] hash = md5.ComputeHash(stream);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (byte b in hash)
|
||||
sb.Append(b.ToString("x2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CreateHDiff(string oldFile, string newFile, string patchFile)
|
||||
{
|
||||
if (!File.Exists(hdiffzPath))
|
||||
{
|
||||
Console.WriteLine("警告: hdiffz.exe 未找到,将使用完整文件替换");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProcessStartInfo psi = new ProcessStartInfo();
|
||||
psi.FileName = hdiffzPath;
|
||||
// -m: 使用内存匹配(更快),-c-zlib: 使用zlib压缩
|
||||
psi.Arguments = String.Format("-m -c-zlib \"{0}\" \"{1}\" \"{2}\"", oldFile, newFile, patchFile);
|
||||
psi.UseShellExecute = false;
|
||||
psi.RedirectStandardOutput = true;
|
||||
psi.RedirectStandardError = true;
|
||||
psi.CreateNoWindow = true;
|
||||
|
||||
using (Process process = Process.Start(psi))
|
||||
{
|
||||
process.WaitForExit(60000); // 最多等待60秒
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("HDiff错误: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateManifestJson(PatchManifest manifest)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" \"from_version\": \"" + EscapeJson(manifest.from_version) + "\",");
|
||||
sb.AppendLine(" \"to_version\": \"" + EscapeJson(manifest.to_version) + "\",");
|
||||
sb.AppendLine(" \"created_at\": \"" + EscapeJson(manifest.created_at) + "\",");
|
||||
|
||||
// unchanged
|
||||
sb.AppendLine(" \"unchanged\": [");
|
||||
for (int i = 0; i < manifest.unchanged.Count; i++)
|
||||
{
|
||||
sb.Append(" \"" + EscapeJson(manifest.unchanged[i]) + "\"");
|
||||
if (i < manifest.unchanged.Count - 1) sb.Append(",");
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.AppendLine(" ],");
|
||||
|
||||
// modified
|
||||
sb.AppendLine(" \"modified\": [");
|
||||
for (int i = 0; i < manifest.modified.Count; i++)
|
||||
{
|
||||
PatchDiffInfo pdi = manifest.modified[i];
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" \"path\": \"" + EscapeJson(pdi.path) + "\",");
|
||||
sb.AppendLine(" \"patch_file\": \"" + EscapeJson(pdi.patch_file) + "\",");
|
||||
sb.AppendLine(" \"old_hash\": \"" + pdi.old_hash + "\",");
|
||||
sb.AppendLine(" \"new_hash\": \"" + pdi.new_hash + "\",");
|
||||
sb.AppendLine(" \"new_size\": " + pdi.new_size);
|
||||
sb.Append(" }");
|
||||
if (i < manifest.modified.Count - 1) sb.Append(",");
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.AppendLine(" ],");
|
||||
|
||||
// new_files
|
||||
sb.AppendLine(" \"new_files\": [");
|
||||
for (int i = 0; i < manifest.new_files.Count; i++)
|
||||
{
|
||||
PatchFileInfo pfi = manifest.new_files[i];
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" \"path\": \"" + EscapeJson(pfi.path) + "\",");
|
||||
sb.AppendLine(" \"hash\": \"" + pfi.hash + "\",");
|
||||
sb.AppendLine(" \"size\": " + pfi.size);
|
||||
sb.Append(" }");
|
||||
if (i < manifest.new_files.Count - 1) sb.Append(",");
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.AppendLine(" ],");
|
||||
|
||||
// deleted
|
||||
sb.AppendLine(" \"deleted\": [");
|
||||
for (int i = 0; i < manifest.deleted.Count; i++)
|
||||
{
|
||||
sb.Append(" \"" + EscapeJson(manifest.deleted[i]) + "\"");
|
||||
if (i < manifest.deleted.Count - 1) sb.Append(",");
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.AppendLine(" ]");
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string EscapeJson(string s)
|
||||
{
|
||||
if (s == null) return "";
|
||||
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
|
||||
}
|
||||
|
||||
private string FormatFileSize(long bytes)
|
||||
{
|
||||
if (bytes >= 1073741824)
|
||||
return String.Format("{0:F2} GB", bytes / 1073741824.0);
|
||||
if (bytes >= 1048576)
|
||||
return String.Format("{0:F2} MB", bytes / 1048576.0);
|
||||
if (bytes >= 1024)
|
||||
return String.Format("{0:F2} KB", bytes / 1024.0);
|
||||
return String.Format("{0} B", bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// 清单数据结构
|
||||
public class PatchManifest
|
||||
{
|
||||
public string from_version = "";
|
||||
public string to_version = "";
|
||||
public string created_at = "";
|
||||
public List<string> unchanged = new List<string>();
|
||||
public List<PatchDiffInfo> modified = new List<PatchDiffInfo>();
|
||||
public List<PatchFileInfo> new_files = new List<PatchFileInfo>();
|
||||
public List<string> deleted = new List<string>();
|
||||
}
|
||||
|
||||
public class PatchDiffInfo
|
||||
{
|
||||
public string path = "";
|
||||
public string patch_file = "";
|
||||
public string old_hash = "";
|
||||
public string new_hash = "";
|
||||
public long new_size = 0;
|
||||
}
|
||||
|
||||
public class PatchFileInfo
|
||||
{
|
||||
public string path = "";
|
||||
public string hash = "";
|
||||
public long size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace R0PatchGenerator
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine(" R0对战平台 增量包生成器");
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine();
|
||||
|
||||
if (args.Length < 3)
|
||||
{
|
||||
PrintUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
string oldVersionPath = args[0];
|
||||
string newVersionPath = args[1];
|
||||
string outputPath = args[2];
|
||||
|
||||
// 可选参数:版本号
|
||||
string fromVersion = args.Length > 3 ? args[3] : "unknown";
|
||||
string toVersion = args.Length > 4 ? args[4] : "unknown";
|
||||
|
||||
Console.WriteLine("源版本目录: " + oldVersionPath);
|
||||
Console.WriteLine("目标版本目录: " + newVersionPath);
|
||||
Console.WriteLine("输出路径: " + outputPath);
|
||||
Console.WriteLine("从版本: " + fromVersion);
|
||||
Console.WriteLine("到版本: " + toVersion);
|
||||
Console.WriteLine();
|
||||
|
||||
if (!Directory.Exists(oldVersionPath))
|
||||
{
|
||||
Console.WriteLine("错误: 源版本目录不存在!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(newVersionPath))
|
||||
{
|
||||
Console.WriteLine("错误: 目标版本目录不存在!");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
PatchGenerator generator = new PatchGenerator();
|
||||
bool success = generator.GeneratePatch(oldVersionPath, newVersionPath, outputPath, fromVersion, toVersion);
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("增量包生成成功!");
|
||||
Console.WriteLine("输出文件: " + outputPath);
|
||||
|
||||
FileInfo fi = new FileInfo(outputPath);
|
||||
Console.WriteLine("文件大小: " + FormatFileSize(fi.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("增量包生成失败!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("错误: " + ex.Message);
|
||||
Console.WriteLine(ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("用法:");
|
||||
Console.WriteLine(" R0PatchGenerator.exe <旧版本目录> <新版本目录> <输出文件.zip> [从版本] [到版本]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("示例:");
|
||||
Console.WriteLine(" R0PatchGenerator.exe \"C:\\versions\\1.0.0\" \"C:\\versions\\1.0.1\" \"patch_1.0.0_to_1.0.1.zip\" 1.0.0 1.0.1");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("说明:");
|
||||
Console.WriteLine(" - 旧版本目录: 包含旧版本完整文件的目录");
|
||||
Console.WriteLine(" - 新版本目录: 包含新版本完整文件的目录");
|
||||
Console.WriteLine(" - 输出文件: 生成的增量包文件路径(.zip格式)");
|
||||
Console.WriteLine(" - 从版本/到版本: 可选,用于生成版本信息");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("增量包内容:");
|
||||
Console.WriteLine(" - patch.json: 更新清单文件");
|
||||
Console.WriteLine(" - patches/: 差异文件目录(使用HDiffPatch格式)");
|
||||
Console.WriteLine(" - new/: 新增文件目录");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("注意: 需要将 hdiffz.exe 放在同一目录下");
|
||||
}
|
||||
|
||||
static string FormatFileSize(long bytes)
|
||||
{
|
||||
if (bytes >= 1073741824)
|
||||
return String.Format("{0:F2} GB", bytes / 1073741824.0);
|
||||
if (bytes >= 1048576)
|
||||
return String.Format("{0:F2} MB", bytes / 1048576.0);
|
||||
if (bytes >= 1024)
|
||||
return String.Format("{0:F2} KB", bytes / 1024.0);
|
||||
return String.Format("{0} B", bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("R0PatchGenerator")]
|
||||
[assembly: AssemblyDescription("R0对战平台增量包生成器")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("R0")]
|
||||
[assembly: AssemblyProduct("R0PatchGenerator")]
|
||||
[assembly: AssemblyCopyright("Copyright © R0 2024")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
[assembly: Guid("b2c3d4e5-f6a7-8901-bcde-f23456789012")]
|
||||
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{B2C3D4E5-F6A7-8901-BCDE-F23456789012}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>R0PatchGenerator</RootNamespace>
|
||||
<AssemblyName>R0PatchGenerator</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationIcon>..\icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="PatchGenerator.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="..\icon.ico" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user