How to get local path for payload in WiX/Burn Managed Bootstrapper Application? - wix

I am currently working in a WiX/Burn Managed Bootstrapper Application and cannot figure out how to get the local path for a payload (MSI).
I let the user select which applications they want to install in my custom UI, and I want to not show applications for which the MSI is missing. I also need to see information in the MSI's database.
I know I can determine missing payloads by handling "ResolveSource" but that doesn't happen until right before the application in installed.
I deserialize the BootstrapperApplicationData.xml file first thing so I have information about which MSIs MIGHT be installed, but it still doesn't help me determine the source of the MSIs.
Does anyone know how to determine the local path to a payload?
EDIT: Here is an example for how I reference all the installers:
<MsiPackage Id="AppName"
SourceFile="$(var.ProjectName.TargetDir)ProjectName.msi"
Name="MSI\ProjectName.msi"
Compressed="no"/>

In the GetLastUsedSourceFolder function in cache.cpp, you can see that the engine gets the source folder from the WixBundleLastUsedSource variable, and the parent directory of the WixBundleOriginalSource variable if WixBundleLastUsedSource isn't set.
You can use this along with the Name attribute of the WixPayloadProperties element in the BootstrapperApplicationData.xml file to predetermine where the engine will look for a payload. Note that the engine will actually look in the cache first.

The MSI files are embedded into the bundle .exe and aren't extracted from the bundle until right before the application is installed, which corresponds to when the ResolveSource event fires. However, if you really want to get this information, you can programatically extract the MSI files yourself and inspect them using the WiX DTF library (wix.dll in the /bin folder of your WiX install).
using Microsoft.Tools.WindowsInstallerXml;
private void ExtractEmbeddedMsiInstallers()
{
var tmpFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var bundlePath = Engine.StringVariables["WixBundleOriginalSource"];
Unbinder unbinder = null;
try
{
unbinder = new Unbinder();
//The next line will extract the MSIs into the tmpFolder in a subfolder named "AttachedContainer"
unbinder.Unbind(bundlePath, OutputType.Bundle, tmpFolder);
}
finally
{
if (null != unbinder)
unbinder.DeleteTempFiles();
}
}
You also mentioned needing to inspect data in the MSI database. Here's a sample of how to do that:
using (var database = new InstallPackage(msiFilePath, DatabaseOpenMode.Transact) { WorkingDirectory = _someTempFolder })
{
if (database.Tables.Contains("CustomAction"))
{
using (View view = database.OpenView("SELECT `Action`, `Type`, `Source`, `Target` FROM `CustomAction`"))
{
view.Execute();
foreach (Record rowRecord in view)
using (rowRecord)
{
var actionName = rowRecord.GetString(1);
var actionType = rowRecord.GetInteger(2);
var binaryName = rowRecord.GetString(3);
var methodName = rowRecord.GetString(4);
//Do something with the values
}
}
}
}

Related

Project Doesn't Build After Installed Sitecore TDS

