param( [Parameter(Position = 0, ValueFromRemainingArguments)] [string[]] $CrystalArgs ) # https://www.powershellgallery.com/packages/PowerGit/0.6.1/Content/Functions%5CResolve-RealPath.ps1 function Resolve-RealPath { <# .SYNOPSIS Implementation of Unix realpath(). .PARAMETER Path Must exist #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [string] $Path ) if ($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows) { return [IO.Path]::GetFullPath($Path) } [string[]] $parts = ($Path.TrimStart([IO.Path]::DirectorySeparatorChar).Split([IO.Path]::DirectorySeparatorChar)) [string] $realPath = '' foreach ($part in $parts) { $realPath += [string] ([IO.Path]::DirectorySeparatorChar + $part) $item = Get-Item $realPath if ($item.Target) { $realPath = $item.Target } } $realPath } # adopted from https://stackoverflow.com/a/15669365 function Write-StdErr { <# .SYNOPSIS Writes text to stderr when running in a regular console window, to the host''s error stream otherwise. .DESCRIPTION Writing to true stderr allows you to write a well-behaved CLI as a PS script that can be invoked from a batch file, for instance. Note that PS by default sends ALL its streams to *stdout* when invoked from cmd.exe. #> param( [Parameter(Mandatory)] [string] $Line, $ForegroundColor ) if ($Host.Name -eq 'ConsoleHost') { if ($ForegroundColor) { [Console]::ForegroundColor = $ForegroundColor } [Console]::Error.WriteLine($Line) if ($ForegroundColor) { [Console]::ResetColor() } } else { [void] $host.ui.WriteErrorLine($Line) } } # https://stackoverflow.com/a/43030126 function Invoke-WithEnvironment { <# .SYNOPSIS Invokes commands with a temporarily modified environment. .DESCRIPTION Modifies environment variables temporarily based on a hashtable of values, invokes the specified script block, then restores the previous environment. .PARAMETER Environment A hashtable that defines the temporary environment-variable values. Assign $null to (temporarily) remove an environment variable that is currently set. .PARAMETER ScriptBlock The command(s) to execute with the temporarily modified environment. .EXAMPLE > Invoke-WithEnvironment @{ PORT=8080 } { node index.js } Runs node with environment variable PORT temporarily set to 8080, with its previous value, if any #> param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Environment, [Parameter(Mandatory)] [scriptblock] $ScriptBlock ) # Modify the environment based on the hashtable and save the original # one for later restoration. $htOrgEnv = @{} foreach ($kv in $Environment.GetEnumerator()) { $htOrgEnv[$kv.Key] = (Get-Item -EA SilentlyContinue "env:$($kv.Key)").Value Set-Item "env:$($kv.Key)" $kv.Value } # Invoke the script block try { & $ScriptBlock } finally { # Restore the original environment. foreach ($kv in $Environment.GetEnumerator()) { # Note: setting an environment var. to $null or '' *removes* it. Set-Item "env:$($kv.Key)" $htOrgEnv[$kv.Key] } } } # Code ported from: # https://source.dot.net/#System.Diagnostics.Process/System/Diagnostics/ProcessStartInfo.cs # https://source.dot.net/#System.Diagnostics.Process/PasteArguments.cs # Licensed to the .NET Foundation under one or more agreements. # The .NET Foundation licenses this file to you under the MIT license. function Append-Argument { param( [Parameter(Mandatory)] [Text.StringBuilder] $Builder, [Parameter(Mandatory)] [AllowEmptyString()] [string] $Arg ) if (-not $Builder.Length -eq 0) { [void]$Builder.Append(' ') } # Parsing rules for non-argv[0] arguments: # - Backslash is a normal character except followed by a quote. # - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote # - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote # - Parsing stops at first whitespace outside of quoted region. # - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode. if ((-not $Builder.Length -eq 0) -and (ContainsNoWhitespaceOrQuotes $Arg)) { # Simple case - no quoting or changes needed. [void]$Builder.Append($Arg) } else { [void]$Builder.Append('"') $idx = 0 while ($idx -lt $Arg.Length) { $c = $Arg[$idx++] if ($c -eq '\') { $numBackSlash = 1 while (($idx -lt $Arg.Length) -and ($Arg[$idx] -eq '\')) { $idx++ $numBackslash++ } if ($idx -eq $Arg.Length) { # We'll emit an end quote after this so must double the number of backslashes. [void]$Builder.Append('\', $numBackSlash * 2) } elseif ($Arg[$idx] -eq '"') { # Backslashes will be followed by a quote. Must double the number of backslashes. [void]$Builder.Append('\', $numBackSlash * 2 + 1) [void]$Builder.Append('"') $idx++ } else { # Backslash will not be followed by a quote, so emit as normal characters. [void]$Builder.Append('\', $numBackSlash) } continue } if ($c -eq '"') { # Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed # by another quote (which parses differently pre-2008 vs. post-2008.) [void]$Builder.Append('\') [void]$Builder.Append('"') continue } [void]$Builder.Append($c) } [void]$Builder.Append('"') } } function ContainsNoWhitespaceOrQuotes { param( [Parameter(Mandatory)] [AllowEmptyString()] [string] $s ) for ($i = 0; $i -lt $s.Length; $i += 1) { $c = $s[$i] if ([char]::IsWhiteSpace($c) -or ($c -eq '"')) { return $False } } $True } function Build-Arguments { param( [Parameter(Mandatory)] [AllowEmptyString()] [AllowNull()] [string[]] $Args ) if ($Args.count -eq 0) { return '' } $Builder = [Text.StringBuilder]::new() $Args | Foreach-Object { Append-Argument $Builder $_ } $Builder.ToString() } function Exec-Process { param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [AllowEmptyString()] [AllowNull()] [string[]] $Args ) if ($Args[0] -eq '--%') { $null, $Args = $Args if ($Args) { $Process = Start-Process $Path -Argument $Args -NoNewWindow -PassThru } else { $Process = Start-Process $Path -NoNewWindow -PassThru } } else { # `Start-Process` simply joins all arguments without escaping and passes it to `ProcessStartInfo.Arguments`; # here we replicate the logic for `ProcessStartInfo.ArgumentList` on .NET 5 and above # See also: https://github.com/PowerShell/PowerShell/issues/14747 # (note that we must nonetheless implement it by ourselves if we support Windows Powershell 5.1) $EscapedArgs = Build-Arguments $Args if ($EscapedArgs) { $Process = Start-Process $Path -ArgumentList $EscapedArgs -NoNewWindow -PassThru } else { $Process = Start-Process $Path -NoNewWindow -PassThru } } # workaround to obtain the exit status properly: https://stackoverflow.com/a/23797762 $hnd = $Process.Handle Wait-Process -Id $Process.Id Exit $Process.ExitCode } $ScriptPath = Resolve-RealPath $PSCommandPath $ScriptRoot = Split-Path -Path $ScriptPath -Parent $CrystalRoot = Split-Path -Path $ScriptRoot -Parent $CrystalDir = "$CrystalRoot\.build" Invoke-WithEnvironment @{ CRYSTAL_PATH = if ($env:CRYSTAL_PATH) { $env:CRYSTAL_PATH } else { "lib;$CrystalRoot\src" } CRYSTAL_HAS_WRAPPER = "true" CRYSTAL = if ($env:CRYSTAL) { $env:CRYSTAL } else { "crystal" } CRYSTAL_CONFIG_LIBRARY_PATH = $env:CRYSTAL_CONFIG_LIBRARY_PATH CRYSTAL_LIBRARY_PATH = $env:CRYSTAL_LIBRARY_PATH } { if (!$env:CRYSTAL_PATH.Contains("$CrystalRoot\src")) { Write-StdErr "CRYSTAL_PATH env variable does not contain $CrystalRoot\src" -ForegroundColor DarkYellow } if (!$env:CRYSTAL_CONFIG_LIBRARY_PATH -or !$env:CRYSTAL_LIBRARY_PATH) { Invoke-WithEnvironment @{ PATH = $env:PATH.Split(';') -ne $ScriptRoot -ne "bin" -join ';' } { $CrystalInstalled = Get-Command "crystal" -CommandType ExternalScript, Application -ErrorAction SilentlyContinue $CrystalInstalledLibraryPath = if ($CrystalInstalled) { crystal env CRYSTAL_LIBRARY_PATH } else { $null } if (!$env:CRYSTAL_CONFIG_LIBRARY_PATH) { $env:CRYSTAL_CONFIG_LIBRARY_PATH = $CrystalInstalledLibraryPath } if (!$env:CRYSTAL_LIBRARY_PATH) { $env:CRYSTAL_LIBRARY_PATH = $CrystalInstalledLibraryPath } } } if (Test-Path -Path "$CrystalDir/crystal.exe" -PathType Leaf) { Write-StdErr "Using compiled compiler at $($CrystalDir.Replace($pwd, "."))\crystal.exe" -ForegroundColor DarkYellow Exec-Process "$CrystalDir/crystal.exe" $CrystalArgs } else { $CrystalCmd = Get-Command $env:CRYSTAL -CommandType ExternalScript, Application -ErrorAction SilentlyContinue if (!$CrystalCmd) { Write-StdErr 'You need to have a crystal executable in your path! or set CRYSTAL env variable' -ForegroundColor Red Exit 1 } else { $CrystalInstalledDir = Split-Path -Path $CrystalCmd.Path -Parent if ($CrystalInstalledDir -eq $ScriptRoot -or $CrystalInstalledDir -eq "bin") { Invoke-WithEnvironment @{ PATH = $env:PATH.Split(';') -ne $ScriptRoot -ne "bin" -join ';' } { Exec-Process $ScriptPath $CrystalArgs } } else { Exec-Process $CrystalCmd.Path $CrystalArgs } } } }