1- using System . Security . Cryptography ;
2- using System . Text ;
31using Aspire . Hosting . ApplicationModel ;
42using Microsoft . Extensions . DependencyInjection ;
53
@@ -77,33 +75,6 @@ public IAspireC4Builder WithAdditionalDSLFile(string sourcePath)
7775 opts . AdditionalDSLFiles . Add ( absoluteSource )
7876 ) ;
7977
80- // For container resources, bind-mount the source directory directly into the container
81- // so the LikeC4 server sees live edits without Aspire restarting.
82- if ( LikeC4ResourceBuilder . Resource is ContainerResource containerResource )
83- {
84- var sourceDir = Path . GetDirectoryName ( absoluteSource ) ! ;
85- var hash = ComputeShortHash ( sourceDir ) ;
86- var mountTarget = $ "{ LikeC4ServerResource . WorkspacePath } /ext/{ hash } ";
87-
88- // De-duplicate: only add the bind mount once per unique source directory.
89- var alreadyMounted = containerResource
90- . Annotations . OfType < ContainerMountAnnotation > ( )
91- . Any ( a => a . Target == mountTarget ) ;
92-
93- if ( ! alreadyMounted )
94- {
95- containerResource . Annotations . Add (
96- new ContainerMountAnnotation ( sourceDir , mountTarget , ContainerMountType . BindMount , isReadOnly : true )
97- ) ;
98- }
99-
100- // Tell the lifecycle hook not to also sync this file into the named Docker volume,
101- // which would create a duplicate definition visible to LikeC4.
102- ApplicationBuilder . Services . Configure < LikeC4ContainerWorkspaceOptions > ( wsOpts =>
103- wsOpts . BindMountedSourceFiles . Add ( absoluteSource )
104- ) ;
105- }
106-
10778 return this ;
10879 }
10980
@@ -119,33 +90,6 @@ public IAspireC4Builder WithAdditionalDSLFolder(string folderPath)
11990 opts . AdditionalDSLFolders . Add ( absoluteFolder )
12091 ) ;
12192
122- if ( LikeC4ResourceBuilder . Resource is ContainerResource containerResource )
123- {
124- var hash = ComputeShortHash ( absoluteFolder ) ;
125- var containerRelative = $ "ext/{ hash } ";
126- var mountTarget = $ "{ LikeC4ServerResource . WorkspacePath } /{ containerRelative } ";
127-
128- var alreadyMounted = containerResource
129- . Annotations . OfType < ContainerMountAnnotation > ( )
130- . Any ( a => a . Target == mountTarget ) ;
131-
132- if ( ! alreadyMounted )
133- {
134- containerResource . Annotations . Add (
135- new ContainerMountAnnotation (
136- absoluteFolder ,
137- mountTarget ,
138- ContainerMountType . BindMount ,
139- isReadOnly : true
140- )
141- ) ;
142- }
143-
144- ApplicationBuilder . Services . Configure < LikeC4ContainerWorkspaceOptions > ( wsOpts =>
145- wsOpts . BindMountedFolderTargets . TryAdd ( absoluteFolder , containerRelative )
146- ) ;
147- }
148-
14993 return this ;
15094 }
15195
@@ -165,33 +109,6 @@ public IAspireC4Builder WithImageAliasFolder(string aliasKey, string folderPath)
165109 opts . ImageAliases [ aliasKey ] = absoluteFolder
166110 ) ;
167111
168- if ( LikeC4ResourceBuilder . Resource is ContainerResource containerResource )
169- {
170- var hash = ComputeShortHash ( absoluteFolder ) ;
171- var containerRelative = $ "img/{ hash } ";
172- var mountTarget = $ "{ LikeC4ServerResource . WorkspacePath } /{ containerRelative } ";
173-
174- var alreadyMounted = containerResource
175- . Annotations . OfType < ContainerMountAnnotation > ( )
176- . Any ( a => a . Target == mountTarget ) ;
177-
178- if ( ! alreadyMounted )
179- {
180- containerResource . Annotations . Add (
181- new ContainerMountAnnotation (
182- absoluteFolder ,
183- mountTarget ,
184- ContainerMountType . BindMount ,
185- isReadOnly : true
186- )
187- ) ;
188- }
189-
190- ApplicationBuilder . Services . Configure < LikeC4ContainerWorkspaceOptions > ( wsOpts =>
191- wsOpts . BindMountedImageAliasFolderTargets . TryAdd ( aliasKey , containerRelative )
192- ) ;
193- }
194-
195112 return this ;
196113 }
197114
@@ -202,12 +119,89 @@ public IAspireC4Builder WithoutConfigFileGeneration()
202119 return this ;
203120 }
204121
205- static string ComputeShortHash ( string value )
122+ /// <summary>
123+ /// Returns the correct bind-mount source path for the given host directory.
124+ /// </summary>
125+ /// <remarks>
126+ /// On Windows the correct format depends on the container runtime:
127+ /// <list type="bullet">
128+ /// <item><description>
129+ /// <b>Docker Desktop (official)</b> — natively understands Windows paths
130+ /// (<c>C:\…</c>); DCP passes them verbatim to the Docker API so the path is
131+ /// returned unchanged.
132+ /// </description></item>
133+ /// <item><description>
134+ /// <b>Rancher Desktop</b> — runs a Linux <c>dockerd</c> inside WSL2 that only
135+ /// understands Linux paths. Windows drives are accessible inside WSL2 at
136+ /// <c>/mnt/<drive>/…</c> (standard WSL2 mount points), so the path is
137+ /// converted to that format.
138+ /// </description></item>
139+ /// </list>
140+ /// On non-Windows the path is returned unchanged.
141+ /// </remarks>
142+ internal static string NormalizeBindMountPath ( string absolutePath ) =>
143+ NormalizeBindMountPath ( absolutePath , _isRancherDesktop . Value ) ;
144+
145+ /// <summary>Overload with an explicit runtime flag — used by unit tests to avoid
146+ /// spawning a <c>docker</c> process.</summary>
147+ internal static string NormalizeBindMountPath ( string absolutePath , bool isRancherDesktop )
148+ {
149+ if ( ! OperatingSystem . IsWindows ( ) )
150+ return absolutePath ;
151+
152+ var fullPath = Path . GetFullPath ( absolutePath ) ;
153+
154+ // Rancher Desktop exposes Windows drives under /mnt/<letter>/ inside WSL2.
155+ // CA1308: intentional — Linux paths require lower-case.
156+ if ( isRancherDesktop && fullPath . Length >= 2 && char . IsAsciiLetter ( fullPath [ 0 ] ) && fullPath [ 1 ] == ':' )
157+ {
158+ #pragma warning disable CA1308
159+ return $ "/mnt/{ char . ToLowerInvariant ( fullPath [ 0 ] ) } { fullPath [ 2 ..] . Replace ( '\\ ' , '/' ) } ". ToLowerInvariant ( ) ;
160+ #pragma warning restore CA1308
161+ }
162+
163+ // Docker Desktop or other runtime: return Windows path as-is; DCP / Docker Desktop
164+ // translate it internally.
165+ return fullPath ;
166+ }
167+
168+ static readonly Lazy < bool > _isRancherDesktop = new (
169+ DetectRancherDesktop ,
170+ LazyThreadSafetyMode . ExecutionAndPublication
171+ ) ;
172+
173+ [ System . Diagnostics . CodeAnalysis . SuppressMessage (
174+ "Design" ,
175+ "CA1031:Do not catch general exception types" ,
176+ Justification = "Runtime detection must not throw; failure falls back to Docker Desktop behavior."
177+ ) ]
178+ static bool DetectRancherDesktop ( )
206179 {
207- var hashBytes = SHA256 . HashData ( Encoding . UTF8 . GetBytes ( value ) ) ;
208- #pragma warning disable CA1308 // Normalize strings to uppercase
209- return Convert . ToHexString ( hashBytes ) [ ..8 ] . ToLowerInvariant ( ) ;
210- #pragma warning restore CA1308 // Normalize strings to uppercase
180+ if ( ! OperatingSystem . IsWindows ( ) )
181+ return false ;
182+
183+ try
184+ {
185+ using var process = System . Diagnostics . Process . Start (
186+ new System . Diagnostics . ProcessStartInfo
187+ {
188+ FileName = "docker" ,
189+ Arguments = "info --format {{.OperatingSystem}}" ,
190+ RedirectStandardOutput = true ,
191+ RedirectStandardError = true ,
192+ UseShellExecute = false ,
193+ CreateNoWindow = true ,
194+ }
195+ ) ;
196+
197+ process ? . WaitForExit ( 5_000 ) ;
198+ var os = process ? . StandardOutput . ReadToEnd ( ) . Trim ( ) ?? "" ;
199+ return os . Contains ( "Rancher Desktop" , StringComparison . OrdinalIgnoreCase ) ;
200+ }
201+ catch
202+ {
203+ return false ;
204+ }
211205 }
212206
213207 static LikeC4LocalCLIRuntime DetectRuntime ( )
@@ -231,7 +225,7 @@ static LikeC4LocalCLIRuntime DetectRuntime()
231225 throw new DistributedApplicationException (
232226 "No supported JavaScript package manager was found on the system PATH. "
233227 + "Install one of: Node.js (npx), pnpm, yarn, bun, or Deno, then retry. "
234- + "Alternatively, remove WithLocalCli () to use the Docker container (default)."
228+ + "Alternatively, remove WithLocalCLI () to use the Docker container (default)."
235229 ) ;
236230 }
237231
@@ -273,7 +267,9 @@ static bool IsExecutableOnPath(string executable)
273267 /// <example>
274268 /// Npx → <c>("npx", ["likec4"])</c> so the full call is <c>npx likec4 format ...</c>
275269 /// Pnpm → <c>("pnpm", ["exec", "likec4"])</c>
276- /// Bun → <c>("bunx", ["likec4"])</c>
270+ /// Yarn → <c>("yarn", ["dlx", "likec4"])</c>
271+ /// Bun → <c>("bunx", ["--bun", "likec4"])</c>
272+ /// Deno → <c>("deno", ["run", "--allow-all", "likec4"])</c>
277273 /// </example>
278274 internal static ( string Command , string [ ] Prefix ) BuildLikeC4CliPrefix ( LikeC4LocalCLIRuntime runtime ) =>
279275 runtime switch
@@ -302,7 +298,7 @@ int port
302298 LikeC4LocalCLIRuntime . Npx => ( "npx" , [ "likec4" , "serve" , outputDirectory , "--port" , portStr ] ) ,
303299 LikeC4LocalCLIRuntime . Pnpm => ( "pnpm" , [ "exec" , "likec4" , "serve" , outputDirectory , "--port" , portStr ] ) ,
304300 LikeC4LocalCLIRuntime . Yarn => ( "yarn" , [ "dlx" , "likec4" , "serve" , outputDirectory , "--port" , portStr ] ) ,
305- LikeC4LocalCLIRuntime . Bun => ( "bunx" , [ "likec4" , "serve" , outputDirectory , "--port" , portStr ] ) ,
301+ LikeC4LocalCLIRuntime . Bun => ( "bunx" , [ "--bun" , " likec4", "serve" , outputDirectory , "--port" , portStr ] ) ,
306302 LikeC4LocalCLIRuntime . Deno => (
307303 "deno" ,
308304 [ "run" , "--allow-all" , "likec4" , "serve" , outputDirectory , "--port" , portStr ]
0 commit comments