Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Usage: dnx vs -- install [options]
| `sku` | Edition, one of `e\|ent\|enterprise`, `p\|pro\|professional`, `c\|com\|community`, `b\|build\|buildtools` or `t\|test\|testagent` |
| `filter` | Expression to filter VS instances. E.g. `x => x.InstanceId = '123'` |
| `nick\|nickname` | Optional nickname to use |
| `v\|version` | Install specific (semantic) version, such as 18.7 or 18.7.3 |
| `add` | A workload ID |


Expand All @@ -129,6 +130,12 @@ Examples:
# Install VS community with the .NET Core, ASP.NET and Azure workloads,
# shows installation progress and waits for it to finish before returning
> dnx vs -- install +core +web +azure

# Install VS 18 Enterprise
> dnx vs -- install -v:18 -sku:ent

# Install the latest VS 17 (2022) Community
> dnx vs -- install --version 17
```
<!-- EXAMPLES_END -->

Expand Down Expand Up @@ -279,7 +286,20 @@ Usage: dnx vs -- update [options]
| `filter` | Expression to filter VS instances. E.g. `x => x.InstanceId = '123'` |
| `first` | Update first matching instance. |
| `all` | Update all instances. |
| `v\|version` | Update specific (semantic) version, such as 18.7 or 18.7.3 |


Examples:

<!-- EXAMPLES_BEGIN -->
```
# Update the installed VS 18.7 instance
> dnx vs -- update -v:18.7

