How to apply EWDK's SetupBuildEnv.cmd to running powershell session - powershell

I am using EWDK and msbuild to build some drivers.
I have a build.cmd file that works.
call <EWDK root>/BuildEnv\SetupBuildEnv.cmd
cd <working directory>
msbuild /t:build /p:...
Calling this file from PowerShell works
& build.cmd
I want to write it natively in PowerShell so I won't have a mix of languages.
I tried running it with PowerShell
& <EWDK root>/BuildEnv\SetupBuildEnv.cmd
& msbuild /t:build /p:...
And the build failed because none of the environment variables set in SetupBuildEnv.cmd were kept in the PowerShell session that called the script.
I can't re-write SetupBuildEnv.cmd as it comes with the SDK package so this is the only .cmd script which PowerShell should call.
I'd like to have msbuild.exe called directly in PowerShell.
Is there anyway to make this work?

Batch/cmd scripts called from PowerShell run in a new process (cmd.exe). Environment variables set by a batch script (using the set command) don't persist when the cmd.exe process ends, so the PowerShell parent process can't access them.
As a workaround, create a helper batch script that calls SetupBuildEnv.cmd and then outputs the current values of all environment variables. This output can be captured by the PowerShell script which can then set the environment variables of the PowerShell process accordingly, so msbuild called from PowerShell will inherit them.
The helper script doesn't need to be a separate file, it can be a cmd.exe /c one-liner, as in the following code sample:
$envBlockFound = $false
cmd /c 'call "<EWDK root>\BuildEnv\SetupBuildEnv.cmd" & echo ###EnvVars### & set' | ForEach-Object {
if( $envBlockFound ) {
$name, $value = $_ -split '='
Set-Item env:$name $value
}
elseif( $_.StartsWith('###EnvVars###') ) {
$envBlockFound = $true
}
}
# uncomment to verify env vars
# Get-Item env:
msbuild /t:build /p:...
The cmd /c … line breaks down to:
call "<EWDK root>\BuildEnv\SetupBuildEnv.cmd" …runs the given script and waits until it has finished.
echo ###EnvVars### …outputs a delimiter line so the PowerShell script can ignore the stdout from SetupBuildEnv. Note that the space character before the next & ends up in the output as a trailing space, which the PowerShell script has to handle.
set …without arguments outputs all environment variables as key/value pairs separated by =.
Using ForEach-Object, we process each line ($_) from stdout of cmd.exe, which also includes any stdout lines of SetupBuildEnv.cmd.
Ignore all lines until the delimiter line '###EnvVars###' is found.
When the delimiter string has been found (comparing using .StartsWith() instead of -eq to ignore the trailing space), then for each line split the line on = and set an environment variable.
Finally call the msbuild process which now inherits the env vars.

Related

How do I have to change PowerShell variables code so that I can run it via CMD?

