diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 new file mode 100755 index 00000000000..90048705549 --- /dev/null +++ b/bin/elixir.ps1 @@ -0,0 +1,304 @@ +#!/usr/bin/env pwsh + +$ELIXIR_VERSION = "1.18.0-dev" + +$scriptPath = Split-Path -Parent $PSCommandPath +$erlExec = "erl" + +# The iex.ps1, elixirc.ps1 and mix.ps1 scripts may populate this var. +if ($null -eq $allArgs) { + $allArgs = $args +} + +function PrintElixirHelp { + $scriptName = Split-Path -Leaf $PSCommandPath + $help = @" +Usage: $scriptName [options] [.exs file] [data] + +## General options + + -e "COMMAND" Evaluates the given command (*) + -h, --help Prints this message (standalone) + -r "FILE" Requires the given files/patterns (*) + -S SCRIPT Finds and executes the given script in `$PATH + -pr "FILE" Requires the given files/patterns in parallel (*) + -pa "PATH" Prepends the given path to Erlang code path (*) + -pz "PATH" Appends the given path to Erlang code path (*) + -v, --version Prints Erlang/OTP and Elixir versions (standalone) + + --erl "SWITCHES" Switches to be passed down to Erlang (*) + --eval "COMMAND" Evaluates the given command, same as -e (*) + --logger-otp-reports BOOL Enables or disables OTP reporting + --logger-sasl-reports BOOL Enables or disables SASL reporting + --no-halt Does not halt the Erlang VM after execution + --short-version Prints Elixir version (standalone) + +Options given after the .exs file or -- are passed down to the executed code. +Options can be passed to the Erlang runtime using `$ELIXIR_ERL_OPTIONS or --erl. + +## Distribution options + +The following options are related to node distribution. + + --cookie COOKIE Sets a cookie for this distributed node + --hidden Makes a hidden node + --name NAME Makes and assigns a name to the distributed node + --rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*) + --sname NAME Makes and assigns a short name to the distributed node + +--name and --sname may be set to undefined so one is automatically generated. + +## Release options + +The following options are generally used under releases. + + --boot "FILE" Uses the given FILE.boot to start the system + --boot-var VAR "VALUE" Makes `$VAR available as VALUE to FILE.boot (*) + --erl-config "FILE" Loads configuration in FILE.config written in Erlang (*) + --vm-args "FILE" Passes the contents in file as arguments to the VM + +--pipe-to is not supported via PowerShell. + +** Options marked with (*) can be given more than once. +** Standalone options can't be combined with other options. +"@ + + Write-Host $help +} + +if (($allArgs.Count -eq 1) -and ($allArgs[0] -eq "--short-version")) { + Write-Host "$ELIXIR_VERSION" + exit +} + +if (($allArgs.Count -eq 0) -or (($allArgs.Count -eq 1) -and ($allArgs[0] -in @("-h", "--help")))) { + PrintElixirHelp + exit 1 +} + +function NormalizeArg { + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string[]] $Items + ) + $Items -join "," +} + +function QuoteString { + param( + [Parameter(ValueFromPipeline = $true)] + [string] $Item + ) + + # We surround the string with double quotes, in order to preserve its contents as + # only one command arg. + # This is needed because PowerShell consider spaces as separator of arguments. + # The double quotes around will be removed when PowerShell process the argument. + # See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#passing-quoted-strings-to-external-commands + if ($Item.Contains(" ")) { + '"' + $Item + '"' + } + else { + $Item + } +} + +$elixirParams = @() +$erlangParams = @() +$beforeExtras = @() +$allOtherParams = @() + +$runErlPipe = $null +$runErlLog = $null + +for ($i = 0; $i -lt $allArgs.Count; $i++) { + $private:arg = $allArgs[$i] + + switch -exact ($arg) { + { $_ -in @("-e", "-r", "-pr", "-pa", "-pz", "--eval", "--remsh", "--dot-iex", "--dbg") } { + $private:nextArg = NormalizeArg($allArgs[++$i]) + + $elixirParams += $arg + $elixirParams += $nextArg + + break + } + + { $_ -in @("-v", "--version") } { + # Standalone options goes only once in the Elixir params, when they are empty. + if (($elixirParams.Count -eq 0) -and ($allOtherParams.Count -eq 0)) { + $elixirParams += $arg + } + else { + $allOtherParams += $arg + } + break + } + + "--no-halt" { + $elixirParams += $arg + break + } + + "--cookie" { + $erlangParams += "-setcookie" + $erlangParams += $allArgs[++$i] + break + } + + "--hidden" { + $erlangParams += "-hidden" + break + } + + "--name" { + $erlangParams += "-name" + $erlangParams += $allArgs[++$i] + break + } + + "--sname" { + $erlangParams += "-sname" + $erlangParams += $allArgs[++$i] + break + } + + "--boot" { + $erlangParams += "-boot" + $erlangParams += $allArgs[++$i] + break + } + + "--erl-config" { + $erlangParams += "-config" + $erlangParams += $allArgs[++$i] + break + } + + "--vm-args" { + $erlangParams += "-args_file" + $erlangParams += $allArgs[++$i] + break + } + + "--logger-otp-reports" { + $private:tempVal = $allArgs[$i + 1] + if ($tempVal -in @("true", "false")) { + $erlangParams += @("-logger", "handle_otp_reports", $allArgs[++$i]) + } + break + } + + "--logger-sasl-reports" { + $private:tempVal = $allArgs[$i + 1] + if ($tempVal -in @("true", "false")) { + $erlangParams += @("-logger", "handle_sasl_reports", $allArgs[++$i]) + } + break + } + + "--erl" { + $private:erlFlags = $allArgs[++$i] -split " " + $beforeExtras += $erlFlags + break + } + + "+iex" { + $elixirParams += "+iex" + $useIex = $true + + break + } + + "+elixirc" { + $elixirParams += "+elixirc" + break + } + + "--rpc-eval" { + $private:key = $allArgs[++$i] + $private:value = $allArgs[++$i] + + if ($null -eq $key) { + Write-Error "--rpc-eval: NODE must be present" + exit 1 + } + + if ($null -eq $value) { + Write-Error "--rpc-eval: COMMAND for the '$key' node must be present" + exit 1 + } + + $elixirParams += "--rpc-eval" + $elixirParams += $key + $elixirParams += $value + break + } + + "--boot-var" { + $private:key = $allArgs[++$i] + $private:value = $allArgs[++$i] + + if ($null -eq $key) { + Write-Error "--boot-var: VAR must be present" + exit 1 + } + + if ($null -eq $value) { + Write-Error "--boot-var: Value for the '$key' var must be present" + exit 1 + } + + $elixirParams += "-boot_var" + $elixirParams += $key + $elixirParams += $value + break + } + + Default { + $private:normalized = NormalizeArg $arg + $allOtherParams += $normalized + break + } + } +} + +if ($null -eq $useIEx) { + $beforeExtras = @("-s", "elixir", "start_cli") + $beforeExtras +} + +$beforeExtras = @("-pa", "$(Join-Path $scriptPath -ChildPath "../lib/elixir/ebin")") + $beforeExtras +$beforeExtras = @("-noshell", "-elixir_root", "$(Join-Path $scriptPath -ChildPath "../lib")") + $beforeExtras + +$allParams = @() + +if ($null -ne $env:ELIXIR_ERL_OPTIONS) { + $private:erlFlags = $env:ELIXIR_ERL_OPTIONS -split " " + $allParams += $erlFlags +} + +$allParams += $erlangParams +$allParams += $beforeExtras +$allParams += "-extra" +$allParams += $elixirParams +$allParams += $allOtherParams + +$binSuffix = "" + +# The variable is available after PowerShell 7.2. Previous to that, PS only worked on Windows. +if ($isWindows -or ($null -eq $isWindows)) { + $binSuffix = ".exe" +} + +$binPath = "$erlExec$binSuffix" + +# We double the double-quotes because they are going to be escaped by arguments parsing. +$paramsPart = $allParams | ForEach-Object -Process { QuoteString($_ -replace "`"", "`"`"") } + +if ($env:ELIXIR_CLI_DRY_RUN) { + Write-Host "$binPath $paramsPart" +} +else { + $output = Start-Process -FilePath $binPath -ArgumentList $paramsPart -NoNewWindow -Wait -PassThru + exit $output.ExitCode +} diff --git a/bin/elixirc.ps1 b/bin/elixirc.ps1 new file mode 100755 index 00000000000..29742f010d6 --- /dev/null +++ b/bin/elixirc.ps1 @@ -0,0 +1,35 @@ +#!/usr/bin/env pwsh + +$scriptName = Split-Path -Leaf $PSCommandPath + +if (($args.Count -eq 0) -or ($args[0] -in @("-h", "--help"))) { + Write-Host @" +Usage: $scriptName [elixir switches] [compiler switches] [.ex files] + + -h, --help Prints this message and exits + -o The directory to output compiled files + -v, --version Prints Elixir version and exits (standalone) + + --ignore-module-conflict Does not emit warnings if a module was previously defined + --no-debug-info Does not attach debug info to compiled modules + --no-docs Does not attach documentation to compiled modules + --profile time Profile the time to compile modules + --verbose Prints compilation status + --warnings-as-errors Treats warnings as errors and returns non-zero exit status + +** Options given after -- are passed down to the executed code +** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS +** Options can be passed to the Erlang compiler using ERL_COMPILER_OPTIONS +"@ + exit +} + +$scriptPath = Split-Path -Parent $PSCommandPath +$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" + +$prependedArgs = @("+elixirc") + +$allArgs = $prependedArgs + $args + +# The dot is going to evaluate the script with the vars defined here. +. $elixirMainScript diff --git a/bin/iex.ps1 b/bin/iex.ps1 new file mode 100755 index 00000000000..870909377fb --- /dev/null +++ b/bin/iex.ps1 @@ -0,0 +1,30 @@ +#!/usr/bin/env pwsh + +$scriptName = Split-Path -Leaf $PSCommandPath + +if ($args[0] -in @("-h", "--help")) { + Write-Host @" +Usage: $scriptName [options] [.exs file] [data] + +The following options are exclusive to IEx: + + --dbg pry Sets the backend for Kernel.dbg/2 to IEx.pry/0 + --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment. + Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists. + If FILE is empty, then no file will be loaded. + --remsh NAME Connects to a node using a remote shell. + +It accepts all other options listed by "elixir --help". +"@ + exit +} + +$scriptPath = Split-Path -Parent $PSCommandPath +$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" + +$prependedArgs = @("--no-halt", "--erl", "-user elixir", "+iex") + +$allArgs = $prependedArgs + $args + +# The dot is going to evaluate the script with the vars defined here. +. $elixirMainScript diff --git a/bin/mix.ps1 b/bin/mix.ps1 old mode 100644 new mode 100755 index 369c4942113..a1d3bd29458 --- a/bin/mix.ps1 +++ b/bin/mix.ps1 @@ -1,23 +1,13 @@ -# Store path to mix.bat as a FileInfo object -$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat')) -$newArgs = @() +#!/usr/bin/env pwsh -for ($i = 0; $i -lt $args.length; $i++) -{ - if ($args[$i] -is [array]) - { - # Commas created the array so we need to reintroduce those commas - for ($j = 0; $j -lt $args[$i].length - 1; $j++) - { - $newArgs += ($args[$i][$j] + ',') - } - $newArgs += $args[$i][-1] - } - else - { - $newArgs += $args[$i] - } -} +$scriptPath = Split-Path -Parent $PSCommandPath +$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" -# Corrected arguments are ready to pass to batch file -& $mixBatPath $newArgs +$mixFile = Join-Path -Path $scriptPath -ChildPath "mix" + +$prependedArgs = @($mixFile) + +$allArgs = $prependedArgs + $args + +# The dot is going to evaluate the script with the vars defined here. +. $elixirMainScript diff --git a/lib/elixir/test/elixir/kernel/cli_test.exs b/lib/elixir/test/elixir/kernel/cli_test.exs index 30b457a1586..0d402249d02 100644 --- a/lib/elixir/test/elixir/kernel/cli_test.exs +++ b/lib/elixir/test/elixir/kernel/cli_test.exs @@ -61,8 +61,18 @@ defmodule Kernel.CLITest do end end +test_parameters = + if(PathHelpers.windows?(), + do: [%{cli_extension: ".bat"}], + else: + [%{cli_extension: ""}] ++ + if(System.find_executable("pwsh"), do: [%{cli_extension: ".ps1"}], else: []) + ) + defmodule Kernel.CLI.ExecutableTest do - use ExUnit.Case, async: true + use ExUnit.Case, + async: true, + parameterize: test_parameters import Retry @@ -70,78 +80,89 @@ defmodule Kernel.CLI.ExecutableTest do test "file smoke test", context do file = Path.join(context.tmp_dir, "hello_world!.exs") File.write!(file, "IO.puts :hello_world123") - {output, 0} = System.cmd(elixir_executable(), [file]) + {output, 0} = System.cmd(elixir_executable(context.cli_extension), [file]) assert output =~ "hello_world123" end - test "--eval smoke test" do - {output, 0} = System.cmd(elixir_executable(), ["--eval", "IO.puts :hello_world123"]) + test "--eval smoke test", context do + {output, 0} = + System.cmd(elixir_executable(context.cli_extension), ["--eval", "IO.puts :hello_world123"]) + assert output =~ "hello_world123" # Check for -e and exclamation mark handling on Windows - assert {_output, 0} = System.cmd(elixir_executable(), ["-e", "Time.new!(0, 0, 0)"]) + assert {_output, 0} = + System.cmd(elixir_executable(context.cli_extension), ["-e", "Time.new!(0, 0, 0)"]) # TODO: remove this once we bump CI to 26.3 unless windows?() and System.otp_release() == "26" do {output, 0} = - System.cmd(iex_executable(), ["--eval", "IO.puts :hello_world123; System.halt()"]) + System.cmd(iex_executable(context.cli_extension), [ + "--eval", + "IO.puts :hello_world123; System.halt()" + ]) assert output =~ "hello_world123" - {output, 0} = System.cmd(iex_executable(), ["-e", "IO.puts :hello_world123; System.halt()"]) + {output, 0} = + System.cmd(iex_executable(context.cli_extension), [ + "-e", + "IO.puts :hello_world123; System.halt()" + ]) + assert output =~ "hello_world123" end end - test "--version smoke test" do - output = elixir(~c"--version") + test "--version smoke test", %{cli_extension: cli_extension} do + output = elixir(~c"--version", cli_extension) assert output =~ "Erlang/OTP #{System.otp_release()}" assert output =~ "Elixir #{System.version()}" - output = iex(~c"--version") + output = iex(~c"--version", cli_extension) assert output =~ "Erlang/OTP #{System.otp_release()}" assert output =~ "IEx #{System.version()}" - output = elixir(~c"--version -e \"IO.puts(:test_output)\"") + output = elixir(~c"--version -e \"IO.puts(:test_output)\"", cli_extension) assert output =~ "Erlang/OTP #{System.otp_release()}" assert output =~ "Elixir #{System.version()}" assert output =~ "Standalone options can't be combined with other options" end - test "--short-version smoke test" do - output = elixir(~c"--short-version") + test "--short-version smoke test", %{cli_extension: cli_extension} do + output = elixir(~c"--short-version", cli_extension) assert output =~ System.version() refute output =~ "Erlang" end - stderr_test "--help smoke test" do - output = elixir(~c"--help") + stderr_test "--help smoke test", %{cli_extension: cli_extension} do + output = elixir(~c"--help", cli_extension) assert output =~ "Usage: elixir" end - stderr_test "combining --help results in error" do - output = elixir(~c"-e 1 --help") + stderr_test "combining --help results in error", %{cli_extension: cli_extension} do + output = elixir(~c"-e 1 --help", cli_extension) assert output =~ "--help : Standalone options can't be combined with other options" - output = elixir(~c"--help -e 1") + output = elixir(~c"--help -e 1", cli_extension) assert output =~ "--help : Standalone options can't be combined with other options" end - stderr_test "combining --short-version results in error" do - output = elixir(~c"--short-version -e 1") + stderr_test "combining --short-version results in error", %{cli_extension: cli_extension} do + output = elixir(~c"--short-version -e 1", cli_extension) assert output =~ "--short-version : Standalone options can't be combined with other options" - output = elixir(~c"-e 1 --short-version") + output = elixir(~c"-e 1 --short-version", cli_extension) assert output =~ "--short-version : Standalone options can't be combined with other options" end - test "parses paths" do + test "parses paths", %{cli_extension: cli_extension} do root = fixture_path("../../..") |> to_charlist args = ~c"-pa \"#{root}/*\" -pz \"#{root}/lib/*\" -e \"IO.inspect(:code.get_path(), limit: :infinity)\"" - list = elixir(args) + list = elixir(args, cli_extension) {path, _} = Code.eval_string(list, []) # pa @@ -153,25 +174,31 @@ defmodule Kernel.CLI.ExecutableTest do assert to_charlist(Path.expand(~c"lib/list", root)) in path end - stderr_test "formats errors" do - assert String.starts_with?(elixir(~c"-e \":erlang.throw 1\""), "** (throw) 1") + stderr_test "formats errors", %{cli_extension: cli_extension} do + assert String.starts_with?(elixir(~c"-e \":erlang.throw 1\"", cli_extension), "** (throw) 1") assert String.starts_with?( - elixir(~c"-e \":erlang.error 1\""), + elixir(~c"-e \":erlang.error 1\"", cli_extension), "** (ErlangError) Erlang error: 1" ) - assert String.starts_with?(elixir(~c"-e \"1 +\""), "** (TokenMissingError)") + assert String.starts_with?(elixir(~c"-e \"1 +\"", cli_extension), "** (TokenMissingError)") - assert elixir(~c"-e \"Task.async(fn -> raise ArgumentError end) |> Task.await\"") =~ + assert elixir( + ~c"-e \"Task.async(fn -> raise ArgumentError end) |> Task.await\"", + cli_extension + ) =~ "an exception was raised:\n ** (ArgumentError) argument error" - assert elixir(~c"-e \"IO.puts(Process.flag(:trap_exit, false)); exit({:shutdown, 1})\"") == + assert elixir( + ~c"-e \"IO.puts(Process.flag(:trap_exit, false)); exit({:shutdown, 1})\"", + cli_extension + ) == "false\n" end - stderr_test "blames exceptions" do - error = elixir(~c"-e \"Access.fetch :foo, :bar\"") + stderr_test "blames exceptions", %{cli_extension: cli_extension} do + error = elixir(~c"-e \"Access.fetch :foo, :bar\"", cli_extension) assert error =~ "** (FunctionClauseError) no function clause matching in Access.fetch/2" assert error =~ "The following arguments were given to Access.fetch/2" assert error =~ ":foo" @@ -238,7 +265,9 @@ defmodule Kernel.CLI.RPCTest do end defmodule Kernel.CLI.CompileTest do - use ExUnit.Case, async: true + use ExUnit.Case, + async: true, + parameterize: test_parameters import Retry @moduletag :tmp_dir @@ -250,7 +279,7 @@ defmodule Kernel.CLI.CompileTest do end test "compiles code", context do - assert elixirc(~c"#{context.fixture} -o #{context.tmp_dir}") == "" + assert elixirc(~c"#{context.fixture} -o #{context.tmp_dir}", context.cli_extension) == "" assert File.regular?(context.beam_file_path) # Assert that the module is loaded into memory with the proper destination for the BEAM file. @@ -267,7 +296,7 @@ defmodule Kernel.CLI.CompileTest do try do fixture = String.replace(context.fixture, "/", "\\") tmp_dir_path = String.replace(context.tmp_dir, "/", "\\") - assert elixirc(~c"#{fixture} -o #{tmp_dir_path}") == "" + assert elixirc(~c"#{fixture} -o #{tmp_dir_path}", context.cli_extension) == "" assert File.regular?(context[:beam_file_path]) # Assert that the module is loaded into memory with the proper destination for the BEAM file. @@ -283,7 +312,9 @@ defmodule Kernel.CLI.CompileTest do end stderr_test "fails on missing patterns", context do - output = elixirc(~c"#{context.fixture} non_existing.ex -o #{context.tmp_dir}") + output = + elixirc(~c"#{context.fixture} non_existing.ex -o #{context.tmp_dir}", context.cli_extension) + assert output =~ "non_existing.ex" refute output =~ "compile_sample.ex" refute File.exists?(context.beam_file_path) @@ -292,7 +323,7 @@ defmodule Kernel.CLI.CompileTest do stderr_test "fails on missing write access to .beam file", context do compilation_args = ~c"#{context.fixture} -o #{context.tmp_dir}" - assert elixirc(compilation_args) == "" + assert elixirc(compilation_args, context.cli_extension) == "" assert File.regular?(context.beam_file_path) # Set the .beam file to read-only @@ -301,7 +332,7 @@ defmodule Kernel.CLI.CompileTest do # Can only assert when read-only applies to the user if access != :read_write do - output = elixirc(compilation_args) + output = elixirc(compilation_args, context.cli_extension) expected = "(File.Error) could not write to file #{inspect(context.beam_file_path)}: permission denied" diff --git a/lib/elixir/test/elixir/test_helper.exs b/lib/elixir/test/elixir/test_helper.exs index 66661c01162..8118ba918c7 100644 --- a/lib/elixir/test/elixir/test_helper.exs +++ b/lib/elixir/test/elixir/test_helper.exs @@ -23,28 +23,28 @@ defmodule PathHelpers do Path.join(tmp_path(), extra) end - def elixir(args) do - run_cmd(elixir_executable(), args) + def elixir(args, executable_extension \\ "") do + run_cmd(elixir_executable(executable_extension), args) end - def elixir_executable do - executable_path("elixir") + def elixir_executable(extension \\ "") do + executable_path("elixir", extension) end - def elixirc(args) do - run_cmd(elixirc_executable(), args) + def elixirc(args, executable_extension \\ "") do + run_cmd(elixirc_executable(executable_extension), args) end - def elixirc_executable do - executable_path("elixirc") + def elixirc_executable(extension \\ "") do + executable_path("elixirc", extension) end - def iex(args) do - run_cmd(iex_executable(), args) + def iex(args, executable_extension \\ "") do + run_cmd(iex_executable(executable_extension), args) end - def iex_executable do - executable_path("iex") + def iex_executable(extension \\ "") do + executable_path("iex", extension) end def write_beam({:module, name, bin, _} = res) do @@ -60,8 +60,8 @@ defmodule PathHelpers do |> :unicode.characters_to_binary() end - defp executable_path(name) do - Path.expand("../../../../bin/#{name}#{executable_extension()}", __DIR__) + defp executable_path(name, extension) do + Path.expand("../../../../bin/#{name}#{extension}", __DIR__) end if match?({:win32, _}, :os.type()) do