Skip to content

Commit c6bad82

Browse files
Detect when run as dotnet global tool
1 parent 4a9e06b commit c6bad82

File tree

10 files changed

+271
-7
lines changed

10 files changed

+271
-7
lines changed

CommandDotNet.DocExamples/DocExamplesDefaultTestConfig.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class DocExamplesDefaultTestConfig : IDefaultTestConfig
88
public TestConfig Default => new()
99
{
1010
AppInfoOverride = new AppInfo(
11-
false, false, false,
11+
false, false, false, false,
1212
typeof(DocExamplesDefaultTestConfig).Assembly,
1313
"doc-examples.dll", "doc-examples.dll", "1.1.1.1")
1414
};

CommandDotNet.Tests/CmdNetDefaultTestConfig.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class CmdNetDefaultTestConfig : IDefaultTestConfig
99
{
1010
OnError = {Print = {ConsoleOutput = true, CommandContext = true}},
1111
AppInfoOverride = new AppInfo(
12-
false, false, false,
12+
false, false, false, false,
1313
typeof(CmdNetDefaultTestConfig).Assembly,
1414
"testhost.dll", "testhost.dll", "1.1.1.1")
1515
};

CommandDotNet.Tests/CommandDotNet.NewerReleasesAlerts/NewReleaseAlertOnGitHubTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public void Do()
9999
}
100100

101101
private static AppInfo BuildAppInfo(string version) => new(
102-
false, false, false,
102+
false, false, false, false,
103103
typeof(NewReleaseAlertOnGitHubTests).Assembly, "blah", version);
104104

105105
public static string BuildGitHubApiResponse(string version) =>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.IO;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using Xunit;
6+
7+
namespace CommandDotNet.Tests.FeatureTests.SuggestDirective;
8+
9+
public class DotNetSuggestSync
10+
{
11+
private const string RepoRoot = "https://raw.githubusercontent.com/dotnet/command-line-api/main/src";
12+
13+
[Fact(Skip = "unskip to run")]
14+
public async Task Sync()
15+
{
16+
var client = new HttpClient();
17+
await SyncFile(client, "DotnetProfileDirectory.cs");
18+
await SyncFile(client, "FileSuggestionRegistration.cs",
19+
f => f.Replace(" : ISuggestionRegistration", ""));
20+
await SyncFile(client, "RegistrationPair.cs");
21+
}
22+
23+
private static async Task SyncFile(HttpClient client, string fileName, Func<string, string>? alter = null)
24+
{
25+
var source = $"https://raw.githubusercontent.com/dotnet/command-line-api/main/src/System.CommandLine.Suggest/{fileName}";
26+
var fileContent = await client.GetStringAsync(source);
27+
if (alter is not null)
28+
{
29+
fileContent = alter(fileContent);
30+
}
31+
32+
fileContent = fileContent.Replace("namespace System.CommandLine.Suggest",
33+
@"#pragma warning disable CS8600
34+
#pragma warning disable CS8603
35+
#pragma warning disable CS8625
36+
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
37+
// ReSharper disable CheckNamespace
38+
39+
namespace System.CommandLine.Suggest");
40+
41+
fileContent = $@"// copied from: {source}
42+
// via: {nameof(DotNetSuggestSync)} test class
43+
44+
{fileContent}";
45+
46+
await File.WriteAllTextAsync($"../../../../CommandDotNet/DotNetSuggest/System.CommandLine.Suggest/{fileName}", fileContent);
47+
}
48+
}

CommandDotNet/Builders/AppInfo.cs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public static IDisposable SetResolver(Func<AppInfo> appInfoResolver)
4343
/// <summary>True if the application's filename ends with .exe</summary>
4444
public bool IsExe { get; }
4545

46+
/// <summary>True if the application is located in the global dotnet tool directory</summary>
47+
public bool IsGlobalTool { get; }
48+
4649
/// <summary>True if published as a self-contained single executable</summary>
4750
public bool IsSelfContainedExe { get; }
4851

@@ -60,8 +63,21 @@ public static IDisposable SetResolver(Func<AppInfo> appInfoResolver)
6063

6164
public string? Version => _version ??= GetVersion(Instance.EntryAssembly);
6265

66+
// second ctor to avoid breaking change
6367
public AppInfo(
6468
bool isExe, bool isSelfContainedExe, bool isRunViaDotNetExe,
69+
bool isGlobalTool,
70+
Assembly entryAssembly,
71+
string filePath, string fileName,
72+
string? version = null)
73+
: this(isExe, isSelfContainedExe, isRunViaDotNetExe, entryAssembly, filePath, fileName, version)
74+
{
75+
IsGlobalTool = isGlobalTool;
76+
}
77+
78+
[Obsolete("Use ctor with isGlobalTool param")]
79+
public AppInfo(
80+
bool isExe, bool isSelfContainedExe, bool isRunViaDotNetExe,
6581
Assembly entryAssembly,
6682
string filePath, string fileName,
6783
string? version = null)
@@ -108,14 +124,20 @@ private static AppInfo BuildAppInfo()
108124
var isRunViaDotNetExe = false;
109125
var isSelfContainedExe = false;
110126
var isExe = false;
111-
if (mainModuleFileName != null)
127+
var isGlobalTool = false;
128+
if (mainModuleFileName is not null)
112129
{
113130
// osx uses 'dotnet' instead of 'dotnet.exe'
114-
if (!(isRunViaDotNetExe = mainModuleFileName.Equals("dotnet.exe") || mainModuleFileName.Equals("dotnet")))
131+
isRunViaDotNetExe = mainModuleFileName.Equals("dotnet.exe") || mainModuleFileName.Equals("dotnet");
132+
if (!isRunViaDotNetExe)
115133
{
116134
var entryAssemblyFileNameWithoutExt = Path.GetFileNameWithoutExtension(entryAssemblyFileName);
117135
isSelfContainedExe = isExe = mainModuleFileName.EndsWith($"{entryAssemblyFileNameWithoutExt}.exe");
118136
}
137+
138+
var globalToolsDirectory = DotNetSuggest.DotNetTools.GlobalToolDirectory;
139+
isGlobalTool = globalToolsDirectory is not null
140+
&& mainModuleFilePath!.StartsWith(globalToolsDirectory);
119141
}
120142

121143
isExe = isExe || entryAssemblyFileName.EndsWith("exe");
@@ -129,16 +151,17 @@ private static AppInfo BuildAppInfo()
129151
Log.Debug($" {nameof(FileName)}={fileName} " +
130152
$"{nameof(IsRunViaDotNetExe)}={isRunViaDotNetExe} " +
131153
$"{nameof(IsSelfContainedExe)}={isSelfContainedExe} " +
154+
$"{nameof(IsGlobalTool)}={isGlobalTool} " +
132155
$"{nameof(FilePath)}={filePath}");
133156

134-
return new AppInfo(isExe, isSelfContainedExe, isRunViaDotNetExe, entryAssembly, filePath!, fileName);
157+
return new AppInfo(isExe, isSelfContainedExe, isRunViaDotNetExe, isGlobalTool, entryAssembly, filePath!, fileName);
135158
}
136159