How do I have to change PowerShell code so that I can run it via CMD?
I came up with the following code:
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
$text_auslesen.Replace("Count :","") > $env:APPDATA\BIOS-Benchmark\Count_only.txt
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
$text_auslesen.Replace("Average :","") > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt
If I copy and paste it completely into a powershell, it can run. But now I have to put the code next to other code in a batch file. How do I have to adjust the code so that the cmd.exe executes the whole thing?
I suspect setting the variables via Powershell code is problematic here.
Unfortunately, a PS1 file is out of the question for my project.
To execute PowerShell commands from a batch file / cmd.exe, you need to create a PowerShell child process, using the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+) and pass the command(s) to the -Command (-c) parameter.
However, batch-file syntax does not support multi-line strings, so you have two options (the examples use two simple sample commands):
Pass all commands as a double-quoted, single-line string:
powershell.exe -Command "Get-Date; Write-Output hello > test.txt"
Do not use quoting, which allows you to use cmd.exe's line continuations, by placing ^ at the end of each line.
powershell.exe -Command Get-Date;^
Write-Output hello ^> test.txt
Note:
In both cases multiple statements must be separated with ;, because ^ at the end of a batch-file line continues the string on the next line without a newline.
Especially with the unquoted solution, you need to carefully ^-escape individual characters that cmd.exe would otherwise interpret itself, such as & and >
See this answer for detailed guidance.
Powershell -c executes PowerShell commands. You can do this from cmd, however, it looks like it needs to be run as administrator.
PowerShell -c "$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt;
$text_auslesen.Replace('Count :','') > $env:APPDATA\BIOS-Benchmark\Count_only.txt;
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt;
$text_auslesen.Replace('Average :','') > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt"
It is possible to execute the PowerShell code in a batch file, but technically what you are doing is pulling a copy of it out and executing it someplace else. Here are 3 methods that I know of.
mklement0's answer addresses executing a copy of it that is passed as a parameter to PowerShell.
You could build a ps1 file from CMD, and then execute that ps1 file by passing it as a parameter to PowerShell.
And the method I've worked with the most is to pass specially designed PowerShell code to PowerShell that, when it runs, will load all, or part, of the current CMD file into memory and execute it there as a ScriptBlock. I have tried loading parts of the current CMD file, but my experience has been that this gets too complicated and I just stick with loading the entire current CMD file.
That last method is what I'm presenting here. The trick is to make the batch/CMD portion of the script look like a comment that is ignored by PowerShell, but still runs without throwing error messages in CMD. I'm not sure where I first found this trick, but it goes like this:
First, place <# : at the start of script. PowerShell sees this as the start of a comment, but CMD seems to ignore this line. I think CMD is trying to redirect < the contents of a non-existing file : to a non-existing command. But what does CMD do with the #? It works, and that's the important thing.
Place your batch code in lines following the <# :.
You end the batch/CMD part with a GOTO :EOF.
You then end the PowerShell comment with #>, but visually I find it easier to find <#~#>, which does the same job.
The rest of the file is your PowerShell code.
This version treats the PowerShell code as a function with defined parameters. The batch part builds %ARGS% and passes, with double quotes intact, to a PowerShell ScriptBlock that in turn is wrapped in another ScriptBlock. The PowerShell function is called twice with the same SourceFile parameter, but different DestinationFile and TextToRemove parameters. Perhaps there is a simpler way to reliably pass double quotes " in arguments passed to a ScriptBlock from batch, but this is the method I got working.
<# :
#ECHO OFF
SET f0=%~f0
SET SourceFile=%APPDATA%\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
SET ARGS="%SourceFile%" "%APPDATA%\BIOS-Benchmark\Count_only.txt" "Count :"
PowerShell -NoProfile -Command ".([scriptblock]::Create('.([scriptblock]::Create((get-content -raw $Env:f0))) ' + $Env:ARGS))"
SET ARGS="%SourceFile%" "%APPDATA%\BIOS-Benchmark\Durchschnitt_only.txt" "Average :"
PowerShell -NoProfile -Command ".([scriptblock]::Create('.([scriptblock]::Create((get-content -raw $Env:f0))) ' + $Env:ARGS))"
GOTO :EOF
<#~#>
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$SourceFile,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFile,
[Parameter(Mandatory = $true, Position = 2)]
[string]$TextToRemove
)
(Get-Content $SourceFile).Replace($TextToRemove, '') > $DestinationFile
This script passes a single parameter that, in PowerShell, is used by the Switch command to decide which section of PowerShell you intend on executing. Since we are not passing double quotes " in the args, the PowerShell lines can be greatly simplified. Information could still be passed to PowerShell by defining environmental variables in batch and reading them in PowerShell.
<# :
#ECHO OFF
SET f0=%~f0
PowerShell -NoProfile -Command .([scriptblock]::Create((get-content -raw $Env:f0))) Script1
PowerShell -NoProfile -Command .([scriptblock]::Create((get-content -raw $Env:f0))) Script2
GOTO :EOF
<#~#>
switch ($args[0]) {
'Script1' {
(Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt).Replace("Count :", '') > $env:APPDATA\BIOS-Benchmark\Count_only.txt
break
}
'Script2' {
(Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt).Replace("Average :", '') > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt
break
}
default {}
}
The -c parameter is intended to solve this scenario.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh?view=powershell-7.2#-command---c
If possible, it would be more efficient to invoke PowerShell\Pwsh directly rather than using a cmd wrapper.

