Default install path to ProgramFilesFolder without specifying ProgramFilesFolder in wxs - c#

I currently have a legacy Visual Studio Install Projects project that creates an MSI. With this I can specify "TARGETDIR="somepath"" on the command line and have it install to "somepath". Now with WIX, if I don't specify ProgramFilesFolder in my wxs, "TARGETDIR" still works, however in my UI of the installer the default path is "C:\Manufacturer\Product" whereas I still want it to default to ProgramFilesFolder. Having support of "TARGETDIR" is necessary to also support upgrading to the legacy MSI from within the app itself.
I have found some ways to change the UI default directory to ProgramFilesFolder, however TARGETDIR doesn't change to this directory (or a directory the user specifies) so it still installs to C:\Manufacturer\Product.
Does anyone have any ideas here? I'm guessing some kind of custom action will do it but I feel like I've tried most of the suggestions such as:
https://stackoverflow.com/a/13058798/9993088 but this would override any selected directory in the or at the command line
I've also tried creating a WiX property "TARGETDIR" to expose it on the command line but as this is already an internal one it didn't make a difference.
How to set default value of WiX Msi install path in the ui to Program Files? as mentioned this would then prevent me from using "TARGETDIR" on the command line
https://wixtoolset.org//documentation/manual/v3/wixui/dialog_reference/wixui_advanced.html and then using a custom action to set "TARGETDIR" to "APPLICATIONFOLDER". With this I can set the default location to ProgramFilesFolder but then I haven't found a way to set "TARGETDIR" at the right time to use the command line value or the user selected one. I almost need a way to do "if 'APPLICATIONFOLDER' is not default; use its value OR if 'TARGETDIR' is not default; use its value ELSE use the default value". This also seems to not allow variable expansion so I can't use "[ProgramFilesFolder]", I have to explicitly write out "C:\Program Files..." which isn't ideal.
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLDIR" Name="blah">
As mentioned, I must be able to use "TARGETDIR" and not "INSTALLDIR" (although that works).
If I really have to use "INSTALLDIR" then I can make it work but it makes maintaining the legacy MSI and the WiX one tricky due to the nature of how they're used.
Edit
Solution:
<Custom Action="SetINSTALLDIR" Before="AppSearch">Not Installed</Custom> in both InstallExecuteSequence and InstallUISequence.
This points to:
<CustomAction Id="SetINSTALLDIR" BinaryKey="CustomActionsBinary" DllEntry="SetInstallDir" />
SetInstallDir is the following:
[CustomAction]
public static ActionResult SetInstallDir(Session session)
{
TraceLogger.yRTraceInfo(nameof(SetInstallDir));
string installDir = session["APPLICATIONFOLDER"];
string targetDir = string.Empty;
try
{
targetDir = session["TARGETDIR"];
}
catch (Exception e)
{
Console.log(e.Message);
}
if (string.IsNullOrEmpty(installDir) && !string.IsNullOrEmpty(targetDir))
{
session["APPLICATIONFOLDER"] = targetDir;
console.log($"Setting APPLICATIONFOLDER to {targetDir}");
}
return ActionResult.Success;
}

I suppose you could have a SetProperty custom action to assign INSTALLDIR a value if it is empty and TARGETDIR has a value and Not Installed. Schedule it early on in both the Install UI and Install Execute sequences ahead of AppSearch.
FYI in WiX INSTALLLOCATION is more commonly used. INSTALLDIR is more of an InstallShield thing and TARGETDIR a Visual Studio thing.
<SetProperty Id="INSTALLDIR" Value="[TARGETDIR]" Before="AppSearch">Not INSTALLDIR and TARGETDIRDIR and Not Installed</SetProperty>

Related

System.IO.FileInfo adds unexpected string to path when used in WIX Custom Action