137160
private static string? GetVersion(Assembly hostAssembly) =>
138161
// thanks Spectre console for figuring this out https://github.com/spectreconsole/spectre.console/issues/242
139162
hostAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "?";
140163

141-
public object Clone() => new AppInfo(IsExe, IsSelfContainedExe, IsRunViaDotNetExe, EntryAssembly, FilePath, FileName, _version);
164+
public object Clone() => new AppInfo(IsExe, IsSelfContainedExe, IsRunViaDotNetExe, IsGlobalTool, EntryAssembly, FilePath, FileName, _version);
142165

143166
public override string ToString() => ToString(new Indent());
144167

CommandDotNet/CommandDotNet.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@
2727
<ItemGroup>
2828
<None Remove="CommandDotNet.csproj.DotSettings" />
2929
</ItemGroup>
30+
<ItemGroup>
31+
<Folder Include="DotNetSuggest\System.CommandLine.Suggest" />
32+
</ItemGroup>
3033
</Project>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.CommandLine.Suggest;
3+
using System.IO;
4+
using CommandDotNet.Builders;
5+
6+
namespace CommandDotNet.DotNetSuggest;
7+
8+
public static class DotNetTools
9+
{
10+
// see https://github.com/dotnet/command-line-api/blob/main/src/System.CommandLine.Suggest/GlobalToolsSuggestionRegistration.cs
11+
// for example
12+
public static string? GlobalToolDirectory =>
13+
DotnetProfileDirectory.TryGet(out string directory)
14+
? Path.Combine(directory, "tools")
15+
: null;
16+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// copied from: https://raw.githubusercontent.com/dotnet/command-line-api/main/src/System.CommandLine.Suggest/DotnetProfileDirectory.cs
2+
// via: DotNetSuggestSync test class
3+
4+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
5+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
6+
7+
using System.IO;
8+
using System.Runtime.InteropServices;
9+
10+
#pragma warning disable CS8600
11+
#pragma warning disable CS8603
12+
#pragma warning disable CS8625
13+
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
14+
// ReSharper disable CheckNamespace
15+
16+
namespace System.CommandLine.Suggest
17+
{
18+
public static class DotnetProfileDirectory
19+
{
20+
private const string DotnetHomeVariableName = "DOTNET_CLI_HOME";
21+
private const string DotnetProfileDirectoryName = ".dotnet";
22+
23+
private static string PlatformHomeVariableName =>
24+
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "USERPROFILE" : "HOME";
25+
26+
public static bool TryGet(out string dotnetProfileDirectory)
27+
{
28+
dotnetProfileDirectory = null;
29+
var home = Environment.GetEnvironmentVariable(DotnetHomeVariableName);
30+
if (string.IsNullOrEmpty(home))
31+
{
32+
home = Environment.GetEnvironmentVariable(PlatformHomeVariableName);
33+
if (string.IsNullOrEmpty(home))
34+
{
35+
return false;
36+
}
37+
}
38+
39+
dotnetProfileDirectory = Path.Combine(home, DotnetProfileDirectoryName);
40+
return true;
41+
}
42+
}
43+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// copied from: https://raw.githubusercontent.com/dotnet/command-line-api/main/src/System.CommandLine.Suggest/FileSuggestionRegistration.cs
2+
// via: DotNetSuggestSync test class
3+
4+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
5+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
6+
7+
using System.Collections.Generic;
8+
using System.IO;
9+
using System.Text;
10+
using static System.Environment;
11+
12+
#pragma warning disable CS8600
13+
#pragma warning disable CS8603
14+
#pragma warning disable CS8625
15+
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
16+
// ReSharper disable CheckNamespace
17+
18+
namespace System.CommandLine.Suggest
19+
{
20+
public class FileSuggestionRegistration
21+
{
22+
private const string RegistrationFileName = ".dotnet-suggest-registration.txt";
23+
private const string TestDirectoryOverride = "INTERNAL_TEST_DOTNET_SUGGEST_HOME";
24+
private readonly string _registrationConfigurationFilePath;
25+
26+
public FileSuggestionRegistration(string registrationsConfigurationFilePath = null)
27+
{
28+
if (!string.IsNullOrWhiteSpace(registrationsConfigurationFilePath))
29+
{
30+
_registrationConfigurationFilePath = registrationsConfigurationFilePath;
31+
return;
32+
}
33+
34+
var testDirectoryOverride = GetEnvironmentVariable(TestDirectoryOverride);
35+
if (!string.IsNullOrWhiteSpace(testDirectoryOverride))
36+
{
37+
_registrationConfigurationFilePath = Path.Combine(testDirectoryOverride, RegistrationFileName);
38+
return;
39+
}
40+
41+
var userProfile = GetFolderPath(SpecialFolder.UserProfile);
42+
43+
_registrationConfigurationFilePath = Path.Combine(userProfile, RegistrationFileName);
44+
}
45+
46+
public Registration FindRegistration(FileInfo soughtExecutable)
47+
{
48+
if (soughtExecutable == null)
49+
{
50+
return null;
51+
}
52+
53+
if (_registrationConfigurationFilePath == null
54+
|| !File.Exists(_registrationConfigurationFilePath))
55+
{
56+
return null;
57+
}
58+
59+
string completionTarget = null;
60+
using (var sr = new StreamReader(_registrationConfigurationFilePath, Encoding.UTF8))
61+
{
62+
while (sr.ReadLine() is string line)
63+
{
64+
if (line.StartsWith(soughtExecutable.FullName, StringComparison.OrdinalIgnoreCase))
65+
{
66+
completionTarget = line;
67+
}
68+
}
69+
}
70+
71+
if (completionTarget is null)
72+
{
73+
// Completion provider not found!
74+
return null;
75+
}
76+
77+
return new Registration(completionTarget);
78+
}
79+
80+
public IEnumerable<Registration> FindAllRegistrations()
81+
{
82+
var allRegistration = new List<Registration>();
83+
84+
if (_registrationConfigurationFilePath != null && File.Exists(_registrationConfigurationFilePath))
85+
{
86+
using (var sr = new StreamReader(_registrationConfigurationFilePath, Encoding.UTF8))
87+
{
88+
string line;
89+
while ((line = sr.ReadLine()) != null)
90+
{
91+
if (!string.IsNullOrWhiteSpace(line))
92+
{
93+
allRegistration.Add(new Registration(line.Trim()));
94+
}
95+
}
96+
}
97+
}
98+
99+
return allRegistration;
100+
}
101+
102+
public void AddSuggestionRegistration(Registration registration)
103+
{
104+
using (var writer = new StreamWriter(_registrationConfigurationFilePath, true))
105+
{
106+
writer.WriteLine(registration.ExecutablePath);
107+
}
108+
}
109+
}
110+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// copied from: https://raw.githubusercontent.com/dotnet/command-line-api/main/src/System.CommandLine.Suggest/RegistrationPair.cs
2+
// via: DotNetSuggestSync test class
3+
4+
#pragma warning disable CS8600
5+
#pragma warning disable CS8603
6+
#pragma warning disable CS8625
7+
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
8+
// ReSharper disable CheckNamespace
9+
10+
namespace System.CommandLine.Suggest
11+
{
12+
public class Registration
13+
{
14+
public Registration(string executablePath)
15+
{
16+
ExecutablePath = executablePath ?? throw new ArgumentNullException(nameof(executablePath));
17+
}
18+
19+
public string ExecutablePath { get; }
20+
}
21+
}

0 commit comments

Comments
 (0)