Browse Source

Convert BAML to XAML when exporting a project from the command line

A WPF assembly keeps its windows and pages as BAML, so a project exported
without converting them back is missing the parts that make it a WPF
application - and the reader has no XAML to look at either. The CLI could
do the conversion since --decompile-baml was added, but only if asked,
which meant that everything the project exporter learned about WPF (Page
and ApplicationDefinition items, resources, generated members removed
from the code-behind) was invisible to anyone following issue #2253 from
the command line.

The flag is kept and ignored: it is documented and scripted against, and
asking for what is now the default has to keep working.

The test fixture is a real .g.resources container rather than a directly
embedded .baml stream, because only entries inside a container reach
WriteResourceToFile - a standalone .baml is copied out untouched, which
is worth its own look.

Assisted-by: Claude:claude-opus-5:Claude Code
pull/4108/head
Siegfried Pammer 2 weeks ago
parent
commit
16737cc582
  1. 6
      ICSharpCode.ILSpyCmd.Tests/ICSharpCode.ILSpyCmd.Tests.csproj
  2. 94
      ICSharpCode.ILSpyCmd.Tests/ProjectExportBamlTests.cs
  3. BIN
      ICSharpCode.ILSpyCmd.Tests/fixtures/test.g.resources
  4. 27
      ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
  5. 8
      ICSharpCode.ILSpyCmd/README.md

6
ICSharpCode.ILSpyCmd.Tests/ICSharpCode.ILSpyCmd.Tests.csproj

@ -24,4 +24,10 @@ @@ -24,4 +24,10 @@
<ProjectReference Include="..\ICSharpCode.ILSpyCmd\ICSharpCode.ILSpyCmd.csproj" />
</ItemGroup>
<ItemGroup>
<!-- A BAML stream inside a .g.resources container, the way the WPF build puts one into an
assembly, so that exporting this assembly as a project has one to convert. -->
<EmbeddedResource Include="fixtures\test.g.resources" LogicalName="ICSharpCode.ILSpyCmd.Tests.g.resources" />
</ItemGroup>
</Project>

94
ICSharpCode.ILSpyCmd.Tests/ProjectExportBamlTests.cs

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using static ICSharpCode.ILSpyCmd.Tests.CliTestRunner;
namespace ICSharpCode.ILSpyCmd.Tests
{
/// <summary>
/// This assembly carries "mainwindow.baml" as an embedded resource, so exporting it as a
/// project exercises what happens to a BAML stream on the way into the project.
/// </summary>
[TestFixture]
public class ProjectExportBamlTests
{
static readonly string testAssemblyPath = typeof(ProjectExportBamlTests).Assembly.Location;
string outputDirectory;
[SetUp]
public void SetUp()
{
outputDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(outputDirectory);
}
[TearDown]
public void TearDown()
{
if (Directory.Exists(outputDirectory))
Directory.Delete(outputDirectory, recursive: true);
}
string ProjectFileContent()
{
string projectFile = Directory.EnumerateFiles(outputDirectory, "*.csproj").Single();
return File.ReadAllText(projectFile);
}
[Test]
public async Task BamlBecomesXamlWithoutAskingForIt()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "-o", outputDirectory);
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
string xamlFile = Path.Combine(outputDirectory, "mainwindow.xaml");
Assert.That(File.Exists(xamlFile), Is.True, "the BAML resource is exported as XAML");
Assert.That(File.ReadAllText(xamlFile), Does.Contain("Hello from BAML"));
}
[Test]
public async Task TheProjectNamesTheXamlAsAPage()
{
await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "-o", outputDirectory);
string project = ProjectFileContent();
Assert.Multiple(() => {
Assert.That(project, Does.Contain("<Page Include=\"mainwindow.xaml\""));
Assert.That(project, Does.Not.Contain("mainwindow.baml"), "the raw stream is not carried along as well");
});
}
[Test]
public async Task TheOldOptInFlagStillWorks()
{
// It is documented and scripted against; asking for what is now the default has to
// keep meaning the same thing.
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "-o", outputDirectory, "--decompile-baml");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(File.Exists(Path.Combine(outputDirectory, "mainwindow.xaml")), Is.True);
}
}
}

BIN
ICSharpCode.ILSpyCmd.Tests/fixtures/test.g.resources vendored

Binary file not shown.

27
ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

