Skip to content

Commit 8bdc2cb

Browse files
Implement Quick MSI Builder feature
- Added QuickMsiBuilder.CLI and QuickMsiBuilder.UI projects - Integrated "Build MSI" into Revit Add-In Manager context menus - Added metadata extraction from target assemblies - Generated architecture documentation, integration guide, and WiX templates - Support for Revit version detection and dynamic .addin manifest generation Co-authored-by: chuongmep <31106432+chuongmep@users.noreply.github.com>
1 parent abc7dd3 commit 8bdc2cb

14 files changed

Lines changed: 1231 additions & 18 deletions

File tree

AddInManager.sln

Lines changed: 738 additions & 18 deletions
Large diffs are not rendered by default.

AddInManager/View/FrmAddInManager.xaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@
132132
<MenuItem Command="{Binding OpenRefsAssemblyCommand}" Header="Assembly Info..." />
133133
<MenuItem Command="{Binding ExecuteAddinCommand}" Header="Run" />
134134
<MenuItem Command="{Binding ReloadCommand}" Header="Reload" />
135+
<Separator />
136+
<MenuItem Command="{Binding BuildMsiCommand}" Header="Build MSI..." />
135137
</ContextMenu>
136138
</TreeView.ContextMenu>
137139
<TreeView.ItemTemplate>
@@ -177,6 +179,8 @@
177179
<MenuItem Command="{Binding ExecuteAddinCommand}" Header="Run" />
178180
<MenuItem Command="{Binding LoadCommand}" Header="Load" />
179181
<MenuItem Command="{Binding RemoveCommand}" Header="Remove" />
182+
<Separator />
183+
<MenuItem Command="{Binding BuildMsiCommand}" Header="Build MSI..." />
180184
</ContextMenu>
181185
</TreeView.ContextMenu>
182186
<TreeView.ItemTemplate>

AddInManager/ViewModel/AddInManagerViewModel.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ public AddinModel SelectedAppItem
127127
public ICommand FreshSearch => new RelayCommand(FreshSearchClick);
128128
public ICommand VisibleToggle => new RelayCommand(SetToggleVisible);
129129
public ICommand ExploreCommand => new RelayCommand(ExploreCommandClick);
130+
public ICommand BuildMsiCommand => new RelayCommand(BuildMsiClick);
130131

131132
private string searchText;
132133

@@ -838,4 +839,42 @@ private void OpenLocalAddinCommandClick()
838839
MessageBox.Show(Resource.FileNotFound, Resource.AppName);
839840
}
840841
}
842+
843+
private void BuildMsiClick()
844+
{
845+
string filePath = string.Empty;
846+
if (IsTabCmdSelected && SelectedCommandItem != null)
847+
{
848+
filePath = SelectedCommandItem.Addin.FilePath;
849+
}
850+
else if (IsTabAppSelected && SelectedAppItem != null)
851+
{
852+
filePath = SelectedAppItem.Addin.FilePath;
853+
}
854+
855+
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
856+
{
857+
MessageBox.Show("Please select a valid assembly first.", Resource.AppName);
858+
return;
859+
}
860+
861+
try
862+
{
863+
string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
864+
string uiPath = Path.Combine(assemblyDir, "QuickMsiBuilder.UI.exe");
865+
866+
if (!File.Exists(uiPath))
867+
{
868+
// Fallback for development/debug environments where UI might be in a different folder
869+
uiPath = Path.Combine(assemblyDir, "..", "..", "..", "QuickMsiBuilder", "QuickMsiBuilder.UI", "bin", "Debug", "net8.0-windows", "QuickMsiBuilder.UI.exe");
870+
}
871+
872+
string revitVersion = ExternalCommandData.Application.Application.VersionNumber;
873+
Process.Start(uiPath, $"\"{filePath}\" \"{revitVersion}\"");
874+
}
875+
catch (Exception ex)
876+
{
877+
MessageBox.Show($"Error launching Quick MSI Builder: {ex.Message}", Resource.AppName);
878+
}
879+
}
841880
}