Process files dragged to BAT file in PowerShell

I trying to create a script that will convert Markdown files that are dropped on a processing script.
To accomplish this, I created a (process.bat) that would pass the names of the dropped files to a PowerShell script:
powershell.exe -NoProfile -File "./process.ps1" -Document %*
#pause
The PowerShell file (process.ps1) would process each file individually:
[parameter(Mandatory=$true)]
[String[]]$Document
Write-Host $args[1]
$Document | ForEach-Object {
Write-Host "Document: $_"
# convert Markdown to Html
pandoc -o ($_ -Replace '.md', '.html') -f markdown -t html $_
}
When I drop two files on the batch file:
C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown>powershell.exe -NoProfile -File "./process.ps1" -Document "C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown\FOO.md"
"C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown\BAR.md"
C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown\BAR.md
Document:
Press any key to continue . . .
The documents are not being processed.
What's the recommended approach to passing the batch file's file list %* to PowerShell?
When powershell.exe, the PowerShell CLI, is called from the outside with the -File parameter, it doesn't support arrays - only individual arguments.[1]
(Additionally, you neglected to wrap your parameter definition in a param(...) block, which effectively caused it be ignored.)
The simplest solution is to define your parameter with the ValueFromRemainingArguments option so that it automatically collects all positional arguments in the parameter variable:
param(
[Parameter(Mandatory, ValueFromRemainingArguments)]
[String[]] $Document
)
Then invoke your script via the PowerShell CLI without -Document:
powershell.exe -NoProfile -File "./process.ps1" %*
As an alternative to using a helper batch file, you can:
define a shortcut file (*.lnk) that explicitly invokes your PowerShell script as powershell.exe -File \path\to\your\script.ps1 (without additional arguments)
and then use that as the drop target.
Note: The reason that you cannot use a PowerShell script (*.ps1) directly as a drop target is that PowerShell script files aren't directly executable - instead, opening (double-clicking) a *.ps1 file opens it for editing.
You'd then need to add pause (or something similar, such as Read-Host -Prompt 'Press Enter to exit.') to your PowerShell script, to prevent it from closing the window instantly on finishing.
Alternatively, leave the script as-is and use -NoExit (placed before -File) to keep the PowerShell session open.
[1] The same applies to the PowerShell Core CLI, pwsh.

How can I convert a multi-line batch file statement into Powershell?

