// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
namespace WixToolset.PowerShell
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using WixToolset.Data;
using WixToolset.Data.Symbols;
using WixToolset.Extensibility;
///
/// The compiler for the WiX Toolset PowerShell Extension.
///
public sealed class PSCompiler : BaseCompilerExtension
{
private const string KeyFormat = @"SOFTWARE\Microsoft\PowerShell\{0}\PowerShellSnapIns\{1}";
private const string VarPrefix = "PSVersionMajor";
public override XNamespace Namespace => "http://wixtoolset.org/schemas/v4/wxs/powershell";
///
/// Processes an element for the Compiler.
///
/// Source line number for the parent element.
/// Parent element of element to process.
/// Element to process.
/// Extra information about the context in which this element is being parsed.
public override void ParseElement(Intermediate intermediate, IntermediateSection section, XElement parentElement, XElement element, IDictionary context)
{
switch (parentElement.Name.LocalName)
{
case "File":
var fileId = context["FileId"];
var componentId = context["ComponentId"];
switch (element.Name.LocalName)
{
case "FormatsFile":
this.ParseExtensionsFile(intermediate, section, element, "Formats", fileId, componentId);
break;
case "SnapIn":
this.ParseSnapInElement(intermediate, section, element, fileId, componentId);
break;
case "TypesFile":
this.ParseExtensionsFile(intermediate, section, element, "Types", fileId, componentId);
break;
default:
this.ParseHelper.UnexpectedElement(parentElement, element);
break;
}
break;
default:
this.ParseHelper.UnexpectedElement(parentElement, element);
break;
}
}
///
/// Parses a SnapIn element.
///
/// Element to parse.
/// Identifier for parent file.
/// Identifier for parent component.
private void ParseSnapInElement(Intermediate intermediate, IntermediateSection section, XElement node, string fileId, string componentId)
{
var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(node);
string id = null;
string customSnapInType = null;
string description = null;
string descriptionIndirect = null;
var requiredPowerShellVersion = CompilerConstants.IllegalVersion;
string vendor = null;
string vendorIndirect = null;
string version = null;
foreach (var attrib in node.Attributes())
{
if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
{
switch (attrib.Name.LocalName)
{
case "Id":
id = this.ParseHelper.GetAttributeIdentifierValue(sourceLineNumbers, attrib);
break;
case "CustomSnapInType":
customSnapInType = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
break;
case "Description":
description = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
break;
case "DescriptionIndirect":
descriptionIndirect = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
break;
case "RequiredPowerShellVersion":
var ver = this.ParseHelper.GetAttributeVersionValue(sourceLineNumbers, attrib);
requiredPowerShellVersion = new Version(ver);
break;
case "Vendor":
vendor = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
break;
case "VendorIndirect":
vendorIndirect = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
break;
case "Version":
version = this.ParseHelper.GetAttributeVersionValue(sourceLineNumbers, attrib);
break;
default:
this.ParseHelper.UnexpectedAttribute(node, attrib);
break;
}
}
else
{
this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, node, attrib);
}
}
if (null == id)
{
this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Id"));
}
// Default to require PowerShell 1.0.
if (CompilerConstants.IllegalVersion == requiredPowerShellVersion)
{
requiredPowerShellVersion = new Version(1, 0);
}
// If the snap-in version isn't explicitly specified, get it
// from the assembly version at bind time.
if (null == version)
{
version = String.Format("!(bind.assemblyVersion.{0})", fileId);
}
foreach (var child in node.Elements())
{
if (this.Namespace == child.Name.Namespace)
{
switch (child.Name.LocalName)
{
case "FormatsFile":
this.ParseExtensionsFile(intermediate, section, child, "Formats", id, componentId);
break;
case "TypesFile":
this.ParseExtensionsFile(intermediate, section, child, "Types", id, componentId);
break;
default:
this.ParseHelper.UnexpectedElement(node, child);
break;
}
}
else
{
this.ParseHelper.ParseExtensionElement(this.Context.Extensions, intermediate, section, node, child);
}
}
// Get the major part of the required PowerShell version which is
// needed for the registry key, and put that into a WiX variable
// for use in Formats and Types files. PowerShell v2 still uses 1.
var major = (2 == requiredPowerShellVersion.Major) ? 1 : requiredPowerShellVersion.Major;
var variableId = new Identifier(AccessModifier.Global, String.Format(CultureInfo.InvariantCulture, "{0}_{1}", VarPrefix, id));
section.AddSymbol(new WixVariableSymbol(sourceLineNumbers, variableId)
{
Value = major.ToString(CultureInfo.InvariantCulture),
Overridable = false,
});
var registryRoot = RegistryRootType.LocalMachine; // HKLM
var registryKey = String.Format(CultureInfo.InvariantCulture, KeyFormat, major, id);
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "ApplicationBase", String.Format(CultureInfo.InvariantCulture, "[${0}]", componentId), componentId);
// set the assembly name automatically when binding.
// processorArchitecture is not handled correctly by PowerShell v1.0
// so format the assembly name explicitly.
var assemblyName = String.Format(CultureInfo.InvariantCulture, "!(bind.assemblyName.{0}), Version=!(bind.assemblyVersion.{0}), Culture=!(bind.assemblyCulture.{0}), PublicKeyToken=!(bind.assemblyPublicKeyToken.{0})", fileId);
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "AssemblyName", assemblyName, componentId);
if (null != customSnapInType)
{
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "CustomPSSnapInType", customSnapInType, componentId);
}
if (null != description)
{
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "Description", description, componentId);
}
if (null != descriptionIndirect)
{
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "DescriptionIndirect", descriptionIndirect, componentId);
}
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "ModuleName", String.Format(CultureInfo.InvariantCulture, "[#{0}]", fileId), componentId);
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "PowerShellVersion", requiredPowerShellVersion.ToString(2), componentId);
if (null != vendor)
{
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "Vendor", vendor, componentId);
}
if (null != vendorIndirect)
{
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "VendorIndirect", vendorIndirect, componentId);
}
if (null != version)
{
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, "Version", version, componentId);
}
}
///
/// Parses a FormatsFile and TypesFile element.
///
/// Element to parse.
/// Registry value name.
/// Idendifier for parent file or snap-in.
/// Identifier for parent component.
private void ParseExtensionsFile(Intermediate intermediate, IntermediateSection section, XElement node, string valueName, string id, string componentId)
{
var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(node);
string fileId = null;
string snapIn = null;
foreach (var attrib in node.Attributes())
{
if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
{
switch (attrib.Name.LocalName)
{
case "FileId":
fileId = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
snapIn = id;
break;
case "SnapIn":
fileId = id;
snapIn = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
break;
default:
this.ParseHelper.UnexpectedAttribute(node, attrib);
break;
}
}
else
{
this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, node, attrib);
}
}
if (null == fileId && null == snapIn)
{
this.Messaging.Write(PSErrors.NeitherIdSpecified(sourceLineNumbers, valueName));
}
this.ParseHelper.ParseForExtensionElements(this.Context.Extensions, intermediate, section, node);
var registryRoot = RegistryRootType.LocalMachine; // HKLM
var registryKey = String.Format(CultureInfo.InvariantCulture, KeyFormat, String.Format(CultureInfo.InvariantCulture, "!(wix.{0}_{1})", VarPrefix, snapIn), snapIn);
this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, SymbolDefinitions.File, fileId);
this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, registryRoot, registryKey, valueName, String.Format(CultureInfo.InvariantCulture, "[#{0}]", fileId), componentId, RegistryValueType.MultiString, RegistryValueActionType.Append);
}
}
}