Skip to content

Commit fbe8e04

Browse files
committed
fix(host-sdk): align default module directories with CLI
1 parent 07fe322 commit fbe8e04

File tree

4 files changed

+80
-14
lines changed

4 files changed

+80
-14
lines changed

docs/host-app-development.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,17 @@ The generated templates use `ModulusHostSdkBuilder.AddDefaultModuleDirectories()
127127

128128
- **System modules**: `{AppBaseDir}/Modules`
129129
- **User modules (Windows)**: `%APPDATA%/Modulus/Modules`
130-
- **User modules (macOS/Linux)**: `Environment.SpecialFolder.ApplicationData/Modulus/Modules`
130+
- **User modules (macOS/Linux)**: `~/.modulus/Modules`
131131

132-
### Align the host with the CLI default on macOS/Linux
132+
### CLI alignment note
133133

134134
The Modulus CLI installs modules under:
135135

136136
- Windows: `%APPDATA%/Modulus/Modules`
137137
- macOS/Linux: `~/.modulus/Modules`
138138

139-
If you want your host to load modules installed by the CLI on macOS/Linux, add the `.modulus` directory explicitly:
139+
The default Host App templates are aligned with this location.
140+
If you are building a custom host and need to add it explicitly:
140141

141142
```csharp
142143
using Modulus.Core.Paths;

docs/host-app-development.zh-CN.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,15 @@ Host 模板使用:
7878

7979
- 系统模块:`{AppBaseDir}/Modules`
8080
- 用户模块(Windows):`%APPDATA%/Modulus/Modules`
81-
- 用户模块(macOS/Linux):`Environment.SpecialFolder.ApplicationData/Modulus/Modules`
81+
- 用户模块(macOS/Linux):`~/.modulus/Modules`
8282

8383
CLI 默认安装路径为:
8484

8585
- Windows:`%APPDATA%/Modulus/Modules`
8686
- macOS/Linux:`~/.modulus/Modules`
8787

88-
因此在 macOS/Linux 上,**Host 默认不会自动加载** CLI 安装到 `~/.modulus/Modules` 的模块。
89-
90-
如果你希望 Host 在 macOS/Linux 上也能直接加载 CLI 安装的模块,请在 Host 代码里额外加入 `.modulus` 目录:
88+
默认 Host App 模板已经与 CLI 安装目录对齐。
89+
如果你在自定义 Host 中需要显式加入该目录,可以在 Host 代码里添加:
9190