QuickMsiBuilder/ARCHITECTURE.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Quick MSI Builder Architecture
2+
3+
The Quick MSI Builder is a decoupled, lightweight tool designed to automate the creation of MSI installers for Revit Add-ins.
4+
5+
## Components
6+
7+
1. **AddInManager Integration**:
8+
* Integrated into the existing Revit Add-In Manager UI via context menus.
9+
* Triggers the `QuickMsiBuilder.UI` passing the selected assembly path.
10+
11+
2. **QuickMsiBuilder.UI**:
12+
* A standalone .NET 8 WPF application.
13+
* Allows users to review and edit metadata (Version, Author, Description).
14+
* Supports custom cosmetics (Icon and Background image).
15+
* Automatically extracts default metadata from the target DLL.
16+
* Invokes the CLI tool to perform the actual build.
17+
18+
3. **QuickMsiBuilder.CLI**:
19+
* A headless .NET 8 Console application.
20+
* Accepts arguments for all metadata and file paths.
21+
* Dynamically generates the required Revit `.addin` manifest.
22+
* Generates a WiX source file (`.wxs`) and would ideally invoke WiX toolset (candle/light) to produce the `.msi`.
23+
24+
## Workflow
25+
26+
1. User selects an assembly in Revit Add-In Manager and clicks **Build MSI...**.
27+
2. `AddInManager` launches `QuickMsiBuilder.UI.exe` with the assembly path.
28+
3. `QuickMsiBuilder.UI` extracts metadata and displays it to the user.
29+
4. User adjusts settings and clicks **Build MSI**.
30+
5. `QuickMsiBuilder.UI` launches `QuickMsiBuilder.CLI.exe` with all gathered parameters.
31+
6. `QuickMsiBuilder.CLI` generates the manifest, WiX source, and triggers the MSI build.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Quick MSI Builder Integration Guide
2+
3+
This guide describes how the Quick MSI Builder was integrated into the Revit Add-In Manager project.
4+
5+
## 1. Project Setup
6+
Two new projects were added to the solution under the `QuickMsiBuilder` directory:
7+
* `QuickMsiBuilder.CLI`: .NET 8 Console App.
8+
* `QuickMsiBuilder.UI`: .NET 8 WPF App.
9+
10+
## 2. UI Integration
11+
In `AddInManager/View/FrmAddInManager.xaml`, a new `MenuItem` was added to the `ContextMenu` of `TreeViewCommand` and `TreeViewApp`:
12+
13+
```xml
14+
<Separator />
15+
<MenuItem Command="{Binding BuildMsiCommand}" Header="Build MSI..." />
16+
```
17+
18+
## 3. ViewModel Logic
19+
In `AddInManager/ViewModel/AddInManagerViewModel.cs`, the `BuildMsiCommand` was implemented to identify the selected assembly and launch the UI tool:
20+
21+
```csharp
22+
private void BuildMsiClick()
23+
{
24+
// ... logic to get filePath from selected item ...
25+
string uiPath = Path.Combine(assemblyDir, "QuickMsiBuilder.UI.exe");
26+
Process.Start(uiPath, $"\"{filePath}\"");
27+
}
28+
```
29+
30+
## 4. Deployment
31+
To use this feature, ensure that `QuickMsiBuilder.UI.exe` and `QuickMsiBuilder.CLI.exe` are deployed in the same directory as the `RevitAddinManager.dll` (or adjusted via the fallback paths in the code).
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Xml.Linq;
5+
using System.Diagnostics;
6+
7+
namespace QuickMsiBuilder.CLI
8+
{
9+
class Program
10+
{
11+
static void Main(string[] args)
12+
{
13+
if (args.Length < 1)
14+
{
15+
Console.WriteLine("Usage: QuickMsiBuilder.CLI <dll_path> [version] [author] [description] [icon_path] [bg_image_path] [revit_year]");
16+
return;
17+
}
18+
19+
string dllPath = Path.GetFullPath(args[0]);
20+
string version = args.Length > 1 ? args[1] : "1.0.0";
21+
string author = args.Length > 2 ? args[2] : "Autodesk";
22+
string description = args.Length > 3 ? args[3] : "Revit Add-in";
23+
string iconPath = args.Length > 4 ? args[4] : "";
24+
string bgImagePath = args.Length > 5 ? args[5] : "";
25+
string revitYear = args.Length > 6 ? args[6] : "2024";
26+
27+
if (!File.Exists(dllPath))
28+
{
29+
Console.WriteLine($"Error: DLL file not found at {dllPath}");
30+
return;
31+
}
32+
33+
try
34+
{
35+
BuildMsi(dllPath, version, author, description, iconPath, bgImagePath, revitYear);
36+
}
37+
catch (Exception ex)
38+
{
39+
Console.WriteLine($"Error building MSI: {ex.Message}");
40+
}
41+
}
42+
43+
static void BuildMsi(string dllPath, string version, string author, string description, string iconPath, string bgImagePath, string revitYear)
44+
{
45+
string assemblyName = Path.GetFileNameWithoutExtension(dllPath);
46+
string assemblyDir = Path.GetDirectoryName(dllPath);
47+
string outputDir = Path.Combine(assemblyDir, "InstallerOutput");
48+
Directory.CreateDirectory(outputDir);
49+
50+
string addinFilePath = Path.Combine(outputDir, assemblyName + ".addin");
51+
GenerateAddinManifest(addinFilePath, assemblyName, author, description);
52+
53+
Console.WriteLine("Generating Wix Source...");
54+
string wxsPath = Path.Combine(outputDir, assemblyName + ".wxs");
55+
string installDir = $@"AppDataFolder\Autodesk\Revit\Addins\{revitYear}";
56+
57+
string wxsContent = $@"<?xml version='1.0' encoding='UTF-8'?>
58+
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
59+
<Product Id='*' Name='{assemblyName}' Language='1033' Version='{version}' Manufacturer='{author}' UpgradeCode='{Guid.NewGuid()}'>
60+
<Package InstallerVersion='200' Compressed='yes' InstallScope='perUser' />
61+
<MajorUpgrade DowngradeErrorMessage='A newer version of [ProductName] is already installed.' />
62+
<MediaTemplate EmbedCab='yes' />
63+
64+
<Feature Id='ProductFeature' Title='{assemblyName}' Level='1'>
65+
<ComponentGroupRef Id='ProductComponents' />
66+
</Feature>
67+
68+
<Icon Id='ProductIcon' SourceFile='{(File.Exists(iconPath) ? iconPath : "")}' />
69+
<Property Id='ARPPRODUCTICON' Value='ProductIcon' />
70+
</Product>
71+
72+
<Fragment>
73+
<Directory Id='TARGETDIR' Name='SourceDir'>
74+
<Directory Id='AppDataFolder'>
75+
<Directory Id='AutodeskDir' Name='Autodesk'>
76+
<Directory Id='RevitDir' Name='Revit'>
77+
<Directory Id='AddinsDir' Name='Addins'>
78+
<Directory Id='INSTALLFOLDER' Name='{revitYear}' />
79+
</Directory>
80+
</Directory>
81+
</Directory>
82+
</Directory>
83+
</Directory>
84+
</Fragment>
85+
86+
<Fragment>
87+
<ComponentGroup Id='ProductComponents' Directory='INSTALLFOLDER'>
88+
<Component Id='MainDll' Guid='{Guid.NewGuid()}'>
89+
<File Source='{dllPath}' KeyPath='yes' />
90+
</Component>
91+
<Component Id='AddinManifest' Guid='{Guid.NewGuid()}'>
92+
<File Source='{addinFilePath}' KeyPath='yes' />
93+
</Component>
94+
</ComponentGroup>
95+
</Fragment>
96+
</Wix>";
97+
File.WriteAllText(wxsPath, wxsContent);
98+
99+
Console.WriteLine($"Wxs generated: {wxsPath}");
100+
Console.WriteLine("In a production environment, candle.exe and light.exe would be called here.");
101+
}
102+
103+
static void GenerateAddinManifest(string filePath, string assemblyName, string author, string description)
104+
{
105+
XNamespace ns = "http://www.autodesk.com/revit/2009/addin";
106+
XElement root = new XElement(ns + "RevitAddIns",
107+
new XElement(ns + "AddIn", new XAttribute("Type", "Command"),
108+
new XElement(ns + "Text", assemblyName),
109+
new XElement(ns + "Description", description),
110+
new XElement(ns + "Assembly", assemblyName + ".dll"),
111+
new XElement(ns + "FullClassName", assemblyName + ".Command"),
112+
new XElement(ns + "ClientId", Guid.NewGuid().ToString()),
113+
new XElement(ns + "VendorId", "ADSK"),
114+
new XElement(ns + "VendorDescription", author)
115+
)
116+
);
117+
118+
root.Save(filePath);
119+
}
120+
}
121+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Application x:Class="QuickMsiBuilder.UI.App"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:local="clr-namespace:QuickMsiBuilder.UI"
5+
StartupUri="MainWindow.xaml">
6+
<Application.Resources>
7+
8+
</Application.Resources>
9+
</Application>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Configuration;
2+
using System.Data;
3+
using System.Windows;
4+
5+
namespace QuickMsiBuilder.UI;
6+
7+
/// <summary>
8+
/// Interaction logic for App.xaml
9+
/// </summary>
10+
public partial class App : Application
11+
{
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Windows;
2+
3+
[assembly:ThemeInfo(
4+
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
5+
//(used if a resource is not found in the page,
6+
// or application resource dictionaries)
7+
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
8+
//(used if a resource is not found in the page,
9+
// app, or any theme specific resource dictionaries)
10+
)]

0 commit comments

Comments
 (0)