After setting up Sitecore TDS, my project will not build. I'm new to Visual Studio and also new to working with Sitecore. It seems that it cannot find a particular setting, but a Google search is not coming up with anything:
Severity Code Description Project Path File Line Suppression State
Error The "AnalyzeProject" task failed unexpectedly.
System.MissingFieldException: Field not found: 'HedgehogDevelopment.SitecoreProject.Tasks.SitecoreDeployInfo.ParsedItem'.
at HedgehogDevelopment.SitecoreProject.Analysis.TemplateStructure.Validate(Dictionary`2 projectItems, XDocument scprojDocument)
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.AnalysisEngine.<>c__DisplayClass4_1.<GetReport>b__0()
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.ExecutionTimer.Time(Action action)
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.AnalysisEngine.GetReport(Dictionary`2 projectItems, XDocument scprojDocument)
at HedgehogDevelopment.SitecoreProject.Tasks.AnalyzeProject.Execute()
at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
at Microsoft.Build.BackEnd.TaskBuilder.<ExecuteInstantiatedTask>d__26.MoveNext() B2B.Core C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0 C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0\HedgehogDevelopment.SitecoreProject.targets 144
Apparently my project does still build and will run, but that error pops up each time regardless.
This can happen when you have TDS validations enabled and are missing some DLLs.
In this directory:
C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0\
Add the following DLLs:
Microsoft.Web.Infrastructure.dll
TDSWebDeploy.Services.Contracts.dll
If you have TDS installed, you may be able to source those DLLs from somewhere in your C:\Program Files (x86)\MSBuild\HedgehogDevelopment directory. If not, someone else on your team may have them.
You can also try disabling and enabling validations by right clicking your TDS project --> Properties --> Validations tab.
I have seen this issue on numerous occasions across numerous dev boxes. We have opened support tickets regarding it, but no conclusions were drawn.
The specific error is that the HedgehogDevelopment.SitecoreProject.Tasks.SitecoreDeployInfo.ParsedItem field is missing on an object. The fact that ParsedItem is mentioned implies that some form of parsing may be occurring, and that it didn't work as expected. This is conjecture, but if parsing is occurring, it would be worth ensuring that your serialized item files are accessible to the user/group that is performing this parsing.
Here is the code that is failing:
public override IEnumerable<Problem> Validate(Dictionary<Guid, SitecoreDeployInfo> projectItems, XDocument scprojDocument)
{
List<Problem> problems = new List<Problem>();
foreach (KeyValuePair<Guid, SitecoreDeployInfo> projectItem in projectItems)
{
// ** Presumably, it's failing here **
string item = projectItem.Value.ParsedItem.Properties["template"];
if (string.IsNullOrEmpty(item))
{
continue;
}
Guid guid = new Guid(item);
if (guid == TemplateStructure.TEMPLATE)
{
problems.AddRange(this.ValidateTemplate(projectItems, projectItem.Value.Item));
}
else if (guid != TemplateStructure.TEMPLATE_SECTION)
{
if (guid != TemplateStructure.TEMPLATE_FIELD)
{
continue;
}
problems.AddRange(this.ValidateField(projectItems, projectItem.Value.Item));
}
else
{
problems.AddRange(this.ValidateSection(projectItems, projectItem.Value.Item));
}
}
problems.RemoveAll((Problem r) => r == null);
return problems;
}
Here is the definition for SitecoreDeployInfo:
using HedgehogDevelopment.SitecoreCommon.Data.Items;
using System;
namespace HedgehogDevelopment.SitecoreProject.Tasks
{
public class SitecoreDeployInfo
{
public IItem ParsedItem;
public SitecoreItem Item;
public SitecoreDeployInfo()
{
}
}
}

Setting the version number for .NET Core projects