@ -82,8 +82,8 @@ Examples: @@ -82,8 +82,8 @@ Examples:
Extract a single resource. If the name ends with .baml, the output is decompiled XAML; otherwise raw bytes.
ilspycmd sample.dll --resource sample.g.resources/mainwindow.baml -o c:\decompiled
Decompile assembly as a compilable project and convert all BAML resources to XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled --decompile-baml
Decompile assembly as a compilable project. BAML resources become XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled
")]
[HelpOption("-h|--help")]
[ProjectOptionRequiresOutputDirectoryValidation]
@ -145,7 +145,7 @@ Examples: @@ -145,7 +145,7 @@ Examples:
[Option("--resource <name>", "Extract a single resource by name (as printed by --list-resources). Resources whose name ends with '.baml' are decompiled to XAML.", CommandOptionType.SingleValue)]
public string ResourceName { get; }
[Option("--decompile-baml", "When used with -p, decompile BAML resources to XAML files (Page items) instead of leaving them as raw byte streams.", CommandOptionType.NoValue)]
[Option("--decompile-baml", "Deprecated: -p decompiles BAML resources to XAML files (Page items) on its own. Accepted so that existing scripts keep working.", CommandOptionType.NoValue)]
public bool DecompileBamlFlag { get; }
[Option("--dump-table <table>", "Dump a metadata table: prints RID, token, names, heap offsets and coded indexes of every row. <table> is the ECMA-335 table name (e.g. TypeDef, Property, MethodSemantics; case-insensitive) or table number (decimal or 0x-prefixed hex, e.g. 0x17).", CommandOptionType.SingleValue)]
@ -795,19 +795,14 @@ Examples: @@ -795,19 +795,14 @@ Examples:
}
var settings = GetSettings(module);
var debugInfo = TryLoadPDB(module);
WholeProjectDecompiler decompiler;
if (DecompileBamlFlag)
{
var bamlTypeSystem = new BamlDecompilerTypeSystem(module, resolver);
var bamlSettings = new BamlDecompilerSettings {
ThrowOnAssemblyResolveErrors = settings.ThrowOnAssemblyResolveErrors
};
decompiler = new BamlAwareWholeProjectDecompiler(settings, resolver, resolver, debugInfo, bamlTypeSystem, bamlSettings);
}
else
{
decompiler = new WholeProjectDecompiler(settings, resolver, null, resolver, debugInfo);
}
// A WPF assembly keeps its XAML as BAML, so a project exported without converting it
// back is missing every window and page it is made of.
var bamlTypeSystem = new BamlDecompilerTypeSystem(module, resolver);
var bamlSettings = new BamlDecompilerSettings {
ThrowOnAssemblyResolveErrors = settings.ThrowOnAssemblyResolveErrors
};
WholeProjectDecompiler decompiler = new BamlAwareWholeProjectDecompiler(settings, resolver, resolver,
debugInfo, bamlTypeSystem, bamlSettings);
ProjectId projectId;
using (var projectFileWriter = new StreamWriter(File.Create(projectFileName)))
projectId = decompiler.DecompileProject(module, Path.GetDirectoryName(projectFileName), projectFileWriter);

8
ICSharpCode.ILSpyCmd/README.md

@ -39,8 +39,8 @@ Options: @@ -39,8 +39,8 @@ Options:
containers are listed individually as '<container>/<entry>'.
--resource <name> Extract a single resource by name (as printed by --list-resources). Resources
whose name ends with '.baml' are decompiled to XAML.
--decompile-baml When used with -p, decompile BAML resources to XAML files (Page items) instead
of leaving them as raw byte streams.
--decompile-baml Deprecated: -p decompiles BAML resources to XAML files (Page items) on its
own. Accepted so that existing scripts keep working.
--dump-table <table> Dump a metadata table: prints RID, token, names, heap offsets and coded
indexes of every row. <table> is the ECMA-335 table name (e.g. TypeDef,
Property, MethodSemantics; case-insensitive) or table number (decimal or
@ -121,8 +121,8 @@ Examples: @@ -121,8 +121,8 @@ Examples:
Extract a single resource. If the name ends with .baml, the output is decompiled XAML; otherwise raw bytes.
ilspycmd sample.dll --resource sample.g.resources/mainwindow.baml -o c:\decompiled
Decompile assembly as a compilable project and convert all BAML resources to XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled --decompile-baml
Decompile assembly as a compilable project. BAML resources become XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled
```
## Generate HTML diagrammers

Loading…
Cancel
Save