I'm in the process of converting an old batch file to Powershell. In the batch file, the SET command is used to declare and set multiple variables, and then an executable is called that uses those variables, along with additional flags. How would I do this in Powershell?
Batch File code excerpt:
SET VAR1=VAL1
SET VAR2=VAL2
SET VAR3=VAL3
SET VAR4=VAL4
%DIRECTORY%\%SUBDIR%\EXECUTABLE.EXE -FLAG1 -FLAG2 -FLAG3
I first attempted to declare the Powershell variables and call the exe using Start-Process, but the executable is looking for specific variable names. I'm not sure if those variables can be seen by the exe in this scenario, but it doesn't work.
$VAR1 = VAL1
$VAR2 = VAL2
$VAR3 = VAL3
Start-Process "$DRIVE\DIR\EXECUTABLE.exe -FLAG1 -FLAG2 -FLAG3"
I've also unsuccessfully attempted passing the multiple line command to the command shell:
$Command = "CMD.exe /C
SET VAR1=VAL1
SET VAR2=VAL2
SET VAR3=VAL3
SET VAR4=VAL4
$DRIVE\$DIR\EXECUTABLE.exe -FLAG1 -FLAG2 -FLAG3
Invoke-Expression $Command
Note** The variables have to be set on multiple lines, even when I run the exe from a DOS prompt. Using the "&" (Batch) or ";" (Powershell) and passing all variables on one line doesn't work.
For external programs (child processes) to see variables, they must be environment variables: use $env:VAR1 = 'VAL1' rather than $VAR1 = 'VAL1' - also note how the values must be quoted.
In cmd.exe (batch files), all variables are invariably also environment variables; in PowerShell, regular variables such as $VAR1 are visible only to the PowerShell session itself.
Do not use Start-Process to invoke external console applications, invoke them directly (synchronously, with standard streams connected to PowerShell's streams), using &, if the executable name/path is quoted and/or contains variable references; do not quote the command line as a whole; specify and - if necessary - quote the executable name/path and arguments individually.
Similarly, Invoke-Expression should generally be avoided.
Therefore:
$env:VAR1 = 'VAL1'
$env:VAR2 = 'VAL2'
$env:VAR3 = 'VAL3'
& "$DRIVE\DIR\EXECUTABLE.exe" -FLAG1 -FLAG2 -FLAG3

How to simulate a batch script

I need to setup a PowerShell script based upon a batch script. The original batch script looks like the following:
call %SYSTEMROOT%\setup_Env.BAT
command_name command_arguments
The command is dependent upon the environmental variables from the setup_ENV.BAT being setup.
$tempFile = [IO.Path]::GetTempFileName()
$script = "%SYSTEMROOT%\setup_Env.BAT"
cmd /c " $script && set > $tempFile "
cmd /c " command_name command_arguments"
I received the error:
cmd : 'command_name is not recognized as an internal or external command,...
If there is a better way to do this in PowerShell, I am open to it.
You need to pass a single command line to cmd to make this work:
cmd /c "call %SYSTEMROOT%\setup_Env.BAT && command_name command_arguments"
As Ansgar Wiechers points out, every cmd invocation runs in a child process, and any environment modifications made in a child process are not visible to the calling process and therefore also not to future child processes.
By contrast, in the single command line above, the environment-variable modifications performed by setup_Env.BAT are visible to command_name by the time it executes.
Caveat: If command_arguments contains %...%-style references to the environment variables defined in setup_Env.BAT, more work is needed:
Change the %...%-style references to !...!-style references.
Additionally invoke cmd with /v to enable delayed variable expansion (the equivalent of setlocal enabledelayedexpansion inside a script`:
cmd /v /c "call %SYSTEMROOT%\setup_Env.BAT && command_name args_with_delayed_var_refs"
Caveat: The above may still not work as intended if command_arguments happens to contain ! chars. that should be treated as literals (and/or command_name is another batch file containing such).
In that event, the simplest approach is to simply recreate the entire batch file in a temporary file and invoke that:
# Get temp. file path
$tempBatFile = [IO.Path]::GetTempFileName() + '.bat'
# Write the content of the temp. batch file
#'
#echo off
call %SYSTEMROOT%\setup_Env.BAT
command_name command_arguments
'# | Set-Content $tempBatFile
# Execute it.
& $tempBatFile
# Clean up.
Remove-Item -LiteralPath $tempBatFile

Setting environment variables with batch file lauched by Powershell script

I have a batch script called SET_ENV.bat which contains environment variables that are used by other batch scripts. Currently this SET_ENV.bat is lauched by existing batch scripts.
Now I have a need to use Powershell script and I would like to launch the same SET_ENV.bat. I managed to do this using:
cmd.exe /c ..\..\SET_ENV.bat
I know that the batch file was run because it contained an echo
echo *** Set the environment variables for the processes ***
But after looking at the environment variables, I can see that none of them have been updated. Is there something that is preventing me from updating environment variables with Powershell + batch file combo?
I have tried SET_ENV.bat directly from command line and it works. I have also tried Start-Process cmdlet with "-Verb runAs" but that didn't do any good.
Launching PowerShell again at the end of the batch commands will keep every environment variable so far.
My use case was: set up Anaconda environment, set up MSVC environment, continue with that. Problem is both Anaconda and MSCV have a separate batch script that initialises the env.
The following command starting from PowerShell will:
initialise Anaconda
initialise MSVC
re-launch PowerShell
cmd.exe "/K" '%USERPROFILE%\apps\anaconda3\Scripts\activate.bat %USERPROFILE%\apps\anaconda3 && "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" && powershell'
Just swap the paths with what you need. Note that if the path contains spaces in needs to be inside double quotes ".
Breaking down the call above:
cmd.exe "/K": call cmd and do not exit after the commands finish executing /K
The rest is the full command, it is wrapped in single quotes '.
%USERPROFILE%\apps\anaconda3\Scripts\activate.bat %USERPROFILE%\apps\anaconda3: calls activate.bat with parameter ...\anaconda3
&& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat": && and if the previous command didn't fail, run the MSVC vars setup file. This is wrapped in " as it has spaces in it.
&& powershell: finally run PowerShell. This will now contain all environment variables from the ones above.
Just adding a better way of doing the aforementioned setup: using Anaconda's PowerShell init script to actually get it to display the environment name on the prompt. I won't break down this as it's just a modified command above.
cmd.exe "/K" '"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" && powershell -noexit -command "& ''~\apps\anaconda3\shell\condabin\conda-hook.ps1'' ; conda activate ''~\apps\anaconda3'' "'
Note that the single quotes in the powershell call are all doubled up to escape them
Environment variables are local to a process and get inherited (by default at least) to new child processes. In your case you launch a new instance of cmd, which inherits your PowerShell's environment variables, but has its own environment. The batch file then changes the environment of that cmd instance, which closes afterwards and you return back to your PowerShell script. Naturally, nothing in PowerShell's environment has changed.
It works in cmd since batch files are executed in the same process, so a batch file can set environment variables and subsequently they are available, since the batch file wasn't executed in a new process. If you use cmd /c setenv.cmd in an interactive cmd session you will find that your environment hasn't changed either.
You can try another option, such as specifying the environment variables in a language-agnostic file, to be read by either cmd or PowerShell to set the environment accordingly. Or you could launch your PowerShell scripts from cmd after first running your batch file. Or you could set those environment variables under your user account to no longer have to care for them. Or you just have one setenv.cmd and one setenv.ps1 and keep them updated in sync.
Summary
Write the environment variables to file and load them after.
Example
I've included an MWE below that exemplifies this by saving and loading the VS-studio environment.
Usage
To run the script, call New-Environment. You will now be in the VS2022 environment.
How it works
The first time New-Environment is called, the VS-studio environment batch file runs, but the results are saved to disk. On returning to PowerShell the results are loaded from disk. Subsequent times just use the saved results without running the environment activator again (because it's slow). The New-Environment -refresh parameter may be used if you do want to resave the VS-studio environment again, for instance if anything has changed.
Script
NOTE: This script must be present in your powershell $profile so the second instance can access the function! Please ensure to change the VS path to reflect your own installation.
function New-Environment()
{
param (
[switch]
$refresh
)
Write-Host "Env vars now: $($(Get-ChildItem env: | measure-object).Count)"
$fileName = "$home\my_vsenviron.json"
if ((-not $refresh) -and (Test-Path $fileName -PathType Leaf))
{
Import-Environment($fileName)
return;
}
$script = '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && '
$script += "pwsh --command Export-Environment `"$fileName`""
&"cmd.exe" "/C" $script
Import-Environment($fileName)
}
function Export-Environment($fileName)
{
Get-ChildItem env: | Select-Object name,value | ConvertTo-Json | Out-File $fileName
Write-Host "I have exported the environment to $fileName"
}
function Import-Environment($fileName)
{
Get-Content $fileName | ConvertFrom-json | ForEach-Object -process {Set-Item "env:$($_.Name)" "$($_.Value)"}
Write-Host "I have imported the environment from $fileName"
Write-Host "Env vars now: $($(Get-ChildItem env: | measure-object).Count)"
}