# Update all matching VS 17 instances
> dnx vs -- update --version 17 --all
```
<!-- EXAMPLES_END -->

## where

Expand Down
58 changes: 58 additions & 0 deletions src/VisualStudio.Tests/Commands/VersionOptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.CommandLine;
using System.Linq;
using Xunit;

namespace Devlooped.Tests
{
public class VersionOptionTests
{
[Theory]
[InlineData(Commands.Run)]
[InlineData(Commands.Install)]
[InlineData(Commands.Update)]
public void when_command_is_defined_then_it_accepts_version_option(string commandName)
{
var root = new VsRootCommand();
var command = root.Subcommands.Single(c => c.Name == commandName);

Assert.Contains(command.Options, o => o.Name == "--version" && o.Aliases.Contains("-v"));
}

[Theory]
[InlineData(Commands.Install, "--version", "18.7")]
[InlineData(Commands.Install, "-v", "18.7.3")]
[InlineData(Commands.Update, "--version", "18.7")]
[InlineData(Commands.Update, "-v", "17.14")]
[InlineData(Commands.Run, "--version", "18.7")]
[InlineData(Commands.Run, "-v", "18.7.3")]
public void when_parsing_version_then_option_is_bound_and_not_unmatched(
string commandName, string option, string value)
{
var root = new VsRootCommand();
var parse = root.Parse(new[] { commandName, option, value });

Assert.Empty(parse.Errors);
Assert.DoesNotContain(option, parse.UnmatchedTokens);
Assert.DoesNotContain(value, parse.UnmatchedTokens);

var version = parse.CommandResult.Command.Options.OfType<Option<string>>().Single(o => o.Name == "--version");
Assert.Equal(value, parse.GetValue(version));
}

[Theory]
[InlineData(Commands.Install, "-v:18.7", "18.7")]
[InlineData(Commands.Update, "--version=18.7.3", "18.7.3")]
[InlineData(Commands.Run, "-v:18", "18")]
public void when_parsing_legacy_version_syntax_then_option_is_bound(
string commandName, string token, string expected)
{
var rewritten = ArgumentPreprocessor.RewriteForCommand(commandName, new[] { token });
var root = new VsRootCommand();
var parse = root.Parse(new[] { commandName }.Concat(rewritten).ToArray());

Assert.Empty(parse.Errors);
var version = parse.CommandResult.Command.Options.OfType<Option<string>>().Single(o => o.Name == "--version");
Assert.Equal(expected, parse.GetValue(version));
}
}
}
21 changes: 20 additions & 1 deletion src/VisualStudio.Tests/VisualStudioOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ static ParseResult ParseSelection(params string[] args)
var all = SharedOptions.AllOption("test");
var nick = SharedOptions.NicknameOption();
var exp = SharedOptions.ExperimentalOption("test");
var version = SharedOptions.VersionOption("test");
cmd.Options.Add(sku);
cmd.Options.Add(filter);
cmd.Options.Add(first);
cmd.Options.Add(all);
cmd.Options.Add(nick);
cmd.Options.Add(exp);
cmd.Options.Add(version);
cmd.TreatUnmatchedTokensAsErrors = false;

var root = new RootCommand();
Expand All @@ -49,7 +51,8 @@ static VisualStudioFilter GetFilter(ParseResult parse)
cmd.Options.OfType<Option<string>>().First(o => o.Name == "--sku"),
cmd.Options.OfType<Option<string>>().First(o => o.Name == "--filter"),
cmd.Options.OfType<Option<bool>>().First(o => o.Name == "--first"),
cmd.Options.OfType<Option<bool>>().First(o => o.Name == "--all"));
cmd.Options.OfType<Option<bool>>().First(o => o.Name == "--all"),
cmd.Options.OfType<Option<string>>().First(o => o.Name == "--version"));
}

[Theory]
Expand Down Expand Up @@ -187,6 +190,22 @@ public void when_parsing_all_argument_then_all_is_set(string argument, bool expe
Assert.Equal(expectedValue, filter.All);
}

[Theory]
[InlineData("", null)]
[InlineData("--version=18.7", "18.7")]
[InlineData("-v:18.7.3", "18.7.3")]
[InlineData("--version", "18")]
public void when_parsing_version_argument_then_version_is_set(string argument, string expectedValue)
{
var args = string.IsNullOrEmpty(argument)
? Array.Empty<string>()
: argument == "--version"
? new[] { "--version", "18" }
: new[] { argument };
var filter = GetFilter(ParseSelection(args));
Assert.Equal(expectedValue, filter.Version);
}

[Theory]
[InlineData("", false)]
[InlineData("first", true)]
Expand Down
22 changes: 20 additions & 2 deletions src/VisualStudio.Tests/VisualStudioPredicateBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,25 @@ public async Task when_evaluating_combined_criterias_then_predicate_matches_conf
Assert.False(predicate(new vswhere.VisualStudioInstance() { InstanceId = "123" }.WithSku(Sku.Professional).WithChannel(Channel.Stable)));
}

static VisualStudioFilter GetFilter(Sku? sku = null, Channel? channel = null, string expression = null) =>
new VisualStudioFilter(Channel: channel, Sku: sku, Expression: expression);
[Fact]
public async Task when_evaluating_version_then_predicate_matches_semantic_prefix()
{
var builder = new VisualStudioPredicateBuilder();

var predicate = await builder.BuildPredicateAsync(GetFilter(version: "18.7"));

Assert.True(predicate(new vswhere.VisualStudioInstance
{
Catalog = new vswhere.VisualStudioCatalog { ProductSemanticVersion = "18.7.3" }
}));
Assert.False(predicate(new vswhere.VisualStudioInstance
{
Catalog = new vswhere.VisualStudioCatalog { ProductSemanticVersion = "18.8.0" }
}));
Assert.False(predicate(new vswhere.VisualStudioInstance()));
}

static VisualStudioFilter GetFilter(Sku? sku = null, Channel? channel = null, string expression = null, string version = null) =>
new VisualStudioFilter(Channel: channel, Sku: sku, Expression: expression, Version: version);
}
}
29 changes: 29 additions & 0 deletions src/VisualStudio.Tests/VisualStudioVersionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Xunit;

namespace Devlooped.Tests
{
public class VisualStudioVersionTests
{
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData("18", "18")]
[InlineData("18.7", "18")]
[InlineData("18.7.3", "18")]
[InlineData("17.14.16", "17")]
public void when_getting_major_then_returns_major_component(string version, string expected) =>
Assert.Equal(expected, VisualStudioVersion.GetMajor(version));

[Theory]
[InlineData("18.7.3", null, true)]
[InlineData("18.7.3", "", true)]
[InlineData("18.7.3", "18", true)]
[InlineData("18.7.3", "18.7", true)]
[InlineData("18.7.3", "18.7.3", true)]
[InlineData("18.8.0", "18.7", false)]
[InlineData("17.14.16", "18", false)]
[InlineData(null, "18.7", false)]
public void when_matching_then_uses_semantic_prefix(string product, string requested, bool expected) =>
Assert.Equal(expected, VisualStudioVersion.Matches(product, requested));
}
}
6 changes: 4 additions & 2 deletions src/VisualStudio/CommandHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public static VisualStudioFilter GetFilter(
Option<string> skuOption,
Option<string> filterOption = null,
Option<bool> firstOption = null,
Option<bool> allOption = null)
Option<bool> allOption = null,
Option<string> versionOption = null)
{
return new VisualStudioFilter(
Channel: channelOptions.GetChannel(parse),
Sku: SharedOptions.ParseSku(parse.GetValue(skuOption)),
Expression: filterOption != null ? parse.GetValue(filterOption) : null,
First: firstOption != null && parse.GetValue(firstOption),
All: allOption != null && parse.GetValue(allOption));
All: allOption != null && parse.GetValue(allOption),
Version: versionOption != null ? parse.GetValue(versionOption) : null);
}

public static string[] GetWorkloadIds(ParseResult parse, Option<string[]> option)
Expand Down
6 changes: 5 additions & 1 deletion src/VisualStudio/Commands/InstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class InstallCommand : Command
readonly Option<string> skuOption;
readonly Option<string> filterOption;
readonly Option<string> nicknameOption;
readonly Option<string> versionOption;
readonly Option<string[]> addOption = new("--add")
{
Description = "A workload ID",
Expand All @@ -28,10 +29,12 @@ public InstallCommand(InstallerService installerService)
skuOption = SharedOptions.SkuOption();
filterOption = SharedOptions.FilterOption();
nicknameOption = SharedOptions.NicknameOption();
versionOption = SharedOptions.VersionOption("Install");

Options.Add(skuOption);
Options.Add(filterOption);
Options.Add(nicknameOption);
Options.Add(versionOption);
Options.Add(addOption);

TreatUnmatchedTokensAsErrors = false;
Expand All @@ -48,6 +51,7 @@ async Task ExecuteAsync(ParseResult parse, TextWriter output)
var channel = channelOptions.GetChannel(parse);
var sku = SharedOptions.ParseSku(parse.GetValue(skuOption)) ?? Sku.Community;
var nickname = parse.GetValue(nicknameOption);
var version = parse.GetValue(versionOption);
var workloads = CommandHelpers.GetWorkloadIds(parse, addOption);
var extra = parse.UnmatchedTokens;

Expand All @@ -65,7 +69,7 @@ async Task ExecuteAsync(ParseResult parse, TextWriter output)

args.AddRange(extra);

var vs = await installerService.GetLatestMajorAsync();
var vs = VisualStudioVersion.GetMajor(version) ?? await installerService.GetLatestMajorAsync();
var installBase = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"Microsoft Visual Studio",
Expand Down
15 changes: 3 additions & 12 deletions src/VisualStudio/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ class RunCommand : Command
{
Description = "If more than one instance matches the criteria, run the first one sorted by descending build version.",
};
readonly Option<string> versionOption = new("--version", "-v")
{
Description = "Run specific (semantic) version, such as 18.7 or 18.7.3",
};
readonly Option<string> versionOption;
readonly Option<bool> waitOption = new("--wait", "-w")
{
Description = "Wait for the started Visual Studio to exit.",
Expand Down Expand Up @@ -58,6 +55,7 @@ public RunCommand(WhereService whereService)
skuOption = SharedOptions.SkuOption();
filterOption = SharedOptions.FilterOption();
experimentalOption = SharedOptions.ExperimentalOption("run");
versionOption = SharedOptions.VersionOption("Run");

Options.Add(skuOption);
Options.Add(filterOption);
Expand Down Expand Up @@ -90,12 +88,11 @@ async Task ExecuteAsync(ParseResult parse, TextWriter output)
: (bool?)null;

var id = parse.GetValue(idOption);
var version = parse.GetValue(versionOption);
var first = parse.GetValue(firstOption);
var wait = parse.GetValue(waitOption);
var disableNodeReuse = parse.GetValue(nodeReuseOption);
var isExperimental = parse.GetValue(experimentalOption);
var filter = CommandHelpers.GetFilter(parse, channelOptions, skuOption, filterOption, firstOption);
var filter = CommandHelpers.GetFilter(parse, channelOptions, skuOption, filterOption, firstOption, versionOption: versionOption);

var devenv = settings.Get("devenv");
if (!string.IsNullOrEmpty(devenv))
Expand All @@ -118,13 +115,7 @@ async Task ExecuteAsync(ParseResult parse, TextWriter output)
.OrderByDescending(i => i.Catalog.BuildVersion);

if (!string.IsNullOrEmpty(id))
{
instances = instances.Where(i => i.InstanceId.Equals(id, StringComparison.OrdinalIgnoreCase));
}
else if (version != null)
{
instances = instances.Where(i => i.Catalog.ProductSemanticVersion.StartsWith(version));
}

var matches = instances.ToArray();
if (matches.Length == 1 || (matches.Length > 0 && first))
Expand Down
5 changes: 4 additions & 1 deletion src/VisualStudio/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class UpdateCommand : Command
readonly Option<string> filterOption;
readonly Option<bool> firstOption;
readonly Option<bool> allOption;
readonly Option<string> versionOption;

public UpdateCommand(WhereService whereService, InstallerService installerService)
: base(Commands.Update, "Updates an installation of Visual Studio.")
Expand All @@ -28,11 +29,13 @@ public UpdateCommand(WhereService whereService, InstallerService installerServic
filterOption = SharedOptions.FilterOption();
firstOption = SharedOptions.FirstOption("Update");
allOption = SharedOptions.AllOption("Update");
versionOption = SharedOptions.VersionOption("Update");

Options.Add(skuOption);
Options.Add(filterOption);
Options.Add(firstOption);
Options.Add(allOption);
Options.Add(versionOption);

TreatUnmatchedTokensAsErrors = false;

Expand All @@ -45,7 +48,7 @@ public UpdateCommand(WhereService whereService, InstallerService installerServic

async Task ExecuteAsync(ParseResult parse, TextWriter output)
{
var filter = CommandHelpers.GetFilter(parse, channelOptions, skuOption, filterOption, firstOption, allOption);
var filter = CommandHelpers.GetFilter(parse, channelOptions, skuOption, filterOption, firstOption, allOption, versionOption);
var all = parse.GetValue(allOption);
var extraArgs = parse.UnmatchedTokens.ToList();

Expand Down
6 changes: 6 additions & 0 deletions src/VisualStudio/Docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,11 @@ Examples:
# Install VS community with the .NET Core, ASP.NET and Azure workloads,
# shows installation progress and waits for it to finish before returning
> dnx vs -- install +core +web +azure

# Install VS 18 Enterprise
> dnx vs -- install -v:18 -sku:ent

# Install the latest VS 17 (2022) Community
> dnx vs -- install --version 17
```
<!-- EXAMPLES_END -->
12 changes: 12 additions & 0 deletions src/VisualStudio/Docs/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@
```

{Options}

Examples:

<!-- EXAMPLES_BEGIN -->
```
# Update the installed VS 18.7 instance
> dnx vs -- update -v:18.7

# Update all matching VS 17 instances
> dnx vs -- update --version 17 --all
```
<!-- EXAMPLES_END -->
Loading
Loading