@@ -138,13 +138,21 @@ private static BuildArtifacts BuildAppWithProperties(Dictionary<string, string>
138138 var psi = new ProcessStartInfo
139139 {
140140 FileName = "dotnet" ,
141- Arguments = string . Join ( " " , buildArgs ) ,
142141 UseShellExecute = false ,
143142 RedirectStandardOutput = true ,
144143 RedirectStandardError = true ,
145144 CreateNoWindow = true
146145 } ;
147146
147+ #if NET
148+ foreach ( string buildArg in buildArgs )
149+ {
150+ psi . ArgumentList . Add ( buildArg ) ;
151+ }
152+ #else
153+ psi . Arguments = string . Join ( " " , buildArgs . Select ( QuoteArgument ) ) ;
154+ #endif
155+
148156 using ( var process = Process . Start ( psi ) )
149157 {
150158 if ( process == null )
@@ -162,12 +170,21 @@ private static BuildArtifacts BuildAppWithProperties(Dictionary<string, string>
162170 process . Kill ( ) ;
163171 throw new InvalidOperationException ( "Build timed out after 60 seconds" ) ;
164172 }
165- #else
166- process . WaitForExit ( 60000 ) ;
167- #endif
168173
169174 stdout = process . StandardOutput . ReadToEnd ( ) ;
170175 stderr = process . StandardError . ReadToEnd ( ) ;
176+ #else
177+ Task < string > stdoutTask = process . StandardOutput . ReadToEndAsync ( ) ;
178+ Task < string > stderrTask = process . StandardError . ReadToEndAsync ( ) ;
179+ bool completed = process . WaitForExit ( 60000 ) ;
180+ stdout = stdoutTask . GetAwaiter ( ) . GetResult ( ) ;
181+ stderr = stderrTask . GetAwaiter ( ) . GetResult ( ) ;
182+ if ( ! completed )
183+ {
184+ process . Kill ( ) ;
185+ throw new InvalidOperationException ( "Build timed out after 60 seconds" ) ;
186+ }
187+ #endif
171188
172189 // Fail fast with full command/stdout/stderr context for easy diagnosis.
173190 if ( process . ExitCode != 0 )
@@ -219,7 +236,7 @@ private static void CopyDirectory(string sourceDirectory, string destinationDire
219236
220237 foreach ( string directory in Directory . GetDirectories ( sourceDirectory , "*" , SearchOption . AllDirectories ) )
221238 {
222- string relativePath = Path . GetRelativePath ( sourceDirectory , directory ) ;
239+ string relativePath = GetRelativePath ( sourceDirectory , directory ) ;
223240 if ( ShouldSkip ( relativePath ) )
224241 {
225242 continue ;
@@ -230,7 +247,7 @@ private static void CopyDirectory(string sourceDirectory, string destinationDire
230247
231248 foreach ( string file in Directory . GetFiles ( sourceDirectory , "*" , SearchOption . AllDirectories ) )
232249 {
233- string relativePath = Path . GetRelativePath ( sourceDirectory , file ) ;
250+ string relativePath = GetRelativePath ( sourceDirectory , file ) ;
234251 if ( ShouldSkip ( relativePath ) )
235252 {
236253 continue ;
@@ -251,7 +268,7 @@ private static bool ShouldSkip(string relativePath)
251268 {
252269 // Ignore generated artifacts and the test project itself to avoid recursive/self-copy issues.
253270 string normalizedPath = relativePath . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ) ;
254- string [ ] segments = normalizedPath . Split ( Path . DirectorySeparatorChar , StringSplitOptions . RemoveEmptyEntries ) ;
271+ string [ ] segments = normalizedPath . Split ( new [ ] { Path . DirectorySeparatorChar } , StringSplitOptions . RemoveEmptyEntries ) ;
255272
256273 foreach ( string segment in segments )
257274 {
@@ -308,4 +325,42 @@ private static string GetPackageCompatibilityDirectory()
308325 /// <param name="OutputDirectory">Build output directory for binaries.</param>
309326 /// <param name="GeneratedVersionsFile">Path to generated <c>PackageVersions.g.cs</c>.</param>
310327 private sealed record BuildArtifacts ( string ProjectDirectory , string OutputDirectory , string GeneratedVersionsFile ) ;
328+
329+ #if ! NET
330+ /// <summary>
331+ /// Quotes a single command-line argument for use with <see cref="System.Diagnostics.ProcessStartInfo.Arguments"/>
332+ /// on .NET Framework, where <c>ArgumentList</c> is unavailable.
333+ /// Wraps the value in double-quotes and escapes any embedded double-quotes.
334+ /// </summary>
335+ /// <param name="arg">The argument to quote.</param>
336+ /// <returns>The argument, quoted if it contains whitespace or double-quote characters.</returns>
337+ private static string QuoteArgument ( string arg )
338+ {
339+ if ( arg . IndexOfAny ( new [ ] { ' ' , '\t ' , '"' } ) < 0 )
340+ {
341+ return arg ;
342+ }
343+
344+ return "\" " + arg . Replace ( "\\ " , "\\ \\ " ) . Replace ( "\" " , "\\ \" " ) + "\" " ;
345+ }
346+ #endif
347+
348+ /// <summary>
349+ /// Returns the relative path from <paramref name="relativeTo"/> to <paramref name="path"/>.
350+ /// Polyfills <see cref="Path.GetRelativePath"/> which is unavailable on .NET Framework.
351+ /// </summary>
352+ private static string GetRelativePath ( string relativeTo , string path )
353+ {
354+ #if NET
355+ return Path . GetRelativePath ( relativeTo , path ) ;
356+ #else
357+ // Ensure the base URI ends with a separator so MakeRelativeUri treats it as a directory.
358+ string baseStr = relativeTo . TrimEnd ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar )
359+ + Path . DirectorySeparatorChar ;
360+ Uri baseUri = new Uri ( baseStr ) ;
361+ Uri targetUri = new Uri ( path ) ;
362+ return Uri . UnescapeDataString ( baseUri . MakeRelativeUri ( targetUri ) . ToString ( ) )
363+ . Replace ( '/' , Path . DirectorySeparatorChar ) ;
364+ #endif
365+ }
311366}
0 commit comments