9291
```csharp
9392
using Modulus.Core.Paths;

src/Modulus.HostSdk.Runtime/ModulusHostSdkBuilder.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Modulus.Core;
55
using Modulus.Core.Data;
66
using Modulus.Core.Installation;
7+
using Modulus.Core.Paths;
78
using Modulus.Core.Runtime;
89
using Modulus.HostSdk.Abstractions;
910
using Modulus.Infrastructure.Data.Repositories;
@@ -37,18 +38,26 @@ public ModulusHostSdkBuilder(IServiceCollection services, IConfiguration configu
3738
/// <summary>
3839
/// Adds default module directories:
3940
/// - {AppBaseDir}/Modules (system)
40-
/// - %APPDATA%/Modulus/Modules (user)
41+
/// - {UserRoot}/Modules (user)
4142
/// </summary>
4243
public ModulusHostSdkBuilder AddDefaultModuleDirectories()
4344
{
4445
var appModules = Path.Combine(AppContext.BaseDirectory, "Modules");
4546
_moduleDirectories.Add(new HostModuleDirectory(appModules, IsSystem: true));
4647

47-
var userModules = Path.Combine(
48+
// Canonical user modules directory: align with Modulus.Core.Paths.LocalStorage (Windows: %APPDATA%/Modulus; others: $HOME/.modulus).
49+
var userModules = Path.Combine(LocalStorage.GetUserRoot(), "Modules");
50+
_moduleDirectories.Add(new HostModuleDirectory(userModules, IsSystem: false));
51+
52+
// Backward compatibility: some hosts may have used ApplicationData directly on non-Windows before LocalStorage aligned paths.
53+
var legacyUserModules = Path.Combine(
4854
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
4955
"Modulus",
5056
"Modules");
51-
_moduleDirectories.Add(new HostModuleDirectory(userModules, IsSystem: false));
57+
if (!IsSameDirectory(userModules, legacyUserModules))
58+
{
59+
_moduleDirectories.Add(new HostModuleDirectory(legacyUserModules, IsSystem: false));
60+
}
5261

5362
return this;
5463
}
@@ -86,8 +95,12 @@ public async Task<IModulusApplication> BuildAsync<TStartupModule>(ILoggerFactory
8695
{
8796
ArgumentNullException.ThrowIfNull(loggerFactory);
8897

89-
// Filter directories that actually exist to avoid noisy warnings.
98+
// Filter directories that actually exist (and de-duplicate paths) to avoid noisy warnings and double-scanning.
99+
var pathComparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
90100
var dirs = _moduleDirectories
101+
.Select(d => new { Dir = d, NormalizedPath = NormalizeDirectoryPath(d.Path) })
102+
.GroupBy(x => x.NormalizedPath, pathComparer)
103+
.Select(g => g.OrderByDescending(x => x.Dir.IsSystem).First().Dir)
91104
.Where(d => Directory.Exists(d.Path))
92105
.Select(d => new ModuleDirectory(d.Path, d.IsSystem))
93106
.ToList();
@@ -103,6 +116,18 @@ public async Task<IModulusApplication> BuildAsync<TStartupModule>(ILoggerFactory
103116

104117
return app;
105118
}
119+
120+
private static string NormalizeDirectoryPath(string path)
121+
{
122+
var full = Path.GetFullPath(path);
123+
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
124+
}
125+
126+
private static bool IsSameDirectory(string a, string b)
127+
{
128+
var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
129+
return comparer.Equals(NormalizeDirectoryPath(a), NormalizeDirectoryPath(b));
130+
}
106131
}
107132

108133

tests/Modulus.HostSdk.Tests/HostSdkBuilderTests.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using Modulus.HostSdk.Runtime;
77
using Modulus.Sdk;
88
using Modulus.Core.Architecture;
9+
using Modulus.Core.Paths;
10+
using System.Reflection;
911

1012
namespace Modulus.HostSdk.Tests;
1113

@@ -16,7 +18,7 @@ private sealed class TestHostModule : ModulusPackage
1618
}
1719

1820
[Fact]
19-
public void AddDefaultModuleDirectories_AddsTwoDirectories()
21+
public void AddDefaultModuleDirectories_AddsExpectedDefaultDirectories()
2022
{
2123
var services = new ServiceCollection();
2224
var config = new ConfigurationBuilder().Build();
@@ -33,8 +35,24 @@ public void AddDefaultModuleDirectories_AddsTwoDirectories()
3335

3436
Assert.True(builder.Options.ModuleDirectories.Count == 0, "Options.ModuleDirectories should not be mutated by builder methods.");
3537

36-
// We can't access the builder's private list; this test is mainly a behavioral guard that it doesn't throw and is chainable.
37-
Assert.NotNull(builder);
38+
// Validate defaults via reflection (avoid adding public API solely for tests).
39+
var list = GetPrivateModuleDirectories(builder);
40+
41+
var systemDir = Path.Combine(AppContext.BaseDirectory, "Modules");
42+
var userDir = Path.Combine(LocalStorage.GetUserRoot(), "Modules");
43+
var legacyUserDir = Path.Combine(
44+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
45+
"Modulus",
46+
"Modules");
47+
48+
Assert.Contains(list, d => d.IsSystem && PathEquals(d.Path, systemDir));
49+
Assert.Contains(list, d => !d.IsSystem && PathEquals(d.Path, userDir));
50+
51+
// Only included when distinct from the canonical user directory.
52+
if (!PathEquals(userDir, legacyUserDir))
53+
{
54+
Assert.Contains(list, d => !d.IsSystem && PathEquals(d.Path, legacyUserDir));
55+
}
3856
}
3957

4058
[Fact]
@@ -154,6 +172,29 @@ public async Task BuildAsync_CreatesApplication()
154172
try { Directory.Delete(tempDir, recursive: true); } catch { }
155173
}
156174
}
175+
176+
private static List<HostModuleDirectory> GetPrivateModuleDirectories(ModulusHostSdkBuilder builder)
177+
{
178+
var field = typeof(ModulusHostSdkBuilder).GetField("_moduleDirectories", BindingFlags.Instance | BindingFlags.NonPublic);
179+
Assert.NotNull(field);
180+
181+
var value = field!.GetValue(builder);
182+
Assert.NotNull(value);
183+
184+
return Assert.IsAssignableFrom<List<HostModuleDirectory>>(value);
185+
}
186+
187+
private static bool PathEquals(string a, string b)
188+
{
189+
var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
190+
return comparer.Equals(Normalize(a), Normalize(b));
191+
}
192+
193+
private static string Normalize(string path)
194+
{
195+
var full = Path.GetFullPath(path);
196+
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
197+
}
157198
}
158199

159200

0 commit comments

Comments
 (0)