@@ -513,23 +513,119 @@ public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
513513 // i.e. I've got only private install and no global installation, in this case apphost needs to use env var to locate runtime.
514514 if ( testHostExeFound )
515515 {
516- string prefix = "VSTEST_WINAPPHOST_" ;
517- string dotnetRootEnvName = $ " { prefix } DOTNET_ROOT(x86)" ;
518- var dotnetRoot = _environmentVariableHelper . GetEnvironmentVariable ( dotnetRootEnvName ) ;
519- if ( dotnetRoot is null )
516+ // This change needs to happen first on vstest side, and then on dotnet/sdk, so prefer this approach and fallback to the old one.
517+ // VSTEST_DOTNET_ROOT v2
518+ string ? dotnetRootPath = _environmentVariableHelper . GetEnvironmentVariable ( "VSTEST_DOTNET_ROOT_PATH" ) ;
519+ if ( ! StringUtils . IsNullOrWhiteSpace ( dotnetRootPath ) )
520520 {
521- dotnetRootEnvName = $ "{ prefix } DOTNET_ROOT";
522- dotnetRoot = _environmentVariableHelper . GetEnvironmentVariable ( dotnetRootEnvName ) ;
523- }
521+ // This is v2 of the environment variables that we are passing, we are in new dotnet sdk. So also grab the architecture.
522+ string ? dotnetRootArchitecture = _environmentVariableHelper . GetEnvironmentVariable ( "VSTEST_DOTNET_ROOT_ARCHITECTURE" ) ;
524523
525- if ( dotnetRoot != null )
526- {
527- EqtTrace . Verbose ( $ "DotnetTestHostmanager.LaunchTestHostAsync: Found '{ dotnetRootEnvName } ' in env variables, value '{ dotnetRoot } ', forwarding to '{ dotnetRootEnvName . Replace ( prefix , string . Empty ) } '") ;
528- startInfo . EnvironmentVariables . Add ( dotnetRootEnvName . Replace ( prefix , string . Empty ) , dotnetRoot ) ;
524+ if ( StringUtils . IsNullOrWhiteSpace ( dotnetRootArchitecture ) )
525+ {
526+ throw new InvalidOperationException ( "'VSTEST_DOTNET_ROOT_PATH' and 'VSTEST_DOTNET_ROOT_ARCHITECTURE' must be both always set. If you are seeing this error, this is a bug in dotnet SDK that sets those variables." ) ;
527+ }
528+
529+ EqtTrace . Verbose ( $ "DotnetTestHostmanager.LaunchTestHostAsync: VSTEST_DOTNET_ROOT_PATH={ dotnetRootPath } ") ;
530+ EqtTrace . Verbose ( $ "DotnetTestHostmanager.LaunchTestHostAsync: VSTEST_DOTNET_ROOT_ARCHITECTURE={ dotnetRootArchitecture } ") ;
531+
532+ // The parent process is passing to us the path in which the dotnet.exe is and is passing the architecture of the dotnet.exe,
533+ // so if the child process (testhost) is the same architecture it can pick up that dotnet.exe location and run. This is to allow
534+ // local installations of dotnet/sdk to work with testhost.
535+ //
536+ // There are 2 complications in this process:
537+ // 1) There are differences between how .NET Apphosts are handling DOTNET_ROOT, versions pre-net6 are only looking at
538+ // DOTNET_ROOT(x86) and then DOTNET_ROOT. This makes is really easy to set DOTNET_ROOT to point at x64 dotnet installation
539+ // and have that picked up by x86 testhost and fail.
540+ // Unfortunately vstest.console has to support both new (17.14+) testhosts that are built against net8, and old (pre 17.14)
541+ // testhosts that are built using netcoreapp3.1 apphost, and so their approach to resolving DOTNET_ROOT differ.
542+ //
543+ // /!\ The apphost version does not align with the targeted framework (tfm), an older testhost is built against netcoreapp3.1
544+ // but can be used to run net8 tests. The only way to tell is the version of the testhost.
545+ //
546+ // netcoreapp3.1 hosts only support DOTNET_ROOT and DOTNET_ROOT(x86) env variables.
547+ // net8 hosts, support also DOTNET_ROOT_<ARCH> variables, which is what we should prefer to set the location of dotnet
548+ // in a more architecture specific way.
549+ //
550+ // 2) The surrounding environment might already have the environment variables set, most likely by setting DOTNET_ROOT, which is
551+ // a universal way of setting where the dotnet is, that works across all different architectures of the .NET apphost.
552+ // By setting our (hopefully more specific variable) we might overwrite what user specified, and in case of DOTNET_ROOT it is probably
553+ // preferable when we can set the DOTNET_ROOT_<ARCH> variable.
554+ var testhostDllPath = Path . ChangeExtension ( startInfo . FileName , ".dll" ) ;
555+ // This file check is for unit tests, we expect the file to always be there. Otherwise testhost.exe would not be able to run.
556+ var testhostVersionInfo = _fileHelper . Exists ( testhostDllPath ) ? FileVersionInfo . GetVersionInfo ( testhostDllPath ) : null ;
557+ if ( testhostVersionInfo != null && testhostVersionInfo . ProductMajorPart >= 17 && testhostVersionInfo . ProductMinorPart >= 14 )
558+ {
559+ // This is a new testhost that builds at least against net8 we should set the architecture specific DOTNET_ROOT_<ARCH>.
560+ //
561+ // We ship just testhost.exe and testhost.x86.exe if the architecture is different we won't find the testhost*.exe and
562+ // won't reach this code, but let's write this in a generic way anyway, to avoid breaking if we add more variants of testhost*.exe.
563+ var environmentVariableName = $ "DOTNET_ROOT_{ _architecture . ToString ( ) . ToUpperInvariant ( ) } ";
564+
565+ var existingDotnetRoot = _environmentVariableHelper . GetEnvironmentVariable ( environmentVariableName ) ;
566+ if ( ! StringUtilities . IsNullOrWhiteSpace ( existingDotnetRoot ) )
567+ {
568+ // The variable is already set in the surrounding environment, don't set it, because we want to keep what user provided.
569+ }
570+ else
571+ {
572+ var architectureFromEnv = ( Architecture ) Enum . Parse ( typeof ( Architecture ) , dotnetRootArchitecture , ignoreCase : true ) ;
573+ if ( architectureFromEnv == _architecture )
574+ {
575+ // Set the architecture specific variable to the environment of the process so it is picked up.
576+ startInfo . EnvironmentVariables . Add ( environmentVariableName , dotnetRootPath ) ;
577+ }
578+ }
579+ }
580+ else
581+ {
582+ // This is an old testhost that built against netcoreapp3.1, it does not understand architecture specific DOTNET_ROOT_<ARCH>, we have to set it more carefully
583+ // to avoid setting DOTNET_ROOT that points to x64 but is picked up by x86 host.
584+ //
585+ // Also avoid setting it if we are already getting it from the surrounding environment.
586+ var architectureFromEnv = ( Architecture ) Enum . Parse ( typeof ( Architecture ) , dotnetRootArchitecture , ignoreCase : true ) ;
587+ if ( architectureFromEnv == _architecture )
588+ {
589+ if ( _architecture == Architecture . X86 )
590+ {
591+ const string dotnetRootX86 = "DOTNET_ROOT(x86)" ;
592+ if ( StringUtils . IsNullOrWhiteSpace ( _environmentVariableHelper . GetEnvironmentVariable ( dotnetRootX86 ) ) )
593+ {
594+ startInfo . EnvironmentVariables . Add ( dotnetRootX86 , dotnetRootPath ) ;
595+ }
596+ }
597+ else
598+ {
599+ const string dotnetRoot = "DOTNET_ROOT" ;
600+ if ( StringUtils . IsNullOrWhiteSpace ( _environmentVariableHelper . GetEnvironmentVariable ( dotnetRoot ) ) )
601+ {
602+ startInfo . EnvironmentVariables . Add ( dotnetRoot , dotnetRootPath ) ;
603+ }
604+ }
605+ }
606+ }
529607 }
530608 else
531609 {
532- EqtTrace . Verbose ( $ "DotnetTestHostmanager.LaunchTestHostAsync: Prefix '{ prefix } *' not found in env variables") ;
610+ // Fallback, can delete this once the change is in dotnet sdk. because they are always used together.
611+ string prefix = "VSTEST_WINAPPHOST_" ;
612+ string dotnetRootEnvName = $ "{ prefix } DOTNET_ROOT(x86)";
613+ var dotnetRoot = _environmentVariableHelper . GetEnvironmentVariable ( dotnetRootEnvName ) ;
614+ if ( dotnetRoot is null )
615+ {
616+ dotnetRootEnvName = $ "{ prefix } DOTNET_ROOT";
617+ dotnetRoot = _environmentVariableHelper . GetEnvironmentVariable ( dotnetRootEnvName ) ;
618+ }
619+
620+ if ( dotnetRoot != null )
621+ {
622+ EqtTrace . Verbose ( $ "DotnetTestHostmanager.LaunchTestHostAsync: Found '{ dotnetRootEnvName } ' in env variables, value '{ dotnetRoot } ', forwarding to '{ dotnetRootEnvName . Replace ( prefix , string . Empty ) } '") ;
623+ startInfo . EnvironmentVariables . Add ( dotnetRootEnvName . Replace ( prefix , string . Empty ) , dotnetRoot ) ;
624+ }
625+ else
626+ {
627+ EqtTrace . Verbose ( $ "DotnetTestHostmanager.LaunchTestHostAsync: Prefix '{ prefix } *' not found in env variables") ;
628+ }
533629 }
534630 }
535631
0 commit comments