What are the options for setting a project version with .NET Core / ASP.NET Core projects?
Found so far:
Set the version property in project.json. Source: DNX Overview, Working with DNX projects. This seems to set the AssemblyVersion, AssemblyFileVersion and AssemblyInformationalVersion unless overridden by an attribute (see next point).
Setting the AssemblyVersion, AssemblyFileVersion, AssemblyInformationalVersion attributes also seems to work and override the version property specified in project.json.
For example, including 'version':'4.1.1-*' in project.json and setting [assembly:AssemblyFileVersion("4.3.5.0")] in a .cs file will result in AssemblyVersion=4.1.1.0, AssemblyInformationalVersion=4.1.1.0 and AssemblyFileVersion=4.3.5.0
Is setting the version number via attributes, e.g. AssemblyFileVersion, still supported?
Have I missed something - are there other ways?
Context
The scenario I'm looking at is sharing a single version number between multiple related projects. Some of the projects are using .NET Core (project.json), others are using the full .NET Framework (.csproj). All are logically part of a single system and versioned together.
The strategy we used up until now is having a SharedAssemblyInfo.cs file at the root of our solution with the AssemblyVersion and AssemblyFileVersion attributes. The projects include a link to the file.
I'm looking for ways to achieve the same result with .NET Core projects, i.e. have a single file to modify.
You can create a Directory.Build.props file in the root/parent folder of your projects and set the version information there.
However, now you can add a new property to every project in one step by defining it in a single file called Directory.Build.props in the root folder that contains your source. When MSBuild runs, Microsoft.Common.props searches your directory structure for the Directory.Build.props file (and Microsoft.Common.targets looks for Directory.Build.targets). If it finds one, it imports the property. Directory.Build.props is a user-defined file that provides customizations to projects under a directory.
For example:
<Project>
<PropertyGroup>
<Version>0.0.0.0</Version>
<FileVersion>0.0.0.0</FileVersion>
<InformationalVersion>0.0.0.0.myversion</InformationalVersion>
</PropertyGroup>
</Project>
Another option for setting version info when calling build or publish is to use the undocumented /p option.
dotnet command internally passes these flags to MSBuild.
Example:
dotnet publish ./MyProject.csproj /p:Version="1.2.3" /p:InformationalVersion="1.2.3-qa"
See here for more information: https://github.com/dotnet/docs/issues/7568
Not sure if this helps, but you can set version suffixes at publish time. Our versions are usually datetime driven, so that developers don't have to remember to update them.
If your json has something like "1.0-*"
"dotnet publish --version-suffix 2016.01.02" will make it "1.0-2016.01.02".
It's important to stick to "semvar" standards, or else you'll get errors. Dotnet publish will tell you.
Why not just change the value in the project.json file. Using CakeBuild you could do something like this (optimizations probably possible)
Task("Bump").Does(() => {
var files = GetFiles(config.SrcDir + "**/project.json");
foreach(var file in files)
{
Information("Processing: {0}", file);
var path = file.ToString();
var trg = new StringBuilder();
var regExVersion = new System.Text.RegularExpressions.Regex("\"version\":(\\s)?\"0.0.0-\\*\",");
using (var src = System.IO.File.OpenRead(path))
{
using (var reader = new StreamReader(src))
{
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if(line == null)
continue;
line = regExVersion.Replace(line, string.Format("\"version\": \"{0}\",", config.SemVer));
trg.AppendLine(line);
}
}
}
System.IO.File.WriteAllText(path, trg.ToString());
}
});
Then if you have e.g. a UnitTest project that takes a dependency on the project, use "*" for dependency resolution.
Also, do the bump before doing dotnet restore. My order is as follows:
Task("Default")
.IsDependentOn("InitOutDir")
.IsDependentOn("Bump")
.IsDependentOn("Restore")
.IsDependentOn("Build")
.IsDependentOn("UnitTest");
Task("CI")
.IsDependentOn("Default")
.IsDependentOn("Pack");
Link to full build script: https://github.com/danielwertheim/Ensure.That/blob/3a278f05d940d9994f0fde9266c6f2c41900a884/build.cake
The actual values, e.g. the version is coming from importing a separate build.config file, in the build script:
#load "./buildconfig.cake"
var config = BuildConfig.Create(Context, BuildSystem);
The config file looks like this (taken from https://github.com/danielwertheim/Ensure.That/blob/3a278f05d940d9994f0fde9266c6f2c41900a884/buildconfig.cake):
public class BuildConfig
{
private const string Version = "5.0.0";
public readonly string SrcDir = "./src/";
public readonly string OutDir = "./build/";
public string Target { get; private set; }
public string Branch { get; private set; }
public string SemVer { get; private set; }
public string BuildProfile { get; private set; }
public bool IsTeamCityBuild { get; private set; }
public static BuildConfig Create(
ICakeContext context,
BuildSystem buildSystem)
{
if (context == null)
throw new ArgumentNullException("context");
var target = context.Argument("target", "Default");
var branch = context.Argument("branch", string.Empty);
var branchIsRelease = branch.ToLower() == "release";
var buildRevision = context.Argument("buildrevision", "0");
return new BuildConfig
{
Target = target,
Branch = branch,
SemVer = Version + (branchIsRelease ? string.Empty : "-b" + buildRevision),
BuildProfile = context.Argument("configuration", "Release"),
IsTeamCityBuild = buildSystem.TeamCity.IsRunningOnTeamCity
};
}
}
If you still want to have the Solution Level SharedVersionInfo.cs you can do it by adding these lines to your project.json file:
"buildOptions": {
"compile": {
"includeFiles": [
"../../SharedVersionInfo.cs"
]
}
}
Your relative path may vary, of course.
use external version.txt file with version, and prebuild step to publish this version in projects

Is it possible to read MSI properties from MSI files in chain before detect phase in bootstrapper?

In "Bundle.wxs" I have a "chain" with "MsiPackages" which contain "InstallConditions". In order for the user to decide which packages he/she wants installed/upgraded I would like to display properties found in them.
For instance, I want to read the property "ProductName" and "ProductVersion" in the "Property" table of every MSI in the chain and display it to the user next to a checkbox for every MSI in the chain. The checkbox is wired to the burn variable used in "InstallConditions".
But the problem is, it doesn't seem like I have access to the MSI files before the "Apply" step. They are not extracted from the Bootstrapper Application EXE before this step. So, my question is, Is there a way to load these values programatically in order to display them to the user before the Apply step? I could use variables and populate them myself with the values but this information is already in the MSI so this seems inefficient.
Is there a way to do this? Thanks for the help.
<Bundle>
<Variable Name="InstallProduct1" Type="string" Value="true" />
<Variable Name="ProductName1" Type="string" Value="My Product 1"/> <!-- Better way? -->
<Variable Name="ProductVersion1" Type="version" Value="1.2.3.4"/> <!-- Better way? -->
<Chain>
<MsiPackage SourceFile="my_product_1.msi"
InstallCondition="InstallProduct1">
</MsiPackage>
</Chain>
</Bundle>
WiX does generate a BootstrapperApplicationData.xml file which includes a lot of the information used to build the exe and is included in the files available at runtime. You can parse that file at runtime in order to access that metadata. Since the file, along with all of our assemblies and .msi files, are placed in a randomly-name temp folder, we can’t know ahead of time where the file will live, so we must use our assembly’s path to find it. You can then parse the XML to get the metadata.
I have a blog post with additional details here: https://www.wrightfully.com/part-3-of-writing-your-own-net-based-installer-with-wix-context-data/
In my case, I use the tag instead of '', so have some of the info available to me may not be there for you, so your experience may vary. I would suggest running a makeshift installer in debug mode and setting a breakpoint to inspect the contents of the XML in order to get a full list of what’s available.
Here’s an example of how I get data from the file in my ManagedBootstrapperApplication (c#). Note: in this example, my domain objects are MBAPrereqPackage, BundlePackage and PackageFeature, each of which take an XML node object in their constructor and further parse the data into the object’s properties.
const XNamespace ManifestNamespace = ( XNamespace) “http://schemas.microsoft.com/wix/2010/BootstrapperApplicationData” ;
public void Initialize()
{
//
// parse the ApplicationData to find included packages and features
//
var bundleManifestData = this.ApplicationData;
var bundleDisplayName = bundleManifestData
.Element(ManifestNamespace + “WixBundleProperties“ )
.Attribute( “DisplayName“)
.Value;
var mbaPrereqs = bundleManifestData.Descendants(ManifestNamespace + “WixMbaPrereqInformation“)
.Select(x => new MBAPrereqPackage(x))
.ToList();
//
//exclude the MBA prereq packages, such as the .Net 4 installer
//
var pkgs = bundleManifestData.Descendants(ManifestNamespace + “WixPackageProperties“)
.Select(x => new BundlePackage(x))
.Where(pkg => !mbaPrereqs.Any(preReq => preReq.PackageId == pkg.Id));
//
// Add the packages to a collection of BundlePackages
//
BundlePackages.AddRange(pkgs);
//
// check for features and associate them with their parent packages
//
var featureNodes = bundleManifestData.Descendants(ManifestNamespace + “WixPackageFeatureInfo“);
foreach ( var featureNode in featureNodes)
{
var feature = new PackageFeature(featureNode);
var parentPkg = BundlePackages.First(pkg => pkg.Id == feature.PackageId);
parentPkg.AllFeatures.Add(feature);
feature.Package = parentPkg;
}
}
///
/// Fetch BootstrapperApplicationData.xml and parse into XDocument.
///
public XElement ApplicationData
{
get
{
var workingFolder = Path.GetDirectoryName(this.GetType().Assembly.Location);
var bootstrapperDataFilePath = Path.Combine(workingFolder, “BootstrapperApplicationData.xml”);
using (var reader = new StreamReader(bootstrapperDataFilePath))
{
var xml = reader.ReadToEnd();
var xDoc = XDocument.Parse(xml);
return xDoc.Element(ManifestNamespace + “BootstrapperApplicationData“);
}
}
}

How to get [ProgramFilesFolder] to populate to "C:\Program Files" burn UI project

I have a Burn Bundle with the following variable
<Variable Name="INSTALLFOLDER" Type="string "Value="[ProgramFilesFolder]" />
With the following property in my bootstrapper UI project's main view model
public string InstallDirectory
{
get
{
if (_Engine.StringVariables.Contains("INSTALLFOLDER"))
return _Engine.StringVariables["INSTALLFOLDER"];
return string.Empty;
}
set
{
if (_Engine.StringVariables.Contains("INSTALLFOLDER"))
{
_Engine.StringVariables["INSTALLFOLDER"] = value;
OnPropertyChanged("InstallDirectory");
}
}
}
In my WPF view which has a textbox bound to the InstallDirectory property I only see "[ProgramFilesfolder]" but I was hoping to see something like "C:\Program Files"
I would like to end up with something like the following which will populate my install directory textbox with the default install folder and give the user the option to change it there.
<Variable Name='INSTALLFOLDER' Type='string' Value='[ProgramFilesFolder]$(var.AppName)' />
I could use the Net Framework to get the program files folder for my WPF UI but seems like I should be able to get it from the Wix Bundle. Also the Wix log shows that I am setting the INSTALLFOLDER property from my UI.
My bootstrapper Run looks like this:
protected override void Run()
{
this.Engine.Log(LogLevel.Verbose, "Run has been called on the UI application.");
CurrentDispatcher = Dispatcher.CurrentDispatcher;
_MainWindow = new MainWindow(new MainWindowViewModel(this));
Engine.Detect();
_MainWindow.Show();
Dispatcher.Run();
Engine.Quit(0);
}
I have thought I might need to listen to some event on the BootstrapperApplication after which I could fire on property changed for the InstallDirectory property but haven't found anything interesting yet.
I have been through the Developer Guide book for 3.6 and it doesn't seem to address this exact issue although the final two chapters do deal with burn projects and WPF.
In your get method you should be able to use this to get the actual value of the property:
get
{
if (_Engine.StringVariables.Contains("INSTALLFOLDER"))
return _Engine.FormatString("[INSTALLFOLDER]");
return string.Empty;
}

Check if a file exists in the project in WinRT

I have a WinRT Metro project which displays images based on a selected item. However, some of the images selected will not exist. What I want to be able to do is trap the case where they don't exist and display an alternative.
Here is my code so far:
internal string GetMyImage(string imageDescription)
{
string myImage = string.Format("Assets/MyImages/{0}.jpg", imageDescription.Replace(" ", ""));
// Need to check here if the above asset actually exists
return myImage;
}
Example calls:
GetMyImage("First Picture");
GetMyImage("Second Picture");
So Assets/MyImages/SecondPicture.jpg exists, but Assets/MyImages/FirstPicture.jpg does not.
At first I thought of using the WinRT equivalent of File.Exists(), but there doesn't appear to be one. Without having to go to the extent of trying to open the file and catching an error, can I simply check if either the file exists, or the file exists in the project?
You could use GetFilesAsync from here to enumerate the existing files. This seems to make sense considering you have multiple files which might not exist.
Gets a list of all files in the current folder and its sub-folders. Files are filtered and sorted based on the specified CommonFileQuery.
var folder = await StorageFolder.GetFolderFromPathAsync("Assets/MyImages/");
var files = await folder.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == "fileName");
if (file != null)
{
//do stuff
}
Edit:
As #Filip Skakun pointed out, the resource manager has a resource mapping on which you can call ContainsKey which has the benefit of checking for qualified resources as well (i.e. localized, scaled etc).
Edit 2:
Windows 8.1 introduced a new method for getting files and folders:
var result = await ApplicationData.Current.LocalFolder.TryGetItemAsync("fileName") as IStorageFile;
if (result != null)
//file exists
else
//file doesn't exist
There's two ways you can handle it.
1) Catch the FileNotFoundException when trying to get the file:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
try
{
// Don't forget to decorate your method or event with async when using await
var file = await installedLocation.GetFileAsync(fileName);
// Exception wasn't raised, therefore the file exists
System.Diagnostics.Debug.WriteLine("We have the file!");
}
catch (System.IO.FileNotFoundException fileNotFoundEx)
{
System.Diagnostics.Debug.WriteLine("File doesn't exist. Use default.");
}
catch (Exception ex)
{
// Handle unknown error
}
2) as mydogisbox recommends, using LINQ. Although the method I tested is slightly different:
Windows.Storage.StorageFolder installedLocation =
Windows.ApplicationModel.Package.Current.InstalledLocation;
var files = await installedLocation.GetFilesAsync(CommonFileQuery.OrderByName);
var file = files.FirstOrDefault(x => x.Name == fileName);
if (file != null)
{
System.Diagnostics.Debug.WriteLine("We have the file!");
}
else
{
System.Diagnostics.Debug.WriteLine("No File. Use default.");
}
BitmapImage has an ImageFailed event that fires if the image can't be loaded. This would let you try to load the original image, and then react if it's not there.
Of course, this requires that you instantiate the BitmapImage yourself, rather than just build the Uri.
Sample checking for resource availability for c++ /cx (tested with Windows Phone 8.1):
std::wstring resPath = L"Img/my.bmp";
std::wstring resKey = L"Files/" + resPath;
bool exists = Windows::ApplicationModel::Resources::Core::ResourceManager::Current->MainResourceMap->HasKey(ref new Platform::String(resKey.c_str()));