Update of /cvsroot/wix/wix3.5/src/wix In directory sfp-cvsdas-1.v30.ch3.sourceforge.com:/tmp/cvs-serv18774/src/wix Modified Files: Binder.cs BinderExtension.cs Compiler.cs Linker.cs Wix.csproj WixComplexReferenceRow.cs Added Files: BurnWriter.cs Log Message: JaredR: Added UX/Payload/PayloadGroup to Bundle language schema. Added Parsing for Payload/PayloadGroup(Ref) to Compiler. Added flattening of UX payloads to Linker. Added binding of UX payloads into output in Binder Added "just enough" of burnexe into Binder to start generation of bundle output (stub exe with attached UX container). JaredR: Created BurnWriter to replace SectionWriter and handle PE re-writing in safe code. Modified Binder to use BurnWriter for creating Bundles. Removed SectionWriter. Removed "Allow unsafe code" from Wix.csproj JaredR: Changed Bundle to require one UX element. Added simple Chain/MsiPackage/ExePackage to Bundle language schema. Updated Binder bundle code to generate chain/package information in Burn manifest and ParameterInfo to create a basic chainer. APaloma: Changed heat to use the new MSBuild API when harvesting VS 2010 projects. Fixed WiX projects being detected as non-MSBuildable in VS 2010. Fixed discrepancies between what projects were referenced in the new generated file and which ones heat actually generated authoring for. Fixed problems with missing or wrong project information being defined in project reference variables for VS 2010. Fixed build action not being changable on the property grid in VS 2010. Fixed heat generating Directory/@Name attributes containing "..". JaredR: Changed Bundle Chain to require one package element, and updated a failing unit test to include the now-required element. Refactored Binder.ChainPackageInfo.PackageType to Compiler.ChainPackageType so that hard-coded in order to remove some hard-coded strings. Changed Payload and *Package ID generation to use deterministic CompilerCore.GenerateIdentifier() method. Added end-to-end support for Variables in Bundles. CAraman: fixes for /W4 and static analysis APaloma: Added ability for heat to harvest projects to PayloadGroups. Added better error messages for when heat is unable to build targets in an MSBuild project when harvesting projects. Fixed heat generating Ids with invalid characters when project names contain characters that aren't valid for identifiers. APaloma: Added enforcing of valid identifer characters on ComponentGroup and ComponentGroupRef. MikeHo: Fix issue with DoesShareExist code, fix static analysis errors in IIS7 CAs APaloma: Added property for project references in Votive to completely disable automatic harvesting for a reference. APaloma: SFBUG:2940161 - Fixed heat generating ComponentGroups with duplicate Ids when using the -cg switch. MiCarls: Fix some issues found by static analysis MiCarls: Fix a scenario related to website locators, broken from previous fix RobMen: introduce buxutil.lib, Burn User Experience Utility library. --- NEW FILE: BurnWriter.cs --- (This appears to be a binary file; contents omitted.) Index: Binder.cs =================================================================== RCS file: /cvsroot/wix/wix3.5/src/wix/Binder.cs,v retrieving revision 1.8 retrieving revision 1.9 diff -C2 -d -r1.8 -r1.9 *** Binder.cs 22 Jan 2010 10:05:13 -0000 1.8 --- Binder.cs 29 Jan 2010 11:40:28 -0000 1.9 *************** *** 30,33 **** --- 30,34 ---- using System.Reflection; using System.Runtime.InteropServices; + using System.Security.Cryptography; using System.Text; using System.Xml; *************** *** 47,50 **** --- 48,56 ---- private static readonly char[] tabCharacter = "\t".ToCharArray(); + private const string BurnNamespace = "http://schemas.microsoft.com/wix/2008/Burn"; + private const string BurnUXContainerEmbeddedIdFormat = "u{0}"; + private const string BurnUXContainerPayloadIdFormat = "p{0}"; + private const string BurnAttachedContainerEmbeddedIdFormat = "a{0}"; + private string emptyFile; *************** *** 505,515 **** this.core.EncounteredError = false; ! if (OutputType.Transform == output.Type) ! { ! return this.BindTransform(output, file); ! } ! else { ! return this.BindDatabase(output, file); } } --- 511,522 ---- this.core.EncounteredError = false; ! switch (output.Type) { ! case OutputType.Bundle: ! return this.BindBundle(output, file); ! case OutputType.Transform: ! return this.BindTransform(output, file); ! default: ! return this.BindDatabase(output, file); } } *************** *** 1691,1695 **** if (delayedFields.Count != 0) ! { this.ResolveDelayedFields(delayedFields, fileInformationCache); } --- 1698,1702 ---- if (delayedFields.Count != 0) ! { this.ResolveDelayedFields(delayedFields, fileInformationCache); } *************** *** 2459,2462 **** --- 2466,3017 ---- /// <summary> + /// Binds a bundle. + /// </summary> + /// <param name="bundle">The bundle to bind.</param> + /// <param name="bundleFile">The bundle to create.</param> + /// <returns>true if binding completed successfully; false otherwise</returns> + private bool BindBundle(Output bundle, string bundleFile) + { + // First look for data we expect to find... Chain, WixGroups, etc. + Table chainPackageTable = bundle.Tables["ChainPackage"]; + if (null == chainPackageTable || 0 == chainPackageTable.Rows.Count) + { + // We shouldn't really get past the linker phase if there are + // no group items... that means that there's no UX, no Chain, + // *and* no Containers! + throw new WixException(WixErrors.MissingBundleInformation("ChainPackage")); + } + + Table wixGroupTable = bundle.Tables["WixGroup"]; + if (null == wixGroupTable || 0 == wixGroupTable.Rows.Count) + { + // We shouldn't really get past the linker phase if there are + // no group items... that means that there's no UX, no Chain, + // *and* no Containers! + throw new WixException(WixErrors.MissingBundleInformation("WixGroup")); + } + + foreach (BinderExtension extension in this.extensions) + { + extension.BundleInitialize(bundle); + } + + // To make lookups easier, we load the constituent data bottom-up, so + // that we can index by ID. + Table variableTable = bundle.Tables["Variable"]; + // TODO: Do we need a dictionary for variables? + List<VariableInfo> allVariables = new List<VariableInfo>(); + if (null != variableTable && 0 < variableTable.Rows.Count) + { + foreach (Row row in variableTable.Rows) + { + allVariables.Add(new VariableInfo(row)); + } + } + + // For now, we have only UX payloads... this may need to get refactored + // into more sensible parts as the Chain/MsiPackages and Containers + // get implemented end-to-end. + Table payloadTable = bundle.Tables["Payload"]; + Dictionary<string, PayloadInfo> allPayloads = new Dictionary<string, PayloadInfo>(); + foreach (Row row in payloadTable.Rows) + { + PayloadInfo payloadInfo = new PayloadInfo(row, this.FileManager); + allPayloads.Add(payloadInfo.Id, payloadInfo); + } + + // Get the UX payloads... + List<PayloadInfo> uxPayloads = new List<PayloadInfo>(); + foreach (Row row in wixGroupTable.Rows) + { + string rowParentName = (string)row[0]; + string rowParentType = (string)row[1]; + string rowChildName = (string)row[2]; + string rowChildType = (string)row[3]; + + if ("UX" == rowParentType && "WixUXContainer" == rowParentName && "Payload" == rowChildType) + { + uxPayloads.Add(allPayloads[rowChildName]); + } + } + + // If we didn't get any UX payloads, we provide a default... + if (0 == uxPayloads.Count) + { + string wixExeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + foreach (string uxFile in new string[] { "stdux.dll", "thm.xml", "en-us.wxl", "stdux.png", "tou.htm" }) + { + uxPayloads.Add(new PayloadInfo(null, null, Path.Combine(wixExeDirectory, uxFile), this.FileManager)); + } + } + + // Get the chain packages... + Dictionary<string, ChainPackageInfo> allPackages = new Dictionary<string, ChainPackageInfo>(); + foreach (Row row in chainPackageTable.Rows) + { + ChainPackageInfo packageInfo = new ChainPackageInfo(row, this.FileManager, this.core); + allPackages.Add(packageInfo.Id, packageInfo); + } + + // TODO: Read this list from the flattened group table, rather than re-creating + // the ChainPackage table directly. + List<ChainPackageInfo> chainPackages = new List<ChainPackageInfo>(); + foreach (Row row in chainPackageTable.Rows) + { + chainPackages.Add(allPackages[(string)row[0]]); + } + + foreach (BinderExtension extension in this.extensions) + { + extension.BundleFinalize(bundle); + } + + // As more parts of the bundle get implemented, we may prefer to + // make some of the local variables here into members of Binder + // so that we don't have to pass them explicitly to the sub-methods. + // For now, though, this isn't too bad. + Guid bundleGuid = Guid.NewGuid(); + string stubFile = null; + + // default to burnstub.exe if the source manifest didn't say otherwise + if (String.IsNullOrEmpty(stubFile)) + { + string wixExeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + stubFile = Path.Combine(wixExeDirectory, "burnstub.exe"); + } + + // start with a writable copy of the stub .exe + this.core.OnMessage(WixVerboses.GeneratingBundle(bundleFile, stubFile)); + File.Copy(stubFile, bundleFile, true); + File.SetAttributes(bundleFile, File.GetAttributes(bundleFile) & ~System.IO.FileAttributes.ReadOnly); + + // Create our manifests, CABs and final EXE... + bool perMachineBundle = true; + RegistrationInfo registrationInfo = null; + this.GetChainInformation(chainPackages, ref perMachineBundle, ref registrationInfo); + string parameterInfoPath = this.CreateBurnParameterInfo(bundleFile, bundleGuid, allVariables, uxPayloads, chainPackages, perMachineBundle, registrationInfo); + string manifestPath = this.CreateBurnManifest(allVariables, uxPayloads, chainPackages, perMachineBundle, registrationInfo); + + string manifestUXContainerPath = Path.Combine(this.TempFilesLocation, "bundle-ux.cab"); + string attachedContainerPath = null; + + this.CreateContainer(manifestUXContainerPath, uxPayloads, manifestPath, Binder.BurnUXContainerEmbeddedIdFormat); + + // create the attached container, if it is needed. + // TODO: properly determine which packages/payloads go into the attached container... + if (0 < chainPackages.Count) + { + attachedContainerPath = Path.Combine(this.TempFilesLocation, "bundle-attached.cab"); + // CreateContainer needs a list of PayloadInfo... + List<PayloadInfo> chainPayloads = new List<PayloadInfo>(); + foreach (ChainPackageInfo package in chainPackages) + { + chainPayloads.Add(package); + // TODO: add any dependent payloads? + } + this.CreateContainer(attachedContainerPath, chainPayloads, null, Binder.BurnAttachedContainerEmbeddedIdFormat); + } + + // update the .wixburn section to point to at the UX and attached container(s) then attach the container(s). + using (BurnWriter writer = new BurnWriter(bundleFile, this.core, bundleGuid)) + { + writer.AppendContainer(manifestUXContainerPath, BurnWriter.Container.UX); + + if (!String.IsNullOrEmpty(attachedContainerPath)) + { + writer.AppendContainer(attachedContainerPath, BurnWriter.Container.Attached); + } + } + + return !this.core.EncounteredError; + } + + private void GetChainInformation(List<ChainPackageInfo> chainPackages, ref bool perMachineBundle, ref RegistrationInfo registrationInfo) + { + foreach (ChainPackageInfo package in chainPackages) + { + if (perMachineBundle && !package.PerMachine) + { + this.core.OnMessage(WixVerboses.SwitchingToPerUserPackage(package.FileInfo.FullName)); + perMachineBundle = false; + } + + if (package.RegistrationInfo != null) + { + registrationInfo = package.RegistrationInfo; + } + } + } + + private void CreateContainer(string containerPath, List<PayloadInfo> payloads, string manifestFile, string embeddedIdFormat) + { + int payloadCount = payloads.Count; // The number of embedded payloads + if (!String.IsNullOrEmpty(manifestFile)) + { + ++payloadCount; + } + + using (WixCreateCab cab = new WixCreateCab(Path.GetFileName(containerPath), Path.GetDirectoryName(containerPath), payloadCount, 0, 0, this.defaultCompressionLevel)) + { + // If a manifest was provided always add it as "payload 0" to the container. + if (!String.IsNullOrEmpty(manifestFile)) + { + cab.AddFile(manifestFile, "0"); + } + + for (int payloadIndex = 0; payloadIndex < payloads.Count; ++payloadIndex) + { + PayloadInfo payload = payloads[payloadIndex]; + this.core.OnMessage(WixVerboses.LoadingPayload(payload.FileInfo.FullName)); + cab.AddFile(payload.FileInfo.FullName, String.Format(CultureInfo.InvariantCulture, embeddedIdFormat, payloadIndex)); + } + + cab.Complete(); + } + } + + + private string CreateBurnParameterInfo(string bundleFile, Guid bundleGuid, List<VariableInfo> allVariables, List<PayloadInfo> uxPayloads, List<ChainPackageInfo> chainPackages, bool perMachineBundle, RegistrationInfo registrationInfo) + { + string parameterInfoPath = Path.Combine(this.TempFilesLocation, "bundle-paraminfo.xml"); + using (XmlTextWriter writer = new XmlTextWriter(parameterInfoPath, Encoding.Unicode)) + { + writer.Formatting = Formatting.Indented; + writer.WriteStartDocument(); + writer.WriteStartElement("Setup", "http://schemas.microsoft.com/Setup/2008/01/im"); + writer.WriteAttributeString("SetupVersion", "1.0"); + + writer.WriteStartElement("UI"); + writer.WriteAttributeString("Name", "UX"); + writer.WriteAttributeString("Version", "Ignored"); + writer.WriteAttributeString("Dll", uxPayloads[0].FileName); + writer.WriteEndElement(); + + // Write the variables + foreach (VariableInfo variable in allVariables) + { + writer.WriteStartElement("Variable"); + writer.WriteAttributeString("Id", variable.Id); + writer.WriteAttributeString("Value", variable.Value); + writer.WriteAttributeString("Type", variable.Type); + writer.WriteEndElement(); + } + + //// Copy over searches. + //foreach (XmlElement searchElement in manifest.DocumentElement.SelectNodes("burner:DirectorySearch | burner:FileSearch | burner:RegistrySearch | burner:MsiComponentSearch | burner:MsiProductSearch", namespaceManager)) + //{ + // Burner.CopyElement(searchElement, writer); + //} + + writer.WriteStartElement("EnterMaintenanceModeIf"); + writer.WriteStartElement("NeverTrue"); + writer.WriteEndElement(); + writer.WriteEndElement(); + + // Create the Ux element. + int payloadIndex = 0; + writer.WriteStartElement("Ux"); + writer.WriteAttributeString("UxDllPayloadId", String.Format(CultureInfo.InvariantCulture, Binder.BurnUXContainerPayloadIdFormat, payloadIndex)); + + for (payloadIndex = 0; payloadIndex < uxPayloads.Count; ++payloadIndex) + { + PayloadInfo payload = uxPayloads[payloadIndex]; + writer.WriteStartElement("Payload"); + writer.WriteAttributeString("Id", String.Format(CultureInfo.InvariantCulture, Binder.BurnUXContainerPayloadIdFormat, payloadIndex)); + writer.WriteAttributeString("FilePath", payload.FileName); + writer.WriteAttributeString("FileSize", payload.FileSize.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Sha1Hash", payload.Sha1); + // TODO: when we get to containers, we may want + // to refactor this code to a separate method (as it was + // in burnexe) and then we'll need embeddedId as a member + // of the PayloadInfo. + //if (!String.IsNullOrEmpty(embeddedId)) + //{ + writer.WriteAttributeString("Packaging", "embedded"); + writer.WriteAttributeString("SourcePath", String.Format(CultureInfo.InvariantCulture, Binder.BurnUXContainerEmbeddedIdFormat, payloadIndex)); + //} + // TODO: Add downloadUrl to payloadinfo... + //else if (null != downloadUrlAttribute) + //{ + // writer.WriteAttributeString("Packaging", "download"); + //} + //else + //{ + // writer.WriteAttributeString("Packaging", "external"); + // writer.WriteAttributeString("SourcePath", payload.FileName); + //} + writer.WriteEndElement(); + } + + writer.WriteEndElement(); + + // Create the Registration element and Arp element if appropriate. + writer.WriteStartElement("Registration"); + writer.WriteAttributeString("Id", bundleGuid.ToString("B")); + writer.WriteAttributeString("PerMachine", perMachineBundle ? "yes" : "no"); + writer.WriteAttributeString("ExecutableName", Path.GetFileName(bundleFile)); + + if (null != registrationInfo) + { + writer.WriteStartElement("Arp"); + + // skip this attribute because it is on the registration element instead. + //writer.WriteAttributeString("PerMachine", perMachineBundle ? "yes" : "no"); + writer.WriteAttributeString("DisplayName", registrationInfo.Name); + writer.WriteAttributeString("Publisher", registrationInfo.Publisher); + writer.WriteAttributeString("HelpLink", registrationInfo.HelpLink); + writer.WriteAttributeString("HelpTelephone", registrationInfo.HelpTelephone); + writer.WriteAttributeString("AboutUrl", registrationInfo.AboutUrl); + writer.WriteAttributeString("UpdateUrl", registrationInfo.UpdateUrl); + + if (registrationInfo.DisableModify) + { + writer.WriteAttributeString("DisableModify", "yes"); + } + + if (registrationInfo.DisableRepair) + { + writer.WriteAttributeString("DisableRepair", "yes"); + } + + if (registrationInfo.DisableRemove) + { + writer.WriteAttributeString("DisableRemove", "yes"); + } + + writer.WriteEndElement(); + } + writer.WriteEndElement(); + + // Now add all of the packages. + writer.WriteStartElement("Items"); + writer.WriteAttributeString("OnSubFailureAction", "Rollback"); + + foreach (ChainPackageInfo package in chainPackages) + { + Compiler.ChainPackageType packageType = package.ChainPackageType; + + // TODO: Is this really MSIs only, or MS*s? + writer.WriteStartElement((Compiler.ChainPackageType.Msi == packageType) ? "MSI" : "Exe"); + writer.WriteAttributeString("Id", package.Id); + writer.WriteAttributeString("Name", package.FileName); + writer.WriteAttributeString("DownloadSize", package.FileSize.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Sha1HashValue", package.Sha1); + writer.WriteAttributeString("InstalledProductSize", package.FileSize.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("EstimatedInstallTime", "0"); // TODO: Burnexe hard-coded this to zero; should we have a value? + writer.WriteAttributeString("SystemDriveSize", "0"); // TODO: Burnexe hard-coded this to zero; should we have a value? + writer.WriteAttributeString("CanonicalTargetName", "Why Does the Engine need Product Name"); + writer.WriteAttributeString("PerMachine", package.PerMachine ? "true" : "false"); + writer.WriteAttributeString("Rollback", "true"); + writer.WriteAttributeString("Cache", package.Cache ? "true" : "false"); + + // if (packageElement.HasAttribute("DownloadUrl")) + // { + // writer.WriteAttributeString("URL", packageElement.Attributes["DownloadUrl"].Value); + // } + + switch (packageType) + { + // TODO: Is this really MSIs only, or MS*s? + case Compiler.ChainPackageType.Msi: + { + writer.WriteAttributeString("ProductCode", package.ProductCode); + writer.WriteAttributeString("ProductVersion", package.ProductVersion); + + StringBuilder sb = new StringBuilder(); + // foreach (XmlElement msiPropertyElement in packageElement.SelectNodes("burner:MsiProperty", namespaceManager)) + // { + // if (0 < sb.Length) + // { + // sb.Append(" "); + // } + + // sb.AppendFormat("{0}=\"{1}\"", msiPropertyElement.Attributes["Id"].Value, msiPropertyElement.Attributes["Value"].Value); + // } + + if (0 < sb.Length) + { + writer.WriteAttributeString("MSIOption", sb.ToString()); + } + } + break; + //case Compiler.ChainPackageType.Msp: + // break; + //case Compiler.ChainPackageType.Msu: + // break; + case Compiler.ChainPackageType.Exe: + writer.WriteAttributeString("InstallCommandLine", package.InstallCommand); + writer.WriteAttributeString("RepairCommandLine", package.RepairCommand); + writer.WriteAttributeString("UninstallCommandLine", package.UninstallCommand); + break; + } + + writer.WriteStartElement("IsPresent"); + // TODO: Is this really MSIs only, or MS*s? + if (Compiler.ChainPackageType.Msi == packageType) + { + writer.WriteStartElement("Exists"); + writer.WriteStartElement("MsiProductVersion"); + writer.WriteAttributeString("ProductCode", "Self"); + writer.WriteEndElement(); + writer.WriteEndElement(); + } + // TODO: Is this really non-MSIs only, or EXEs only? + else + { + // TODO: this will cause executables to never be "installed". Need to use the detect condition instead. + writer.WriteStartElement("NeverTrue"); + writer.WriteEndElement(); + } + writer.WriteEndElement(); + + writer.WriteStartElement("ApplicableIf"); + writer.WriteStartElement("AlwaysTrue"); + writer.WriteEndElement(); + writer.WriteEndElement(); + + writer.WriteStartElement("ActionTable"); + writer.WriteStartElement("InstallAction"); + writer.WriteAttributeString("IfPresent", "noop"); + writer.WriteAttributeString("IfAbsent", "install"); + writer.WriteEndElement(); + writer.WriteStartElement("UninstallAction"); + writer.WriteAttributeString("IfPresent", "uninstall"); + writer.WriteAttributeString("IfAbsent", "noop"); + writer.WriteEndElement(); + writer.WriteStartElement("RepairAction"); + writer.WriteAttributeString("IfPresent", "repair"); + writer.WriteAttributeString("IfAbsent", "install"); + writer.WriteEndElement(); + writer.WriteEndElement(); + + writer.WriteEndElement(); + } + + writer.WriteEndElement(); + + writer.WriteEndDocument(); + } + + // Notice that the ParameterInfo.xml's "Ux" element above does not include a reference + // for itself. It is impossible to get the SHA1 hash for the ParameterInfo.xml then update the + // ParameterInfo.xml with data about itself. So only the burner.manifest will know about the + // ParameterInfo.xml file. + uxPayloads.Add(new PayloadInfo(null, "ParameterInfo.xml", parameterInfoPath, this.FileManager)); + + return parameterInfoPath; + } + + private string CreateBurnManifest(List<VariableInfo> allVariables, List<PayloadInfo> uxPayloads, List<ChainPackageInfo> chainPackages, bool perMachineBundle, RegistrationInfo registrationInfo) + { + string manifestPath = Path.Combine(this.TempFilesLocation, "bundle-manifest.xml"); + this.core.OnMessage(WixVerboses.ResolvingManifest(manifestPath)); + using (XmlTextWriter writer = new XmlTextWriter(manifestPath, Encoding.UTF8)) + { + writer.WriteStartDocument(); + + writer.WriteStartElement("BurnManifest", Binder.BurnNamespace); + + // Write the variables + foreach (VariableInfo variable in allVariables) + { + writer.WriteStartElement("Variable"); + writer.WriteAttributeString("Id", variable.Id); + writer.WriteAttributeString("Value", variable.Value); + writer.WriteAttributeString("Type", variable.Type); + writer.WriteEndElement(); + } + + // TODO: write the searches... + + // write the first UX payload / entrypoint + int payloadIndex = 0; + writer.WriteStartElement("UX"); + WriteBurnManifestPayloadAttributes(writer, uxPayloads[payloadIndex], Binder.BurnUXContainerEmbeddedIdFormat, payloadIndex); + + // write the remaining UX allPayloads... + for (payloadIndex = 1; payloadIndex < uxPayloads.Count; ++payloadIndex) + { + writer.WriteStartElement("Resource"); + WriteBurnManifestPayloadAttributes(writer, uxPayloads[payloadIndex], Binder.BurnUXContainerEmbeddedIdFormat, payloadIndex); + writer.WriteEndElement(); + } + + writer.WriteEndElement(); + + // Write the registration information... + if (registrationInfo != null) + { + writer.WriteStartElement("Registration"); + + writer.WriteAttributeString("PerMachine", perMachineBundle ? "yes" : "no"); + writer.WriteAttributeString("DisplayName", registrationInfo.Name); + writer.WriteAttributeString("Publisher", registrationInfo.Publisher); + writer.WriteAttributeString("HelpLink", registrationInfo.HelpLink); + writer.WriteAttributeString("HelpTelephone", registrationInfo.HelpTelephone); + writer.WriteAttributeString("AboutUrl", registrationInfo.AboutUrl); + writer.WriteAttributeString("UpdateUrl", registrationInfo.UpdateUrl); + + if (registrationInfo.DisableModify) + { + writer.WriteAttributeString("DisableModify", "yes"); + } + + if (registrationInfo.DisableRepair) + { + writer.WriteAttributeString("DisableRepair", "yes"); + } + + if (registrationInfo.DisableRemove) + { + writer.WriteAttributeString("DisableRemove", "yes"); + } + + writer.WriteEndElement(); + } + + // write the Chain... + writer.WriteStartElement("Chain"); + int packageIndex = 0; + foreach (ChainPackageInfo package in chainPackages) + { + writer.WriteStartElement(String.Format(CultureInfo.InvariantCulture, "{0}Package", package.ChainPackageType)); + + // TODO: we aren't guaranteed to be attached here, are we? + WriteBurnManifestPayloadAttributes(writer, package, Binder.BurnAttachedContainerEmbeddedIdFormat, packageIndex++); + + writer.WriteAttributeString("Register", package.Register ? "yes" : "no"); + writer.WriteAttributeString("PerMachine", package.PerMachine ? "yes" : "no"); + writer.WriteAttributeString("ProductCode", package.ProductCode); + writer.WriteAttributeString("ProductVersion", package.ProductVersion); + writer.WriteAttributeString("Cache", package.Cache ? "yes" : "no"); + writer.WriteAttributeString("Uninstall", package.Uninstall ? "yes" : "no"); + + if (Compiler.ChainPackageType.Exe == package.ChainPackageType) + { + writer.WriteAttributeString("InstallArguments", package.InstallCommand); + writer.WriteAttributeString("RepairArguments", package.RepairCommand); + writer.WriteAttributeString("UninstallArguments", package.UninstallCommand); + } + + writer.WriteEndElement(); + } + writer.WriteEndElement(); + + writer.WriteEndDocument(); + } + + return manifestPath; + } + + private static void WriteBurnManifestPayloadAttributes(XmlTextWriter writer, PayloadInfo payload, string embeddedIdFormat, int embeddedIdValue) + { + writer.WriteAttributeString("FileName", payload.FileName); + writer.WriteAttributeString("FileSize", payload.FileSize.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("SHA1", payload.Sha1); + writer.WriteAttributeString("EmbeddedId", String.Format(CultureInfo.InvariantCulture, embeddedIdFormat, embeddedIdValue)); + } + + /// <summary> /// Binds a transform. /// </summary> *************** *** 4300,4304 **** private void ProcessLayoutDirectory(string path, Stack parents, RowCollection dirs, RowCollection refs, RowCollection files, ArrayList fileTransfers) { ! Row thisDir = (Row) parents.Peek(); string currentDir = Path.Combine(path, thisDir[1].ToString()); foreach (Row row in files) --- 4855,4859 ---- private void ProcessLayoutDirectory(string path, Stack parents, RowCollection dirs, RowCollection refs, RowCollection files, ArrayList fileTransfers) { ! Row thisDir = (Row)parents.Peek(); string currentDir = Path.Combine(path, thisDir[1].ToString()); foreach (Row row in files) *************** *** 4909,4912 **** --- 5464,5727 ---- } } + + /// <summary> + /// Payload info for binding Bundles. + /// </summary> + private class PayloadInfo + { + private string id; + private string name; + private FileInfo fileInfo; + + public PayloadInfo(Row row, BinderFileManager fileManager) + : this((string)row[0], (string)row[1], (string)row[2], fileManager) + { + } + + public PayloadInfo(string id, string name, string sourceFile, BinderFileManager fileManager) + { + this.id = id; + this.name = name; + this.fileInfo = new FileInfo(fileManager.ResolveFile(sourceFile)); + } + + public string Id { get { return this.id; } } + public FileInfo FileInfo { get { return this.fileInfo; } } + public string FileName { get { return (!String.IsNullOrEmpty(this.name) ? this.name : this.fileInfo.Name); } } + public long FileSize { get { return this.fileInfo.Length; } } + + private string sha1; + public string Sha1 + { + get + { + if (String.IsNullOrEmpty(this.sha1)) + { + byte[] hashBytes; + using (SHA1Managed managed = new SHA1Managed()) + { + using (FileStream stream = this.fileInfo.OpenRead()) + { + hashBytes = managed.ComputeHash(stream); + } + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hashBytes.Length; i++) + { + sb.AppendFormat("{0:X2}", hashBytes[i]); + } + this.sha1 = sb.ToString(); + } + + return this.sha1; + } + } + } + + /// <summary> + /// Chain package info for binding Bundles. + /// </summary> + private class ChainPackageInfo : PayloadInfo + { + private const string propertySqlFormat = "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'"; + + public ChainPackageInfo(Row row, BinderFileManager fileManager, BinderCore core) + : this((string)row[0], (string)row[1], (string)row[2], (string)row[3], (string)row[4], (string)row[5], (string)row[6], fileManager, core) + { + } + + public ChainPackageInfo(string id, string packageType, string name, string sourceFile, + string installCommand, string repairCommand, string uninstallCommand, + BinderFileManager fileManager, BinderCore core) + : base(id, name, sourceFile, fileManager) + { + this.ChainPackageType = (Compiler.ChainPackageType)Enum.Parse(typeof(Compiler.ChainPackageType), packageType, true); + this.InstallCommand = installCommand; + this.RepairCommand = repairCommand; + this.UninstallCommand = uninstallCommand; + + this.PerMachine = true; + this.ProductCode = null; + this.ProductVersion = null; + this.Register = false; + this.Cache = false; + this.Uninstall = false; + + // TODO: might have better perf to resolve just-in-time, but this is easier for now. + switch (this.ChainPackageType) + { + case Compiler.ChainPackageType.Msi: + this.ResolveMsiPackage(core); + break; + + //case Compiler.ChainPackageType.Msp: + // break; + + //case Compiler.ChainPackageType.Msu: + // break; + + case Compiler.ChainPackageType.Exe: + // TODO: Nothing to do here? + break; + } + } + + public Compiler.ChainPackageType ChainPackageType { get; private set; } + public string InstallCommand { get; private set; } + public string RepairCommand { get; private set; } + public string UninstallCommand { get; private set; } + + public bool PerMachine { get; private set; } + public string ProductCode { get; private set; } + public string ProductVersion { get; private set; } + public bool Register { get; private set; } + public bool Cache { get; private set; } + public bool Uninstall { get; private set; } + + public RegistrationInfo RegistrationInfo { get; private set; } + + private void ResolveMsiPackage(BinderCore core) + { + // Read data out of the msi database... + using (Microsoft.Deployment.WindowsInstaller.SummaryInfo sumInfo = new Microsoft.Deployment.WindowsInstaller.SummaryInfo(this.FileInfo.FullName, false)) + { + // 8 is the Word Count summary information stream bit that means + // "Elevated privileges are not required to install this package." + // in MSI 4.5 and below, if this bit is 0, elevation is required. + this.PerMachine = 0 == (sumInfo.WordCount & 8); + } + + using (Microsoft.Deployment.WindowsInstaller.Database db = new Microsoft.Deployment.WindowsInstaller.Database(this.FileInfo.FullName)) + { + try + { + this.ProductCode = ChainPackageInfo.GetProperty(db, "ProductCode"); + this.ProductVersion = ChainPackageInfo.GetProperty(db, "ProductVersion"); + + // TODO: read these from MSI or add to ChainPackage table? + this.Cache = true; + this.Uninstall = true; + + // TODO: Burn supported a "Register" attribute on a package element, which + // would set this.Register to true. Do we need a similar construct in + // Bundle/Chain/MsiPackage, or can we rely on the "ARPSYSTEMCOMPONENT" + // property? (Burn also had a note about warning when the two conditions + // were in conflict. + this.Register = HasProperty(db, "ARPSYSTEMCOMPONENT"); + + if (this.Register) + { + this.RegistrationInfo = new RegistrationInfo(); + this.RegistrationInfo.Name = ChainPackageInfo.GetProperty(db, "ProductName"); + this.RegistrationInfo.Publisher = ChainPackageInfo.GetProperty(db, "Manufacturer"); + this.RegistrationInfo.AboutUrl = ChainPackageInfo.GetProperty(db, "ARPURLINFOABOUT"); + this.RegistrationInfo.HelpLink = ChainPackageInfo.GetProperty(db, "ARPHELPLINK"); + this.RegistrationInfo.HelpTelephone = ChainPackageInfo.GetProperty(db, "ARPHELPTELEPHONE"); + this.RegistrationInfo.UpdateUrl = ChainPackageInfo.GetProperty(db, "ARPURLUPDATEINFO"); + this.RegistrationInfo.DisableModify = ChainPackageInfo.HasProperty(db, "ARPNOMODIFY"); + this.RegistrationInfo.DisableRemove = ChainPackageInfo.HasProperty(db, "ARPNOREMOVE"); + this.RegistrationInfo.DisableRepair = ChainPackageInfo.HasProperty(db, "ARPNOREPAIR"); + } + + // TODO: add all external cabinets as package resources + //foreach (string cabinet in db.ExecuteStringQuery("SELECT `Cabinet` FROM `Media`")) + //{ + // if (!String.IsNullOrEmpty(cabinet) && !cabinet.StartsWith("#")) + // { + // // TODO: add cabinet to attached container? + // // TODO: what if this package is external? + // this.CreatePayload(element, Path.Combine(info.DirectoryName, cabinet), null, this.attachedContainer); + // } + //} + } + catch (Microsoft.Deployment.WindowsInstaller.InstallerException e) + { + core.OnMessage(WixErrors.UnexpectedException(e.Message, "InstallerException", e.StackTrace)); + } + } + } + + /// <summary> + /// Queries a Windows Installer database to determine if one or more rows exist in the Property table. + /// </summary> + /// <param name="db">Database to query.</param> + /// <param name="property">Property to examine.</param> + /// <returns>True if query matches at least one result.</returns> + private static bool HasProperty(Microsoft.Deployment.WindowsInstaller.Database db, string property) + { + try + { + return 0 < db.ExecuteQuery(PropertyQuery(property)).Count; + } + catch (Microsoft.Deployment.WindowsInstaller.InstallerException) + { + } + + return false; + } + + /// <summary> + /// Queries a Windows Installer database for a Property value. + /// </summary> + /// <param name="db">Database to query.</param> + /// <param name="property">Property to examine.</param> + /// <returns>String value for result or null if query doesn't match a single result.</returns> + private static string GetProperty(Microsoft.Deployment.WindowsInstaller.Database db, string property) + { + try + { + return db.ExecuteScalar(PropertyQuery(property)).ToString(); + } + catch (Microsoft.Deployment.WindowsInstaller.InstallerException) + { + } + + return null; + } + + private static string PropertyQuery(string property) + { + // quick sanity check that we'll be creating a valid query... + // TODO: Are there any other special characters we should be looking for? + Debug.Assert(!property.Contains("'")); + return String.Format(CultureInfo.InvariantCulture, propertySqlFormat, property); + } + } + + /// <summary> + /// Add/Remove Programs registration for the bundle. + /// </summary> + private class RegistrationInfo + { + public string Name { get; set; } + public string Publisher { get; set; } + public string HelpLink { get; set; } + public string HelpTelephone { get; set; } + public string AboutUrl { get; set; } + public string UpdateUrl { get; set; } + public bool DisableModify { get; set; } + public bool DisableRepair { get; set; } + public bool DisableRemove { get; set; } + } + + private class VariableInfo + { + public VariableInfo(Row row) + : this((string)row[0], (string)row[1], (string)row[2]) + { + } + + public VariableInfo(string id, string value, string type) + { + this.Id = id; + this.Value = value; + this.Type = type; + } + + public string Id { get; private set; } + public string Value { get; private set; } + public string Type { get; private set; } + } } } Index: Wix.csproj =================================================================== RCS file: /cvsroot/wix/wix3.5/src/wix/Wix.csproj,v retrieving revision 1.5 retrieving revision 1.6 diff -C2 -d -r1.5 -r1.6 *** Wix.csproj 17 Jan 2010 07:51:36 -0000 1.5 --- Wix.csproj 29 Jan 2010 11:40:28 -0000 1.6 *************** *** 7,10 **** --- 7,11 ---- <NoWarn>618</NoWarn> <OldToolsVersion>2.0</OldToolsVersion> + <ProductVersion>9.0.30729</ProductVersion> </PropertyGroup> <ItemGroup> *************** *** 18,21 **** --- 19,23 ---- <Compile Include="BinderExtension.cs" /> <Compile Include="BinderFileManager.cs" /> + <Compile Include="BurnWriter.cs" /> <Compile Include="CabinetBuilder.cs" /> <Compile Include="CabinetWorkItem.cs" /> *************** *** 230,233 **** --- 232,238 ---- <Reference Include="System.Windows.Forms" /> <Reference Include="System.Xml" /> + <Reference Include="Microsoft.Deployment.WindowsInstaller"> + <HintPath>$(TargetDir)</HintPath> + </Reference> </ItemGroup> </Project> \ No newline at end of file Index: BinderExtension.cs =================================================================== RCS file: /cvsroot/wix/wix3.5/src/wix/BinderExtension.cs,v retrieving revision 1.1.1.1 retrieving revision 1.2 diff -C2 -d -r1.1.1.1 -r1.2 *** BinderExtension.cs 29 May 2009 10:43:38 -0000 1.1.1.1 --- BinderExtension.cs 29 Jan 2010 11:40:28 -0000 1.2 *************** *** 66,69 **** --- 66,83 ---- { } + + /// <summary> + /// Called before bundle binding occurs. + /// </summary> + public virtual void BundleInitialize(Output bundle) + { + } + + /// <summary> + /// Called after all output changes occur and right before the output is bound into its final format. + /// </summary> + public virtual void BundleFinalize(Output bundle) + { + } } } \ No newline at end of file Index: WixComplexReferenceRow.cs =================================================================== RCS file: /cvsroot/wix/wix3.5/src/wix/WixComplexReferenceRow.cs,v retrieving revision 1.1.1.1 retrieving revision 1.2 diff -C2 -d -r1.1.1.1 -r1.2 *** WixComplexReferenceRow.cs 29 May 2009 10:43:42 -0000 1.1.1.1 --- WixComplexReferenceRow.cs 29 Jan 2010 11:40:28 -0000 1.2 *************** *** 45,49 **** /// <summary>Product parent of complex reference.</summary> ! Product } --- 45,55 ---- /// <summary>Product parent of complex reference.</summary> ! Product, ! ! /// <summary>UX parent of complex reference.</summary> ! UX, ! ! /// <summary>PayloadGroup parent of complex reference.</summary> ! PayloadGroup, } *************** *** 69,73 **** /// <summary>Module child of complex reference.</summary> ! Module } --- 75,85 ---- /// <summary>Module child of complex reference.</summary> ! Module, ! ! /// <summary>Payload child of complex reference.</summary> ! Payload, ! ! /// <summary>PayloadGroup child of complex reference.</summary> ! PayloadGroup, } Index: Linker.cs =================================================================== RCS file: /cvsroot/wix/wix3.5/src/wix/Linker.cs,v retrieving revision 1.5 retrieving revision 1.6 diff -C2 -d -r1.5 -r1.6 *** Linker.cs 17 Jan 2010 07:51:36 -0000 1.5 --- Linker.cs 29 Jan 2010 11:40:28 -0000 1.6 *************** *** 23,26 **** --- 23,27 ---- using System.Collections.ObjectModel; using System.Collections.Specialized; + using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; *************** *** 468,471 **** --- 469,476 ---- break; + case "ChainPackage": + copyRows = true; + break; + case "CustomAction": if (OutputType.Module == this.activeOutput.Type) *************** *** 589,592 **** --- 594,601 ---- break; + case "Variable": + copyRows = true; + break; + case "WixAction": if (this.sectionIdOnRows) *************** *** 646,649 **** --- 655,662 ---- break; + case "WixGroup": + copyRows = true; + break; + case "WixInstanceTransforms": copyRows = true; *************** *** 1030,1033 **** --- 1043,1054 ---- } + // Bundles have groups of data that must be flattened in a way different from other types. + this.FlattenBundleGroups(output); + + if (this.encounteredError) + { + return null; + } + this.CreateInstanceTransforms(output); this.CheckOutputConsistency(output); *************** *** 1930,1933 **** --- 1951,2075 ---- /// <summary> + /// Flattens the groups used in a Bundle. + /// </summary> + /// <param name="output">Output containing the groups to process.</param> + private void FlattenBundleGroups(Output output) + { + if (OutputType.Bundle != output.Type) + { + return; + } + + // We need to flatten the nested PayloadGroups and PackageGroups under + // UX, Chain, and any Containers. When we're done, the WixGroups table + // will hold Payloads under UX, ChainPackages (references?) under Chain, + // and ChainPackages/Payloads under the attached and any detatched + // Containers. + Table wixGroupTable = output.Tables["WixGroup"]; + if (null == wixGroupTable || wixGroupTable.Rows.Count == 0) + { + this.OnMessage(WixErrors.MissingBundleInformation("WixGroup")); + } + + Dictionary<string, int> referencedItems = new Dictionary<string, int>(); + List<Row> rowsInUx = new List<Row>(); + + AddGroupMembersToList(wixGroupTable, rowsInUx, referencedItems, "UX", "WixUXContainer", "Payload", "PayloadGroup"); + + // Remove the original rows from the table and add the flattened ones back in... + List<int> rowsUsed = new List<int>(referencedItems.Values); + rowsUsed.Sort(); + rowsUsed.Reverse(); + foreach (int iRow in rowsUsed) + { + wixGroupTable.Rows.RemoveAt(iRow); + } + + foreach (Row row in rowsInUx) + { + // Update the parent type/name to the proper parent. + row[1] = "UX"; + row[0] = "WixUXContainer"; + wixGroupTable.Rows.Add(row); + } + + // TODO: Flatten/re-order Chain groups (once they are supported) + // TODO: Flatten/re-order Searches (once they are supported) + } + + /// <summary> + /// Climbs through the WixGroup table and flattens a nested group. + /// </summary> + /// <param name="wixGroupTable">Table to use (should be 'WixGroup').</param> + /// <param name="newRows">List of rows ultimately included in the group.</param> + /// <param name="referencedItems">Set of items already referenced to prevent loops and to collect row indices to remove later.</param> + /// <param name="groupParentType">Type of group parent to match.</param> + /// <param name="groupParentName">Name/Id of group parent to match.</param> + /// <param name="groupTypeInclude">Group type to include directly in list.</param> + /// <param name="groupTypeRecurse">Group type on which to recurse.</param> + private void AddGroupMembersToList(Table wixGroupTable, List<Row> newRows, Dictionary<string, int> referencedItems, string groupParentType, string groupParentName, string groupTypeInclude, string groupTypeRecurse) + { + for (int iRow = 0; iRow < wixGroupTable.Rows.Count; ++iRow) + { + Row row = wixGroupTable.Rows[iRow]; + string rowParentName = (string)row[0]; + string rowParentType = (string)row[1]; + + if (groupParentType == rowParentType && groupParentName == rowParentName) + { + string rowChildName = (string)row[2]; + string rowChildType = (string)row[3]; + string childKey = String.Format(CultureInfo.InvariantCulture, "{0}_{1}", rowChildType, rowChildName); + + // If this child has already been referenced, we don't + // want to include it again. This also prevents expanding + // a sub-group more than once. + if (referencedItems.ContainsKey(childKey)) + { + // We do still want to track the row for later removal, + // so we use a fake key, but we have to make sure it + // doesn't conflict with anything else. This isn't + // the most efficient, but shouldn't happen all that + // often either. + string availableDuplicateKey; + int duplicateCount = 0; + do + { + ++duplicateCount; + availableDuplicateKey = String.Format("duplicate{0}:{1}", duplicateCount, childKey); + } while (referencedItems.ContainsKey(availableDuplicateKey)); + referencedItems.Add(availableDuplicateKey, iRow); + continue; + } + + referencedItems.Add(childKey, iRow); + + if (groupTypeInclude == rowChildType) + { + // Create a new row for the list we're collecting, to ensure + // that multiple lists don't conflict or steal rows from + // each other. We use the Row constructor (rather than the + // Table.CreateRow() method) to prevent from updating the + // table while we're iterating it. + Row rowNew = new Row(row.SourceLineNumbers, wixGroupTable); + rowNew[0] = rowParentName; + rowNew[1] = rowParentType; + rowNew[2] = rowChildName; + rowNew[3] = rowChildType; + newRows.Add(rowNew); + } + else if (groupTypeRecurse == rowChildType) + { + AddGroupMembersToList(wixGroupTable, newRows, referencedItems, rowChildType, rowChildName, groupTypeInclude, groupTypeRecurse); + } + else + { + this.OnMessage(WixErrors.UnexpectedGroupChild(rowParentType, rowParentName, rowChildType, rowChildName)); + } + } + } + } + + /// <summary> /// Resolves the features connected to other features in the active output. /// </summary> Index: Compiler.cs =================================================================== RCS file: /cvsroot/wix/wix3.5/src/wix/Compiler.cs,v retrieving revision 1.7 retrieving revision 1.8 diff -C2 -d -r1.7 -r1.8 *** Compiler.cs 22 Jan 2010 10:05:13 -0000 1.7 --- Compiler.cs 29 Jan 2010 11:40:28 -0000 1.8 *************** *** 122,125 **** --- 122,137 ---- /// <summary> + /// Type of packages in a Chain + /// ... [truncated message content] |