I've got an WIX(V3.11.1) installer in which I'm creating an FileInfo based on value which is passed to Custom Action.
Value which was passed to Custom Action is correct, session.CustomActionData["INSTALLFOLDER"] returns proper path, which is C:\Program Files(x86)\MyApplication.
Unfortunately, when I create FileInfo targetDir = new FileInfo(session.CustomActionData["INSTALLFOLDER"]), the result of targetDir.FullName is C:\Windows\Installer\MSIE335.tmp-\C:\Program Files(x86)\MyApplication\.
I've tried to find any information about how constructor of FileInfo works, but without any result.
Do you have any ideas why C:\Windows\Installer\MSIE335.tmp-\ appears in FileInfo and how to create it with real path?
Code which is used by me to check all values:
string path = session.CustomActionData["INSTALLFOLDER"];
session.Log(path); //result is C:\Program Files(x86)\MyApplication
FileInfo targetDir = new FileInfo(path);
session.Log(targetDir.FullName); // result is C:\Windows\Installer\MSIE335.tmp-\C:\Program Files(x86)\MyApplication\
My setup-sense is guessing that the value of INSTALLFOLDER in your CustomActionData is actually the value [INSTALLFOLDER]. When logging, that syntax will get resolved to its proper value. That is why it looks good. However, what FileInfo is actually getting is a value like:
FileInfo targetDir = new FileInfo("[INSTALLFOLDER]");
Which of course is "The file named "[INSTALLFOLDER]" in the current directory". That matches your second log line.
The fix would be to ensure you're passing the value of INSTALLFOLDER in your CustomActionData. A few different ways to do that depending how you are scheduling your deferred custom action and setting the named property. For example, using SetProperty should be an easy way to fix it.
Update: Hawex provided a snippet that defined the custom action. It looked like:
<Property Id="CustomActionOnInstall" Value="INSTALLFOLDER=[INSTALLFOLDER]" />
<CustomAction Id="CustomActionOnInstall" BinaryKey="CustomActions" Execute="deferred"
Impersonate="no" DllEntry="OnInstall" Return="check" />
<InstallExecuteSequence>
<Custom Action="CustomActionOnInstall" Before="InstallFinalize">NOT Installed</Custom>
</InstallExecuteSequence>
to fix, just change the static (unevaluated) Property to a SetProperty like so:
<SetProperty Id="CustomActionOnInstall" Value="INSTALLFOLDER=[INSTALLFOLDER]"
Before="CustomActionOnInstall" Sequence="execute" />

How to output a file from a roslyn code analyzer?

