Skip to content

Commit

Permalink
Implement default-feature feature.
Browse files Browse the repository at this point in the history
Use the WiX stdlib.

See WIP at wixtoolset/issues#7581.
  • Loading branch information
barnson committed Dec 26, 2023
1 parent 6f4fda5 commit 1c249dd
Showing 9 changed files with 250 additions and 8 deletions.
14 changes: 14 additions & 0 deletions src/api/wix/WixToolset.Data/WixStandardLibrary.cs
Original file line number Diff line number Diff line change
@@ -83,6 +83,20 @@ private static IEnumerable<IntermediateSection> YieldSections(Platform platform)
yield return section;
}

// Default feature.
{
var symbol = new FeatureSymbol(sourceLineNumber, new Identifier(AccessModifier.Virtual, WixStandardLibraryIdentifiers.DefaultFeatureName))
{
Level = 1,
Display = 0,
InstallDefault = FeatureInstallDefault.Local,
};

var section = CreateSectionAroundSymbol(symbol);

yield return section;
}

// Package References.
{
var section = CreateSection(WixStandardLibraryIdentifiers.WixStandardPackageReferences);
5 changes: 5 additions & 0 deletions src/api/wix/WixToolset.Data/WixStandardLibraryIdentifiers.cs
Original file line number Diff line number Diff line change
@@ -16,5 +16,10 @@ public static class WixStandardLibraryIdentifiers
/// WiX Standard references for modules.
/// </summary>
public static readonly string WixStandardModuleReferences = "WixStandardModuleReferences";

/// <summary>
/// Default feature name.
/// </summary>
public static readonly string DefaultFeatureName = "WixDefaultFeature";
}
}
57 changes: 57 additions & 0 deletions src/wix/WixToolset.Core/AssignDefaultFeatureCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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.Core
{
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using WixToolset.Data;
using WixToolset.Data.Symbols;

internal class AssignDefaultFeatureCommand
{
public AssignDefaultFeatureCommand(IntermediateSection entrySection, IEnumerable<IntermediateSection> sections)
{
this.EntrySection = entrySection;
this.Sections = sections;
}

public IntermediateSection EntrySection { get; }

public IEnumerable<IntermediateSection> Sections { get; }

public void Execute()
{
foreach (var section in this.Sections)
{
var components = section.Symbols.OfType<ComponentSymbol>().ToList();
foreach (var component in components)
{
this.EntrySection.AddSymbol(new WixComplexReferenceSymbol(component.SourceLineNumbers)
{
Parent = WixStandardLibraryIdentifiers.DefaultFeatureName,
ParentType = ComplexReferenceParentType.Feature,
ParentLanguage = null,
Child = component.Id.Id,
ChildType = ComplexReferenceChildType.Component,
IsPrimary = true,
});

this.EntrySection.AddSymbol(new WixGroupSymbol(component.SourceLineNumbers)
{
ParentId = WixStandardLibraryIdentifiers.DefaultFeatureName,
ParentType = ComplexReferenceParentType.Feature,
ChildId = component.Id.Id,
ChildType = ComplexReferenceChildType.Component,
});
}
}

this.EntrySection.AddSymbol(new WixSimpleReferenceSymbol()
{
Table = "Feature",
PrimaryKeys = WixStandardLibraryIdentifiers.DefaultFeatureName,
});
}
}
}
4 changes: 2 additions & 2 deletions src/wix/WixToolset.Core/Compiler.cs
Original file line number Diff line number Diff line change
@@ -2681,7 +2681,7 @@ private void ParseComponentGroupElement(XElement node, ComplexReferenceParentTyp
/// <param name="parentLanguage">Optional language of parent (only useful for Modules).</param>
private void ParseComponentGroupRefElement(XElement node, ComplexReferenceParentType parentType, string parentId, string parentLanguage)
{
Debug.Assert(ComplexReferenceParentType.ComponentGroup == parentType || ComplexReferenceParentType.FeatureGroup == parentType || ComplexReferenceParentType.Feature == parentType || ComplexReferenceParentType.Module == parentType);
Debug.Assert(ComplexReferenceParentType.ComponentGroup == parentType || ComplexReferenceParentType.FeatureGroup == parentType || ComplexReferenceParentType.Feature == parentType || ComplexReferenceParentType.Module == parentType || ComplexReferenceParentType.Product == parentType);

var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);
string id = null;
@@ -2730,7 +2730,7 @@ private void ParseComponentGroupRefElement(XElement node, ComplexReferenceParent
/// <param name="parentLanguage">Optional language of parent (only useful for Modules).</param>
private void ParseComponentRefElement(XElement node, ComplexReferenceParentType parentType, string parentId, string parentLanguage)
{
Debug.Assert(ComplexReferenceParentType.FeatureGroup == parentType || ComplexReferenceParentType.ComponentGroup == parentType || ComplexReferenceParentType.Feature == parentType || ComplexReferenceParentType.Module == parentType);
Debug.Assert(ComplexReferenceParentType.FeatureGroup == parentType || ComplexReferenceParentType.ComponentGroup == parentType || ComplexReferenceParentType.Feature == parentType || ComplexReferenceParentType.Module == parentType || ComplexReferenceParentType.Product == parentType);

var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(node);
string id = null;
10 changes: 8 additions & 2 deletions src/wix/WixToolset.Core/Compiler_Package.cs
Original file line number Diff line number Diff line change
@@ -240,10 +240,16 @@ private void ParsePackageElement(XElement node)
this.ParseComplianceCheckElement(child);
break;
case "Component":
this.ParseComponentElement(child, ComplexReferenceParentType.Unknown, null, null, CompilerConstants.IntegerNotSet, null, null);
this.ParseComponentElement(child, ComplexReferenceParentType.Product, null, null, CompilerConstants.IntegerNotSet, null, null);
break;
case "ComponentRef":
this.ParseComponentRefElement(child, ComplexReferenceParentType.Product, null, null);
break;
case "ComponentGroup":
this.ParseComponentGroupElement(child, ComplexReferenceParentType.Unknown, null);
this.ParseComponentGroupElement(child, ComplexReferenceParentType.Product, null);
break;
case "ComponentGroupRef":
this.ParseComponentGroupRefElement(child, ComplexReferenceParentType.Product, null, null);
break;
case "CustomAction":
this.ParseCustomActionElement(child);
17 changes: 15 additions & 2 deletions src/wix/WixToolset.Core/Linker.cs
Original file line number Diff line number Diff line change
@@ -126,6 +126,14 @@ public Intermediate Link(ILinkContext context)
}
}

// If there are no authored features, create a default feature and assign the components to it.
if (find.EntrySection.Type == SectionType.Package
&& !sections.Where(s => s.Id != WixStandardLibraryIdentifiers.DefaultFeatureName).SelectMany(s => s.Symbols).OfType<FeatureSymbol>().Any())
{
var command = new AssignDefaultFeatureCommand(find.EntrySection, sections);
command.Execute();
}

// Resolve the symbol references to find the set of sections we care about for linking.
// Of course, we start with the entry section (that's how it got its name after all).
var resolve = new ResolveReferencesCommand(this.Messaging, find.EntrySection, find.SymbolsByName);
@@ -162,7 +170,7 @@ public Intermediate Link(ILinkContext context)
return null;
}

