Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit 29c3ce6

Browse files
Initial support for generating a configuration file from machine configuration (#2466)
1 parent 356f4ef commit 29c3ce6

19 files changed

+272
-135
lines changed

tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTaskGroup.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,18 @@ public interface ISetupTaskGroup
2929
public ReviewTabViewModelBase GetReviewTabViewModel();
3030

3131
/// <summary>
32-
/// Gets all the individual setup tasks that make up this group
32+
/// Gets all the setup tasks that make up this group
3333
/// </summary>
3434
public IEnumerable<ISetupTask> SetupTasks
3535
{
3636
get;
3737
}
38+
39+
/// <summary>
40+
/// Gets all the DSC tasks that make up this group
41+
/// </summary>
42+
public IEnumerable<ISetupTask> DSCTasks
43+
{
44+
get;
45+
}
3846
}

tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/WinGetDscSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using YamlDotNet.Core;
5+
using YamlDotNet.Serialization;
6+
47
namespace DevHome.SetupFlow.Models.WingetConfigure;
58

69
/// <summary>
@@ -9,6 +12,7 @@ namespace DevHome.SetupFlow.Models.WingetConfigure;
912
/// </summary>
1013
public class WinGetDscSettings : WinGetConfigSettingsBase
1114
{
15+
[YamlMember(ScalarStyle = ScalarStyle.DoubleQuoted)]
1216
public string Id { get; set; }
1317

1418
public string Source { get; set; }

tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using DevHome.SetupFlow.Models;
99
using DevHome.SetupFlow.Models.WingetConfigure;
1010
using DevHome.SetupFlow.TaskGroups;
11+
using Serilog;
1112
using YamlDotNet.Serialization;
1213
using YamlDotNet.Serialization.NamingConventions;
1314

@@ -21,13 +22,8 @@ public enum ConfigurationFileKind
2122

2223
public class ConfigurationFileBuilder
2324
{
24-
private readonly SetupFlowOrchestrator _orchestrator;
25-
26-
public ConfigurationFileBuilder(SetupFlowOrchestrator orchestrator)
27-
{
28-
_orchestrator = orchestrator;
29-
}
30-
25+
private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ConfigurationFileBuilder));
26+
3127
/// <summary>
3228
/// Builds an object that represents a config file that can be used by WinGet Configure to install
3329
/// apps and clone repositories.This is already formatted as valid yaml and can be written
@@ -36,21 +32,33 @@ public ConfigurationFileBuilder(SetupFlowOrchestrator orchestrator)
3632
/// <returns>The config file object representing the yaml file.</returns>
3733
public WinGetConfigFile BuildConfigFileObjectFromTaskGroups(IList<ISetupTaskGroup> taskGroups, ConfigurationFileKind configurationFileKind)
3834
{
39-
var listOfResources = new List<WinGetConfigResource>();
40-
35+
List<WinGetConfigResource> repoResources = [];
36+
List<WinGetConfigResource> appResources = [];
4137
foreach (var taskGroup in taskGroups)
4238
{
4339
if (taskGroup is RepoConfigTaskGroup repoConfigGroup)
4440
{
4541
// Add the GitDSC resource blocks to yaml
46-
listOfResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind));
42+
repoResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind));
4743
}
4844
else if (taskGroup is AppManagementTaskGroup appManagementGroup)
4945
{
5046
// Add the WinGetDsc resource blocks to yaml
51-
listOfResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind));
47+
appResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind));
5248
}
5349
}
50+
51+
// If Git is not added to the apps to install and there are
52+
// repositories to clone, add Git as a pre-requisite
53+
var isGitAdded = appResources
54+
.Select(r => r.Settings as WinGetDscSettings)
55+
.Any(s => s.Id == DscHelpers.GitWinGetPackageId);
56+
if (!isGitAdded && repoResources.Count > 0)
57+
{
58+
appResources.Add(CreateWinGetInstallForGitPreReq());
59+
}
60+
61+
List<WinGetConfigResource> listOfResources = [..appResources, ..repoResources];
5462

