From 37f708449810e7c3ac06f6c0508a1c96c344cff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 00:34:39 +0200 Subject: [PATCH 01/18] Add `process-utils` --- spec/std/process_spec.cr | 7 +++++++ spec/support/process-utils.cr | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 spec/support/process-utils.cr diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 30172e02f541..904e4642698f 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -5,6 +5,13 @@ require "process" require "./spec_helper" require "../support/env" require "../support/wait_for" +require "../support/process-utils" + +PROCESS_UTILS_PATH = Process.executable_path.not_nil! + +private def exe + PROCESS_UTILS_PATH +end private def exit_code_command(code) {% if flag?(:win32) %} diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr new file mode 100644 index 000000000000..bc2c3c543645 --- /dev/null +++ b/spec/support/process-utils.cr @@ -0,0 +1,29 @@ +require "spec" + +class Spec::CLI + def main(args) + return previous_def unless args[0]? == "pu" + + args.shift # Remove "pu" from the arguments + + output = STDOUT + exit_status = 0 + + OptionParser.parse(args) do |opts| + opts.on("", "--exit status", "Exit with the given status code") do |status| + exit_status = status.to_i + end + opts.on("", "--stderr", "Write to stderr instead of stdout") do + output = STDERR + end + end + + case command = args.shift? + when Nil + else + ::abort "Unknown process util command: #{command}" + end + + exit exit_status + end +end From 6494d717c06d1c89cafe14b7a4a20d944f751b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 00:35:56 +0200 Subject: [PATCH 02/18] `exit` --- spec/std/process_spec.cr | 66 +++++++++++++++-------------------- spec/support/process-utils.cr | 3 +- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 904e4642698f..3f365db32667 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -13,14 +13,6 @@ private def exe PROCESS_UTILS_PATH end -private def exit_code_command(code) - {% if flag?(:win32) %} - {"cmd.exe", {"/c", "exit #{code}"}} - {% else %} - {"/bin/sh", {"-c", "exit #{code}"}} - {% end %} -end - private def shell_command(command) {% if flag?(:win32) %} {"cmd.exe", {"/c", command}} @@ -160,14 +152,14 @@ describe Process do end it "doesn't break if process is collected before completion", tags: %w[slow] do - 200.times { Process.new(to_ary(exit_code_command(0))) } + 200.times { Process.new([exe, "pu", "exit", "0"]) } # run the GC multiple times to unmap as much memory as possible 10.times { GC.collect } # the processes above have now been queued after completion; if this last # one finishes at all, nothing was broken by the GC - Process.run(*exit_code_command(0)) + Process.run(exe, ["pu", "exit", "0"]) end it "accepts tuple args" do @@ -177,7 +169,7 @@ describe Process do describe ".new (splat)" do it "works" do - Process.new(*to_splat(exit_code_command(0))).wait.success?.should be_true + Process.new(exe, "pu", "exit", "0").wait.success?.should be_true end end @@ -229,37 +221,37 @@ describe Process do end it "doesn't break if process is collected before completion", tags: %w[slow] do - 200.times { Process.new(*exit_code_command(0)) } + 200.times { Process.new(exe, ["pu", "exit", "0"]) } # run the GC multiple times to unmap as much memory as possible 10.times { GC.collect } # the processes above have now been queued after completion; if this last # one finishes at all, nothing was broken by the GC - Process.run(*exit_code_command(0)) + Process.run(exe, ["pu", "exit", "0"]) end end describe "#wait" do it "successful exit code" do - process = Process.new(*exit_code_command(0)) + process = Process.new(exe, ["pu", "exit", "0"]) process.wait.exit_code.should eq(0) end it "unsuccessful exit code" do - process = Process.new(*exit_code_command(1)) + process = Process.new(exe, ["pu", "exit", "1"]) process.wait.exit_code.should eq(1) end end describe ".run?(args)" do it "waits for successful process" do - status = Process.run?(to_ary(exit_code_command(0))).should be_a(Process::Status) + status = Process.run?([exe, "pu", "exit", "0"]).should be_a(Process::Status) status.exit_code.should eq(0) end it "waits for unsuccessful process" do - status = Process.run?(to_ary(exit_code_command(1))).should be_a(Process::Status) + status = Process.run?([exe, "pu", "exit", "1"]).should be_a(Process::Status) status.exit_code.should eq(1) end @@ -304,51 +296,49 @@ describe Process do describe ".run? (splat)" do it "works" do - status = Process.run?(*to_splat(exit_code_command(0))).should be_a(Process::Status) + status = Process.run?(exe, "pu", "exit", "0").should be_a(Process::Status) status.exit_code.should eq(0) end end describe ".run" do it "waits for the process" do - Process.run(to_ary(exit_code_command(0))).exit_code.should eq(0) + Process.run([exe, "pu", "exit", "0"]).exit_code.should eq(0) end end describe ".run (splat)" do it "works" do - status = Process.run(*to_splat(exit_code_command(0))).exit_code.should eq(0) + status = Process.run(exe, "pu", "exit", "0").exit_code.should eq(0) end end describe ".run(args, &)" do it "waits for the process" do - Process.run(to_ary(exit_code_command(0))) { }[0].exit_code.should eq(0) + Process.run([exe, "pu", "exit", "0"]) { }[0].exit_code.should eq(0) end it "returns block result" do - Process.run(to_ary(exit_code_command(0))) { 42 }[1].should eq 42 + Process.run([exe, "pu", "exit", "0"]) { 42 }[1].should eq 42 end end describe ".run(command, args)" do it "waits for the process" do - Process.run(*exit_code_command(0)).exit_code.should eq(0) + Process.run(exe, ["pu", "exit", "0"]).exit_code.should eq(0) end it "runs true in block" do - Process.run(*exit_code_command(0)) { } + Process.run(exe, ["pu", "exit", "0"]) { } $?.exit_code.should eq(0) end it "receives arguments in array" do - command, args = exit_code_command(123) - Process.run(command, args.to_a).exit_code.should eq(123) + Process.run(exe, ["pu", "exit", "123"]).exit_code.should eq(123) end it "receives arguments in tuple" do - command, args = exit_code_command(123) - Process.run(command, args.as(Tuple)).exit_code.should eq(123) + Process.run(exe, {"pu", "exit", "123"}).exit_code.should eq(123) end it "redirects output to /dev/null" do @@ -491,7 +481,7 @@ describe Process do end pending_win32 "looks up programs in the $PATH with a shell" do - proc = Process.run(*exit_code_command(0), shell: true, output: Process::Redirect::Close) + proc = Process.run(exe, ["pu", "exit", "0"], shell: true, output: Process::Redirect::Close) proc.exit_code.should eq(0) end @@ -904,8 +894,8 @@ describe Process do end it "reports status" do - Process.capture_result(to_ary(exit_code_command(0))).status.exit_code.should eq(0) - Process.capture_result(to_ary(exit_code_command(123))).status.exit_code.should eq(123) + Process.capture_result([exe, "pu", "exit", "0"]).status.exit_code.should eq(0) + Process.capture_result([exe, "pu", "exit", "123"]).status.exit_code.should eq(123) end it "raises if process cannot execute" do @@ -1005,9 +995,9 @@ describe Process do end it "reports status" do - result = Process.capture_result?(to_ary(exit_code_command(0))).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "exit", "0"]).should be_a(Process::Result) result.status.exit_code.should eq(0) - result = Process.capture_result?(to_ary(exit_code_command(123))).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "exit", "123"]).should be_a(Process::Result) result.status.exit_code.should eq(123) end @@ -1030,8 +1020,8 @@ describe Process do end it "raises on non-zero exit status" do - error = expect_raises(Process::ExitError, /^Command \[.*exit 1.*\] failed: Process exited with status 1$/) do - Process.capture(to_ary(exit_code_command(1))) + error = expect_raises(Process::ExitError, /^Command \[.*"exit", "1"\] failed: Process exited with status 1$/) do + Process.capture([exe, "pu", "exit", "1"]) end error.result.status.exit_code.should eq 1 end @@ -1064,11 +1054,11 @@ describe Process do end it "returns nil on unsuccessful exit" do - Process.capture?(to_ary(exit_code_command(1))).should be_nil + Process.capture?([exe, "pu", "exit", "1"]).should be_nil end it "returns nil on unsuccessful exit (splat)" do - Process.capture?(*to_splat(exit_code_command(1))).should be_nil + Process.capture?(exe, "pu", "exit", "1").should be_nil end it "raises if process cannot execute" do @@ -1225,7 +1215,7 @@ describe Process do it "raises if chdir doesn't exist" do expect_raises(File::NotFoundError, "Error while changing directory: 'doesnotexist'") do - Process.exec(*exit_code_command(1), chdir: "doesnotexist") + Process.exec(exe, ["pu", "exit", "1"], chdir: "doesnotexist") end end diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index bc2c3c543645..97d541531bf0 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -19,7 +19,8 @@ class Spec::CLI end case command = args.shift? - when Nil + when "exit" + exit args[0].to_i else ::abort "Unknown process util command: #{command}" end From a6eddddc76fb78fad260788b687e914eb2b33a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 00:07:56 +0200 Subject: [PATCH 03/18] `cat` --- spec/std/process_spec.cr | 64 +++++++++++++---------------------- spec/support/process-utils.cr | 2 ++ 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 3f365db32667..97199047a4dc 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -21,22 +21,6 @@ private def shell_command(command) {% end %} end -private def stdin_to_stdout_command - {% if flag?(:win32) %} - {"powershell.exe", {"-C", "$Input"}} - {% else %} - {"/bin/cat", [] of String} - {% end %} -end - -private def stdin_to_stderr_command(status = 0) - {% if flag?(:win32) %} - {"powershell.exe", {"-C", "[Console]::OpenStandardInput().CopyTo([Console]::OpenStandardError()); exit #{status}"}} - {% else %} - {"/bin/sh", {"-c", "cat 1>&2; exit #{status}"}} - {% end %} -end - private def print_env_command {% if flag?(:win32) %} # cmd adds these by itself, clear them out before printing. @@ -359,7 +343,7 @@ describe Process do end it "sends input in IO" do - value = Process.run(*stdin_to_stdout_command, input: IO::Memory.new("hello")) do |proc| + value = Process.run(exe, ["pu", "cat"], input: IO::Memory.new("hello")) do |proc| proc.input?.should be_nil proc.output.gets_to_end end @@ -387,16 +371,16 @@ describe Process do end it "controls process in block" do - value = Process.run(*stdin_to_stdout_command, error: :inherit) do |proc| + value = Process.run(exe, ["pu", "cat"], error: :inherit) do |proc| proc.input.puts "hello" proc.input.close proc.output.gets_to_end end - value.should eq("hello#{newline}") + value.should eq("hello\n") end it "closes input after block" do - Process.run(*stdin_to_stdout_command) { } + Process.run(exe, ["pu", "cat"]) { } $?.exit_code.should eq(0) end @@ -405,7 +389,7 @@ describe Process do channel = Channel(Process).new spawn do - Process.run(*stdin_to_stdout_command, input: reader, output: :pipe, error: :pipe) do |process| + Process.run(exe, ["pu", "cat"], input: reader, output: :pipe, error: :pipe) do |process| channel.send process channel.receive end @@ -429,9 +413,9 @@ describe Process do it "forwards closed io" do closed_io = IO::Memory.new closed_io.close - Process.run(*stdin_to_stdout_command, input: closed_io) - Process.run(*stdin_to_stdout_command, output: closed_io) - Process.run(*stdin_to_stdout_command, error: closed_io) + Process.run(exe, ["pu", "cat"], input: closed_io) + Process.run(exe, ["pu", "cat"], output: closed_io) + Process.run(exe, ["pu", "cat"], error: closed_io) end it "forwards non-blocking file" do @@ -440,7 +424,7 @@ describe Process do File.open(out_path, "w+", blocking: false) do |output| input.puts "hello" input.rewind - Process.run(*stdin_to_stdout_command, input: input, output: output) + Process.run(exe, ["pu", "cat"], input: input, output: output) output.rewind output.gets_to_end.chomp.should eq("hello") end @@ -794,8 +778,8 @@ describe Process do it "can link processes together" do buffer = IO::Memory.new - Process.run(*stdin_to_stdout_command) do |cat| - Process.run(*stdin_to_stdout_command, input: cat.output, output: buffer) do + Process.run(exe, ["pu", "cat"]) do |cat| + Process.run(exe, ["pu", "cat"], input: cat.output, output: buffer) do 1000.times { cat.input.puts "line" } cat.close end @@ -820,14 +804,14 @@ describe Process do end it "captures stdout from stdin" do - result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")) + result = Process.capture_result([exe, "pu", "cat"], input: IO::Memory.new("hello")) result.status.success?.should be_true result.output.chomp.should eq "hello" end it "ignores stdout if output is IO" do io = IO::Memory.new - result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"), output: io) + result = Process.capture_result([exe, "pu", "cat"], input: IO::Memory.new("hello"), output: io) result.status.success?.should be_true result.output?.should be_nil result.error?.should eq "" @@ -836,7 +820,7 @@ describe Process do it "ignores stdout if output is FileDescriptor" do reader, writer = IO.pipe - result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello\n"), output: writer) + result = Process.capture_result([exe, "pu", "cat"], input: IO::Memory.new("hello\n"), output: writer) result.status.success?.should be_true result.output?.should be_nil result.error?.should eq "" @@ -885,7 +869,7 @@ describe Process do it "truncates error output", tags: %w[slow] do dashes32 = "-" * (32 << 10) input = IO::Memory.new("#{dashes32}X#{dashes32}") - result = Process.capture_result(to_ary(stdin_to_stderr_command), input: input) + result = Process.capture_result([exe, "pu", "cat", "--stderr"], input: input) result.status.success?.should be_true result.output?.should eq "" error = result.error.should be_a(String) @@ -921,14 +905,14 @@ describe Process do end it "captures stdout from stdin" do - result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "cat"], input: IO::Memory.new("hello")).should be_a(Process::Result) result.status.success?.should be_true result.output.chomp.should eq "hello" end it "ignores stdout if output is IO" do io = IO::Memory.new - result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"), output: io).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "cat"], input: IO::Memory.new("hello"), output: io).should be_a(Process::Result) result.status.success?.should be_true result.output?.should be_nil result.error?.should eq "" @@ -937,7 +921,7 @@ describe Process do it "ignores stdout if output is FileDescriptor" do reader, writer = IO.pipe - result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello\n"), output: writer).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "cat"], input: IO::Memory.new("hello\n"), output: writer).should be_a(Process::Result) result.status.success?.should be_true result.output?.should be_nil result.error?.should eq "" @@ -986,7 +970,7 @@ describe Process do it "truncates error output", tags: %w[slow] do dashes32 = "-" * (32 << 10) input = IO::Memory.new("#{dashes32}X#{dashes32}") - result = Process.capture_result?(to_ary(stdin_to_stderr_command), input: input).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "cat", "--stderr"], input: input).should be_a(Process::Result) result.status.success?.should be_true result.output?.should eq "" error = result.error.should be_a(String) @@ -1016,7 +1000,7 @@ describe Process do end it "captures stdout from stdin" do - Process.capture(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).chomp.should eq "hello" + Process.capture([exe, "pu", "cat"], input: IO::Memory.new("hello")).chomp.should eq "hello" end it "raises on non-zero exit status" do @@ -1034,7 +1018,7 @@ describe Process do it "captures stderr in error message" do error = expect_raises(Process::ExitError) do - Process.capture(to_ary(stdin_to_stderr_command(status: 1)), input: IO::Memory.new("hello")) + Process.capture([exe, "pu", "cat", "--stderr", "--exit", "1"], input: IO::Memory.new("hello")) end error.result.error.chomp.should eq "hello" end @@ -1050,7 +1034,7 @@ describe Process do end it "captures stdout from stdin" do - Process.capture?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).try(&.chomp).should eq "hello" + Process.capture?([exe, "pu", "cat"], input: IO::Memory.new("hello")).try(&.chomp).should eq "hello" end it "returns nil on unsuccessful exit" do @@ -1191,8 +1175,8 @@ describe Process do File.write(stdin_path, "foobar") status, _, _ = compile_and_run_source <<-CRYSTAL - command = #{stdin_to_stdout_command[0].inspect} - args = #{stdin_to_stdout_command[1].to_a} of String + command = #{exe.inspect} + args = ["pu", "cat"] stdin_path = #{stdin_path.inspect} stdout_path = #{stdout_path.inspect} File.open(stdin_path) do |input| diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index 97d541531bf0..8f51df0b6b2d 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -19,6 +19,8 @@ class Spec::CLI end case command = args.shift? + when "cat" + IO.copy(STDIN, output) when "exit" exit args[0].to_i else From 4a171e8594b250cc2f88eb92db4719e78a472a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 00:11:10 +0200 Subject: [PATCH 04/18] `env` --- spec/std/process_spec.cr | 36 +++++++++++++++++------------------ spec/support/process-utils.cr | 4 ++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 97199047a4dc..8b4a74d22e59 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -21,7 +21,7 @@ private def shell_command(command) {% end %} end -private def print_env_command +private def print_env_shell_command {% if flag?(:win32) %} # cmd adds these by itself, clear them out before printing. shell_command("set COMSPEC=& set PATHEXT=& set PROMPT=& set PROCESSOR_ARCHITECTURE=& set") @@ -498,28 +498,28 @@ describe Process do describe "environ" do it "clears the environment" do - value = Process.run(*print_env_command, clear_env: true) do |proc| + value = Process.run(exe, ["pu", "env"], clear_env: true) do |proc| proc.output.gets_to_end end value.should eq("") end it "clears and sets an environment variable" do - value = Process.run(*print_env_command, clear_env: true, env: {"FOO" => "bar"}) do |proc| + value = Process.run(exe, ["pu", "env"], clear_env: true, env: {"FOO" => "bar"}) do |proc| proc.output.gets_to_end end - value.should eq("FOO=bar#{newline}") + value.should eq("FOO=bar\n") end it "sets an environment variable" do - value = Process.run(*print_env_command, env: {"FOO" => "bar"}) do |proc| + value = Process.run(exe, ["pu", "env"], env: {"FOO" => "bar"}) do |proc| proc.output.gets_to_end end value.should match /(*ANYCRLF)^FOO=bar$/m end it "sets an empty environment variable" do - value = Process.run(*print_env_command, env: {"FOO" => ""}) do |proc| + value = Process.run(exe, ["pu", "env"], env: {"FOO" => ""}) do |proc| proc.output.gets_to_end end value.should match /(*ANYCRLF)^FOO=$/m @@ -527,7 +527,7 @@ describe Process do it "deletes existing environment variable" do with_env("FOO": "bar") do - value = Process.run(*print_env_command, env: {"FOO" => nil}) do |proc| + value = Process.run(exe, ["pu", "env"], env: {"FOO" => nil}) do |proc| proc.output.gets_to_end end value.should_not match /(*ANYCRLF)^FOO=/m @@ -537,7 +537,7 @@ describe Process do {% if flag?(:win32) %} it "deletes existing environment variable case-insensitive" do with_env("FOO": "bar") do - value = Process.run(*print_env_command, env: {"foo" => nil}) do |proc| + value = Process.run(exe, ["pu", "env"], env: {"foo" => nil}) do |proc| proc.output.gets_to_end end value.should_not match /(*ANYCRLF)^FOO=/mi @@ -547,7 +547,7 @@ describe Process do it "preserves existing environment variable" do with_env("FOO": "bar") do - value = Process.run(*print_env_command) do |proc| + value = Process.run(exe, ["pu", "env"]) do |proc| proc.output.gets_to_end end value.should match /(*ANYCRLF)^FOO=bar$/m @@ -556,7 +556,7 @@ describe Process do it "preserves and sets an environment variable" do with_env("FOO": "bar") do - value = Process.run(*print_env_command, env: {"FOO2" => "bar2"}) do |proc| + value = Process.run(exe, ["pu", "env"], env: {"FOO2" => "bar2"}) do |proc| proc.output.gets_to_end end value.should match /(*ANYCRLF)^FOO=bar$/m @@ -566,7 +566,7 @@ describe Process do it "overrides existing environment variable" do with_env("FOO": "bar") do - value = Process.run(*print_env_command, env: {"FOO" => "different"}) do |proc| + value = Process.run(exe, ["pu", "env"], env: {"FOO" => "different"}) do |proc| proc.output.gets_to_end end value.should match /(*ANYCRLF)^FOO=different$/m @@ -576,7 +576,7 @@ describe Process do {% if flag?(:win32) %} it "overrides existing environment variable case-insensitive" do with_env("FOO": "bar") do - value = Process.run(*print_env_command, env: {"fOo" => "different"}) do |proc| + value = Process.run(exe, ["pu", "env"], env: {"fOo" => "different"}) do |proc| proc.output.gets_to_end end value.should_not match /(*ANYCRLF)^FOO=/m @@ -586,27 +586,27 @@ describe Process do {% end %} it "finds binary in parent `$PATH`, not `env`" do - Process.run(*print_env_command, env: {"PATH" => ""}) + Process.run(exe, ["pu", "env"], env: {"PATH" => ""}) end it "errors on invalid key" do expect_raises(ArgumentError, %(Invalid env key "")) do - Process.run(*print_env_command, env: {"" => "baz"}) + Process.run(exe, ["pu", "env"], env: {"" => "baz"}) end expect_raises(ArgumentError, %(Invalid env key "foo=bar")) do - Process.run(*print_env_command, env: {"foo=bar" => "baz"}) + Process.run(exe, ["pu", "env"], env: {"foo=bar" => "baz"}) end end it "errors on zero char in key" do expect_raises({{ flag?(:win32) }} ? ArgumentError : RuntimeError, "String `key` contains null byte") do - Process.run(*print_env_command, env: {"foo\0" => "baz"}) + Process.run(exe, ["pu", "env"], env: {"foo\0" => "baz"}) end end it "errors on zero char in value" do expect_raises({{ flag?(:win32) }} ? ArgumentError : RuntimeError, "String `value` contains null byte") do - Process.run(*print_env_command, env: {"foo" => "baz\0"}) + Process.run(exe, ["pu", "env"], env: {"foo" => "baz\0"}) end end end @@ -737,7 +737,7 @@ describe Process do context "with shell: true" do it "errors with nonexist $PATH" do pending! unless {{ flag?(:unix) }} - Process.run(*print_env_command, shell: true, env: {"PATH" => "/does/not/exist"}).success?.should be_false + Process.run(*print_env_shell_command, shell: true, env: {"PATH" => "/does/not/exist"}).success?.should be_false end it "empty path entry means current directory" do diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index 8f51df0b6b2d..b7d8cfc4a546 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -21,6 +21,10 @@ class Spec::CLI case command = args.shift? when "cat" IO.copy(STDIN, output) + when "env" + ENV.each do |key, value| + output.puts "#{key}=#{value}" + end when "exit" exit args[0].to_i else From dddf1b8405ece19375fef752ea0794345a5bc3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 10:44:05 +0200 Subject: [PATCH 05/18] `echo` --- spec/std/process_spec.cr | 64 +++++++++++++++++------------------ spec/support/process-utils.cr | 4 +++ 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 8b4a74d22e59..0452ece21bcf 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -336,10 +336,10 @@ describe Process do end it "gets output" do - value = Process.run(*shell_command("echo hello")) do |proc| + value = Process.run(exe, ["pu", "echo", "hello"]) do |proc| proc.output.gets_to_end end - value.should eq("hello#{newline}") + value.should eq("hello\n") end it "sends input in IO" do @@ -352,14 +352,14 @@ describe Process do it "sends output to IO" do output = IO::Memory.new - Process.run(*shell_command("echo hello"), output: output) - output.to_s.should eq("hello#{newline}") + Process.run(exe, ["pu", "echo", "hello"], output: output) + output.to_s.should eq("hello\n") end it "sends error to IO" do error = IO::Memory.new - Process.run(*shell_command("1>&2 echo hello"), error: error) - error.to_s.should eq("hello#{newline}") + Process.run(exe, "pu", "echo", "--stderr", "hello", error: error) + error.to_s.should eq("hello\n") end it "sends long output and error to IO" do @@ -790,16 +790,16 @@ describe Process do describe ".capture_result" do it "splat overload" do - result = Process.capture_result(*to_splat(shell_command("echo hello"))) + result = Process.capture_result(exe, "pu", "echo", "hello") result.status.success?.should be_true - result.output?.should eq "hello#{newline}" + result.output?.should eq "hello\n" result.error?.should eq "" end it "captures stdout" do - result = Process.capture_result(to_ary(shell_command("echo hello"))) + result = Process.capture_result([exe, "pu", "echo", "hello"]) result.status.success?.should be_true - result.output?.should eq "hello#{newline}" + result.output?.should eq "hello\n" result.error?.should eq "" end @@ -828,24 +828,24 @@ describe Process do end it "captures stderr" do - result = Process.capture_result(to_ary(shell_command("1>&2 echo hello"))) + result = Process.capture_result([exe, "pu", "echo", "--stderr", "hello"]) result.status.success?.should be_true result.output?.should eq "" - result.error?.should eq "hello#{newline}" + result.error?.should eq "hello\n" end it "ignores stderr if error is IO" do io = IO::Memory.new - result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: io) + result = Process.capture_result([exe, "pu", "echo", "--stderr", "hello"], error: io) result.status.success?.should be_true result.output?.should eq "" result.error?.should be_nil - io.to_s.should eq "hello#{newline}" + io.to_s.should eq "hello\n" end it "ignores stderr if error is FileDescriptor" do reader, writer = IO.pipe - result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: writer) + result = Process.capture_result([exe, "pu", "echo", "--stderr", "hello"], error: writer) result.status.success?.should be_true result.output?.should eq "" result.error?.should be_nil @@ -853,14 +853,14 @@ describe Process do end it "doesn't capture closed stdout" do - result = Process.capture_result(to_ary(shell_command("echo hello")), output: :close) + result = Process.capture_result([exe, "pu", "echo", "hello"], output: :close) result.output?.should be_nil result.error?.should_not be_nil end it "doesn't capture closed stderr" do # FIXME: Autocasting breaks in the interpreter - result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: Process::Redirect::Close) + result = Process.capture_result([exe, "pu", "echo", "--stderr", "hello"], error: Process::Redirect::Close) result.status.success?.should be_true result.output?.should eq "" result.error?.should be_nil @@ -891,16 +891,16 @@ describe Process do describe ".capture_result?" do it "splat overload" do - result = Process.capture_result?(*to_splat(shell_command("echo hello"))).should be_a(Process::Result) + result = Process.capture_result?(exe, "pu", "echo", "hello").should be_a(Process::Result) result.status.success?.should be_true - result.output?.should eq "hello#{newline}" + result.output?.should eq "hello\n" result.error?.should eq "" end it "captures stdout" do - result = Process.capture_result?(to_ary(shell_command("echo hello"))).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "echo", "hello"]).should be_a(Process::Result) result.status.success?.should be_true - result.output?.should eq "hello#{newline}" + result.output?.should eq "hello\n" result.error?.should eq "" end @@ -929,24 +929,24 @@ describe Process do end it "captures stderr" do - result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello"))).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "echo", "--stderr", "hello"]).should be_a(Process::Result) result.status.success?.should be_true result.output?.should eq "" - result.error?.should eq "hello#{newline}" + result.error?.should eq "hello\n" end it "ignores stderr if error is IO" do io = IO::Memory.new - result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: io).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "echo", "--stderr", "hello"], error: io).should be_a(Process::Result) result.status.success?.should be_true result.output?.should eq "" result.error?.should be_nil - io.to_s.should eq "hello#{newline}" + io.to_s.should eq "hello\n" end it "ignores stderr if error is FileDescriptor" do reader, writer = IO.pipe - result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: writer).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "echo", "--stderr", "hello"], error: writer).should be_a(Process::Result) result.status.success?.should be_true result.output?.should eq "" result.error?.should be_nil @@ -954,14 +954,14 @@ describe Process do end it "doesn't capture closed stdout" do - result = Process.capture_result?(to_ary(shell_command("echo hello")), output: :close).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "echo", "hello"], output: :close).should be_a(Process::Result) result.output?.should be_nil result.error?.should_not be_nil end it "doesn't capture closed stderr" do # FIXME: Autocasting breaks in the interpreter - result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: Process::Redirect::Close).should be_a(Process::Result) + result = Process.capture_result?([exe, "pu", "echo", "--stderr", "hello"], error: Process::Redirect::Close).should be_a(Process::Result) result.status.success?.should be_true result.output?.should eq "" result.error?.should be_nil @@ -992,11 +992,11 @@ describe Process do describe ".capture" do it "splat overload" do - Process.capture(*to_splat(shell_command("echo hello"))).should eq "hello#{newline}" + Process.capture(exe, "pu", "echo", "hello").should eq "hello\n" end it "captures stdout" do - Process.capture(to_ary(shell_command("echo hello"))).should eq "hello#{newline}" + Process.capture([exe, "pu", "echo", "hello"]).should eq "hello\n" end it "captures stdout from stdin" do @@ -1026,11 +1026,11 @@ describe Process do describe ".capture?" do it "splat overload" do - Process.capture?(*to_splat(shell_command("echo hello"))).should eq "hello#{newline}" + Process.capture?(exe, "pu", "echo", "hello").should eq "hello\n" end it "captures stdout" do - Process.capture?(to_ary(shell_command("echo hello"))).should eq "hello#{newline}" + Process.capture?([exe, "pu", "echo", "hello"]).should eq "hello\n" end it "captures stdout from stdin" do diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index b7d8cfc4a546..2f0e44d12621 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -21,6 +21,10 @@ class Spec::CLI case command = args.shift? when "cat" IO.copy(STDIN, output) + when "echo" + args.each do |line| + output.puts line + end when "env" ENV.each do |key, value| output.puts "#{key}=#{value}" From f62ac19091233c95d3a17e5b1337f8006c9a9081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 10:44:19 +0200 Subject: [PATCH 06/18] `sleep` --- spec/std/process_spec.cr | 22 +++++++--------------- spec/support/process-utils.cr | 2 ++ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 0452ece21bcf..e58196480125 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -30,14 +30,6 @@ private def print_env_shell_command {% end %} end -private def standing_command - {% if flag?(:win32) %} - {"cmd.exe"} - {% else %} - {"yes"} - {% end %} -end - private def path_search_command {% if flag?(:win32) %} {"cmd.exe"} @@ -1071,15 +1063,15 @@ describe Process do {% unless flag?(:win32) %} describe "#signal(Signal::KILL)" do it "kills a process" do - process = Process.new(*standing_command) + process = Process.new(exe, ["pu", "sleep"]) process.signal(Signal::KILL).should be_nil ensure process.try &.wait end it "kills many process" do - process1 = Process.new(*standing_command) - process2 = Process.new(*standing_command) + process1 = Process.new(exe, ["pu", "sleep"]) + process2 = Process.new(exe, ["pu", "sleep"]) process1.signal(Signal::KILL).should be_nil process2.signal(Signal::KILL).should be_nil ensure @@ -1090,7 +1082,7 @@ describe Process do {% end %} it "#terminate" do - process = Process.new(*standing_command) + process = Process.new(exe, ["pu", "sleep"]) process.exists?.should be_true process.terminated?.should be_false @@ -1099,7 +1091,7 @@ describe Process do process.try(&.wait) end - typeof(Process.new(*standing_command).terminate(graceful: false)) + typeof(Process.new(exe, ["pu", "sleep"]).terminate(graceful: false)) describe ".debugger_present?" do it "compiles" do @@ -1118,7 +1110,7 @@ describe Process do Process.exists?(Process.ppid).should be_true {% end %} - process = Process.new(*standing_command) + process = Process.new(exe, ["pu", "sleep"]) process.exists?.should be_true process.terminated?.should be_false @@ -1141,7 +1133,7 @@ describe Process do {% unless flag?(:win32) %} it ".pgid" do - process = Process.new(*standing_command) + process = Process.new(exe, ["pu", "sleep"]) Process.pgid(process.pid).should be_a(Int64) process.terminate Process.pgid.should eq(Process.pgid(Process.pid)) diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index 2f0e44d12621..82880af8b416 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -31,6 +31,8 @@ class Spec::CLI end when "exit" exit args[0].to_i + when "sleep" + sleep else ::abort "Unknown process util command: #{command}" end From 537b15024c83aabebbbeed4644515b7b724474f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 00:21:51 +0200 Subject: [PATCH 07/18] `long-output` --- spec/std/process_spec.cr | 4 ++-- spec/support/process-utils.cr | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index e58196480125..115d280f2fc1 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -357,8 +357,8 @@ describe Process do it "sends long output and error to IO" do output = IO::Memory.new error = IO::Memory.new - Process.run(*shell_command("echo #{"." * 8000}"), output: output, error: error) - output.to_s.should eq("." * 8000 + newline) + Process.run(exe, ["pu", "long-output"], output: output, error: error) + output.to_s.should eq("." * 8000 + "\n") error.to_s.should be_empty end diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index 82880af8b416..a4d05236f2a2 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -31,6 +31,8 @@ class Spec::CLI end when "exit" exit args[0].to_i + when "long-output" + output.puts "." * 8000 when "sleep" sleep else From 03c0f60fb5a627554e194b7b27a29069e01b2148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 00:30:59 +0200 Subject: [PATCH 08/18] `pwd` --- spec/std/process_spec.cr | 20 ++------------------ spec/support/process-utils.cr | 2 ++ 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 115d280f2fc1..4018a9be8c75 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -426,28 +426,12 @@ describe Process do it "sets working directory with string" do parent = File.dirname(Dir.current) - command = {% if flag?(:win32) %} - "cmd.exe /c echo %cd%" - {% else %} - "pwd" - {% end %} - value = Process.run(command, shell: true, chdir: parent, output: Process::Redirect::Pipe) do |proc| - proc.output.gets_to_end - end - value.should eq "#{parent}#{newline}" + Process.capture(exe, "pu", "pwd", chdir: parent).should eq "#{parent}\n" end it "sets working directory with path" do parent = Path.new File.dirname(Dir.current) - command = {% if flag?(:win32) %} - "cmd.exe /c echo %cd%" - {% else %} - "pwd" - {% end %} - value = Process.run(command, shell: true, chdir: parent, output: Process::Redirect::Pipe) do |proc| - proc.output.gets_to_end - end - value.should eq "#{parent}#{newline}" + Process.capture(exe, "pu", "pwd", chdir: parent).should eq "#{parent}\n" end pending_win32 "disallows passing arguments to nowhere" do diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index a4d05236f2a2..b1d3948e15a2 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -33,6 +33,8 @@ class Spec::CLI exit args[0].to_i when "long-output" output.puts "." * 8000 + when "pwd" + output.puts Dir.current when "sleep" sleep else From bbcc99e5004766ca4cc90f5e53f6e9f50d326f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 00:23:30 +0200 Subject: [PATCH 09/18] Drop obsolete helpers --- spec/std/process_spec.cr | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 4018a9be8c75..9a9eddd89d01 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -38,29 +38,6 @@ private def path_search_command {% end %} end -private def newline - {% if flag?(:win32) %} - "\r\n" - {% else %} - "\n" - {% end %} -end - -private def to_ary(tuple) - [tuple[0]].concat(tuple[1]) -end - -private def to_splat(cmd) - # Splatting in literals was only introduced in Crystal 1.1 - # FIXME: The interpreter still doesn't support it (#13183). - {% if compare_versions(Crystal::VERSION, "1.1.0") >= 0 && !flag?(:interpreted) %} - {cmd[0], *cmd[1]} - {% else %} - args = cmd[1] - {cmd[0], args[0], args[1]} - {% end %} -end - # interpreted code doesn't receive SIGCHLD for `#wait` to work (#12241) {% if flag?(:interpreted) && !flag?(:win32) %} pending Process From dcde20f39a6262b06bf9619fddb187297ebf9dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 19:09:11 +0200 Subject: [PATCH 10/18] Fixup pass path in `clear_env: true` example --- spec/std/process_spec.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 9a9eddd89d01..a0e613c76b62 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -458,10 +458,12 @@ describe Process do end it "clears and sets an environment variable" do - value = Process.run(exe, ["pu", "env"], clear_env: true, env: {"FOO" => "bar"}) do |proc| + # We must pass PATH because otherwise dynamic libraries might not be found, + # particularly on windows-gnu. + value = Process.run(exe, ["pu", "env"], clear_env: true, env: {"PATH" => ENV["PATH"]?, "FOO" => "bar"}) do |proc| proc.output.gets_to_end end - value.should eq("FOO=bar\n") + value.should eq("PATH=#{ENV["PATH"]?}\nFOO=bar\n") end it "sets an environment variable" do From 6fbcb206891d7032962989bb30c60142c67fb2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 22:13:48 +0200 Subject: [PATCH 11/18] Makefile: Add `crystal-pu` --- Makefile | 4 ++++ Makefile.win | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/Makefile b/Makefile index cff6fd00f9c4..df4521927933 100644 --- a/Makefile +++ b/Makefile @@ -300,6 +300,10 @@ $(LLVM_EXT_OBJ): $(LLVM_EXT_DIR)/llvm_ext.cc $(call check_llvm_config) $(CXX) -c $(CXXFLAGS) -o $@ $< $(if $(LLVM_CONFIG),$(shell $(LLVM_CONFIG) --cxxflags)) +$(O)/crystal-pu$(EXE): spec/support/process-utils.cr $(SOURCES) + @mkdir -p $(O) + $(EXPORT_CC) ./bin/crystal build $(FLAGS) -o $@ $< + man/: $(MAN1PAGES) man/%.gz: man/% diff --git a/Makefile.win b/Makefile.win index 3026094915d8..295df80cbdeb 100644 --- a/Makefile.win +++ b/Makefile.win @@ -251,6 +251,11 @@ $(LLVM_EXT_OBJ): $(LLVM_EXT_DIR)\llvm_ext.cc @rem - warning C4624: '...': destructor was implicitly defined as deleted $(CXX) /nologo /c $(CXXFLAGS) /WX /wd4244 /wd4624 /wd4530 "/Fo$@" "$<" $(if $(LLVM_CONFIG),$(shell $(LLVM_CONFIG) --cxxflags)) +$(O)\crystal-pu.exe: spec\support\process-utils.cr $(SOURCES) + @(call MKDIR,"$(O)") + $(call export_vars) + .\bin\crystal build $(FLAGS) -o $@ $< + .PHONY: clean clean: clean_crystal ## Clean up built directories and files $(call RM,"$(LLVM_EXT_OBJ)") From aa24ebaad065c9ad840568ab72a51877860a709d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 22:29:05 +0200 Subject: [PATCH 12/18] Rework `crystal-pu` as standalone executable for interpreter --- spec/std/process_spec.cr | 19 +++++++++++++++++-- spec/support/process-utils.cr | 33 ++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index a0e613c76b62..6fa18eb453aa 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -5,9 +5,24 @@ require "process" require "./spec_helper" require "../support/env" require "../support/wait_for" -require "../support/process-utils" -PROCESS_UTILS_PATH = Process.executable_path.not_nil! +# Process utils are helper programs for testing process spawn behaviour. +# They are defined in spec/support/process-utils.cr which +# we require here in order to build them into the spec executable. +# We then call the spec executable with "pu" as first argument followed +# by the command name. +# This mechanism doesn't work when running in the interpreter, because +# there is no spec executable we could call again. Instead, we need a +# to build process utils into a separate executable `crystal-pu` which +# we can call from the interpreter. +# This approach would also work for compiled specs, but building it into +# the executable is simpler and does not require a separate build step. +{% if flag?(:interpreted) %} + PROCESS_UTILS_PATH ="#{Process.executable_path.not_nil!.rchop(".exe")}-pu#{".exe" if {{ flag?(:win32) }}}" +{% else %} + require "../support/process-utils" + PROCESS_UTILS_PATH = Process.executable_path.not_nil! +{% end %} private def exe PROCESS_UTILS_PATH diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index b1d3948e15a2..b21d87cc54e2 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -1,10 +1,33 @@ -require "spec" +require "option_parser" -class Spec::CLI - def main(args) - return previous_def unless args[0]? == "pu" +# `process-utils` are a set of simple command line tools required for +# testing process spawn behaviour in `spec/std/process_spec.cr`. +# It can be built into the spec executable which can just spawn itself +# with `pu` as first argument, followed by the command name. +# Alternatively, it can be built as a standalone executable which +# we use for interpreter tests because in interpreted mode there is +# no executable we could call again. +{% if @type.has_constant?(:Spec) %} + class Spec::CLI + def main(args) + return previous_def unless args[0]? == "pu" - args.shift # Remove "pu" from the arguments + ProcessUtils.main(args[1..]) + end + end +{% else %} + args = ARGV + if args[0]?.in?("pu", "--") + args = args[1..] + end + ProcessUtils.main(args) +{% end %} + +# Provides simple helper programs for testing process spawn behaviour in `spec/std/process_spec.cr`. +# The commands similar to coreutils, but much simplified and tailored for +# the testing needs. +module ProcessUtils + def self.main(args) output = STDOUT exit_status = 0 From 914769589359ce6d4884c70d1c1d7e60e956906e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 22:33:28 +0200 Subject: [PATCH 13/18] [CI] Build `crystal-pu` for interpreter specs --- .github/workflows/win.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 7e889f788c0f..a61bb9db72e8 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -330,6 +330,9 @@ jobs: Add-Content $env:GITHUB_ENV "LLVM_TARGETS=${{ env.CI_LLVM_TARGETS }}" Add-Content $env:GITHUB_ENV "LLVM_LDFLAGS=${{ env.CI_LLVM_LDFLAGS }}" + - name: Build crystal-pu + run: make -f Makefile.win .build\crystal-pu.exe + - name: Run stdlib specs with interpreter run: bin\crystal i spec\std_spec.cr From 6e0d0fd3d7ce4426120c229b19cefc5643253ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 5 Jun 2026 23:14:08 +0200 Subject: [PATCH 14/18] fixup! Rework `crystal-pu` as standalone executable for interpreter --- spec/support/process-utils.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/support/process-utils.cr b/spec/support/process-utils.cr index b21d87cc54e2..eedecdd7172b 100644 --- a/spec/support/process-utils.cr +++ b/spec/support/process-utils.cr @@ -28,7 +28,6 @@ require "option_parser" # the testing needs. module ProcessUtils def self.main(args) - output = STDOUT exit_status = 0 From 84cd170be83e2cb5f3e4338ff53608985bdb3a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sat, 6 Jun 2026 10:23:44 +0200 Subject: [PATCH 15/18] fixup --- Makefile.win | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.win b/Makefile.win index 295df80cbdeb..4605714b014a 100644 --- a/Makefile.win +++ b/Makefile.win @@ -252,7 +252,7 @@ $(LLVM_EXT_OBJ): $(LLVM_EXT_DIR)\llvm_ext.cc $(CXX) /nologo /c $(CXXFLAGS) /WX /wd4244 /wd4624 /wd4530 "/Fo$@" "$<" $(if $(LLVM_CONFIG),$(shell $(LLVM_CONFIG) --cxxflags)) $(O)\crystal-pu.exe: spec\support\process-utils.cr $(SOURCES) - @(call MKDIR,"$(O)") + @$(call MKDIR,"$(O)") $(call export_vars) .\bin\crystal build $(FLAGS) -o $@ $< From c646e9f6b5ee1732ecffd5209138f556caf2674e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sat, 6 Jun 2026 10:32:51 +0200 Subject: [PATCH 16/18] Fixup `clear_env` example for UCRT --- spec/std/process_spec.cr | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 6fa18eb453aa..67b7f52904fe 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -473,12 +473,22 @@ describe Process do end it "clears and sets an environment variable" do - # We must pass PATH because otherwise dynamic libraries might not be found, - # particularly on windows-gnu. - value = Process.run(exe, ["pu", "env"], clear_env: true, env: {"PATH" => ENV["PATH"]?, "FOO" => "bar"}) do |proc| + env = {"FOO" => "bar"} + {% if flag?(:win32) && flag?(:gnu)%} + # We must pass PATH because otherwise dynamic libraries might not be found. + env["PATH"] = ENV["PATH"] + {% end %} + + value = Process.run(exe, ["pu", "env"], clear_env: true, env: env) do |proc| proc.output.gets_to_end end - value.should eq("PATH=#{ENV["PATH"]?}\nFOO=bar\n") + + {% if flag?(:win32) && flag?(:gnu)%} + # Ignore `PATH` (added above) and `PROCESSOR_ARCHITECTURE` which ucrt + # might inject. + value = value.gsub(/^(PATH|PROCESSOR_ARCHITECTURE)=.*\n/m, "") + {% end %} + value.should eq("FOO=bar\n") end it "sets an environment variable" do From 47cbdd602158f9d3fe70588b10c3a434d82aa6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sat, 6 Jun 2026 11:48:45 +0200 Subject: [PATCH 17/18] format --- spec/std/process_spec.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 67b7f52904fe..035f98f26d27 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -474,7 +474,7 @@ describe Process do it "clears and sets an environment variable" do env = {"FOO" => "bar"} - {% if flag?(:win32) && flag?(:gnu)%} + {% if flag?(:win32) && flag?(:gnu) %} # We must pass PATH because otherwise dynamic libraries might not be found. env["PATH"] = ENV["PATH"] {% end %} @@ -483,7 +483,7 @@ describe Process do proc.output.gets_to_end end - {% if flag?(:win32) && flag?(:gnu)%} + {% if flag?(:win32) && flag?(:gnu) %} # Ignore `PATH` (added above) and `PROCESSOR_ARCHITECTURE` which ucrt # might inject. value = value.gsub(/^(PATH|PROCESSOR_ARCHITECTURE)=.*\n/m, "") From 31746b1ca387a7d329ac2b516f1fe56386926f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 10 Jun 2026 09:31:07 +0200 Subject: [PATCH 18/18] Fix `crystal-pu` location in Windows CI --- .github/workflows/win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index a61bb9db72e8..9b60b7389264 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -331,7 +331,7 @@ jobs: Add-Content $env:GITHUB_ENV "LLVM_LDFLAGS=${{ env.CI_LLVM_LDFLAGS }}" - name: Build crystal-pu - run: make -f Makefile.win .build\crystal-pu.exe + run: make -f Makefile.win build\crystal-pu.exe O=build - name: Run stdlib specs with interpreter run: bin\crystal i spec\std_spec.cr