I am trying to log from my C# Custom Action using session.Log( "Hello World!" ); This does not show up in my log file when executing my msi as follows:
msiexec /i myMsi.msi /lvx myLog.log
My custom action works fine, my only problem is I do not get my logging info. The log shows my CA is getting called, just not the info from my session.Log() call.
I am using Wix 3.5, .Net 4, VS 2010, and 64-bit Windows 7. I am calling my action as follows.
<Control Id="TestConnection" Type="PushButton" X="21" Y="177" Width="100" Height="17" Text="Test Connection">
<Publish Event="DoAction" Value="TestConnection">1</Publish>
</Control>
Per the docs on DoAction ControlEvent, MsiProcessMessage (the API behind session.Log) cannot be used from a ControlEvent. This prevents your message from showing up in the log. If you need to log some information from a ControlEvent (especially for debugging), your best bet is a hack like changing a property's value to contain your desired log information.
I got around this by using the unmanaged OutputDebugString:
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern void OutputDebugString(string message);
public bool IsDebugLogging { get; set; }
public void Log(string message)
{
// Uncomment if you have access to the Session object passed into your custom action.
// Session.Log(message);
if (IsDebugLogging)
{
OutputDebugString(message);
}
}
In my custom action, I set IsDebugLogging = true to enable OutputDebugString logging. This output can be viewed using Sysinternal's DebugView.
Related
Once my program is installed on a client machine, how do I force my program to run as an administrator on Windows 7?
You'll want to modify the manifest that gets embedded in the program. This works on Visual Studio 2008 and higher: Project + Add New Item, select "Application Manifest File". Change the <requestedExecutionLevel> element to:
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
The user gets the UAC prompt when they start the program. Use wisely; their patience can wear out quickly.
Adding a requestedExecutionLevel element to your manifest is only half the battle; you have to remember that UAC can be turned off. If it is, you have to perform the check the old school way and put up an error dialog if the user is not administrator (call IsInRole(WindowsBuiltInRole.Administrator) on your thread's CurrentPrincipal).
The detailed steps are as follow.
Add application manifest file to project
Change application setting to "app.manifest"
Update tag of "requestedExecutionLevel" to requireAdministrator.
Note that using this code you need to turn off the security settings of ClickOnce, for do this, go inside Properties -> Security -> ClickOnce Security
I implemented some code to do it manually:
using System.Security.Principal;
public bool IsUserAdministrator()
{
bool isAdmin;
try
{
WindowsIdentity user = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(user);
isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch (UnauthorizedAccessException ex)
{
isAdmin = false;
}
catch (Exception ex)
{
isAdmin = false;
}
return isAdmin;
}
You can embed a manifest file in the EXE file, which will cause Windows (7 or higher) to always run the program as an administrator.
You can find more details in Step 6: Create and Embed an Application Manifest (UAC) (MSDN).
While working on Visual Studio 2008, right click on Project -> Add New Item and then chose Application Manifest File.
In the manifest file, you will find the tag requestedExecutionLevel, and you may set the level to three values:
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
OR
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
OR
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
To set your application to run as administrator, you have to chose the middle one.
Another way of doing this, in code only, is to detect if the process is running as admin like in the answer by #NG.. And then open the application again and close the current one.
I use this code when an application only needs admin privileges when run under certain conditions, such as when installing itself as a service. So it doesn't need to run as admin all the time like the other answers force it too.
Note in the below code NeedsToRunAsAdmin is a method that detects if under current conditions admin privileges are required. If this returns false the code will not elevate itself. This is a major advantage of this approach over the others.
Although this code has the advantages stated above, it does need to re-launch itself as a new process which isn't always what you want.
private static void Main(string[] args)
{
if (NeedsToRunAsAdmin() && !IsRunAsAdmin())
{
ProcessStartInfo proc = new ProcessStartInfo();
proc.UseShellExecute = true;
proc.WorkingDirectory = Environment.CurrentDirectory;
proc.FileName = Assembly.GetEntryAssembly().CodeBase;
foreach (string arg in args)
{
proc.Arguments += String.Format("\"{0}\" ", arg);
}
proc.Verb = "runas";
try
{
Process.Start(proc);
}
catch
{
Console.WriteLine("This application requires elevated credentials in order to operate correctly!");
}
}
else
{
//Normal program logic...
}
}
private static bool IsRunAsAdmin()
{
WindowsIdentity id = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(id);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
As per
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
you will want to add an application manifest if you don't already have one or don't know how to add one. As some projects don't automatically add a separate manifest file, first go to project properties, navigate to the Application tab and check to make sure your project is not excluding the manifest at the bottom of the tap.
Next, right click project
Add new Item
Last, find and click Application Manifest File
In Visual Studio 2010 right click your project name.
Hit "View Windows Settings", this generates and opens a file called "app.manifest".
Within this file replace "asInvoker" with "requireAdministrator" as explained in the commented sections within the file.
You can create the manifest using ClickOnce Security Settings, and then disable it:
Right click on the Project -> Properties -> Security -> Enable ClickOnce Security Settings
After you clicked it, a file will be created under the Project's properties folder called app.manifest once this is created, you can uncheck the Enable ClickOnce Security Settings option
Open that file and change this line :
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
to:
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
This will make the program require administrator privileges.
In case you want a code-only solution for some reason, here's a standalone class file. Just call "AdminRelauncher.RelaunchIfNotAdmin()" at application start:
using System;
using System.Diagnostics;
using System.Reflection;
using System.Security.Principal;
public static class AdminRelauncher
{
public static void RelaunchIfNotAdmin()
{
if (!RunningAsAdmin())
{
Console.WriteLine("Running as admin required!");
ProcessStartInfo proc = new ProcessStartInfo();
proc.UseShellExecute = true;
proc.WorkingDirectory = Environment.CurrentDirectory;
proc.FileName = Assembly.GetEntryAssembly().CodeBase;
proc.Verb = "runas";
try
{
Process.Start(proc);
Environment.Exit(0);
}
catch (Exception ex)
{
Console.WriteLine("This program must be run as an administrator! \n\n" + ex.ToString());
Environment.Exit(0);
}
}
}
private static bool RunningAsAdmin()
{
WindowsIdentity id = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(id);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
THIS DOES NOT FORCE APPLICATION TO WORK AS ADMINISTRATOR.
This is a simplified version of the this answer, above by #NG
public bool IsUserAdministrator()
{
try
{
WindowsIdentity user = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(user);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch
{
return false;
}
}
I'm developing a Visual Studio extension. The extension's associated VS project template includes a call to a custom task in the extension's DLL:
<UsingTask TaskName="MyTask" AssemblyFile="path to MyDLL.dll" />
The extension will be installed in the usual place, through use of the VSIX installer.
My question is: Is there a good MSBuild property or macro that I can use to construct the path to the extension's DLL (i.e., MyDLL.dll)? I'm aware of $(DevEnvDir) and could extend that path when using the project and extension in Visual Studio 2015 (append \VendorName\ProductName\Version), but that doesn't seem to work in VS 2017, where the appended path uses a mangled name that can't be predicted ahead of time (or can it?). There's also the issue that the project/extension should work in the VS experimental instance, which does not appear to reflect $(DevEnvDir).
Is there any good way to do this with MSBuild properties, or will I need to look at alternatives like environment variables or the registry?
Is there any good way to do this with MSBuild properties, or will I need to look at alternatives like environment variables or the registry?
You can use environment variables or the registry to achieve it.
environment variables
you could use environment variables like this:
<UsingTask TaskName="MyTask" AssemblyFile="$(yourenvironmentvariablesname)MyDLL.dll" />
For more information, please refer to:
https://learn.microsoft.com/en-us/visualstudio/msbuild/how-to-use-environment-variables-in-a-build
registry
You could use registry like this:
<UsingTask TaskName="MyTask" AssemblyFile="$(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework#DbgManagedDebugger)MyDLL.dll" />
Note: please change related registry path as you want.
https://blogs.msdn.microsoft.com/msbuild/2007/05/04/new-registry-syntax-in-msbuild-v3-5/
The solution that made sense for me was to create a wizard and to set the path to the extension install location in the replacements dictionary and use the replacement in the template with the UsingTask.
public class ProjectLocationWizard : IWizard
{
public void BeforeOpeningFile(ProjectItem projectItem)
{
}
public void ProjectFinishedGenerating(Project project)
{
}
public void ProjectItemFinishedGenerating(ProjectItem projectItem)
{
}
public void RunFinished()
{
}
public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
{
var wizardDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
replacementsDictionary.Add("$installlocation$", wizardDirectory);
}
public bool ShouldAddProjectItem(string filePath)
{
return true;
}
}
<UsingTask AssemblyFile="$installlocation$\MyTask.dll" TaskName="MyTask" />
Although the wizard docs say to sign the wizard assembly, I did not, and it works fine without.
Once my program is installed on a client machine, how do I force my program to run as an administrator on Windows 7?
You'll want to modify the manifest that gets embedded in the program. This works on Visual Studio 2008 and higher: Project + Add New Item, select "Application Manifest File". Change the <requestedExecutionLevel> element to:
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
The user gets the UAC prompt when they start the program. Use wisely; their patience can wear out quickly.
Adding a requestedExecutionLevel element to your manifest is only half the battle; you have to remember that UAC can be turned off. If it is, you have to perform the check the old school way and put up an error dialog if the user is not administrator (call IsInRole(WindowsBuiltInRole.Administrator) on your thread's CurrentPrincipal).
The detailed steps are as follow.
Add application manifest file to project
Change application setting to "app.manifest"
Update tag of "requestedExecutionLevel" to requireAdministrator.
Note that using this code you need to turn off the security settings of ClickOnce, for do this, go inside Properties -> Security -> ClickOnce Security
I implemented some code to do it manually:
using System.Security.Principal;
public bool IsUserAdministrator()
{
bool isAdmin;
try
{
WindowsIdentity user = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(user);
isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch (UnauthorizedAccessException ex)
{
isAdmin = false;
}
catch (Exception ex)
{
isAdmin = false;
}
return isAdmin;
}
You can embed a manifest file in the EXE file, which will cause Windows (7 or higher) to always run the program as an administrator.
You can find more details in Step 6: Create and Embed an Application Manifest (UAC) (MSDN).
While working on Visual Studio 2008, right click on Project -> Add New Item and then chose Application Manifest File.
In the manifest file, you will find the tag requestedExecutionLevel, and you may set the level to three values:
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
OR
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
OR
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
To set your application to run as administrator, you have to chose the middle one.
Another way of doing this, in code only, is to detect if the process is running as admin like in the answer by #NG.. And then open the application again and close the current one.
I use this code when an application only needs admin privileges when run under certain conditions, such as when installing itself as a service. So it doesn't need to run as admin all the time like the other answers force it too.
Note in the below code NeedsToRunAsAdmin is a method that detects if under current conditions admin privileges are required. If this returns false the code will not elevate itself. This is a major advantage of this approach over the others.
Although this code has the advantages stated above, it does need to re-launch itself as a new process which isn't always what you want.
private static void Main(string[] args)
{
if (NeedsToRunAsAdmin() && !IsRunAsAdmin())
{
ProcessStartInfo proc = new ProcessStartInfo();
proc.UseShellExecute = true;
proc.WorkingDirectory = Environment.CurrentDirectory;
proc.FileName = Assembly.GetEntryAssembly().CodeBase;
foreach (string arg in args)
{
proc.Arguments += String.Format("\"{0}\" ", arg);
}
proc.Verb = "runas";
try
{
Process.Start(proc);
}
catch
{
Console.WriteLine("This application requires elevated credentials in order to operate correctly!");
}
}
else
{
//Normal program logic...
}
}
private static bool IsRunAsAdmin()
{
WindowsIdentity id = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(id);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
As per
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
you will want to add an application manifest if you don't already have one or don't know how to add one. As some projects don't automatically add a separate manifest file, first go to project properties, navigate to the Application tab and check to make sure your project is not excluding the manifest at the bottom of the tap.
Next, right click project
Add new Item
Last, find and click Application Manifest File
In Visual Studio 2010 right click your project name.
Hit "View Windows Settings", this generates and opens a file called "app.manifest".
Within this file replace "asInvoker" with "requireAdministrator" as explained in the commented sections within the file.
You can create the manifest using ClickOnce Security Settings, and then disable it:
Right click on the Project -> Properties -> Security -> Enable ClickOnce Security Settings
After you clicked it, a file will be created under the Project's properties folder called app.manifest once this is created, you can uncheck the Enable ClickOnce Security Settings option
Open that file and change this line :
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
to:
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
This will make the program require administrator privileges.
In case you want a code-only solution for some reason, here's a standalone class file. Just call "AdminRelauncher.RelaunchIfNotAdmin()" at application start:
using System;
using System.Diagnostics;
using System.Reflection;
using System.Security.Principal;
public static class AdminRelauncher
{
public static void RelaunchIfNotAdmin()
{
if (!RunningAsAdmin())
{
Console.WriteLine("Running as admin required!");
ProcessStartInfo proc = new ProcessStartInfo();
proc.UseShellExecute = true;
proc.WorkingDirectory = Environment.CurrentDirectory;
proc.FileName = Assembly.GetEntryAssembly().CodeBase;
proc.Verb = "runas";
try
{
Process.Start(proc);
Environment.Exit(0);
}
catch (Exception ex)
{
Console.WriteLine("This program must be run as an administrator! \n\n" + ex.ToString());
Environment.Exit(0);
}
}
}
private static bool RunningAsAdmin()
{
WindowsIdentity id = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(id);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
THIS DOES NOT FORCE APPLICATION TO WORK AS ADMINISTRATOR.
This is a simplified version of the this answer, above by #NG
public bool IsUserAdministrator()
{
try
{
WindowsIdentity user = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(user);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch
{
return false;
}
}
Hi I have these two binary files:
<Binary Id="Sentinel" SourceFile="sentinel_setup.exe"/>
<Binary Id="Hasp" SourceFile="HASPUserSetup.exe"/>
And I would like to start them on a button click like so:
<CustomAction Id="LaunchHasp" BinaryKey="Hasp" ExeCommand="" Return="asyncWait" />
<CustomAction Id="LaunchSentinel" BinaryKey="Sentinel" ExeCommand="" Return="asyncWait"/>
<Publish Event="DoAction" Value="LaunchHasp">1</Publish>
But it doesn't work, It only works when I run the installer from the command line with elevated privileges. What am I doing wrong? Thanks
Or can someone tell me how I could extract the file from the binary table using a c++ custom action as I cannot get it working at all..:(
Immediate custom actions doesn't have elevated privileges. You should use deffered custom actions for such needs. Any action that make changes to the destimation environment should be deffered. For more details read this article: http://bonemanblog.blogspot.com/2005/10/custom-action-tutorial-part-i-custom.html
<CustomAction Id="LaunchHasp" Impersonate="no" Execute="deferred" BinaryKey="Hasp" ExeCommand="" Return="asyncWait" />
Though deffered custom actions are executed during installation phase, not on button click. Revise your installer logic. As I understand, your exe file "sentinel_setup.exe" changes the system, so should be scheduled between InstallInitialize and InstallFinalize events in InstallExecuteSequence
I would recommend adding a checkbox, that user should mark to install your "Hasp" (or installer Feature which user should select in the feature tree). And add deffered custom action with condition on this checkbox state.
Sometimes it is really required to launch admin actions during or before installer UI sequence. In this case you need to create a setup bootstrapper which asks for permission elevation and does required actions before running MSI process. To ask for permissions you need to add application manifest to your bootstrapper project. My bootstrapper is quite simple, but works in many cases. It is Windows application (though without any Windows forms - it allows to hide console window) which contains only icon, application manifest and small code file:
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace SetupBootstrapper
{
class Program
{
[STAThread]
static void Main(string[] args)
{
var currentDir = AppDomain.CurrentDomain.BaseDirectory;
var parameters = string.Empty;
if (args.Length > 0)
{
var sb = new StringBuilder();
foreach (var arg in args)
{
if (arg != "/i" && arg != "/x" && arg != "/u")
{
sb.Append(" ");
sb.Append(arg);
}
}
parameters = sb.ToString();
}
bool isUninstall = args.Contains("/x") || args.Contains("/u");
string msiPath = Path.Combine(currentDir, "MyMsiName.msi");
if(!File.Exists(msiPath))
{
MessageBox.Show(string.Format("File '{0}' doesn't exist", msiPath), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string installerParameters = (isUninstall ? "/x" : "/i ") + "\"" + msiPath + "\"" + parameters;
var installerProcess = new Process { StartInfo = new ProcessStartInfo("msiexec", installerParameters) { Verb = "runas" } };
installerProcess.Start();
installerProcess.WaitForExit();
}
}
}
We have a few MSI packages (generated by WIX) that install WCF services. Most of these services need net.tcp for their endpoint bindings.
I'd like to make our deployment life easier and automate the process of adding net.tcp.
I already know the WixIisExtension.dll and make use of its useful functions (create web site, virt. directory, etc.).
Can I use the WixIisExtension to enable the net.tcp protocol?
If not, how can I achieve that?
Add a new Project to your Setup Solution (Windows Installer XML -> C# Custom Action Project)
In this Project add a reference to the Assembly Microsoft.Web.Administration, which can be found here: C:\Windows\System32\inetsrv and is required to add the protocols.
My Custom Action looks like this:
using System;
using System.Linq;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Web.Administration;
namespace Setup.CustomAction.EnableProtocols
{
public class CustomActions
{
[CustomAction]
public static ActionResult EnableProtocols(Session session)
{
session.Log("Begin EnableProtocols");
var siteName = session["SITE"];
if (string.IsNullOrEmpty(siteName))
{
session.Log("Property [SITE] missing");
return ActionResult.NotExecuted;
}
var alias = session["VIRTUALDIRECTORYALIAS"];
if (string.IsNullOrEmpty(alias))
{
session.Log("Property [VIRTUALDIRECTORYALIAS] missing");
return ActionResult.NotExecuted;
}
var protocols = session["PROTOCOLS"];
if (string.IsNullOrEmpty(protocols))
{
session.Log("Property [PROTOCOLS] missing");
return ActionResult.NotExecuted;
}
try
{
var manager = new ServerManager();
var site = manager.Sites.FirstOrDefault(x => x.Name.ToUpper() == siteName.ToUpper());
if (site == null)
{
session.Log("Site with name {0} not found", siteName);
return ActionResult.NotExecuted;
}
var application = site.Applications.FirstOrDefault(x => x.Path.ToUpper().Contains(alias.ToUpper()));
if (application == null)
{
session.Log("Application with path containing {0} not found", alias);
return ActionResult.NotExecuted;
}
application.EnabledProtocols = protocols;
manager.CommitChanges();
return ActionResult.Success;
}
catch (Exception exception)
{
session.Log("Error setting enabled protocols: {0}", exception.ToString());
return ActionResult.Failure;
}
}
}
}
Please note that I am assuming three properties here: SITE, VIRTUALDIRECTORYALIAS & PROTOCOLS
Build the solution now. In the background, WiX creates two assemblies %Project%.dll and %Project%.CA.dll. The CA.dll includes the depending Microsoft.Web.Administration automatically.
Then in your WiX Setup Project include a reference to the new Custom Action Project. The reference is required for referencing the %Projet%.CA.dll.
Edit the product.wxs
First add the properties somewhere inside the product element:
<!-- Properties -->
<Property Id="SITE" Value="MySite" />
<Property Id="VIRTUALDIRECTORYALIAS" Value="MyVirtDirectoryAlias" />
<Property Id="PROTOCOLS" Value="http,net.tcp" />
Below add the binary element:
<!-- Binaries -->
<Binary Id="CustomAction.EnableProtocols" SourceFile="$(var.Setup.CustomAction.EnableProtocols.TargetDir)Setup.CustomAction.EnableProtocols.CA.dll" />
Note that you have to add the CA.dll.
Below add the Custom Action:
<!-- Custom Actions -->
<CustomAction Id="EnableProtocols" BinaryKey="CustomAction.EnableProtocols" DllEntry="EnableProtocols" Execute="immediate" Return="check" />
And finally the Installation Sequence where you want the execution to take place.
<!-- Installation Sequence -->
<InstallExecuteSequence>
<Custom Action="EnableProtocols" After="InstallFinalize">NOT Installed</Custom>
</InstallExecuteSequence>
that's all. Should work.
Thanks to Darin Dimitrov for providing the links above.
Here is the right way to do this in WIX (assuming you are installing on a 64-bit operating system - if not at a guess I'd say change CAQuietExec64 to CAQuietExec although this is untested):
Get a reference to appcmd.exe:
<Property Id="APPCMD">
<DirectorySearch Id="FindAppCmd" Depth="1" Path="[WindowsFolder]\system32\inetsrv\">
<FileSearch Name="appcmd.exe"/>
</DirectorySearch>
</Property>
Define the following custom actions (the properties [WEB_SITE_NAME] and [WEB_APP_NAME] can be populated elsewhere in your installer; or to test you can hard-code them):
<CustomAction
Id="SetEnableNetTCPCommmand"
Property="EnableNetTCP"
Value=""[APPCMD]" set app "[WEB_SITE_NAME]/[WEB_APP_NAME]" /enabledProtocols:http,net.tcp"/>
<CustomAction
Id="EnableNetTCP"
BinaryKey="WixCA"
DllEntry="CAQuietExec64"
Execute="deferred"
Return="ignore"
Impersonate="no" />
Now in the InstallExecuteSequence add
<InstallExecuteSequence>
...
<Custom Action="SetEnableNetTCPCommmand" After="InstallExecute">APPCMD AND NOT Installed</Custom>
<Custom Action="EnableNetTCP" After="SetEnableNetTCPCommmand">APPCMD AND NOT Installed</Custom>
...
</InstallExecuteSequence>
And if all is well in the world that will now update the protocols.
You may take a look at this article on MSDN. There's a section at the end which illustrates how to use the managed API to achieve configure a WAS enabled service. I am not familiar with Wix but you could probably use and plug this code into some custom deployment step.
This can't be done using the standard WiXIIsExtension, as far as I know. Thus, the only option you have is a custom action.
You can also find this thread interesting - it gives a hint how to achieve the similar thing in MSBuild script, but you should be able to translate it to custom action easily.
Good luck!