5563
if (listOfResources.Count == 0)
5664
{
@@ -114,22 +122,24 @@ public string SerializeWingetFileObjectToString(WinGetConfigFile configFile)
114122
private List<WinGetConfigResource> GetResourcesForCloneTaskGroup(RepoConfigTaskGroup repoConfigGroup, ConfigurationFileKind configurationFileKind)
115123
{
116124
var listOfResources = new List<WinGetConfigResource>();
117-
var repoConfigTasks = repoConfigGroup.SetupTasks
125+
var repoConfigTasks = repoConfigGroup.DSCTasks
118126
.Where(task => task is CloneRepoTask)
119127
.Select(task => task as CloneRepoTask)
120128
.ToList();
121129

122-
if (repoConfigTasks.Count != 0)
123-
{
124-
listOfResources.Add(CreateWinGetInstallForGitPreReq());
125-
}
126-
127130
foreach (var repoConfigTask in repoConfigTasks)
128131
{
129-
if (repoConfigTask.RepositoryToClone is GenericRepository genericRepository)
130-
{
131-
listOfResources.Add(CreateResourceFromTaskForGitDsc(repoConfigTask, genericRepository.RepoUri, configurationFileKind));
132-
}
132+
try
133+
{
134+
if (!repoConfigTask.RepositoryToClone.IsPrivate)
135+
{
136+
listOfResources.Add(CreateResourceFromTaskForGitDsc(repoConfigTask, repoConfigTask.RepositoryToClone.RepoUri, configurationFileKind));
137+
}
138+
}
139+
catch (Exception e)
140+
{
141+
_log.Error($"Error creating a repository resource entry", e);
142+
}
133143
}
134144

135145
return listOfResources;
@@ -143,7 +153,7 @@ private List<WinGetConfigResource> GetResourcesForCloneTaskGroup(RepoConfigTaskG
143153
private List<WinGetConfigResource> GetResourcesForAppManagementTaskGroup(AppManagementTaskGroup appManagementGroup, ConfigurationFileKind configurationFileKind)
144154
{
145155
var listOfResources = new List<WinGetConfigResource>();
146-
var installList = appManagementGroup.SetupTasks
156+
var installList = appManagementGroup.DSCTasks
147157
.Where(task => task is InstallPackageTask)
148158
.Select(task => task as InstallPackageTask)
149159
.ToList();
@@ -177,8 +187,16 @@ private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTa
177187
{
178188
Resource = DscHelpers.WinGetDscResource,
179189
Id = id,
180-
Directives = new() { AllowPrerelease = true, Description = $"Installing {arguments.PackageId}" },
181-
Settings = new WinGetDscSettings() { Id = arguments.PackageId, Source = DscHelpers.DscSourceNameForWinGet },
190+
Directives = new()
191+
{
192+
AllowPrerelease = true,
193+
Description = $"Installing {arguments.PackageId}",
194+
},
195+
Settings = new WinGetDscSettings()
196+
{
197+
Id = arguments.PackageId,
198+
Source = arguments.CatalogName,
199+
},
182200
};
183201
}
184202

@@ -190,16 +208,13 @@ private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTa
190208
/// <returns>The WinGetConfigResource object that represents the block of yaml needed by GitDsc to clone the repository. </returns>
191209
private WinGetConfigResource CreateResourceFromTaskForGitDsc(CloneRepoTask task, Uri webAddress, ConfigurationFileKind configurationFileKind)
192210
{
193-
// For normal cases, the Id will be null. This can be changed in the future when a use case for this Dsc File builder is needed outside the setup
194-
// setup target flow. We can likely drop the if statement and just use whats in its body.
195-
string id = null;
211+
// WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI.
212+
// So we add a description to the Id to make it more readable in the UI. These do not need to be localized.
213+
var id = $"Clone {task.RepositoryName}: {task.CloneLocation.FullName}";
196214
var gitDependsOnId = DscHelpers.GitWinGetPackageId;
197215

198216
if (configurationFileKind == ConfigurationFileKind.SetupTarget)
199217
{
200-
// WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI.
201-
// So we add a description to the Id to make it more readable in the UI. These do not need to be localized.
202-
id = $"Clone {task.RepositoryName}" + ": " + task.CloneLocation.FullName;
203218
gitDependsOnId = $"{DscHelpers.GitWinGetPackageId} | Install: {DscHelpers.GitName}";
204219
}
205220

@@ -223,7 +238,7 @@ private WinGetConfigResource CreateWinGetInstallForGitPreReq()
223238
return new WinGetConfigResource()
224239
{
225240
Resource = DscHelpers.WinGetDscResource,
226-
Id = $"{DscHelpers.GitWinGetPackageId} | Install: {DscHelpers.GitName}",
241+
Id = DscHelpers.GitWinGetPackageId,
227242
Directives = new() { AllowPrerelease = true, Description = $"Installing {DscHelpers.GitName}" },
228243
Settings = new WinGetDscSettings() { Id = DscHelpers.GitWinGetPackageId, Source = DscHelpers.DscSourceNameForWinGet },
229244
};

tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private sealed class PackageCache
7373
/// <summary>
7474
/// Occurs when a package selection has changed
7575
/// </summary>
76-
public event EventHandler<PackageViewModel> PackageSelectionChanged;
76+
public event EventHandler SelectedPackagesItemChanged;
7777

7878
public PackageProvider(PackageViewModelFactory packageViewModelFactory)
7979
{
@@ -107,7 +107,7 @@ public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanentl
107107
_log.Debug($"Creating view model for package [{package.Id}]");
108108
var viewModel = _packageViewModelFactory(package);
109109
viewModel.SelectionChanged += OnPackageSelectionChanged;
110-
viewModel.SelectionChanged += (sender, package) => PackageSelectionChanged?.Invoke(sender, package);
110+
viewModel.VersionChanged += OnSelectedPackageVersionChanged;
111111

112112
// Cache if requested
113113
if (cachePermanently)
@@ -122,10 +122,25 @@ public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanentl
122122

123123
return viewModel;
124124
}
125+
}
126+
127+
private void OnSelectedPackageVersionChanged(object sender, string version)
128+
{
129+
var packageViewModel = sender as PackageViewModel;
130+
if (packageViewModel?.IsSelected == true)
131+
{
132+
// Notify subscribers that an item in the list of selected packages has changed
133+
SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty);
134+
}
125135
}
126136

127-
public void OnPackageSelectionChanged(object sender, PackageViewModel packageViewModel)
128-
{
137+
private void OnPackageSelectionChanged(object sender, bool isSelected)
138+
{
139+
if (sender is not PackageViewModel packageViewModel)
140+
{
141+
return;
142+
}
143+
129144
lock (_lock)
130145
{
131146
if (packageViewModel.IsSelected)
@@ -154,12 +169,17 @@ public void OnPackageSelectionChanged(object sender, PackageViewModel packageVie
154169
{
155170
_log.Debug($"Removing package [{packageViewModel.Package.Id}] from cache");
156171
_packageViewModelCache.Remove(packageViewModel.UniqueKey);
172+
packageViewModel.SelectionChanged -= OnPackageSelectionChanged;
173+
packageViewModel.VersionChanged -= OnSelectedPackageVersionChanged;
157174
}
158175

159176
// Remove from the selected package collection
160177
_selectedPackages.Remove(packageViewModel);
161178
}
162179
}
180+
181+
// Notify subscribers that an item in the list of selected packages has changed
182+
SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty);
163183
}
164184

165185
/// <summary>

tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public static class StringResourceKey
5454
public static readonly string EditClonePathDialog = nameof(EditClonePathDialog);
5555
public static readonly string EditClonePathDialogUncheckCheckMark = nameof(EditClonePathDialogUncheckCheckMark);
5656
public static readonly string FilePickerFileTypeOption = nameof(FilePickerFileTypeOption);
57+
public static readonly string FilePickerSingleFileTypeOption = nameof(FilePickerSingleFileTypeOption);
5758
public static readonly string FileTypeNotSupported = nameof(FileTypeNotSupported);
5859
public static readonly string InstalledPackage = nameof(InstalledPackage);
5960
public static readonly string InstalledPackageReboot = nameof(InstalledPackageReboot);

tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,11 @@
323323
</data>
324324
<data name="FilePickerFileTypeOption" xml:space="preserve">
325325
<value>{0} files</value>
326-
<comment>Dropdown option for a file picker. {0} is replaced by a file type (e.g. JSON, YAML, etc ...)</comment>
326+
<comment>{Locked="{0}"}Dropdown option for a file picker. {0} is replaced by a file type (e.g. JSON, YAML, etc ...)</comment>
327+
</data>
328+
<data name="FilePickerSingleFileTypeOption" xml:space="preserve">
329+
<value>{0} file</value>
330+
<comment>{Locked="{0}"}Dropdown option for a file picker. {0} is replaced by a file type (e.g. JSON, YAML, etc ...)</comment>
327331
</data>
328332
<data name="FileTypeNotSupported" xml:space="preserve">
329333
<value>File type not supported</value>
@@ -522,7 +526,7 @@
522526
<comment>Header text for a group of controls giving multiple choices for configuring the machine, but not a full setup flow</comment>
523527
</data>
524528
<data name="MainPage_SetupFlow.Description" xml:space="preserve">
525-
<value>Clone repositories and install applications at once</value>
529+
<value>Clone repositories, install applications, and generate Winget Configuration files together</value>
526530
<comment>Body text description for a card than when clicked takes the user to a multi-step flow for setting up their machine</comment>
527531
</data>
528532
<data name="MainPage_SetupFlow.Header" xml:space="preserve">
@@ -561,6 +565,10 @@
561565
<value>Remove all</value>
562566
<comment>Label for removing all items from selection</comment>
563567
</data>
568+
<data name="AppAlreadyInstalledNotification.Text" xml:space="preserve">
569+
<value>Applications that have been previously installed cannot be installed again. They will be included in your generated configuration files.</value>
570+
<comment>Message displayed when a user selects an application that is already installed</comment>
571+
</data>
564572
<data name="RemoveApplication" xml:space="preserve">
565573
<value>Remove</value>
566574
<comment>Text announced when screen readers focus on the 'Remove' button. The 'Remove' button allows users to remove an application from their cart</comment>
@@ -633,6 +641,14 @@
633641
<value>Restore</value>
634642
<comment>Label for restore button</comment>
635643
</data>
644+
<data name="Review_GenerateConfigurationFileButton.Content" xml:space="preserve">
645+
<value>Generate Configuration file</value>
646+
<comment>Text for a generating configuration file button</comment>
647+
</data>
648+
<data name="Review_DownloadFileTooltip.Text" xml:space="preserve">
649+
<value>Generate a WinGet Configuration file (.winget) to repeat this set up in the future or share it with others.</value>
650+
<comment>{Locked="WinGet",".winget"}Tooltip text about the generated configuration file</comment>
651+
</data>
636652
<data name="Review_SetupDetails.Text" xml:space="preserve">
637653
<value>Set up details</value>
638654
<comment>Header for a section detailing the set up steps to be performed. "Set up" is the noun</comment>

tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
x:Uid="ms-resource:///DevHome.SetupFlow/Resources/Installed"/>
4141
</converters:BoolToObjectConverter.TrueValue>
4242
</converters:BoolToObjectConverter>
43-
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
4443
<converters:BoolToObjectConverter x:Name="SelectButtonGlyphConverter" TrueValue="&#xE845;" FalseValue="&#xE710;" />
4544
</ResourceDictionary>
4645
</Grid.Resources>
@@ -50,7 +49,6 @@
5049
<VisualStateGroup x:Name="CommonStates">
5150
<VisualState>
5251
<VisualState.StateTriggers>
53-
<StateTrigger IsActive="{Binding CanSelect, Converter={StaticResource BoolNegationConverter}}" />
5452
<StateTrigger IsActive="{Binding IsSelected}" />
5553
</VisualState.StateTriggers>
5654
<VisualState.Setters>
@@ -93,7 +91,6 @@
9391
SelectedItem="{Binding SelectedVersion, Mode=TwoWay}"
9492
ItemsSource="{Binding AvailableVersions}" />
9593
<Button
96-
IsEnabled="{Binding CanSelect}"
9794
AutomationProperties.Name="{Binding ButtonAutomationName}"
9895
Padding="5"
9996
Command="{Binding ToggleSelectionCommand,Mode=OneWay}">

tools/SetupFlow/DevHome.SetupFlow/TaskGroups/AppManagementTaskGroup.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ public AppManagementTaskGroup(
2525
_appManagementReviewViewModel = appManagementReviewViewModel;
2626
}
2727

28-
public IEnumerable<ISetupTask> SetupTasks => _packageProvider.SelectedPackages.Select(sp => sp.InstallPackageTask);
28+
public IEnumerable<ISetupTask> SetupTasks => _packageProvider.SelectedPackages
29+
.Where(sp => sp.CanInstall)
30+
.Select(sp => sp.InstallPackageTask);
31+
32+
public IEnumerable<ISetupTask> DSCTasks => _packageProvider.SelectedPackages
33+
.Select(sp => sp.InstallPackageTask);
2934

3035
public SetupPageViewModelBase GetSetupPageViewModel() => _appManagementViewModel;
3136

tools/SetupFlow/DevHome.SetupFlow/TaskGroups/ConfigurationFileTaskGroup.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ public ConfigurationFileTaskGroup(ConfigurationFileViewModel configurationFileVi
2424
public async Task<bool> LoadFromLocalFileAsync(StorageFile file) => await _viewModel.LoadFileAsync(file);
2525

2626
public IEnumerable<ISetupTask> SetupTasks => _viewModel.TaskList;
27-
27+
28+
public IEnumerable<ISetupTask> DSCTasks => SetupTasks;
29+
2830
/// <summary>
2931
/// Gets the task corresponding to the configuration file to apply
3032
/// </summary>

tools/SetupFlow/DevHome.SetupFlow/TaskGroups/DevDriveTaskGroup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public IEnumerable<ISetupTask> SetupTasks
7878
}
7979
}
8080

81+
public IEnumerable<ISetupTask> DSCTasks => SetupTasks;
82+
8183
public SetupPageViewModelBase GetSetupPageViewModel() => null;
8284

8385
// Only show this tab when actually creating a dev drive

tools/SetupFlow/DevHome.SetupFlow/TaskGroups/RepoConfigTaskGroup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public RepoConfigTaskGroup(IHost host, ISetupFlowStringResource stringResource,
4343
/// </summary>
4444
public IEnumerable<ISetupTask> SetupTasks => CloneTasks;
4545

46+
public IEnumerable<ISetupTask> DSCTasks => SetupTasks;
47+
4648
/// <summary>
4749
/// Gets all tasks that need to be ran.
4850
/// </summary>

tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public SetupTargetTaskGroup(
4545

4646
public IEnumerable<ISetupTask> SetupTasks => new List<ISetupTask>() { _setupTargetTaskGroup };
4747

48+
public IEnumerable<ISetupTask> DSCTasks => SetupTasks;
49+
4850
public SetupPageViewModelBase GetSetupPageViewModel() => _setupTargetViewModel;
4951

5052
public ReviewTabViewModelBase GetReviewTabViewModel() => _setupTargetReviewViewModel;

0 commit comments

Comments
 (0)