导出 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-03 12:42:13 +08:00
1 parent 4c3ce31fe1
commit 2aa267839f
17 files changed
+1209 -80

No files matched your search

+26
View File
@@ -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}。");
}
}
+25 -9
View File
@@ -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,
};
}
+33 -21
View File
@@ -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)");
}
}
+625
View File
@@ -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>
+4
View File
@@ -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" />
+155
View File
@@ -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;
}
}