导出 VS 工程:实验页工具栏的"导出 EXE"改成下拉按钮"导出 ▾",里面是"导出 EXE"和"导出 VS 工程"。导出的 zip 是教材那样的 VS 2012 窗体应用工程(.sln + .csproj,.NET Framework 4.5),在机房电脑上解压、双击 .sln 用 VS 打开接着改,按 F5 运行。导出前照机房环境编译一遍要放进 zip 的代码:C# 5(VS 2012 的编译器)、真机版设备类库,有错不导出;新版 C# 写法报 CS8026,按功能名给出改法(字符串插值改 string.Format、?. 改先判断 null、=> 改大括号等 12 种)。导出 EXE 仍按最新 C#。平台的 Properties/Resources.Designer.cs 换成 VS 生成的写法和 Resources.resx,图片放 Resources\;代码里按文件名用的素材(ring.mp3)复制到输出目录,运行时在 bin\Debug\Resources\;每个窗体带 .resx(媒体播放器 OcxState 用教材默认状态),SubType、DependentUpon 和 VS 一样;补上 AssemblyInfo、Settings、App.config。教材 dll 全放 lib\,只引用代码直接用到的(和教材工程一样),只带这些编译不过时引用全部;用到书上的 ledHelper.Close() 时带上 LedHelperCompat.cs。文本文件用带 BOM 的 UTF-8 和 CRLF(VS 2012 打开不带 BOM 的文件会把中文当 GBK);zip 文件名用 GBK,另附 Info-ZIP Unicode 文件名字段(0x7075),中文 Windows 自带解压和 7-Zip 都显示正确(ZipWriter.cs)。23 个示例都能导出,导出的工程在 mono 的 msbuild 里全部编译通过、输出目录和教材 bin\Debug 一样、程序能启动;VS 2012 还要在机房实测。接口 POST /api/export-project,代码有错时 422。export-exe.ts 改名 export.ts(服务器和网页各一个)。使用说明、设计方案、README、部署说明同步
Deploy / deploy (push) Failing after 1m8s
Deploy / deploy (push) Failing after 1m8s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c3ce31fe1
commit
2aa267839f
17 files changed
+1209
-80
No files matched your search
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Sim.Runner;
|
||||
@@ -23,7 +24,32 @@ internal static class CompileHints
|
||||
("CS1002", _) => "C# 每条语句末尾要有英文分号 ;",
|
||||
("CS1056", _) or ("CS1525", _) when source.Length == 1 && source[0] > 127 => "这里出现了中文标点,C# 代码里的符号要用英文半角。",
|
||||
("CS0029", _) => "等号两边的类型不一致。如果要把文本框里的文字变成数字,可以用 int.Parse(...) 或 Convert.ToInt32(...)。",
|
||||
("CS8026", _) => Vs2012(diagnostic),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>导出工程时照机房的 VS 2012 只能用 C# 5,新版 C# 的写法(报错里给出功能名)说明怎么改</summary>
|
||||
private static readonly (string Feature, string Fix)[] NewerFeatures =
|
||||
{
|
||||
("内插字符串", "$\"…{x}…\" 改成 string.Format(\"…{0}…\", x),或者用 + 把字符串连起来"),
|
||||
("空传播运算符", "a?.b 改成先判断是否为 null,例如 a != null ? a.b : null"),
|
||||
("nameof", "nameof(x) 直接写成字符串 \"x\""),
|
||||
("expression-bodied", "=> 改成大括号,方法写成 { return …; },属性写成 { get { return …; } }"),
|
||||
("自动属性初始值", "属性后面的 = 初始值 去掉,改到构造函数里赋值"),
|
||||
("化出变量", "out var x 改成先声明变量(如 int x;),再写 out x"),
|
||||
("字典初始值", "[\"键\"] = 值 改成 { \"键\", 值 }"),
|
||||
("模式匹配", "x is 类型 y 改成先用 as 转换,再判断是否为 null"),
|
||||
("本地函数", "方法里的方法挪到类里,写成一个 private 方法"),
|
||||
("default 字面量", "default 写成 default(类型),例如 default(int)"),
|
||||
("元组", "改成定义一个类,或者用 out 参数返回多个值"),
|
||||
("异常筛选器", "catch (…) when (条件) 改成在 catch 里用 if 判断"),
|
||||
};
|
||||
|
||||
private static string Vs2012(Diagnostic diagnostic)
|
||||
{
|
||||
var message = diagnostic.GetMessage(System.Globalization.CultureInfo.GetCultureInfo("zh-Hans"));
|
||||
var fix = NewerFeatures.FirstOrDefault(f => message.Contains(f.Feature)).Fix;
|
||||
return "机房的 VS 2012 只支持 C# 5,不认这种写法" + (fix == null ? ",换成老的写法。" : $":{fix}。");
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ namespace Sim.Runner;
|
||||
/// <summary>
|
||||
/// 常驻编译服务:从标准输入读取 {"id":…,"dir":…},编译该目录下的学生工程,
|
||||
/// 成功时把 StudentApp.dll / StudentApp.pdb 写到同一目录,回复 {"id":…,"type":"compiled",…}。
|
||||
/// 请求带 "export":{"libs":…,"name":…} 时导出 EXE(见 ExeExporter),写到同一目录的 export.exe。
|
||||
/// 请求带 "export":{"libs":…,"name":…} 时导出 EXE(见 ExeExporter),写到同一目录的 export.exe;
|
||||
/// 带 "export":{"kind":"project",…} 时导出 VS 工程(见 ProjectExporter),写到同一目录的 export.zip。
|
||||
/// 编译器常驻,第一次之后的编译不再有加载和预热开销。
|
||||
/// </summary>
|
||||
internal static class CompileServer
|
||||
@@ -19,6 +20,7 @@ internal static class CompileServer
|
||||
public const string AssemblyFile = "StudentApp.dll";
|
||||
public const string PdbFile = "StudentApp.pdb";
|
||||
public const string ExportFile = "export.exe";
|
||||
public const string ExportProjectFile = "export.zip";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
|
||||
|
||||
@@ -40,7 +42,7 @@ internal static class CompileServer
|
||||
id = request["id"]?.DeepClone();
|
||||
var dir = (string?)request["dir"] ?? throw new ArgumentException("缺少 dir");
|
||||
reply = request["export"] is JsonObject export
|
||||
? Export(dir, (string?)export["libs"] ?? "", (string?)export["name"] ?? "")
|
||||
? Export(dir, (string?)export["kind"] == "project", (string?)export["libs"] ?? "", (string?)export["name"] ?? "")
|
||||
: Compile(dir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -72,19 +74,33 @@ internal static class CompileServer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出 EXE:照教材工程编译成 .NET Framework 程序,嵌入 libs 目录下的教材 dll,成功时写到工程目录的 ExportFile。
|
||||
/// name 是程序集名(exe 的名字),由服务器按工程取好。
|
||||
/// 导出 EXE:照教材工程编译成 .NET Framework 程序,嵌入 libs 目录下的教材 dll,成功时写到工程目录的 ExportFile;
|
||||
/// 导出工程(project):照机房的 VS 2012 检查后打包成 VS 工程,成功时写到 ExportProjectFile。
|
||||
/// name 是程序集名(exe 的名字、工程名),由服务器按工程取好。
|
||||
/// </summary>
|
||||
private static JsonObject Export(string dir, string libs, string name)
|
||||
private static JsonObject Export(string dir, bool project, string libs, string name)
|
||||
{
|
||||
var watch = Stopwatch.StartNew();
|
||||
var result = ExeExporter.Export(dir, libs, Sim.Host.ProjectResources.Directory, string.IsNullOrWhiteSpace(name) ? "上位机程序" : name);
|
||||
if (result.Success) File.WriteAllBytes(Path.Combine(dir, ExportFile), result.Exe);
|
||||
if (string.IsNullOrWhiteSpace(name)) name = "上位机程序";
|
||||
bool ok;
|
||||
JsonArray diagnostics;
|
||||
if (project)
|
||||
{
|
||||
var result = ProjectExporter.Export(dir, libs, Sim.Host.ProjectResources.Directory, name);
|
||||
if (result.Success) File.WriteAllBytes(Path.Combine(dir, ExportProjectFile), result.Zip);
|
||||
(ok, diagnostics) = (result.Success, result.Diagnostics);
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = ExeExporter.Export(dir, libs, Sim.Host.ProjectResources.Directory, name);
|
||||
if (result.Success) File.WriteAllBytes(Path.Combine(dir, ExportFile), result.Exe);
|
||||
(ok, diagnostics) = (result.Success, result.Diagnostics);
|
||||
}
|
||||
return new JsonObject
|
||||
{
|
||||
["type"] = "compiled",
|
||||
["ok"] = result.Success,
|
||||
["diagnostics"] = result.Diagnostics,
|
||||
["ok"] = ok,
|
||||
["diagnostics"] = diagnostics,
|
||||
["ms"] = watch.ElapsedMilliseconds,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ internal static class ExeExporter
|
||||
/// VS 设计器给媒体播放器存的 OcxState(AxHost.State 的 BinaryFormatter 序列化数据,取自教材项目 7 的 Form1.resx,是控件的默认状态)。
|
||||
/// 平台没有 .resx,导出时给每个 resources.GetObject("…OcxState") 都放这一份。
|
||||
/// </summary>
|
||||
private const string MediaPlayerOcxState =
|
||||
internal const string MediaPlayerOcxState =
|
||||
"AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w" +
|
||||
"LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACFTeXN0" +
|
||||
"ZW0uV2luZG93cy5Gb3Jtcy5BeEhvc3QrU3RhdGUBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAtwAAAAIB" +
|
||||
@@ -51,9 +51,9 @@ internal static class ExeExporter
|
||||
private const string EmbeddedResources = "global::IotSimExport.EmbeddedFiles.";
|
||||
|
||||
private static readonly Regex ResourceFile = new(@"ProjectResources\.(?:Image|Bytes)\(""([^""]+)""\)");
|
||||
private static readonly Regex OcxStateKey = new(@"GetObject\(""([^""]+\.OcxState)""\)");
|
||||
internal static readonly Regex OcxStateKey = new(@"GetObject\(""([^""]+\.OcxState)""\)");
|
||||
|
||||
private static readonly CSharpCompilationOptions Options = new CSharpCompilationOptions(OutputKind.WindowsApplication)
|
||||
internal static readonly CSharpCompilationOptions Options = new CSharpCompilationOptions(OutputKind.WindowsApplication)
|
||||
.WithPlatform(Platform.AnyCpu32BitPreferred)
|
||||
.WithOptimizationLevel(OptimizationLevel.Release)
|
||||
.WithNullableContextOptions(NullableContextOptions.Disable)
|
||||
@@ -61,7 +61,7 @@ internal static class ExeExporter
|
||||
.WithConcurrentBuild(true);
|
||||
|
||||
/// <summary>.NET Framework 4.5 的引用程序集(构建时复制到 netfx/,见 Sim.Runner.csproj)</summary>
|
||||
private static readonly Lazy<IReadOnlyList<MetadataReference>> Framework = new(() =>
|
||||
internal static readonly Lazy<IReadOnlyList<MetadataReference>> Framework = new(() =>
|
||||
Directory.EnumerateFiles(Path.Combine(AppContext.BaseDirectory, "netfx"), "*.dll")
|
||||
.Select(path => (MetadataReference)MetadataReference.CreateFromFile(path))
|
||||
.ToList());
|
||||
@@ -69,9 +69,7 @@ internal static class ExeExporter
|
||||
/// <summary>libsDir:教材 dll 所在目录;galleryDir:平台素材库</summary>
|
||||
public static Result Export(string projectDir, string libsDir, string galleryDir, string assemblyName)
|
||||
{
|
||||
var libs = TextbookLibs(libsDir);
|
||||
if (!libs.Any(p => Path.GetFileName(p) == "DeviceCtrlLibrary.dll"))
|
||||
throw new InvalidOperationException("服务器上没有放教材的设备类库(真机版 DeviceCtrlLibrary.dll 等),不能导出 EXE,请联系老师");
|
||||
var libs = TextbookLibs(libsDir, "导出 EXE");
|
||||
|
||||
var sources = StudentCompiler.SourceFiles(projectDir)
|
||||
.Select(path => (Path: Path.GetRelativePath(projectDir, path), Text: File.ReadAllText(path)))
|
||||
@@ -86,15 +84,7 @@ internal static class ExeExporter
|
||||
sources[i] = (sources[i].Path, sources[i].Text.Replace(PlatformResources, EmbeddedResources));
|
||||
}
|
||||
// 代码里按文件名用到的素材(如 BaseDirectory + "//Resources//ring.mp3"),在 VS 里是复制到输出目录的 Resources 文件夹
|
||||
var media = Directory.Exists(galleryDir)
|
||||
? Directory.GetFiles(galleryDir)
|
||||
.Select(Path.GetFileName)
|
||||
.OfType<string>()
|
||||
.Where(name => !name.Contains(".2x.") && name != "index.json")
|
||||
.Where(name => sources.Any(s => s.Text.Contains(name + "\"")))
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToList()
|
||||
: new List<string>();
|
||||
var media = ReferencedMedia(galleryDir, sources.Select(s => s.Text));
|
||||
|
||||
var trees = sources
|
||||
.Select(s => CSharpSyntaxTree.ParseText(SourceText.From(s.Text, Encoding.UTF8), StudentCompiler.ParseOptions, path: s.Path))
|
||||
@@ -135,11 +125,33 @@ internal static class ExeExporter
|
||||
return new Result(emit.Success, pe.ToArray(), diagnostics);
|
||||
}
|
||||
|
||||
/// <summary>代码里按文件名用到的素材库文件(如 BaseDirectory + "//Resources//ring.mp3"),按文件名排序</summary>
|
||||
internal static List<string> ReferencedMedia(string galleryDir, IEnumerable<string> texts)
|
||||
{
|
||||
if (!Directory.Exists(galleryDir)) return new List<string>();
|
||||
var all = texts.ToList();
|
||||
return Directory.GetFiles(galleryDir)
|
||||
.Select(Path.GetFileName)
|
||||
.OfType<string>()
|
||||
.Where(name => !name.Contains(".2x.") && name != "index.json")
|
||||
.Where(name => all.Any(text => text.Contains(name + "\"")))
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 教材 dll 的路径。目录或文件读不了时(服务器上拷进去的文件权限是 600,容器里的平台用户读不到)说明原因,
|
||||
/// 不把英文的系统报错直接给学生看。
|
||||
/// 教材 dll 的路径;没放时说明原因(what:"导出 EXE"或"导出工程")。目录或文件读不了时
|
||||
/// (服务器上拷进去的文件权限是 600,容器里的平台用户读不到)也说明原因,不把英文的系统报错直接给学生看。
|
||||
/// </summary>
|
||||
private static string[] TextbookLibs(string libsDir)
|
||||
internal static string[] TextbookLibs(string libsDir, string what)
|
||||
{
|
||||
var libs = ReadableLibs(libsDir, what);
|
||||
if (!libs.Any(p => Path.GetFileName(p) == "DeviceCtrlLibrary.dll"))
|
||||
throw new InvalidOperationException($"服务器上没有放教材的设备类库(真机版 DeviceCtrlLibrary.dll 等),不能{what},请联系老师");
|
||||
return libs;
|
||||
}
|
||||
|
||||
private static string[] ReadableLibs(string libsDir, string what)
|
||||
{
|
||||
if (!Directory.Exists(libsDir)) return Array.Empty<string>();
|
||||
string? current = null;
|
||||
@@ -155,9 +167,9 @@ internal static class ExeExporter
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
var what = current == null ? "目录" : Path.GetFileName(current);
|
||||
var item = current == null ? "目录" : Path.GetFileName(current);
|
||||
throw new InvalidOperationException(
|
||||
$"服务器上的教材设备类库读不了({what} 的权限不够),不能导出 EXE,请联系老师(服务器上执行 chmod 755 textbook-libs 和 chmod 644 textbook-libs/*.dll)");
|
||||
$"服务器上的教材设备类库读不了({item} 的权限不够),不能{what},请联系老师(服务器上执行 chmod 755 textbook-libs 和 chmod 644 textbook-libs/*.dll)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Sim.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// 导出 VS 工程:把学生工程打包成教材那样的 VS 2012 窗体应用工程(zip),在机房电脑上用 VS 打开接着改、按 F5 运行。
|
||||
/// <list type="bullet">
|
||||
/// <item>先照机房的环境检查:C# 5(VS 2012 的编译器)、.NET Framework 4.5、真机版设备类库,有错误就不导出。</item>
|
||||
/// <item>平台生成的 Properties/Resources.Designer.cs 从素材库取图,导出时换成 VS 生成的写法和 Resources.resx,图片放在 Resources 文件夹;
|
||||
/// 代码里按文件名用到的素材(如 ring.mp3)设成复制到输出目录,运行时在 bin\Debug\Resources 下,和教材一样。</item>
|
||||
/// <item>教材 dll 放在工程的 lib 文件夹,用到的才加引用(和教材工程一样只引用设备类库、控件库),编译时 VS 把它们和依赖复制到 bin\Debug。</item>
|
||||
/// <item>文本文件用带 BOM 的 UTF-8 和 CRLF 换行(VS 2012 打开不带 BOM 的文件会把中文当成 GBK)。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal static class ProjectExporter
|
||||
{
|
||||
public sealed record Result(bool Success, byte[] Zip, JsonArray Diagnostics);
|
||||
|
||||
/// <summary>VS 2012 的编译器支持到 C# 5</summary>
|
||||
internal static readonly CSharpParseOptions Vs2012 = new(LanguageVersion.CSharp5);
|
||||
|
||||
private const string PlatformResourcesFile = "Properties/Resources.Designer.cs";
|
||||
private const string LedCompatFile = "LedHelperCompat.cs";
|
||||
private const string LedCompatClass = "LedHelperCompat";
|
||||
|
||||
private static readonly Regex ResourceMember = new(
|
||||
@"internal static (System\.Drawing\.Bitmap|byte\[\]) ([\p{L}\p{N}_]+) \{\s*get \{ return global::Sim\.Host\.ProjectResources\.(?:Image|Bytes)\(""([^""]+)""\); \}");
|
||||
private static readonly Regex ResourcesNamespace = new(@"namespace\s+([\p{L}\p{N}_.]+)\.Properties\b");
|
||||
private static readonly Regex FirstNamespace = new(@"^\s*namespace\s+([\p{L}\p{N}_.]+)", RegexOptions.Multiline);
|
||||
|
||||
/// <summary>VS 2012 窗体应用模板默认的引用(教材工程都有这些)</summary>
|
||||
private static readonly string[] DefaultReferences =
|
||||
{
|
||||
"System", "System.Core", "System.Xml.Linq", "System.Data.DataSetExtensions", "Microsoft.CSharp",
|
||||
"System.Data", "System.Deployment", "System.Drawing", "System.Windows.Forms", "System.Xml",
|
||||
};
|
||||
|
||||
private const string BitmapType = "System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
|
||||
private const string BytesType = "System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
|
||||
|
||||
private sealed record ResourceItem(string Name, bool Image, string File);
|
||||
|
||||
private sealed record Source(string Path, string Text);
|
||||
|
||||
/// <summary>libsDir:教材 dll 所在目录;galleryDir:平台素材库;name:工程名(也是程序集名、文件夹名)</summary>
|
||||
public static Result Export(string projectDir, string libsDir, string galleryDir, string name)
|
||||
{
|
||||
var libs = ExeExporter.TextbookLibs(libsDir, "导出工程");
|
||||
var sources = StudentCompiler.SourceFiles(projectDir)
|
||||
.Select(path => new Source(Path.GetRelativePath(projectDir, path).Replace('\\', '/'), File.ReadAllText(path)))
|
||||
.ToList();
|
||||
|
||||
var platformResources = sources.FirstOrDefault(s => s.Path == PlatformResourcesFile)?.Text;
|
||||
var items = platformResources == null
|
||||
? new List<ResourceItem>()
|
||||
: ResourceMember.Matches(platformResources)
|
||||
.Select(m => new ResourceItem(m.Groups[2].Value, m.Groups[1].Value != "byte[]", Path.GetFileName(m.Groups[3].Value)))
|
||||
.ToList();
|
||||
var rootNamespace = RootNamespace(sources, platformResources, name);
|
||||
sources.RemoveAll(s => s.Path == PlatformResourcesFile);
|
||||
|
||||
// VS 模板里有、平台上没有的文件(Properties 文件夹由平台管理,学生建不了同名文件)
|
||||
var generated = new List<Source>
|
||||
{
|
||||
new("Properties/AssemblyInfo.cs", AssemblyInfo(name)),
|
||||
new("Properties/Resources.Designer.cs", ResourcesDesigner(rootNamespace, items)),
|
||||
new("Properties/Settings.Designer.cs", SettingsDesigner(rootNamespace)),
|
||||
};
|
||||
var compat = new Source(LedCompatFile, LedCompat);
|
||||
|
||||
// 照机房的环境编译一遍:C# 5、.NET Framework 4.5、真机版设备类库
|
||||
var trees = sources.Concat(generated).Append(compat)
|
||||
.Select(s => CSharpSyntaxTree.ParseText(SourceText.From(s.Text, Encoding.UTF8), Vs2012, path: s.Path))
|
||||
.ToList();
|
||||
var libReferences = libs.ToDictionary(p => (MetadataReference)MetadataReference.CreateFromFile(p), p => p);
|
||||
var compilation = CSharpCompilation.Create(name, trees, ExeExporter.Framework.Value.Concat(libReferences.Keys), ExeExporter.Options);
|
||||
using (var pe = new MemoryStream())
|
||||
{
|
||||
var emit = compilation.Emit(pe);
|
||||
var diagnostics = new JsonArray();
|
||||
foreach (var diagnostic in emit.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error))
|
||||
{
|
||||
diagnostics.Add(StudentCompiler.ToJson(diagnostic));
|
||||
}
|
||||
if (!emit.Success) return new Result(false, Array.Empty<byte>(), diagnostics);
|
||||
}
|
||||
|
||||
var studentTrees = trees.Take(sources.Count).ToList();
|
||||
var code = sources.Concat(generated).ToList();
|
||||
if (UsesLedCompat(compilation, studentTrees))
|
||||
{
|
||||
code.Add(compat);
|
||||
studentTrees.Add(trees[^1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
compilation = compilation.RemoveSyntaxTrees(trees[^1]);
|
||||
}
|
||||
|
||||
// 引用:和教材工程一样,教材 dll 只引用代码里直接用到的(其他的作为依赖由 VS 复制到输出目录),
|
||||
// 框架引用在 VS 模板默认的之外加上用到的(如图表控件)。只带这些引用编译不过时(用到的类型的基类在别的 dll 里等)引用全部教材 dll
|
||||
var direct = DirectlyUsedAssemblies(compilation, studentTrees);
|
||||
var frameworkReferences = DefaultReferences
|
||||
.Concat(ExeExporter.Framework.Value.OfType<PortableExecutableReference>()
|
||||
.Select(r => Path.GetFileNameWithoutExtension(r.FilePath)).OfType<string>()
|
||||
.Where(n => direct.Contains(n) && n != "mscorlib" && !DefaultReferences.Contains(n))
|
||||
.OrderBy(n => n, StringComparer.Ordinal))
|
||||
.ToList();
|
||||
var textbookReferences = libs.Where(p => direct.Contains(Path.GetFileNameWithoutExtension(p))).ToList();
|
||||
var reduced = compilation.WithReferences(ExeExporter.Framework.Value.Concat(libReferences.Where(r => textbookReferences.Contains(r.Value)).Select(r => r.Key)));
|
||||
if (reduced.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) textbookReferences = libs.ToList();
|
||||
textbookReferences = textbookReferences.Select(p => Path.GetFileName(p)).ToList();
|
||||
|
||||
var forms = Forms(compilation, sources, studentTrees);
|
||||
var media = ExeExporter.ReferencedMedia(galleryDir, sources.Select(s => s.Text));
|
||||
var images = items.Select(i => i.File).Where(f => File.Exists(Path.Combine(galleryDir, f))).Distinct().ToList();
|
||||
|
||||
var zip = new ZipWriter(DateTime.UtcNow.AddHours(8));
|
||||
var root = $"{name}/";
|
||||
var dir = $"{name}/{name}/";
|
||||
zip.Add(root + name + ".sln", Text(Solution(name)));
|
||||
zip.Add(dir + name + ".csproj", Text(Project(name, rootNamespace, code, forms, frameworkReferences, textbookReferences, images, media)));
|
||||
zip.Add(dir + "App.config", Text(AppConfig));
|
||||
zip.Add(dir + "Properties/Settings.settings", Text(SettingsFile));
|
||||
zip.Add(dir + "Properties/Resources.resx", Text(Resx(items.Select(ResourceEntry))));
|
||||
foreach (var source in code) zip.Add(dir + source.Path, Text(source.Text));
|
||||
foreach (var form in forms.Where(f => f.Resx))
|
||||
{
|
||||
zip.Add(dir + Path.ChangeExtension(form.Path, ".resx"), Text(Resx(form.OcxKeys.Select(OcxEntry))));
|
||||
}
|
||||
foreach (var file in images.Union(media).OrderBy(f => f, StringComparer.Ordinal))
|
||||
{
|
||||
zip.Add(dir + "Resources/" + file, File.ReadAllBytes(Path.Combine(galleryDir, file)));
|
||||
}
|
||||
foreach (var lib in libs) zip.Add(dir + "lib/" + Path.GetFileName(lib), File.ReadAllBytes(lib));
|
||||
return new Result(true, zip.Finish(), new JsonArray());
|
||||
}
|
||||
|
||||
/// <summary>工程的默认命名空间:平台的资源类所在的命名空间,否则取 Program.cs(再其次其他文件)里的第一个命名空间</summary>
|
||||
private static string RootNamespace(List<Source> sources, string? platformResources, string name)
|
||||
{
|
||||
if (platformResources != null && ResourcesNamespace.Match(platformResources) is { Success: true } r) return r.Groups[1].Value;
|
||||
foreach (var source in sources.OrderBy(s => s.Path == "Program.cs" ? 0 : 1).ThenBy(s => s.Path, StringComparer.Ordinal))
|
||||
{
|
||||
var m = FirstNamespace.Match(source.Text);
|
||||
if (m.Success) return m.Groups[1].Value;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>代码里直接用到的类型、成员所在的程序集(程序集名)</summary>
|
||||
private static HashSet<string> DirectlyUsedAssemblies(CSharpCompilation compilation, List<SyntaxTree> trees)
|
||||
{
|
||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
void Add(ISymbol? symbol)
|
||||
{
|
||||
while (symbol is IArrayTypeSymbol array) symbol = array.ElementType;
|
||||
if (symbol?.ContainingAssembly is { } assembly) names.Add(assembly.Identity.Name);
|
||||
if (symbol is INamedTypeSymbol { IsGenericType: true } generic)
|
||||
{
|
||||
foreach (var argument in generic.TypeArguments) Add(argument);
|
||||
}
|
||||
}
|
||||
foreach (var tree in trees)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(tree);
|
||||
foreach (var node in tree.GetRoot().DescendantNodes())
|
||||
{
|
||||
Add(model.GetSymbolInfo(node).Symbol);
|
||||
Add(model.GetTypeInfo(node).Type);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/// <summary>学生代码是否用到了补上的 ledHelper.Close()(教材类库里没有)</summary>
|
||||
private static bool UsesLedCompat(CSharpCompilation compilation, List<SyntaxTree> trees)
|
||||
{
|
||||
foreach (var tree in trees)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(tree);
|
||||
foreach (var invocation in tree.GetRoot().DescendantNodes().OfType<InvocationExpressionSyntax>())
|
||||
{
|
||||
if (model.GetSymbolInfo(invocation).Symbol is IMethodSymbol { ReducedFrom.ContainingType.Name: LedCompatClass }) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>窗体、用户控件:VS 里显示成可以打开设计器的文件,Designer.cs 和 .resx 挂在它下面</summary>
|
||||
private sealed record Form(string Path, string SubType, bool Resx, List<string> OcxKeys);
|
||||
|
||||
private static List<Form> Forms(CSharpCompilation compilation, List<Source> sources, List<SyntaxTree> trees)
|
||||
{
|
||||
var form = compilation.GetTypeByMetadataName("System.Windows.Forms.Form");
|
||||
var userControl = compilation.GetTypeByMetadataName("System.Windows.Forms.UserControl");
|
||||
var result = new List<Form>();
|
||||
for (var i = 0; i < sources.Count; i++)
|
||||
{
|
||||
var path = sources[i].Path;
|
||||
if (path.EndsWith(".Designer.cs")) continue;
|
||||
var designer = sources.FirstOrDefault(s => s.Path == path[..^3] + ".Designer.cs");
|
||||
var model = compilation.GetSemanticModel(trees[i]);
|
||||
var type = trees[i].GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>()
|
||||
.Select(c => model.GetDeclaredSymbol(c)).FirstOrDefault(t => t != null);
|
||||
string? subType = null;
|
||||
for (var t = type?.BaseType; t != null && subType == null; t = t.BaseType)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(t, form)) subType = "Form";
|
||||
else if (SymbolEqualityComparer.Default.Equals(t, userControl)) subType = "UserControl";
|
||||
}
|
||||
if (subType == null) continue;
|
||||
var keys = designer == null
|
||||
? new List<string>()
|
||||
: ExeExporter.OcxStateKey.Matches(designer.Text).Select(m => m.Groups[1].Value).Distinct().OrderBy(k => k, StringComparer.Ordinal).ToList();
|
||||
result.Add(new Form(path, subType, designer != null, keys));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string Project(string name, string rootNamespace, List<Source> code, List<Form> forms,
|
||||
List<string> frameworkReferences, List<string> textbookReferences, List<string> images, List<string> media)
|
||||
{
|
||||
static string Win(string path) => path.Replace('/', '\\');
|
||||
static string X(string text) => SecurityElement.Escape(text);
|
||||
|
||||
var s = new StringBuilder();
|
||||
s.Append($"""
|
||||
<?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)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{ProjectGuid(name)}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>{X(rootNamespace)}</RootNamespace>
|
||||
<AssemblyName>{X(name)}</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</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>
|
||||
|
||||
""");
|
||||
foreach (var lib in textbookReferences)
|
||||
{
|
||||
s.Append($"""
|
||||
<Reference Include="{X(Path.GetFileNameWithoutExtension(lib))}">
|
||||
<HintPath>lib\{X(lib)}</HintPath>
|
||||
</Reference>
|
||||
|
||||
""");
|
||||
}
|
||||
foreach (var reference in frameworkReferences) s.Append($" <Reference Include=\"{reference}\" />\n");
|
||||
s.Append(" </ItemGroup>\n <ItemGroup>\n");
|
||||
|
||||
var formPaths = forms.ToDictionary(f => f.Path);
|
||||
// 和 VS 一样 Form1.cs 在 Form1.Designer.cs 前面
|
||||
static string Order(string path) => path.EndsWith(".Designer.cs") ? path[..^12] + ".cs\u0001" : path;
|
||||
foreach (var source in code.Where(c => !c.Path.StartsWith("Properties/")).OrderBy(c => Order(c.Path), StringComparer.Ordinal))
|
||||
{
|
||||
var path = source.Path;
|
||||
if (formPaths.TryGetValue(path, out var form))
|
||||
{
|
||||
s.Append($" <Compile Include=\"{X(Win(path))}\">\n <SubType>{form.SubType}</SubType>\n </Compile>\n");
|
||||
}
|
||||
else if (path.EndsWith(".Designer.cs") && formPaths.ContainsKey(path[..^12] + ".cs"))
|
||||
{
|
||||
s.Append($" <Compile Include=\"{X(Win(path))}\">\n <DependentUpon>{X(Path.GetFileName(path[..^12] + ".cs"))}</DependentUpon>\n </Compile>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
s.Append($" <Compile Include=\"{X(Win(path))}\" />\n");
|
||||
}
|
||||
}
|
||||
s.Append(" <Compile Include=\"Properties\\AssemblyInfo.cs\" />\n");
|
||||
foreach (var form in forms.Where(f => f.Resx).OrderBy(f => f.Path, StringComparer.Ordinal))
|
||||
{
|
||||
s.Append($" <EmbeddedResource Include=\"{X(Win(Path.ChangeExtension(form.Path, ".resx")))}\">\n <DependentUpon>{X(Path.GetFileName(form.Path))}</DependentUpon>\n </EmbeddedResource>\n");
|
||||
}
|
||||
s.Append("""
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
</Compile>
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
<Compile Include="Properties\Settings.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
|
||||
""");
|
||||
var files = images.Union(media).OrderBy(f => f, StringComparer.Ordinal).ToList();
|
||||
if (files.Count > 0)
|
||||
{
|
||||
s.Append(" <ItemGroup>\n");
|
||||
foreach (var file in files)
|
||||
{
|
||||
// 代码里按文件名用的素材复制到 bin\Debug\Resources(和教材工程的输出目录一样);Properties.Resources 的图片编译进程序
|
||||
s.Append(media.Contains(file)
|
||||
? $" <Content Include=\"Resources\\{X(file)}\">\n <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n </Content>\n"
|
||||
: $" <None Include=\"Resources\\{X(file)}\" />\n");
|
||||
}
|
||||
s.Append(" </ItemGroup>\n");
|
||||
}
|
||||
s.Append("""
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
""");
|
||||
return s.ToString();
|
||||
}
|
||||
|
||||
private static string Solution(string name) => $$"""
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "{{name}}", "{{name}}\{{name}}.csproj", "{{ProjectGuid(name)}}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{{ProjectGuid(name)}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{{ProjectGuid(name)}}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{{ProjectGuid(name)}}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{{ProjectGuid(name)}}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
""";
|
||||
|
||||
/// <summary>同一个工程名每次导出的 GUID 相同(重新导出后覆盖旧的,VS 认成同一个工程)</summary>
|
||||
private static string ProjectGuid(string name) => "{" + NameGuid("project:" + name).ToString().ToUpperInvariant() + "}";
|
||||
|
||||
private static Guid NameGuid(string text) => new(MD5.HashData(Encoding.UTF8.GetBytes(text)));
|
||||
|
||||
private static string AssemblyInfo(string name) => $$"""
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 有关程序集的常规信息通过以下
|
||||
// 特性集控制。更改这些特性值可修改
|
||||
// 与程序集关联的信息。
|
||||
[assembly: AssemblyTitle({{SymbolDisplay.FormatLiteral(name, true)}})]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct({{SymbolDisplay.FormatLiteral(name, true)}})]
|
||||
[assembly: AssemblyCopyright("Copyright © {{DateTime.UtcNow.AddHours(8).Year}}")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// 将 ComVisible 设置为 false 使此程序集中的类型
|
||||
// 对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型,
|
||||
// 则将该类型上的 ComVisible 特性设置为 true。
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
|
||||
[assembly: Guid("{{NameGuid("assembly:" + name)}}")]
|
||||
|
||||
// 程序集的版本信息由下面四个值组成:
|
||||
//
|
||||
// 主版本
|
||||
// 次版本
|
||||
// 生成号
|
||||
// 修订号
|
||||
//
|
||||
// 可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值,
|
||||
// 方法是按如下所示使用“*”:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
|
||||
""";
|
||||
|
||||
/// <summary>和 VS 2012 的 ResXFileCodeGenerator 生成的一样(中文版 VS 的注释)</summary>
|
||||
private static string ResourcesDesigner(string ns, List<ResourceItem> items)
|
||||
{
|
||||
var members = string.Concat(items.Select(item =>
|
||||
{
|
||||
var type = item.Image ? "System.Drawing.Bitmap" : "byte[]";
|
||||
var what = item.Image ? "System.Drawing.Bitmap" : "System.Byte[]";
|
||||
return $$"""
|
||||
|
||||
/// <summary>
|
||||
/// 查找 {{what}} 类型的本地化资源。
|
||||
/// </summary>
|
||||
internal static {{type}} {{item.Name}} {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("{{item.Name}}", resourceCulture);
|
||||
return (({{type}})(obj));
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
}));
|
||||
return $$"""
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
//
|
||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||
// 重新生成代码,这些更改将会丢失。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace {{ns}}.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 一个强类型的资源类,用于查找本地化的字符串等。
|
||||
/// </summary>
|
||||
// 此类是由 StronglyTypedResourceBuilder
|
||||
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
|
||||
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
|
||||
// (以 /str 作为命令选项),或重新生成 VS 项目。
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回此类使用的缓存的 ResourceManager 实例。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("{{ns}}.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用此强类型资源类,为所有资源查找
|
||||
/// 重写当前线程的 CurrentUICulture 属性。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
{{members}} }
|
||||
}
|
||||
|
||||
""";
|
||||
}
|
||||
|
||||
private static string SettingsDesigner(string ns) => $$"""
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace {{ns}}.Properties
|
||||
{
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
|
||||
{
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default
|
||||
{
|
||||
get
|
||||
{
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
private const string SettingsFile = """
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
||||
""";
|
||||
|
||||
private const string AppConfig = """
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
|
||||
</startup>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 书上项目 2 任务 3 调用 DeviceCtrlHelper.ledHelper.Close(),教材的类库里没有这个方法(LED 屏每次发送才用串口,不占着),
|
||||
/// 平台补上了它。用到时导出工程里也带上这个文件,书上的代码照样能编译。
|
||||
/// </summary>
|
||||
private const string LedCompat = """
|
||||
// 教材的设备类库里 LEDHelper 没有 Close 方法(LED 屏每次发送时才打开串口,发完就关),
|
||||
// 书上的代码调用了 ledHelper.Close(),这里补一个什么都不做的方法,让代码能编译。
|
||||
internal static class LedHelperCompat
|
||||
{
|
||||
internal static void Close(this global::DeviceCtrlLibrary.LEDHelper helper)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
private static readonly Lazy<string> ResxHeader = new(() =>
|
||||
{
|
||||
using var stream = typeof(ProjectExporter).Assembly.GetManifestResourceStream("ResxHeader.xml")!;
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return reader.ReadToEnd().TrimEnd('\n');
|
||||
});
|
||||
|
||||
private static string Resx(IEnumerable<string> entries)
|
||||
{
|
||||
var list = entries.ToList();
|
||||
var s = new StringBuilder(ResxHeader.Value).Append('\n');
|
||||
if (list.Any(e => e.Contains("ResXFileRef")))
|
||||
{
|
||||
s.Append(" <assembly alias=\"System.Windows.Forms\" name=\"System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\" />\n");
|
||||
}
|
||||
foreach (var entry in list) s.Append(entry);
|
||||
return s.Append("</root>").ToString();
|
||||
}
|
||||
|
||||
private static string ResourceEntry(ResourceItem item) =>
|
||||
$" <data name=\"{SecurityElement.Escape(item.Name)}\" type=\"System.Resources.ResXFileRef, System.Windows.Forms\">\n" +
|
||||
$" <value>..\\Resources\\{SecurityElement.Escape(item.File)};{(item.Image ? BitmapType : BytesType)}</value>\n" +
|
||||
" </data>\n";
|
||||
|
||||
/// <summary>媒体播放器的 OcxState:教材 Form1.resx 里的默认状态,和 VS 一样每行 80 个字符</summary>
|
||||
private static string OcxEntry(string key)
|
||||
{
|
||||
var data = ExeExporter.MediaPlayerOcxState;
|
||||
var lines = Enumerable.Range(0, (data.Length + 79) / 80).Select(i => " " + data.Substring(i * 80, Math.Min(80, data.Length - i * 80)));
|
||||
return $" <data name=\"{SecurityElement.Escape(key)}\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n" +
|
||||
" <value>\n" + string.Join("\n", lines) + "\n</value>\n </data>\n";
|
||||
}
|
||||
|
||||
/// <summary>文本文件:带 BOM 的 UTF-8,CRLF 换行</summary>
|
||||
private static byte[] Text(string text)
|
||||
{
|
||||
var crlf = text.Replace("\r\n", "\n").Replace("\n", "\r\n");
|
||||
return Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(crlf)).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
@@ -28,6 +28,10 @@
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(NetFxReference)" DestinationFiles="@(NetFxReference->'$(OutDir)netfx/%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
<!-- 导出 VS 工程:.resx 文件开头的标准内容(VS 生成的架构说明和 resheader) -->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="ProjectTemplate/ResxHeader.xml" LogicalName="ResxHeader.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Sim.WinForms/Sim.WinForms.csproj" />
|
||||
<ProjectReference Include="../Sim.Devices/Sim.Devices.csproj" />
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
|
||||
namespace Sim.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// 写 zip 文件。文件名照中文 Windows 的习惯用 GBK 编码(资源管理器自带的解压、老版本的压缩软件都按 GBK 读,
|
||||
/// 用 UTF-8 时 Windows 7 等会显示乱码),另附 Info-ZIP 的 Unicode 文件名扩展字段(0x7075),
|
||||
/// 7-Zip、Linux 的 unzip 等认这个字段的软件在别的语言的系统上也能显示正确。
|
||||
/// GBK 表示不了的文件名改用 UTF-8 并设置 UTF-8 标志位。
|
||||
/// </summary>
|
||||
internal sealed class ZipWriter
|
||||
{
|
||||
private static readonly Encoding Gbk;
|
||||
private static readonly uint[] CrcTable = new uint[256];
|
||||
|
||||
static ZipWriter()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
Gbk = Encoding.GetEncoding(936, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
|
||||
for (uint n = 0; n < 256; n++)
|
||||
{
|
||||
var c = n;
|
||||
for (var k = 0; k < 8; k++) c = (c & 1) != 0 ? 0xEDB88320 ^ (c >> 1) : c >> 1;
|
||||
CrcTable[n] = c;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record Entry(byte[] Name, byte[] Extra, ushort Flags, ushort Method, uint Crc, uint CompressedSize, uint Size, uint Offset);
|
||||
|
||||
private readonly MemoryStream output = new();
|
||||
private readonly List<Entry> entries = new();
|
||||
private readonly ushort dosTime;
|
||||
private readonly ushort dosDate;
|
||||
|
||||
/// <param name="time">文件的修改时间(机房电脑上解压后看到的时间)</param>
|
||||
public ZipWriter(DateTime time)
|
||||
{
|
||||
dosTime = (ushort)((time.Hour << 11) | (time.Minute << 5) | (time.Second / 2));
|
||||
dosDate = (ushort)(((time.Year - 1980) << 9) | (time.Month << 5) | time.Day);
|
||||
}
|
||||
|
||||
/// <summary>添加文件,path 用 / 分隔</summary>
|
||||
public void Add(string path, byte[] data)
|
||||
{
|
||||
byte[] name;
|
||||
byte[] extra = Array.Empty<byte>();
|
||||
ushort flags = 0;
|
||||
try
|
||||
{
|
||||
name = Gbk.GetBytes(path);
|
||||
var utf8 = Encoding.UTF8.GetBytes(path);
|
||||
if (utf8.Length != name.Length || !utf8.AsSpan().SequenceEqual(name))
|
||||
{
|
||||
// Unicode Path Extra Field:版本 1、GBK 文件名的 CRC、UTF-8 文件名
|
||||
extra = new byte[4 + 1 + 4 + utf8.Length];
|
||||
BitConverter.TryWriteBytes(extra.AsSpan(0), (ushort)0x7075);
|
||||
BitConverter.TryWriteBytes(extra.AsSpan(2), (ushort)(1 + 4 + utf8.Length));
|
||||
extra[4] = 1;
|
||||
BitConverter.TryWriteBytes(extra.AsSpan(5), Crc32(name));
|
||||
utf8.CopyTo(extra, 9);
|
||||
}
|
||||
}
|
||||
catch (EncoderFallbackException)
|
||||
{
|
||||
name = Encoding.UTF8.GetBytes(path);
|
||||
flags = 1 << 11;
|
||||
}
|
||||
|
||||
var crc = Crc32(data);
|
||||
var compressed = Deflate(data);
|
||||
var method = (ushort)8;
|
||||
if (compressed.Length >= data.Length)
|
||||
{
|
||||
compressed = data;
|
||||
method = 0;
|
||||
}
|
||||
|
||||
var entry = new Entry(name, extra, flags, method, crc, (uint)compressed.Length, (uint)data.Length, (uint)output.Position);
|
||||
entries.Add(entry);
|
||||
using var w = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true);
|
||||
w.Write(0x04034b50u);
|
||||
w.Write((ushort)20);
|
||||
w.Write(entry.Flags);
|
||||
w.Write(entry.Method);
|
||||
w.Write(dosTime);
|
||||
w.Write(dosDate);
|
||||
w.Write(entry.Crc);
|
||||
w.Write(entry.CompressedSize);
|
||||
w.Write(entry.Size);
|
||||
w.Write((ushort)entry.Name.Length);
|
||||
w.Write((ushort)entry.Extra.Length);
|
||||
w.Write(entry.Name);
|
||||
w.Write(entry.Extra);
|
||||
w.Write(compressed);
|
||||
}
|
||||
|
||||
/// <summary>写中央目录,返回整个 zip</summary>
|
||||
public byte[] Finish()
|
||||
{
|
||||
var start = (uint)output.Position;
|
||||
using (var w = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true))
|
||||
{
|
||||
foreach (var e in entries)
|
||||
{
|
||||
w.Write(0x02014b50u);
|
||||
w.Write((ushort)20); // 由 MS-DOS 兼容的系统生成:解压时不设置 Unix 权限
|
||||
w.Write((ushort)20);
|
||||
w.Write(e.Flags);
|
||||
w.Write(e.Method);
|
||||
w.Write(dosTime);
|
||||
w.Write(dosDate);
|
||||
w.Write(e.Crc);
|
||||
w.Write(e.CompressedSize);
|
||||
w.Write(e.Size);
|
||||
w.Write((ushort)e.Name.Length);
|
||||
w.Write((ushort)e.Extra.Length);
|
||||
w.Write((ushort)0); // 注释长度
|
||||
w.Write((ushort)0); // 磁盘号
|
||||
w.Write((ushort)0); // 内部属性
|
||||
w.Write(0u); // 外部属性
|
||||
w.Write(e.Offset);
|
||||
w.Write(e.Name);
|
||||
w.Write(e.Extra);
|
||||
}
|
||||
var size = (uint)output.Position - start;
|
||||
w.Write(0x06054b50u);
|
||||
w.Write((ushort)0);
|
||||
w.Write((ushort)0);
|
||||
w.Write((ushort)entries.Count);
|
||||
w.Write((ushort)entries.Count);
|
||||
w.Write(size);
|
||||
w.Write(start);
|
||||
w.Write((ushort)0);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] Deflate(byte[] data)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using (var deflate = new DeflateStream(stream, CompressionLevel.Optimal, leaveOpen: true)) deflate.Write(data);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static uint Crc32(byte[] data)
|
||||
{
|
||||
var c = 0xFFFFFFFFu;
|
||||
foreach (var b in data) c = CrcTable[(c ^ b) & 0xFF] ^ (c >> 8);
|
||||
return c ^ 0xFFFFFFFFu;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user