I'm using the roslyn API to write a DiagnosticAnalyzer and CodeFix.
After I have collected all strings and string-interpolations, I want to write all of them to a file but I am not sure how to do this the best way.
Of course I can always simply do a File.WriteAllText(...) but I'd like to expose more control to the user.
I'm also not sure about how to best trigger the generation of this file, so my questions are:
I do not want to hard-code the filename, what would be the best way to expose this setting to the user of the code-analyzer? A config file? If so, how would I access that? ie: How do I know the directory?
If one string is missing from the file, I'd like to to suggest a code fix like "Project contains changed or new strings, regenerate string file". Is this the best way to do this? Or is it possible to add a button or something to visual studio?
I'm calling the devenv.com executable from the commandline to trigger builds, is there a way to force my code-fix to run either while building, or before/after? Or would I have to "manually" load the solution with roslyn and execute my codefix?
I've just completed a project on this. There are a few things that you will need to do / know.
You will probably need to switch you're portable class library to a class library. otherwise you will have trouble calling the File.WriteAllText()
You can see how to Convert a portable class library to a regular here
This will potentially not appropriately work for when trying to apply all changes to document/project/solution. When Calling from a document/project/solution, the changes are precalcuated and applied in a preview window. If you cancel, an undo action is triggered to undo all changes, if you write to a file during this time, and do not register an undo action you will not undo the changes to the file.
I've opened a bug with roslyn but you can handle instances by override the preview you can see how to do so here
And one more final thing you may need to know is how to access the Solution from the analyzer which, Currently there is a hack I've written to do so here
As Tamas said you can use additional files you can see how to do so here
You can use additional files, but I know on the version I'm using resource files, are not marked as additional files by default they are embeddedResources.
So, for my users to not have to manually mark the resource as additonalFiles I wrote a function to get out the Designer.cs files associated with resource files from the csproj file using xDoc you can use it as an example if you choose to parse the csproj file:
protected List<string> GetEmbeddedResourceResxDocumentPaths(Project project)
{
XDocument xmldoc = XDocument.Load(project.FilePath);
XNamespace msbuild = "http://schemas.microsoft.com/developer/msbuild/2003";
var resxFiles = new List<string>();
foreach (var resource in xmldoc.Descendants(msbuild + "EmbeddedResource"))
{
string includePath = resource.Attribute("Include").Value;
var includeExtension = Path.GetExtension(includePath);
if (0 == string.Compare(includeExtension, RESX_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
var outputTag = resource.Elements(msbuild + LAST_GENERATED_TAG).FirstOrDefault();
if (null != outputTag)
{
resxFiles.Add(outputTag.Value);
}
}
}
return resxFiles;
}
For config files you can use the AdditionalFiles msbuild property, which is passed to the analyzers through the context. See here.

How to set a Directory path in WIX using CustomAction?

I have a directory structure like this in WIX
<Directory Id="TARGETDIR" Name="SourceDir" >
<Directory Id="XXXFOLDER" Name="XXX">
<Directory Id="YYYFOLDER" Name="YYY">
<Directory Id="MAINFOLDER" Name="MAIN">
Now this MAINFOLDER Directory Resolves to D:\XXX\YYY\MAIN\
I get a path for the MAINFOLDER from a service which resolves to E:\XXX\YYY\MAIN
I have also assigned a customAction in a cs file
Below is the code
[CustomAction]
public static ActionResult GetNewDataPath(Session session)
{
sNewDataDir = xxxservice.GetPath();
if (!String.IsNullOrEmpty(sNewDataDir.ToString()))
{
sNewDataDir+= "\\MAIN\\";
}
session["MAINFOLDER"] = sNewDataDir;
return ActionResult.Success;
}
My Custom Actions are as below :
<CustomAction Id="GETDATAPATH" BinaryKey="InstallerCA"
DllEntry="GetNewDataPath" Execute="immediate"/>
This is the Install Sequence:
<Custom Action="GETDATAPATH" Before="CostFinalize" />
The sNewDataDir contains this value = "E:\XXX\YYY\MAIN" and I assign to session["MAINFOLDER"]. It gets assigned. But It does not get reflected on WIX side because my files are still being copied to D:\XXX\YYY\Main inspite of assigning it to E:\XXX\YYY\Main . How do we change the direcory path of session["MAINFOLDER"] using CustomAction?
It's usually a matter of sequence. The values of the properties are assigned to the Directory paths during the CostFinalize action per MSDN. You're custom action above must be sequenced sometime before CostFinalize runs in the execute sequence.
It can also be a matter of privilege: MAINFOLDER may be a restricted public property and isn't making it to the execute sequence (doesn't apply if your custom action ran in the execute sequence). Read about Restricted Public Properties to see if that might be your issue.
And it can also be your computer's anti-virus or some other issue with the script engines.
To have a good idea (or at least find someone else who can figure out what the issue really is) you will need to generate a good log of your failed attempt. Most of the time voicewarmup (or /l*v) is the best value to use (tends to give you most but not all of what you want, along with way too much of what you don't) and is the value most installation development experts use when generating the logs they use and share. It does slow down your installs a fair bit, however.

WiX – copy arbitrary files

The folder where my setup.exe is located contains a subfolder CALhaving files named something like xyz1234.cal – their names vary from customer to customer. These files have to be copied into a folder CAL in the target directory.
So I created a CustomAction and a C# dll which uses the File.Copy() function. My C# function receives the strings srcDir and destDir as parameters, e.g. D:\installation\CAL and C:\MyApp\CAL.
However, when I check the existence of the folders with Directory.Exists(srcDir), an Exception is thrown, although the directory D:\installation\CAL exists:
ERROR in custom action myFunction System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\Windows\Installer\MSID839.tmp-\D:\installation\CAL'.
This happens no matter whether the CustomAcion is executed immediate or deferred. C:\Windows\Installer\MSID839.tmp-\ seems to be the path of the executed assembly, but I certainly don’t want to have it as a part of the FullPath. How can I get rid of it?
CustomAction and properties are defined like so:
<CustomAction Id='myCA' BinaryKey='myCABin' DllEntry='myFunction' Execute="deferred" HideTarget="no" Impersonate="no"/>
<Property Id="myCA" Value="Arg1=[CURRENTDIRECTORY];Arg2=[INSTALLDIR]" />
The parameters are used like so:
CustomActionData data = session.CustomActionData;
string srcDir = data["Arg1"]+ "\\CAL";
string destDir = data["Arg2"]+ "\\CAL";
if (Directory.Exists(srcDir))
// copy files
I recreated your app and it works fine. Here is my wix code (it's inside my product node):
<CustomAction Id='Test' BinaryKey='RegistryHelperCA' DllEntry='Test' Execute="deferred" HideTarget="no" Impersonate="no"/>
<Property Id="Test" Value="Arg1=[CURRENTDIRECTORY];Arg2=[INSTALLDIR]" />
<InstallExecuteSequence>
<Custom Action="Test" After="InstallFiles"></Custom>
</InstallExecuteSequence>
My custom action:
[CustomAction("Test")]
public static ActionResult Test(Session session)
{
string dir = session.CustomActionData["Arg1"];
session.Log("DIRECTORY equals " + dir);
if (Directory.Exists(dir))
session.Log("Success");
return ActionResult.Success;
}
It spits out dir as C:\Users\user\Desktop. Verify you aren't assigning to your CURRENTDIRECTORY property anywhere and, if you don't find anything, try setting your custom action to Execute="immediate" and accessing the data like this
string srcDir = session["CURRENTDIRECTORY"]+ "\\CAL";
If that doesn't work, surely that property is being overwritten somewhere. Good luck!
After some trial-and-error-sessions I found that Directory.Exists(srcDir) and Directory.Exists(destDir) didn't work, because the not the values but the property names are passed as parameter to the Exist() function - in contrast to session.Log(srcDir) which properly yields the value.
Finally I ended up with setting execute="immediate" and retrieving the values like so:
srcDir = session["CURRENTDIRECTORY"];
destDir = session.GetTargetPath("INSTALLDIR");

How do I find the current time and date at compilation time in .net/C# application?

I want to include the current time and date in a .net application so I can include it in the start up log to show the user what version they have. Is it possible to retrieve the current time during compilation, or would I have to get the creation/modification time of the executable?
E.g.
Welcome to ApplicationX. This was built day-month-year at time.
If you're using reflection for your build number you can use that to figure out when a build was compiled.
Version information for an assembly consists of the following four values:
Major Version
Minor Version
Build Number
Revision
You can specify all the values or you can accept the default build number, revision number, or both by using an asterisk (*). Build number and revision are based off Jan 1, 2000 by default.
The following attribute will set Major and minor, but then increment build number and revision.
[assembly: AssemblyVersion("5.129.*")]
Then you can use something like this:
public static DateTime CompileTime
{
get
{
System.Version MyVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
// MyVersion.Build = days after 2000-01-01
// MyVersion.Revision*2 = seconds after 0-hour (NEVER daylight saving time)
DateTime compileTime = new DateTime(2000, 1, 1).AddDays(MyVersion.Build).AddSeconds(MyVersion.Revision * 2);
return compileTime;
}
}
The only way I know of doing this is somewhat convoluted -
You can have a pre-build event that runs a small application which generates the source code on the fly. An easy way to do this is to just overwrite a very small file that includes a class (or partial class) with the day/month/year hardcoded as a string constant.
If you set this to run as a pre-build event, it will rewrite that file before every build.
You could use PostSharp to weave in the date immediately post-build. PostSharp comes with a lightweight aspect-oriented programming library, but it can be extended to weave in anything you need in a wide variety of ways. It works at the IL level, but the API abstracts you a bit from that.
http://www.postsharp.org/
There's nothing built into the language to do this.
You could write a pre-build step to write out the current date and time to a source file though (in a string literal, for example, or as source code to generate a DateTime), and then compile that as part of your build.
I would suggest you make this source file as simple as possible, containing nothing but this information. Alternatively it could edit an existing file.
For an example of this, see the build file for MiscUtil which embeds the current SVN revision into the AssemblyFileVersion attribute. Some assorted bits of the build file:
<!-- See http://msbuildtasks.tigris.org -->
<Import
Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<!-- Find out what the latest version is -->
<SvnInfo RepositoryPath="$(SvnUrl)">
<Output TaskParameter="LastChangedRevision" PropertyName="Revision" />
</SvnInfo>
<!-- Update the AssemblyInfo with the revision number -->
<FileUpdate Files="$(OutputDirectory)\MiscUtil\MiscUtil\Properties\AssemblyInfo.cs"
Regex='(\[\s*assembly:\s*AssemblyFileVersion\(\s*"[^\.]+\.[^\.]+)\.([^\.]+)(\.)([^\.]+)("\)\s*\])'
ReplacementText='$1.$2.$(Revision)$5' />
In makefiles for C programs, it is common to see something like this:
echo char * gBuildSig ="%DATE% %TIME%"; > BuildTimestamp.c
And then the resulting C source file is compiled into the image. The above works on Windows because the %date% and %time% variables are known in cmd.exe, but a similar thing would work on Unix using cat.
You can do the same thing using C#. Once again, this is how it would look if you are using a makefile. You need a class, and a public static property.
BuildTimestamp.cs:
echo public static class Build { public static string Timestamp = "%DATE% %TIME%";} > BuildTimestamp.cs
And then for the thing you are building, a dependency and a delete:
MyApp.exe: BuildTimestamp.cs MyApp.cs
$(_CSC) /target:exe /debug+ /optimize- /r:System.dll /out:MyApp.exe MyApp.cs BuildTimestamp.cs
-del BuildTimestamp.cs
Be sure to delete the BuildTimestamp.cs file after you compile it; you don't want to re-use it. Then, in your app, just reference Build.Timestamp.
Using MSBuild or Visual Studio, it is more complicated. I couldn't get %date% or %time% to resolve. Those things are pseudo environment variables, I guess that is why. So I resorted to an indirect way to get a timestamp, using the Touch task with AlwaysCreate = true. That creates an empty file. The next step writes source code into the same file, referencing the timestamp of the file. One twist - I had to escape the semicolon.
Your pre-build step should build the target "BuildTimestamp". And be sure to include that file into the compile. And delete it afterwards, in the post-build step.
<ItemGroup>
<StampFile Include="BuildTimestamp.cs"/>
</ItemGroup>
<Target Name="BuildTimestamp"
Outputs="#(StampFile)">
<Message Text="Building timestamp..." />
<Touch
AlwaysCreate="true"
Files="#(StampFile)" />
<WriteLinesToFile
File="#(StampFile)"
Lines='public static class Build { public static string Timestamp = "%(StampFile.CreatedTime)" %3B }'
Overwrite="true" />
</Target>
You could update the Assembly version in AssemblyInfo.cs as part of your build. Then you could do something like this
FileVersionInfo lvar = FileVersionInfo.GetVersionInfo(FileName);
FileVersionInfo has the information (build/version,etc) that you looking for. See if this works out for you.
Hi I used following method for the same...
private DateTime ExecutableInfo()
{
System.IO.FileInfo fi = new System.IO.FileInfo(Application.ExecutablePath.Trim());
try
{
return fi.CreationTime;
}
catch (Exception ex)
{
throw ex;
}
finally
{
fi = null;
}
}

Categories