// Display an error message for Components that were not referenced by a Feature.
// If there are authored features, error for any referenced components that aren't assigned to a feature.
foreach (var component in sections.SelectMany(s => s.Symbols.Where(y => y.Definition.Type == SymbolDefinitionType.Component)))
{
if (!referencedComponents.Contains(component.Id.Id))
@@ -370,7 +378,8 @@ private void ProcessComplexReferences(IntermediateSection resolvedSection, IEnum
foreach (var section in sections)
{
// Need ToList since we might want to add symbols while processing.
foreach (var wixComplexReferenceRow in section.Symbols.OfType<WixComplexReferenceSymbol>().ToList())
var wixComplexReferences = section.Symbols.OfType<WixComplexReferenceSymbol>().ToList();
foreach (var wixComplexReferenceRow in wixComplexReferences)
{
ConnectToFeature connection;
switch (wixComplexReferenceRow.ParentType)
@@ -515,6 +524,10 @@ private void ProcessComplexReferences(IntermediateSection resolvedSection, IEnum
featuresToFeatures.Add(new ConnectToFeature(section, wixComplexReferenceRow.Child, null, wixComplexReferenceRow.IsPrimary));
break;

case ComplexReferenceChildType.Component:
case ComplexReferenceChildType.ComponentGroup:
break;

default:
throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Unexpected complex reference child type: {0}", Enum.GetName(typeof(ComplexReferenceChildType), wixComplexReferenceRow.ChildType)));
}
74 changes: 72 additions & 2 deletions src/wix/test/WixToolsetTest.CoreIntegration/FeatureFixture.cs
Original file line number Diff line number Diff line change
@@ -2,11 +2,10 @@

namespace WixToolsetTest.CoreIntegration
{
using System;
using System.IO;
using System.Linq;
using WixInternal.TestSupport;
using WixInternal.Core.TestPackage;
using WixInternal.TestSupport;
using WixToolset.Data;
using Xunit;

@@ -44,6 +43,77 @@ public void CanDetectMissingFeatureComponentMapping()
}
}

[Fact]
public void CanAutomaticallyCreateDefaultFeature()
{
var folder = TestData.Get(@"TestData");

using (var fs = new DisposableFileSystem())
{
var baseFolder = fs.GetFolder();
var intermediateFolder = Path.Combine(baseFolder, "obj");
var msiPath = Path.Combine(baseFolder, @"bin\test.msi");

var result = WixRunner.Execute(new[]
{
"build",
Path.Combine(folder, "Feature", "PackageDefaultFeature.wxs"),
"-bindpath", Path.Combine(folder, "SingleFile", "data"),
"-intermediateFolder", intermediateFolder,
"-o", msiPath
});

Assert.Empty(result.Messages);

Assert.True(File.Exists(msiPath));
var results = Query.QueryDatabase(msiPath, new[] { "Feature", "FeatureComponents", "Shortcut" });
WixAssert.CompareLineByLine(new[]
{
"Feature:WixDefaultFeature\t\t\t\t0\t1\t\t0",
"FeatureComponents:WixDefaultFeature\tAnotherComponentInAFragment",
"FeatureComponents:WixDefaultFeature\tComponentInAFragment",
"FeatureComponents:WixDefaultFeature\tfil6J6CHYPBCOMYclNjnqn0afimmzM",
"FeatureComponents:WixDefaultFeature\tfilcV1yrx0x8wJWj4qMzcH21jwkPko",
"FeatureComponents:WixDefaultFeature\tfilj.cb0sFWqIPHPFSKJSEEaPDuAQ4",
"Shortcut:AdvertisedShortcut\tINSTALLFOLDER\tShortcut\tAnotherComponentInAFragment\tWixDefaultFeature\t\t\t\t\t\t\t\t\t\t\t",
}, results);
}
}

[Fact]
public void WontAutomaticallyCreateDefaultFeature()
{
var folder = TestData.Get(@"TestData");

using (var fs = new DisposableFileSystem())
{
var baseFolder = fs.GetFolder();
var intermediateFolder = Path.Combine(baseFolder, "obj");
var msiPath = Path.Combine(baseFolder, @"bin\test.msi");

var result = WixRunner.Execute(new[]
{
"build",
Path.Combine(folder, "Feature", "PackageBadDefaultFeature.wxs"),
"-bindpath", Path.Combine(folder, "SingleFile", "data"),
"-intermediateFolder", intermediateFolder,
"-o", msiPath
});

var messages = result.Messages.Select(m => m.ToString()).ToList();
messages.Sort();

WixAssert.CompareLineByLine(new[]
{
"Found orphaned Component 'fil6J6CHYPBCOMYclNjnqn0afimmzM'. If this is a Package, every Component must have at least one parent Feature. To include a Component in a Module, you must include it directly as a Component element of the Module element or indirectly via ComponentRef, ComponentGroup, or ComponentGroupRef elements.",
"Found orphaned Component 'filcV1yrx0x8wJWj4qMzcH21jwkPko'. If this is a Package, every Component must have at least one parent Feature. To include a Component in a Module, you must include it directly as a Component element of the Module element or indirectly via ComponentRef, ComponentGroup, or ComponentGroupRef elements.",
"Found orphaned Component 'filj.cb0sFWqIPHPFSKJSEEaPDuAQ4'. If this is a Package, every Component must have at least one parent Feature. To include a Component in a Module, you must include it directly as a Component element of the Module element or indirectly via ComponentRef, ComponentGroup, or ComponentGroupRef elements.",
}, messages.ToArray());

Assert.Equal(267, result.ExitCode);
}
}

[Fact]
public void CannotBuildMsiWithTooLargeFeatureDepth()
{
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package Name="PackageMissingFeatureComponentMapping" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="12E4699F-E774-4D05-8A01-5BDD41BBA127">
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />

<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="PackageMissingFeatureComponentMapping">
<Directory Id="SubFolder" Name="NotMapped">
<Component>
<File Source="test.txt" />
</Component>
</Directory>
</Directory>
</StandardDirectory>

<Feature Id="MissingComponentFeature" />

<Component Directory="INSTALLFOLDER">
<File Source="test.txt" />
</Component>

<ComponentGroup Id="ImplicitFeatureComponentGroup" Directory="INSTALLFOLDER">
<Component>
<File Name="test2.txt" Source="test.txt" />
</Component>
</ComponentGroup>
</Package>
</Wix>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package Name="PackageMissingFeatureComponentMapping" Version="1.0.0.0" Manufacturer="Example Corporation" UpgradeCode="12E4699F-E774-4D05-8A01-5BDD41BBA127">
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />

<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="PackageMissingFeatureComponentMapping">
<Directory Id="SubFolder" Name="NotMapped">
<Component>
<File Source="test.txt" />
</Component>
</Directory>
</Directory>
</StandardDirectory>

<Component Directory="INSTALLFOLDER">
<File Source="test.txt" />
</Component>

<ComponentRef Id="ComponentInAFragment" />
<ComponentGroupRef Id="ComponentGroupInAFragment" />
<FeatureGroupRef Id="FeatureGroupInAFragment" />
</Package>

<Fragment>
<ComponentGroup Id="ComponentGroupInAFragment" Directory="INSTALLFOLDER">
<Component>
<File Name="test2.txt" Source="test.txt" />
</Component>
</ComponentGroup>
</Fragment>

<Fragment>
<!--
Keeping the component outside the feature group, to ensure the component
comes along for the ride when the empty feature group is referenced.
-->
<FeatureGroup Id="FeatureGroupInAFragment" />

<Component Id="AnotherComponentInAFragment" Directory="INSTALLFOLDER">
<File Name="test3.txt" Source="test.txt" />
<Shortcut Id="AdvertisedShortcut" Advertise="yes" Name="Shortcut" />
</Component>
</Fragment>

<Fragment>
<Component Id="ComponentInAFragment" Directory="INSTALLFOLDER">
<File Name="test4.txt" Source="test.txt" />
</Component>
</Fragment>
</Wix>

0 comments on commit 1c249dd

Please sign in to comment.