diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1a9168125f..af214e6a67 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,6 +14,7 @@
+
diff --git a/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs b/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs
index 718304a338..8da05f8bb1 100644
--- a/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs
+++ b/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs
@@ -140,7 +140,7 @@ protected override async void OnExecute(object sender, EventArgs e)
return;
}
- OpenAssembliesInILSpy(new ILSpyParameters(validRefs.Select(r => r.AssemblyFile), "/navigateTo:" +
+ OpenAssembliesInILSpy(new ILSpyParameters(validRefs.Select(r => r.AssemblyFile), "--navigateto:" +
(symbol.OriginalDefinition ?? symbol).GetDocumentationCommentId()));
}
diff --git a/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs b/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs
index 195940b68b..14f3f9227f 100644
--- a/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs
+++ b/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs
@@ -97,6 +97,24 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
OpenReferenceCommand.Register(this);
OpenCodeItemCommand.Register(this);
}
+
+ protected override int QueryClose(out bool canClose)
+ {
+ var tempFiles = ILSpyInstance.TempFiles;
+ while (tempFiles.TryPop(out var filename))
+ {
+ try
+ {
+ System.IO.File.Delete(filename);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ return base.QueryClose(out canClose);
+ }
+
#endregion
public void ShowMessage(string format, params object[] items)
diff --git a/ILSpy.AddIn.Shared/ILSpyInstance.cs b/ILSpy.AddIn.Shared/ILSpyInstance.cs
index dc90e39a20..42059aa775 100644
--- a/ILSpy.AddIn.Shared/ILSpyInstance.cs
+++ b/ILSpy.AddIn.Shared/ILSpyInstance.cs
@@ -1,11 +1,9 @@
-using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading.Tasks;
namespace ICSharpCode.ILSpy.AddIn
{
@@ -23,8 +21,9 @@ public ILSpyParameters(IEnumerable assemblyFileNames, params string[] ar
class ILSpyInstance
{
- readonly ILSpyParameters parameters;
+ internal static readonly ConcurrentStack TempFiles = new ConcurrentStack();
+ readonly ILSpyParameters parameters;
public ILSpyInstance(ILSpyParameters parameters = null)
{
this.parameters = parameters;
@@ -47,85 +46,30 @@ public void Start()
{
var commandLineArguments = parameters?.AssemblyFileNames?.Concat(parameters.Arguments);
string ilSpyExe = GetILSpyPath();
- var process = new Process() {
- StartInfo = new ProcessStartInfo() {
- FileName = ilSpyExe,
- UseShellExecute = false,
- Arguments = "/navigateTo:none"
- }
- };
- process.Start();
+
+ const string defaultOptions = "--navigateto:none";
+ string argumentsToPass = defaultOptions;
if ((commandLineArguments != null) && commandLineArguments.Any())
{
- // Only need a message to started process if there are any parameters to pass
- SendMessage(ilSpyExe, "ILSpy:\r\n" + string.Join(Environment.NewLine, commandLineArguments), true);
- }
- }
+ string assemblyArguments = string.Join("\r\n", commandLineArguments);
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "")]
- void SendMessage(string ilSpyExe, string message, bool activate)
- {
- string expectedProcessName = Path.GetFileNameWithoutExtension(ilSpyExe);
- // We wait asynchronously until target window can be found and try to find it multiple times
- Task.Run(async () => {
- bool success = false;
- int remainingAttempts = 20;
- do
- {
- NativeMethods.EnumWindows(
- (hWnd, lParam) => {
- string windowTitle = NativeMethods.GetWindowText(hWnd, 100);
- if (windowTitle.StartsWith("ILSpy", StringComparison.Ordinal))
- {
- string processName = NativeMethods.GetProcessNameFromWindow(hWnd);
- Debug.WriteLine("Found {0:x4}: '{1}' in '{2}'", hWnd, windowTitle, processName);
- if (string.Equals(processName, expectedProcessName, StringComparison.OrdinalIgnoreCase))
- {
- IntPtr result = Send(hWnd, message);
- Debug.WriteLine("WM_COPYDATA result: {0:x8}", result);
- if (result == (IntPtr)1)
- {
- if (activate)
- NativeMethods.SetForegroundWindow(hWnd);
- success = true;
- return false; // stop enumeration
- }
- }
- }
- return true; // continue enumeration
- }, IntPtr.Zero);
+ string filepath = Path.GetTempFileName();
+ File.WriteAllText(filepath, assemblyArguments);
- // Wait some time before next attempt
- await Task.Delay(500);
- remainingAttempts--;
- } while (!success && (remainingAttempts > 0));
- });
- }
+ TempFiles.Push(filepath);
- unsafe static IntPtr Send(IntPtr hWnd, string message)
- {
- const uint SMTO_NORMAL = 0;
+ argumentsToPass = $"@\"{filepath}\"";
+ }
- CopyDataStruct lParam;
- lParam.Padding = IntPtr.Zero;
- lParam.Size = message.Length * 2;
- fixed (char* buffer = message)
- {
- lParam.Buffer = (IntPtr)buffer;
- IntPtr result;
- // SendMessage with 3s timeout (e.g. when the target process is stopped in the debugger)
- if (NativeMethods.SendMessageTimeout(
- hWnd, NativeMethods.WM_COPYDATA, IntPtr.Zero, ref lParam,
- SMTO_NORMAL, 3000, out result) != IntPtr.Zero)
- {
- return result;
- }
- else
- {
- return IntPtr.Zero;
+ var process = new Process() {
+ StartInfo = new ProcessStartInfo() {
+ FileName = ilSpyExe,
+ UseShellExecute = false,
+ Arguments = argumentsToPass
}
- }
+ };
+ process.Start();
}
}
}
diff --git a/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj b/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj
index e0d1db28c2..83404ae2f7 100644
--- a/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj
+++ b/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj
@@ -70,7 +70,6 @@
-
diff --git a/ILSpy.AddIn/ILSpy.AddIn.csproj b/ILSpy.AddIn/ILSpy.AddIn.csproj
index 5ad17dfb59..9b41766fd2 100644
--- a/ILSpy.AddIn/ILSpy.AddIn.csproj
+++ b/ILSpy.AddIn/ILSpy.AddIn.csproj
@@ -76,7 +76,6 @@
-
diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs
new file mode 100644
index 0000000000..026c86b81a
--- /dev/null
+++ b/ILSpy.Tests/CommandLineArgumentsTests.cs
@@ -0,0 +1,123 @@
+using System;
+
+using FluentAssertions;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests
+{
+ [TestFixture]
+ public class CommandLineArgumentsTests
+ {
+ [Test]
+ public void VerifyEmptyArgumentsArray()
+ {
+ var cmdLineArgs = new CommandLineArguments(new string[] { });
+
+ cmdLineArgs.AssembliesToLoad.Should().BeEmpty();
+ cmdLineArgs.SingleInstance.Should().BeNull();
+ cmdLineArgs.NavigateTo.Should().BeNull();
+ cmdLineArgs.Search.Should().BeNull();
+ cmdLineArgs.Language.Should().BeNull();
+ cmdLineArgs.NoActivate.Should().BeFalse();
+ cmdLineArgs.ConfigFile.Should().BeNull();
+ }
+
+ [Test]
+ public void VerifyHelpOption()
+ {
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--help" });
+ cmdLineArgs.ArgumentsParser.IsShowingInformation.Should().BeTrue();
+ }
+
+ [Test]
+ public void VerifyForceNewInstanceOption()
+ {
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--newinstance" });
+ cmdLineArgs.SingleInstance.Should().BeFalse();
+ }
+
+ [Test]
+ public void VerifyNavigateToOption()
+ {
+ const string navigateTo = "MyNamespace.MyClass";
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto", navigateTo });
+ cmdLineArgs.NavigateTo.Should().BeEquivalentTo(navigateTo);
+ }
+
+ [Test]
+ public void VerifyNavigateToOption_NoneTest_Matching_VSAddin()
+ {
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto:none" });
+ cmdLineArgs.NavigateTo.Should().BeEquivalentTo("none");
+ }
+
+ [Test]
+ public void VerifyCaseSensitivityOfOptionsDoesntThrow()
+ {
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateTo:none" });
+
+ cmdLineArgs.ArgumentsParser.RemainingArguments.Should().HaveCount(1);
+ }
+
+ [Test]
+ public void VerifySearchOption()
+ {
+ const string searchWord = "TestContainers";
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--search", searchWord });
+ cmdLineArgs.Search.Should().BeEquivalentTo(searchWord);
+ }
+
+ [Test]
+ public void VerifyLanguageOption()
+ {
+ const string language = "csharp";
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--language", language });
+ cmdLineArgs.Language.Should().BeEquivalentTo(language);
+ }
+
+ [Test]
+ public void VerifyConfigOption()
+ {
+ const string configFile = "myilspyoptions.xml";
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--config", configFile });
+ cmdLineArgs.ConfigFile.Should().BeEquivalentTo(configFile);
+ }
+
+ [Test]
+ public void VerifyNoActivateOption()
+ {
+ var cmdLineArgs = new CommandLineArguments(new string[] { "--noactivate" });
+ cmdLineArgs.NoActivate.Should().BeTrue();
+ }
+
+ [Test]
+ public void MultipleAssembliesAsArguments()
+ {
+ var cmdLineArgs = new CommandLineArguments(new string[] { "assembly1", "assembly2", "assembly3" });
+ cmdLineArgs.AssembliesToLoad.Should().HaveCount(3);
+ }
+
+ [Test]
+ public void PassAtFileArguments()
+ {
+ string filepath = System.IO.Path.GetTempFileName();
+
+ System.IO.File.WriteAllText(filepath, "assembly1\r\nassembly2\r\nassembly3\r\n--newinstance\r\n--noactivate");
+
+ var cmdLineArgs = new CommandLineArguments(new string[] { $"@{filepath}" });
+
+ try
+ {
+ System.IO.File.Delete(filepath);
+ }
+ catch (Exception)
+ {
+ }
+
+ cmdLineArgs.SingleInstance.Should().BeFalse();
+ cmdLineArgs.NoActivate.Should().BeTrue();
+ cmdLineArgs.AssembliesToLoad.Should().HaveCount(3);
+ }
+ }
+}
diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj
index 79e95586a9..17337e7b24 100644
--- a/ILSpy.Tests/ILSpy.Tests.csproj
+++ b/ILSpy.Tests/ILSpy.Tests.csproj
@@ -39,6 +39,7 @@
+
diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs
index 0e88f19caa..5779727c4b 100644
--- a/ILSpy/App.xaml.cs
+++ b/ILSpy/App.xaml.cs
@@ -87,6 +87,17 @@ public App()
Hyperlink.RequestNavigateEvent,
new RequestNavigateEventHandler(Window_RequestNavigate));
ILSpyTraceListener.Install();
+
+ if (App.CommandLineArguments.ArgumentsParser.IsShowingInformation)
+ {
+ MessageBox.Show(App.CommandLineArguments.ArgumentsParser.GetHelpText(), "ILSpy Command Line Arguments");
+ }
+
+ if (App.CommandLineArguments.ArgumentsParser.RemainingArguments.Any())
+ {
+ string unknownArguments = string.Join(", ", App.CommandLineArguments.ArgumentsParser.RemainingArguments);
+ MessageBox.Show(unknownArguments, "ILSpy Unknown Command Line Arguments Passed");
+ }
}
static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName)
diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs
index c6c1b5947f..df1114e386 100644
--- a/ILSpy/CommandLineArguments.cs
+++ b/ILSpy/CommandLineArguments.cs
@@ -16,12 +16,14 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
-using System;
+using McMaster.Extensions.CommandLineUtils;
+
using System.Collections.Generic;
+using System.Linq;
namespace ICSharpCode.ILSpy
{
- sealed class CommandLineArguments
+ public sealed class CommandLineArguments
{
// see /doc/Command Line.txt for details
public List AssembliesToLoad = new List();
@@ -32,33 +34,70 @@ sealed class CommandLineArguments
public bool NoActivate;
public string ConfigFile;
+ public CommandLineApplication ArgumentsParser { get; }
+
public CommandLineArguments(IEnumerable arguments)
{
- foreach (string arg in arguments)
+ var app = new CommandLineApplication() {
+ // https://natemcmaster.github.io/CommandLineUtils/docs/response-file-parsing.html?tabs=using-attributes
+ ResponseFileHandling = ResponseFileHandling.ParseArgsAsLineSeparated,
+
+ // Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse()
+ UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue
+ };
+
+ app.HelpOption();
+ ArgumentsParser = app;
+
+ var oForceNewInstance = app.Option("--newinstance",
+ "Start a new instance of ILSpy even if the user configuration is set to single-instance",
+ CommandOptionType.NoValue);
+
+ var oNavigateTo = app.Option("-n|--navigateto ",
+ "Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateto:T:ICSharpCode.ILSpy.CommandLineArguments'",
+ CommandOptionType.SingleValue);
+ oNavigateTo.DefaultValue = null;
+
+ var oSearch = app.Option("-s|--search ",
+ "Search for t:TypeName, m:Member or c:Constant; use exact match (=term), 'should not contain' (-term) or 'must contain' (+term); use /reg(ular)?Ex(pressions)?/ or both - t:/Type(Name)?/...",
+ CommandOptionType.SingleValue);
+ oSearch.DefaultValue = null;
+
+ var oLanguage = app.Option("-l|--language ",
+ "Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language:IL'",
+ CommandOptionType.SingleValue);
+ oLanguage.DefaultValue = null;
+
+ var oConfig = app.Option("-c|--config ",
+ "Provide a specific configuration file.\r\nExample: 'ILSpy --config:myconfig.xml'",
+ CommandOptionType.SingleValue);
+ oConfig.DefaultValue = null;
+
+ var oNoActivate = app.Option("--noactivate",
+ "Do not activate the existing ILSpy instance. This option has no effect if a new ILSpy instance is being started.",
+ CommandOptionType.NoValue);
+
+ // https://natemcmaster.github.io/CommandLineUtils/docs/arguments.html#variable-numbers-of-arguments
+ // To enable this, MultipleValues must be set to true, and the argument must be the last one specified.
+ var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true);
+
+ app.Parse(arguments.ToArray());
+
+ if (oForceNewInstance.HasValue())
+ SingleInstance = false;
+
+ NavigateTo = oNavigateTo.ParsedValue;
+ Search = oSearch.ParsedValue;
+ Language = oLanguage.ParsedValue;
+ ConfigFile = oConfig.ParsedValue;
+
+ if (oNoActivate.HasValue())
+ NoActivate = true;
+
+ foreach (var assembly in files.Values)
{
- if (arg.Length == 0)
- continue;
- if (arg[0] == '/')
- {
- if (arg.Equals("/singleInstance", StringComparison.OrdinalIgnoreCase))
- this.SingleInstance = true;
- else if (arg.Equals("/separate", StringComparison.OrdinalIgnoreCase))
- this.SingleInstance = false;
- else if (arg.StartsWith("/navigateTo:", StringComparison.OrdinalIgnoreCase))
- this.NavigateTo = arg.Substring("/navigateTo:".Length);
- else if (arg.StartsWith("/search:", StringComparison.OrdinalIgnoreCase))
- this.Search = arg.Substring("/search:".Length);
- else if (arg.StartsWith("/language:", StringComparison.OrdinalIgnoreCase))
- this.Language = arg.Substring("/language:".Length);
- else if (arg.Equals("/noActivate", StringComparison.OrdinalIgnoreCase))
- this.NoActivate = true;
- else if (arg.StartsWith("/config:", StringComparison.OrdinalIgnoreCase))
- this.ConfigFile = arg.Substring("/config:".Length);
- }
- else
- {
- this.AssembliesToLoad.Add(arg);
- }
+ if (!string.IsNullOrWhiteSpace(assembly))
+ AssembliesToLoad.Add(assembly);
}
}
}
diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj
index 93feab3a11..0ecb0caad8 100644
--- a/ILSpy/ILSpy.csproj
+++ b/ILSpy/ILSpy.csproj
@@ -45,6 +45,7 @@
+
@@ -65,7 +66,6 @@
-
diff --git a/ILSpy/Properties/launchSettings.json b/ILSpy/Properties/launchSettings.json
index 706f4dfc0b..79731519a9 100644
--- a/ILSpy/Properties/launchSettings.json
+++ b/ILSpy/Properties/launchSettings.json
@@ -1,13 +1,13 @@
{
- "profiles": {
- "ILSpy": {
- "commandName": "Executable",
- "executablePath": ".\\ILSpy.exe",
- "commandLineArgs": "/separate"
- },
- "ILSpy single-instance": {
- "commandName": "Executable",
- "executablePath": ".\\ILSpy.exe"
- }
- }
+ "profiles": {
+ "ILSpy": {
+ "commandName": "Executable",
+ "executablePath": "./ilspy.exe",
+ "commandLineArgs": "--newinstance"
+ },
+ "ILSpy single-instance": {
+ "commandName": "Executable",
+ "executablePath": "./ilspy.exe"
+ }
+ }
}
\ No newline at end of file
diff --git a/ILSpy/SingleInstanceHandling.cs b/ILSpy/SingleInstanceHandling.cs
index 5a59f1ee25..5736dffc74 100644
--- a/ILSpy/SingleInstanceHandling.cs
+++ b/ILSpy/SingleInstanceHandling.cs
@@ -64,10 +64,14 @@ internal static string FullyQualifyPath(string argument)
{
// Fully qualify the paths before passing them to another process,
// because that process might use a different current directory.
- if (string.IsNullOrEmpty(argument) || argument[0] == '/')
+ if (string.IsNullOrEmpty(argument) || argument[0] == '-')
return argument;
try
{
+ if (argument.StartsWith("@"))
+ {
+ return "@" + FullyQualifyPath(argument.Substring(1));
+ }
return Path.Combine(Environment.CurrentDirectory, argument);
}
catch (ArgumentException)
diff --git a/doc/Command Line.txt b/doc/Command Line.txt
index 1cc041d66b..62a3a75512 100644
--- a/doc/Command Line.txt
+++ b/doc/Command Line.txt
@@ -1,54 +1,27 @@
ILSpy Command Line Arguments
-Command line arguments can be either options or file names.
-If an argument is a file name, the file will be opened as assembly and added to the current assembly list.
+Usage: [options]
+ @ResponseFile.rsp
-Available options:
- /singleInstance If ILSpy is already running, activates the existing instance
- and passes command line arguments to that instance.
- This is the default value if /list is not used.
-
- /separate Start up a separate ILSpy instance even if it is already running.
-
- /noActivate Do not activate the existing ILSpy instance. This option has no effect
- if a new ILSpy instance is being started.
-
- /list:listname Specifies the name of the assembly list that is loaded initially.
- When this option is not specified, ILSpy loads the previously opened list.
- Specify "/list" (without value) to open the default list.
-
- When this option is used, ILSpy will activate an existing instance
- only if it uses the same list as specified.
-
- [Note: Assembly Lists are not yet implemented]
-
- /clearList Clears the assembly list before loading the specified assemblies.
- [Note: Assembly Lists are not yet implemented]
-
- /navigateTo:tag Navigates to the member specified by the given ID string.
- The member is searched for only in the assemblies specified on the command line.
- Example: 'ILSpy ILSpy.exe /navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments'
-
- The syntax of ID strings is described in appendix A of the C# language specification.
-
- /language:name Selects the specified language.
- Example: 'ILSpy /language:C#' or 'ILSpy /language:IL'
+Arguments:
+ Assemblies Assemblies to load
-WM_COPYDATA (SendMessage API):
- ILSpy can be controlled by other programs that send a WM_COPYDATA message to its main window.
- The message data must be an Unicode (UTF-16) string starting with "ILSpy:\r\n".
- All lines except the first ("ILSpy:") in that string are handled as command-line arguments.
- There must be exactly one argument per line.
-
- That is, by sending this message:
- ILSpy:
- C:\Assembly.dll
- /navigateTo:T:Type
- The target ILSpy instance will open C:\Assembly.dll and navigate to the specified type.
-
- ILSpy will return TRUE (1) if it handles the message, and FALSE (0) otherwise.
- The /separate option will be ignored; WM_COPYDATA will never start up a new instance.
- The /noActivate option has no effect, sending WM_COPYDATA will never activate the window.
- Instead, the calling process should use SetForegroundWindow().
- If you use /list with WM_COPYDATA, you need to specify /singleInstance as well, otherwise
- ILSpy will not handle the message if it has opened a different assembly list.
+Options:
+ --newinstance Start a new instance of ILSpy even if the user configuration is set to single-instance
+ -n|--navigateto Navigates to the member specified by the given ID string.
+ The member is searched for only in the assemblies specified on the command line.
+ Example: 'ILSpy ILSpy.exe --navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments'
+ -s|--search Search for t:TypeName, m:Member or c:Constant; use exact match (=term),
+ 'should not contain' (-term) or 'must contain' (+term); use
+ /reg(ular)?Ex(pressions)?/ or both - t:/Type(Name)?/...
+ -l|--language Selects the specified language.
+ Example: 'ILSpy --language:C#' or 'ILSpy --language:IL'
+ -c|--config Provide a specific configuration file.
+ Example: 'ILSpy --config:myconfig.xml'
+ --noactivate Do not activate the existing ILSpy instance.
+ This option has no effect if a new ILSpy instance is being started.
+
+Note on @ResponseFile.rsp:
+
+* The response file should contain the arguments, one argument per line (not space-separated!).
+* Use it when the list of assemblies is too long to fit on the command line.
\ No newline at end of file
diff --git a/publishlocaldev.ps1 b/publishlocaldev.ps1
new file mode 100644
index 0000000000..a8ebda02fd
--- /dev/null
+++ b/publishlocaldev.ps1
@@ -0,0 +1,13 @@
+# For local development of the VSIX package - build and publish (VS2022 also needs arm64)
+
+$output_x64 = "./ILSpy/bin/Debug/net8.0-windows/win-x64/publish/fwdependent"
+
+dotnet publish ./ILSpy/ILSpy.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64
+dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64
+dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64
+
+$output_arm64 = "./ILSpy/bin/Debug/net8.0-windows/win-arm64/publish/fwdependent"
+
+dotnet publish ./ILSpy/ILSpy.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64
+dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64
+dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64
\ No newline at end of file