diff --git a/Zack.DotNetTrimmer/CmdLineArgsParser.cs b/Zack.DotNetTrimmer/CmdLineArgsParser.cs new file mode 100644 index 0000000..ee71887 --- /dev/null +++ b/Zack.DotNetTrimmer/CmdLineArgsParser.cs @@ -0,0 +1,54 @@ +using Zack.DotNetTrimmerLib; + +namespace Zack.DotNetTrimmer +{ + static class CmdLineArgsParser + { + public static TimmerOptions Parse(string[] args) + { + //--greedy --record d:/1.json --file d:/1.exe + //--record d:/1.json --greedy --file d:/1.exe a b + //--greedy --file d:/1.exe a b + //--file d:/1.exe + //--apply d:/1.json --greedy --file d:/1.exe + TimmerOptions options = new TimmerOptions(); + int greedyIndex = FindArgIndex(args, "--greedy"); + options.Greedy = greedyIndex >= 0; + int recordIndex = FindArgIndex(args, "--record"); + int applyIndex = FindArgIndex(args, "--apply"); + if (recordIndex >= 0) + { + options.Mode = TrimmingMode.Record; + options.RecordFileName = args[recordIndex + 1]; + } + else if (applyIndex >= 0) + { + options.Mode = TrimmingMode.Apply; + options.RecordFileName = args[applyIndex + 1]; + } + else + { + options.Mode = TrimmingMode.Direct; + } + int fileIndex = FindArgIndex(args, "--file"); + if (fileIndex >= 0) + { + options.StartupFile = args[fileIndex + 1]; + options.Arguments = args.Skip(fileIndex + 1).ToArray(); + } + return options; + } + + static int FindArgIndex(string[] args, string value) + { + for (int i = 0; i < args.Length; i++) + { + if (args[i].Equals(value, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + return -1; + } + } +} diff --git a/Zack.DotNetTrimmer/Program.cs b/Zack.DotNetTrimmer/Program.cs index 7c15c54..103eead 100644 --- a/Zack.DotNetTrimmer/Program.cs +++ b/Zack.DotNetTrimmer/Program.cs @@ -3,56 +3,26 @@ //https://docs.microsoft.com/en-us/dotnet/core/dependency-loading/collect-details using System.Reflection; +using Zack.DotNetTrimmer; using Zack.DotNetTrimmerLib; using static Zack.DotNetTrimmerLib.ConsoleHelpers; var asmVer = Assembly.GetExecutingAssembly().GetName().Version; WriteInfo($"Version:{asmVer}({Environment.OSVersion})"); -if (args.Length <= 0) +var options = CmdLineArgsParser.Parse(args); +var startupFile = options.StartupFile; +if (startupFile == null) { - WriteError("Usage: Zack.DotNetTrimmer.exe d:/a/test.exe arg1 arg2"); - WriteError("Usage: Zack.DotNetTrimmer.exe --record d:/1.json d:/a/test.exe arg1 arg2"); - WriteError("Usage: Zack.DotNetTrimmer.exe --apply d:/1.json d:/a/test.exe arg1 arg2"); + WriteError($"Error: Please specify the full path of the application"); + PrintUsage(); return; } -//--record d:/1.json -//--apply d:/1.json -string firstParameter = args[0]; -string? recordFileName = null; - -string startupFile; -string[] arguments; -TrimmingMode mode; -if (firstParameter.StartsWith("--")) -{ - if (firstParameter == "--record") - { - mode = TrimmingMode.Record; - } - else if (firstParameter == "--apply") - { - mode = TrimmingMode.Apply; - } - else - { - WriteError("Error: Invalid mode, only --record and --apply are allowed."); - return; - } - recordFileName = args[1]; - startupFile = args[2]; - arguments = args.Skip(3).ToArray(); -} -else -{ - mode = TrimmingMode.Direct; - startupFile = args[0]; - arguments = args.Skip(1).ToArray(); -} if (!Path.IsPathRooted(startupFile)) { WriteError($"Error: Please specify the full path instead of {startupFile}"); + PrintUsage(); return; } @@ -71,18 +41,16 @@ WriteWarning($"Warning! It looks like {Path.GetFileName(startupFile)} is generated using 'Produce single file', which is not supported. "); } string backupDir = BackupHelper.BackupProject(startupFile); -TimmerOptions cmdOpts = new TimmerOptions { Mode = mode, RecordFileName = recordFileName, StartupFile = startupFile, Arguments = arguments }; -Trimmer trimmer = new Trimmer(cmdOpts); -trimmer.MessageReceived += (s, e) => -{ - WriteInfo(e.Message); -}; -trimmer.FileRemoved += (s, e) => -{ - WriteInfo($"File removed:{e.FileFullPath}"); -}; +Trimmer trimmer = new Trimmer(options); trimmer.Run(); -if (mode == TrimmingMode.Apply || mode == TrimmingMode.Direct) -{ - WriteInfo($"Original files have been backup into {backupDir}"); +WriteInfo($"Original files have been backup into {backupDir}"); + +void PrintUsage() +{ + WriteInfo("Usage:"); + WriteInfo("--greedy --record d:/1.json --file d:/1.exe "); + WriteInfo("--record d:/1.json --greedy --file d:/1.exe a b"); + WriteInfo("--greedy --file d:/1.exe a b"); + WriteInfo("--file d:/1.exe "); + WriteInfo("--apply d:/1.json --greedy --file d:/1.exe"); } diff --git a/Zack.DotNetTrimmer/Properties/launchSettings.json b/Zack.DotNetTrimmer/Properties/launchSettings.json index 2e32fdf..8c78033 100644 --- a/Zack.DotNetTrimmer/Properties/launchSettings.json +++ b/Zack.DotNetTrimmer/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "配置文件 1": { "commandName": "Project", - "commandLineArgs": "\"E:\\主同步盘\\我的坚果云\\MyCode\\DOTNET\\Zack.DotNetTrimmer\\ProjectsForTest\\WinForm.NET5TeeChart-NET-Pro-Samples\\bin\\publish\\TeeChart Examples.exe\" --urls=http://localhost:8888/" + "commandLineArgs": "--apply d:/1.json --greedy --file \"E:\\主同步盘\\我的坚果云\\MyCode\\DOTNET\\Zack.DotNetTrimmer\\ProjectsForTest\\WinForm.NET5TeeChart-NET-Pro-Samples\\bin\\publish\\TeeChart Examples.exe\" --urls=http://localhost:8888/" } } } \ No newline at end of file diff --git a/Zack.DotNetTrimmerLib/AssemblyTrimmer.cs b/Zack.DotNetTrimmerLib/AssemblyTrimmer.cs index 51e94f3..f693ddb 100644 --- a/Zack.DotNetTrimmerLib/AssemblyTrimmer.cs +++ b/Zack.DotNetTrimmerLib/AssemblyTrimmer.cs @@ -1,5 +1,4 @@ using dnlib.DotNet; -using dnlib.DotNet.MD; namespace Zack.DotNetTrimmerLib { @@ -12,7 +11,7 @@ public static void TrimAssemblies(string rootDir, HashSet loadedTypes) if (PEHelpers.IsManagedAssembly(asmFile)) { TrimAssembly(asmFile, loadedTypes); - } + } } } @@ -28,7 +27,11 @@ public static void TrimAssembly(string asmFile, HashSet loadedTypes) { //Assembly contains more than IL code cannot be trimmed as expected. //https://github.com/Washi1337/AsmResolver/issues/267 - if (!module.IsILOnly) return; + if (!module.IsILOnly) + { + ConsoleHelpers.WriteInfo($"{asmFile} is not ILOnly, skipped."); + return; + } List typesToBeRemoved = new List(); foreach (var type in module.Types) { @@ -56,6 +59,7 @@ public static void TrimAssembly(string asmFile, HashSet loadedTypes) } memStream.Position = 0; File.WriteAllBytes(asmFile, memStream.ToArray()); + ConsoleHelpers.WriteInfo($"Unused codes of {asmFile} are trimmed."); } } } diff --git a/Zack.DotNetTrimmerLib/FileRemovedEventArgs.cs b/Zack.DotNetTrimmerLib/FileRemovedEventArgs.cs deleted file mode 100644 index dee44d3..0000000 --- a/Zack.DotNetTrimmerLib/FileRemovedEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Zack.DotNetTrimmerLib -{ - public class FileRemovedEventArgs : EventArgs - { - public string FileFullPath { get; private set; } - public FileRemovedEventArgs(string fileFullPath) - { - FileFullPath = fileFullPath; - } - } -} diff --git a/Zack.DotNetTrimmerLib/IOHelpers.cs b/Zack.DotNetTrimmerLib/IOHelpers.cs index 2bb15e1..be3e2af 100644 --- a/Zack.DotNetTrimmerLib/IOHelpers.cs +++ b/Zack.DotNetTrimmerLib/IOHelpers.cs @@ -8,7 +8,14 @@ public static void RemoveFiles(string path, string searchPattern) foreach (var file in files) { File.Delete(file); + ConsoleHelpers.WriteInfo($"{file} removed."); } } + + public static long GetFolderSize(string dir) + { + var files = Directory.GetFiles(dir, "*.*", SearchOption.AllDirectories); + return files.Sum(f => new FileInfo(f).Length); + } } } diff --git a/Zack.DotNetTrimmerLib/JsonHelper.cs b/Zack.DotNetTrimmerLib/JsonHelper.cs new file mode 100644 index 0000000..ee74dd1 --- /dev/null +++ b/Zack.DotNetTrimmerLib/JsonHelper.cs @@ -0,0 +1,28 @@ +using System.Text.Json; + +namespace Zack.DotNetTrimmerLib +{ + static class JsonHelper + { + public static void SaveAsFile(string file, object value) + { + JsonSerializerOptions jsonOpt = new() { WriteIndented = true }; + using FileStream fileStream = File.Open(file, FileMode.Create); + JsonSerializer.Serialize(fileStream, value, jsonOpt); + } + + public static T LoadFromFile(string file) + { + using FileStream fileStream = File.OpenRead(file); + var obj = JsonSerializer.Deserialize(File.ReadAllText(file)); + if (obj == null) + { + throw new IOException("loading error"); + } + else + { + return obj; + } + } + } +} diff --git a/Zack.DotNetTrimmerLib/MessageReceivedEventArgs.cs b/Zack.DotNetTrimmerLib/MessageReceivedEventArgs.cs deleted file mode 100644 index bbf66b0..0000000 --- a/Zack.DotNetTrimmerLib/MessageReceivedEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Zack.DotNetTrimmerLib -{ - public class MessageReceivedEventArgs : EventArgs - { - public string Message { get; private set; } - public MessageReceivedEventArgs(string msg) - { - Message = msg; - } - } -} diff --git a/Zack.DotNetTrimmerLib/TimmerOptions.cs b/Zack.DotNetTrimmerLib/TimmerOptions.cs index 221f2fd..47c77d8 100644 --- a/Zack.DotNetTrimmerLib/TimmerOptions.cs +++ b/Zack.DotNetTrimmerLib/TimmerOptions.cs @@ -2,9 +2,13 @@ { public class TimmerOptions { + /// + /// Remove unused code from loaded assemblies + /// + public bool Greedy { get; set; } public TrimmingMode Mode { get; set; } public string? RecordFileName { get; set; } - public string StartupFile { get; set; } - public string[] Arguments { get; set; } + public string? StartupFile { get; set; } + public string[] Arguments { get; set; } = new string[0]; } } diff --git a/Zack.DotNetTrimmerLib/Trimmer.cs b/Zack.DotNetTrimmerLib/Trimmer.cs index ba59056..4c748e6 100644 --- a/Zack.DotNetTrimmerLib/Trimmer.cs +++ b/Zack.DotNetTrimmerLib/Trimmer.cs @@ -3,48 +3,30 @@ using Microsoft.Diagnostics.Tracing.Parsers; using System.Diagnostics; using System.Diagnostics.Tracing; -using System.Text.Json; +using static Zack.DotNetTrimmerLib.ConsoleHelpers; using static Zack.DotNetTrimmerLib.PEHelpers; namespace Zack.DotNetTrimmerLib; public class Trimmer { private TimmerOptions options; - public event EventHandler FileRemoved; - public event EventHandler MessageReceived; public Trimmer(TimmerOptions options) { this.options = options; } - private void FireFileRemoved(string fileFullPath) - { - if (FileRemoved != null) - { - FileRemoved(this, new FileRemovedEventArgs(fileFullPath)); - } - } - - private void FireMessageReceived(string msg) - { - if (MessageReceived != null) - { - MessageReceived(this, new MessageReceivedEventArgs(msg)); - } - } - public void Run() { string startupFile = options.StartupFile; string startupDir; - FireMessageReceived($"Entering {options.Mode} Mode."); + WriteInfo($"Entering {options.Mode} Mode."); var dir = Path.GetDirectoryName(startupFile); if (dir == null) { - FireMessageReceived($"GetDirectoryName() of {startupFile} is empty"); + WriteError($"GetDirectoryName() of {startupFile} is empty"); return; } else @@ -57,8 +39,7 @@ public void Run() string recordFileName = options.RecordFileName; if (File.Exists(recordFileName)) { - using FileStream fileStream = File.OpenRead(recordFileName); - recordFileInfo = JsonSerializer.Deserialize(fileStream); + recordFileInfo = JsonHelper.LoadFromFile(recordFileName); } else { @@ -70,9 +51,7 @@ public void Run() } else { - JsonSerializerOptions jsonOpt = new() { WriteIndented = true }; - using FileStream fileStream = File.Open(recordFileName, FileMode.Create); - JsonSerializer.Serialize(fileStream, recordFileInfo, jsonOpt); + JsonHelper.SaveAsFile(recordFileName, recordFileInfo); } } else if (options.Mode == TrimmingMode.Apply) @@ -82,8 +61,7 @@ public void Run() { throw new Exception($"{recordFileName} not found!"); } - using FileStream fileStream = File.OpenRead(recordFileName); - recordFileInfo = JsonSerializer.Deserialize(File.ReadAllText(recordFileName)); + recordFileInfo = JsonHelper.LoadFromFile(recordFileName); } else if (options.Mode == TrimmingMode.Direct) { @@ -100,25 +78,30 @@ public void Run() if (options.Mode == TrimmingMode.Record) { - FireMessageReceived($"Recording completes. {options.RecordFileName}"); + WriteInfo($"Recording completes. {options.RecordFileName}"); } else { + long sizeBefore = IOHelpers.GetFolderSize(startupDir); + var allDllFiles = Directory.GetFiles(startupDir, "*.dll", SearchOption.AllDirectories) .Where(asmPath => !IsFileIgnored(asmPath) && IsManagedAssembly(asmPath)).ToArray(); var assembliesNotLoaded = allDllFiles.Where(d => !recordFileInfo.LoadedAssemblies.Contains(Path.GetFileName(d))); - var totalSize = assembliesNotLoaded.Select(f => new FileInfo(f).Length).Sum() * 1.0 / (1024 * 1024); foreach (var file in assembliesNotLoaded) { File.Delete(file); } - AssemblyTrimmer.TrimAssemblies(startupDir, recordFileInfo.LoadedTypes); + if (options.Greedy) + { + AssemblyTrimmer.TrimAssemblies(startupDir, recordFileInfo.LoadedTypes); + } IOHelpers.RemoveFiles(startupDir, "*.pdb"); IOHelpers.RemoveFiles(startupDir, "*.runtimeconfig.json"); IOHelpers.RemoveFiles(startupDir, "*.deps.json"); - - FireMessageReceived($"Done, reduced file size:{totalSize:0.00} MB"); - FireMessageReceived("Waiting for exit."); + long sizeAfter = IOHelpers.GetFolderSize(startupDir); + double sizeReduced = (sizeBefore - sizeAfter) * 1.0 / (1024 * 1024); + WriteInfo($"Done! size before: {sizeBefore * 1.0 / (1024 * 1024):0.00} MB, size after:{sizeAfter * 1.0 / (1024 * 1024):0.00} MB reduced size:{sizeReduced:0.00} MB"); + WriteInfo("Waiting for exit."); } } @@ -131,11 +114,11 @@ private bool RunApp(string startupFile, RecordFileInfo recordFileInfo) psInfo.WorkingDirectory = startupDir; psInfo.Arguments = string.Join(" ", options.Arguments); - FireMessageReceived("Press Ctrl+C or Ctrl+Break to terminate the application to be trimmed."); + WriteInfo("Press Ctrl+C or Ctrl+Break to terminate the application to be trimmed."); using Process? p = Process.Start(psInfo); if (p == null) { - FireMessageReceived("Error: Starting process failed!"); + WriteError("Error: Starting process failed!"); return false; } Console.CancelKeyPress += (_, ck) => @@ -143,11 +126,11 @@ private bool RunApp(string startupFile, RecordFileInfo recordFileInfo) //when the user pressed Ctrl+C, the trimmed process will be terminated first. if (p != null) { - FireMessageReceived($"Application shutting down."); + WriteInfo($"Application shutting down."); p.CloseMainWindow(); p.WaitForExit(); ck.Cancel = true;//prevent the current Trimmer process from being terminated by Ctrl+C - FireMessageReceived($"Application shut down, waiting for to be trimmed."); + WriteInfo($"Application shut down, waiting for to be trimmed."); } }; var providers = new List() @@ -168,7 +151,7 @@ private bool RunApp(string startupFile, RecordFileInfo recordFileInfo) } catch (Exception e) { - FireMessageReceived($"Error encountered while processing events:{e}"); + WriteError($"Error encountered while processing events:{e}"); return false; } return true;