From bbb7fd05a41dfb102138bbdae247a480b80da2ac Mon Sep 17 00:00:00 2001 From: Largo Date: Wed, 28 Oct 2026 22:47:13 +0100 Subject: [PATCH 1/6] IO#winsize on Windows: the console's size, as CRuby's io/console answers it irb stopped after showing its first result on Windows: io/console.rb:209:in 'IO#winsize': Inappropriate ioctl for device - (Errno::ENOTTY) irb asks $stdout.winsize before it shows each result, and IronRuby's io/console asked only termios - ioctl(TIOCGWINSZ) - which Windows does not have. Termios.winsize now answers a standard stream on Windows from GetConsoleScreenBufferInfo, as CRuby does: srWindow's Bottom - Top + 1 rows and Right - Left + 1 columns. Off a console it raises what CRuby raises there, Errno::EBADF (STDIN, which has no screen buffer, included); Linux and macOS keep ENOTTY. In a real console, $stdout.winsize and $stderr.winsize give the same answer as CRuby 4.0, and irb shows "=> nil" after `puts "hello"` and goes on. IoConsole_WinsizeOffTerminal1 checks the error off a terminal. --- Src/IronRuby.Tests/RubyTests.cs | 1 + Src/IronRuby.Tests/Runtime/MiscTests.cs | 26 +++++++++++ Src/Libraries/Termios/Termios.cs | 57 ++++++++++++++++++++++++- Src/StdLib/ironruby/io/console.rb | 6 ++- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/Src/IronRuby.Tests/RubyTests.cs b/Src/IronRuby.Tests/RubyTests.cs index 234b2c436..cc7ef9917 100644 --- a/Src/IronRuby.Tests/RubyTests.cs +++ b/Src/IronRuby.Tests/RubyTests.cs @@ -733,6 +733,7 @@ public Tests(Driver/*!*/ driver) { Fiddle_AnsiSuffix1, PackPointer_RealAddress1, PackPointer_RelineWin32Api1, + IoConsole_WinsizeOffTerminal1, Dir_ChdirRelative1, File_ExpandTildeOrder1, File_FlockReadpartialRlimit1, diff --git a/Src/IronRuby.Tests/Runtime/MiscTests.cs b/Src/IronRuby.Tests/Runtime/MiscTests.cs index f1ad5e78b..13d01331e 100644 --- a/Src/IronRuby.Tests/Runtime/MiscTests.cs +++ b/Src/IronRuby.Tests/Runtime/MiscTests.cs @@ -1093,6 +1093,32 @@ p File.expand_path('~') == (windows ? 'Y:/profile' : File.expand_path('~')) "); } + /// + /// IO#winsize off a terminal raises what MRI raises: ENOTTY from ioctl on Linux and macOS, + /// EBADF from the console API on Windows, where IronRuby used to have no winsize at all - + /// irb asks $stdout for it before showing each result, and died after the first one. + /// A console's own size needs a console, which a test run does not have. + /// + public void IoConsole_WinsizeOffTerminal1() { + TestOutput(@" +require 'ruby4' +require 'io/console' +require 'tmpdir' +windows = File::ALT_SEPARATOR == '\\' +path = File.join(Dir.tmpdir, ""ir-winsize-#{Process.pid}.txt"") +File.open(path, 'w') do |f| + begin + f.winsize + rescue SystemCallError => e + p e.class == (windows ? Errno::EBADF : Errno::ENOTTY) + end +end +File.delete(path) +", @" +true +"); + } + /// /// What RubyGems and Bundler need from the file system on Windows, where each of these /// used to P/Invoke libc or raise: File#flock (RubyGems locks every binstub it writes), diff --git a/Src/Libraries/Termios/Termios.cs b/Src/Libraries/Termios/Termios.cs index 36729f905..4e21aa556 100644 --- a/Src/Libraries/Termios/Termios.cs +++ b/Src/Libraries/Termios/Termios.cs @@ -15,6 +15,7 @@ using IronRuby.Builtins; using IronRuby.Runtime; using Microsoft.Scripting.Runtime; +using Microsoft.Scripting.Utils; namespace IronRuby.StandardLibrary.Termios { @@ -31,7 +32,8 @@ namespace IronRuby.StandardLibrary.Termios { /// /// The constants are Linux's (asm-generic/termbits.h, asm-generic/ioctls.h). On a /// platform without termios is false and every call returns nil - /// or false, which io/console.rb turns into the ENOTTY MRI raises off a non-terminal. + /// or false, which io/console.rb turns into the error MRI raises off a non-terminal - + /// except winsize, which Windows answers from the console API, as MRI does there. /// [RubyModule("Termios")] public static class TermiosOps { @@ -181,10 +183,15 @@ public static bool TcFlush(RubyModule/*!*/ self, [NotNull]RubyIO/*!*/ io, [Defau #region winsize /// - /// ioctl(TIOCGWINSZ) as [rows, columns], or nil when this is not a terminal. + /// ioctl(TIOCGWINSZ) as [rows, columns], or nil when this is not a terminal. On Windows + /// it is the console window's size, from GetConsoleScreenBufferInfo, as MRI's + /// io/console answers it there - irb asks $stdout for it before it shows each result. /// [RubyMethod("winsize", RubyMethodAttributes.PublicSingleton)] public static object WinSize(RubyModule/*!*/ self, [NotNull]RubyIO/*!*/ io) { + if (!_supported) { + return WindowsWinSize(io); + } int fd = DescriptorOf(io); if (fd < 0) { return null; @@ -218,6 +225,52 @@ public static bool SetWinSize(RubyModule/*!*/ self, [NotNull]RubyIO/*!*/ io, return sys_ioctl(fd, TIOCSWINSZ, buffer) == 0; } + private const int STD_INPUT_HANDLE = -10; + private const int STD_OUTPUT_HANDLE = -11; + private const int STD_ERROR_HANDLE = -12; + + /// + /// CONSOLE_SCREEN_BUFFER_INFO: dwSize, dwCursorPosition (COORDs), wAttributes, then + /// srWindow - Left, Top, Right, Bottom as shorts at 10, 12, 14 and 16 - and + /// dwMaximumWindowSize. 22 bytes, laid out by hand like struct termios above. + /// + private const int ScreenBufferInfoSize = 22; + + [DllImport("kernel32", SetLastError = true)] + private static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32", SetLastError = true)] + private static extern bool GetConsoleScreenBufferInfo(IntPtr hConsoleOutput, byte[] info); + + /// + /// The visible window of the console a standard stream is attached to, as MRI computes + /// it: srWindow.Bottom - Top + 1 rows and Right - Left + 1 columns. nil for an IO + /// that is no standard stream, and for one that is not a console - redirected, or + /// the input side, which has no screen buffer. + /// + private static object WindowsWinSize(RubyIO/*!*/ io) { + if (io.Closed) { + return null; + } + int std; + switch (io.ConsoleStreamType) { + case ConsoleStreamType.Input: std = STD_INPUT_HANDLE; break; + case ConsoleStreamType.Output: std = STD_OUTPUT_HANDLE; break; + case ConsoleStreamType.ErrorOutput: std = STD_ERROR_HANDLE; break; + default: return null; + } + byte[] info = new byte[ScreenBufferInfoSize]; + if (!GetConsoleScreenBufferInfo(GetStdHandle(std), info)) { + return null; + } + short left = BitConverter.ToInt16(info, 10), top = BitConverter.ToInt16(info, 12); + short right = BitConverter.ToInt16(info, 14), bottom = BitConverter.ToInt16(info, 16); + var result = new RubyArray(2); + result.Add(bottom - top + 1); + result.Add(right - left + 1); + return result; + } + #endregion #region constants diff --git a/Src/StdLib/ironruby/io/console.rb b/Src/StdLib/ironruby/io/console.rb index d773097a2..33b8da80d 100644 --- a/Src/StdLib/ironruby/io/console.rb +++ b/Src/StdLib/ironruby/io/console.rb @@ -203,10 +203,12 @@ def ttymode # -------------------------------------------------------------------- size - # [rows, columns] of the terminal this IO is connected to. + # [rows, columns] of the terminal this IO is connected to. Off one, MRI raises + # ENOTTY from ioctl, and on Windows EBADF - the error GetConsoleScreenBufferInfo + # leaves for a handle that is no console output, STDIN's included. def winsize size = ::Termios.winsize(self) - raise Errno::ENOTTY, __console_name__ unless size + raise (::Termios.supported? ? Errno::ENOTTY : Errno::EBADF), __console_name__ unless size size end From f3aecfcdd23a998e433175f0f626f0efc43cffe8 Mon Sep 17 00:00:00 2001 From: Largo Date: Wed, 28 Oct 2026 22:47:15 +0100 Subject: [PATCH 2/6] ironruby at a terminal starts irb, as python starts its interpreter `ironruby` with neither -e nor a script read the program from stdin until EOF, at a terminal too, as `ruby` does (#35). At a terminal it now starts irb instead, as `python` does: it is `iirb`, the irb that -S finds in RUBYPATH, which ironruby.cmd and ironruby set to the bundled one. From a pipe or a file `ironruby` still reads the program from stdin, and so do an explicit `ironruby -`, -n, -p and -c, which are about stdin. ironrubyw has no console, so it still reads nothing and exits. When no irb is found it falls back to stdin. Checked in a real console: `ironruby` alone gives irb's prompt and runs typed lines; `'puts 1 + 1' | ironruby` prints 2; `ironruby < NUL` exits 0. Docs/releases/v4.0.3.md: both changes. --- Docs/releases/v4.0.3.md | 11 ++++++++ Src/Ruby/Hosting/RubyCommandLine.cs | 6 ++--- Src/Ruby/Hosting/RubyOptionsParser.cs | 36 ++++++++++++++++++++++----- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Docs/releases/v4.0.3.md b/Docs/releases/v4.0.3.md index 73c1f2088..1dba01c6c 100644 --- a/Docs/releases/v4.0.3.md +++ b/Docs/releases/v4.0.3.md @@ -31,6 +31,13 @@ gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby Setup 6.7.3 from `Util/installer/ironruby.iss`, and installed, run and uninstalled there before it is published. +### Changed + +- **`ironruby` at a terminal starts irb**, as `python` starts its interactive interpreter: + with no script and no `-e`, and stdin a terminal, `ironruby` is `iirb`. From a pipe or a + file it still reads the program from stdin, as `ruby` does, and so do `ironruby -`, + `-n`, `-p` and `-c`. + ### Fixes - **irb starts on Windows again.** In 4.0.2 it stopped at once with `unknown symbol @@ -40,3 +47,7 @@ gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby IronRuby that was an index, not an address. It is a real address now, of a block holding the String's bytes, and what C writes there through Fiddle comes back into the String, as with CRuby - so reline runs as Ruby ships it. +- **irb shows results on Windows.** After the first result it stopped with + `Errno::ENOTTY` from `IO#winsize`: IronRuby's io/console asked only termios for the + terminal size, which Windows does not have. It now asks the console, as CRuby does, and + raises `Errno::EBADF` off a console, as CRuby does on Windows. diff --git a/Src/Ruby/Hosting/RubyCommandLine.cs b/Src/Ruby/Hosting/RubyCommandLine.cs index 2f9ef5ba7..9b95d37e4 100644 --- a/Src/Ruby/Hosting/RubyCommandLine.cs +++ b/Src/Ruby/Hosting/RubyCommandLine.cs @@ -84,9 +84,9 @@ protected override int Run() { // A script argument of "-" says to read the program from stdin, and is how the CRuby // test suite runs a program in a child process; ARGV is what follows it. The options - // parser gives "-" when there is neither -e nor a script, so `ironruby` alone reads - // stdin as `ruby` does - at a terminal too, until EOF. A host that skips the parser - // and gives neither still gets stdin when it is redirected. + // parser gives "-" when there is neither -e nor a script and stdin is not a terminal + // (at one it gives irb), so `ironruby` alone reads a piped program as `ruby` does. A + // host that skips the parser and gives neither still gets stdin when it is redirected. if (Options.Command == null && (Options.FileName == "-" || (Options.FileName == null && System.Console.IsInputRedirected))) { // The program is read as bytes: a magic comment may say they are not UTF-8, and diff --git a/Src/Ruby/Hosting/RubyOptionsParser.cs b/Src/Ruby/Hosting/RubyOptionsParser.cs index ffe713fcf..fdc00c459 100644 --- a/Src/Ruby/Hosting/RubyOptionsParser.cs +++ b/Src/Ruby/Hosting/RubyOptionsParser.cs @@ -843,13 +843,37 @@ protected override void AfterParse() { ConsoleOptions.Exit = true; } - // `ruby` with neither -e nor a script reads the program from stdin until EOF - at a - // terminal too - as if "-" had been given, and $0 is "-" (MRI, JRuby). IronRuby used to - // start the DLR's interactive console there; irb is `irb` / `iirb`. ironrubyw, which has - // no console, then reads nothing and exits, as rubyw does. + // `ruby` with neither -e nor a script reads the program from stdin until EOF, as if "-" + // had been given, and $0 is "-" (MRI, JRuby); so does `ironruby` when stdin is a pipe + // or a file. At a terminal it starts irb instead, as `python` starts its interactive + // interpreter there: `ironruby` alone is then `iirb`, the -S irb that ironruby.cmd and + // ironruby find in RUBYPATH. An explicit "-", and -n, -p and -c, which are about + // stdin, still read it. ironrubyw, which has no console, reads nothing and exits, as + // rubyw does. if (!ConsoleOptions.Exit && ConsoleOptions.Command == null && ConsoleOptions.FileName == null) { - ConsoleOptions.FileName = "-"; - LanguageSetup.Options["MainFile"] = "-"; + string irb = StartsInteractive() ? FindMainFileFromPath("irb") : null; + if (irb != null && Platform.FileExists(irb)) { + ConsoleOptions.FileName = irb; + SetupOptionsForMainFile(); + } else { + ConsoleOptions.FileName = "-"; + LanguageSetup.Options["MainFile"] = "-"; + } + } + } + + /// + /// Whether `ironruby` with no program should be interactive: stdin is a terminal (a + /// console on Windows, which .NET tells apart from NUL), and no option asked for stdin. + /// + private bool StartsInteractive() { + if (LanguageSetup.Options.ContainsKey("LoopOverInput") || LanguageSetup.Options.ContainsKey("CheckSyntaxOnly")) { + return false; + } + try { + return !Console.IsInputRedirected; + } catch (IOException) { + return false; } } From 7c90f0311f4362e4c2da0aeae3e4d5828ed7352a Mon Sep 17 00:00:00 2001 From: Largo Date: Wed, 28 Oct 2026 22:56:45 +0100 Subject: [PATCH 3/6] igem install with CRuby on the PATH: no host plugins, Ripper::Filter On a machine with CRuby 4.0 on the PATH, `igem install file_exists` printed eight "already initialized constant RDoc::..." warnings and then failed after installing the gem, generating its documentation: uninitialized constant Ripper::Filter (NameError) rdoc/parser/ripper_state_lex.rb:17 - Gem.load_plugins loads plugins/*_plugin.rb from every directory on Gem.path, and that includes the host CRuby's gem tree, which IronRuby offers for third-party gems. CRuby's rdoc_plugin.rb stub loaded rdoc 7.0.4 from the host over the rdoc vendored here and hooked it into every install. Gem.load_plugins now skips the host trees. - host_ruby_dirs spelled a Windows PATH entry's directory with backslashes, while Gem::PathSupport and a gem's base_dir spell it C:/Ruby40-x64/...: the host tree was on Gem.path twice, and IronRubyHostGemFilter, which hides the host's copies of the gems IronRuby ships (rake, minitest, ...), never matched on Windows. It is expanded now. - require 'ripper' did not give Ripper::Filter, which MRI's ripper.rb requires and rdoc's Ruby parser subclasses. It is an autoload here, as filter.rb requires ripper/lexer, which is ripper.rb itself. `igem install file_exists` with C:\Ruby40-x64\bin on the PATH now installs with no warning. Ripper::Filter's tokens, and rdoc's RipperStateLex on top of it, match CRuby's. Ripper_Filter1 is new. --- Docs/releases/v4.0.3.md | 8 ++++++ README.md | 4 +-- Src/IronRuby.Tests/RubyTests.cs | 1 + Src/IronRuby.Tests/Runtime/MiscTests.cs | 25 +++++++++++++++++++ Src/StdLib/ironruby/ripper.rb | 4 +++ .../ruby/4.0/rubygems/defaults/ironruby.rb | 21 +++++++++++++++- 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/Docs/releases/v4.0.3.md b/Docs/releases/v4.0.3.md index 1dba01c6c..5db9d0b21 100644 --- a/Docs/releases/v4.0.3.md +++ b/Docs/releases/v4.0.3.md @@ -51,3 +51,11 @@ gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby `Errno::ENOTTY` from `IO#winsize`: IronRuby's io/console asked only termios for the terminal size, which Windows does not have. It now asks the console, as CRuby does, and raises `Errno::EBADF` off a console, as CRuby does on Windows. +- **`igem install` with CRuby on the PATH.** IronRuby offers a host CRuby's gem directory + for third-party gems, and RubyGems loaded that CRuby's plugins from it too: its rdoc + 7.0.4 hook, over the rdoc IronRuby ships ("already initialized constant RDoc::VERSION"), + which then failed generating the gem's documentation with `uninitialized constant + Ripper::Filter`. The host's plugins are no longer loaded, and `require 'ripper'` gives + `Ripper::Filter`, as in CRuby. On Windows the host directory was also spelled with + backslashes, so the filter that keeps CRuby's copies of the gems IronRuby ships (rake, + minitest, ...) out of the way never matched there; it does now. diff --git a/README.md b/README.md index a48bf4bff..1ae5b287f 100644 --- a/README.md +++ b/README.md @@ -281,8 +281,8 @@ every machine, with or without a CRuby next to IronRuby. [`Util/install-bundled-gems.rb`](Util/install-bundled-gems.rb) installs them from a CRuby source tarball's `gems/*.gem` and lists the ones left out and why (the C extensions IronRuby implements itself, and the ones that need `rbs`). A host CRuby 4.0's gem directory is still -offered for third-party gems, but never for a gem IronRuby ships; -`IRONRUBY_NO_HOST_GEMS=1` turns it off entirely. +offered for third-party gems, but never for a gem IronRuby ships, and its RubyGems plugins +are not loaded; `IRONRUBY_NO_HOST_GEMS=1` turns it off entirely. A C extension is never built: there is no `ruby.h` to compile against, and the gem whose extension is missing is often in a read-only host CRuby tree. Such a gem is left alone - diff --git a/Src/IronRuby.Tests/RubyTests.cs b/Src/IronRuby.Tests/RubyTests.cs index cc7ef9917..d875ee62f 100644 --- a/Src/IronRuby.Tests/RubyTests.cs +++ b/Src/IronRuby.Tests/RubyTests.cs @@ -734,6 +734,7 @@ public Tests(Driver/*!*/ driver) { PackPointer_RealAddress1, PackPointer_RelineWin32Api1, IoConsole_WinsizeOffTerminal1, + Ripper_Filter1, Dir_ChdirRelative1, File_ExpandTildeOrder1, File_FlockReadpartialRlimit1, diff --git a/Src/IronRuby.Tests/Runtime/MiscTests.cs b/Src/IronRuby.Tests/Runtime/MiscTests.cs index 13d01331e..994148c55 100644 --- a/Src/IronRuby.Tests/Runtime/MiscTests.cs +++ b/Src/IronRuby.Tests/Runtime/MiscTests.cs @@ -1119,6 +1119,31 @@ public void IoConsole_WinsizeOffTerminal1() { "); } + /// + /// require 'ripper' gives Ripper::Filter, as MRI's ripper.rb does: rdoc's Ruby parser + /// subclasses it, and `gem install` generating documentation died on the NameError. + /// + public void Ripper_Filter1() { + TestOutput(@" +require 'ruby4' +require 'ripper' +class Words < Ripper::Filter + def on_default(event, tok, data) + data << [lineno, column, event, tok] + end +end +Words.new(""x = 1 # one\n"").parse([]).each { |t| p t } +", @" +[1, 0, :on_ident, ""x""] +[1, 1, :on_sp, "" ""] +[1, 2, :on_op, ""=""] +[1, 3, :on_sp, "" ""] +[1, 4, :on_int, ""1""] +[1, 5, :on_sp, "" ""] +[1, 6, :on_comment, ""# one\n""] +"); + } + /// /// What RubyGems and Bundler need from the file system on Windows, where each of these /// used to P/Invoke libc or raise: File#flock (RubyGems locks every binstub it writes), diff --git a/Src/StdLib/ironruby/ripper.rb b/Src/StdLib/ironruby/ripper.rb index decd93ee2..f14243edd 100644 --- a/Src/StdLib/ironruby/ripper.rb +++ b/Src/StdLib/ironruby/ripper.rb @@ -704,3 +704,7 @@ def match(n = 0) end require 'ripper/sexp_builder' +# MRI's ripper.rb requires ripper/filter too; rdoc's Ruby parser subclasses Ripper::Filter. +# An autoload, as filter.rb requires ripper/lexer, which here is this file: required from +# here it would be a circular require. +Ripper.autoload :Filter, 'ripper/filter' diff --git a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb index 7c9fd80c1..1a7220759 100644 --- a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb +++ b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb @@ -90,7 +90,11 @@ def self.host_ruby_dirs # :nodoc: exe = File.join(dir, "ruby#{RbConfig::CONFIG["EXEEXT"]}") next unless File.file?(exe) - prefix = File.dirname(File.dirname(exe)) + # Expanded, as Gem::PathSupport expands Gem.path's entries: on Windows a PATH + # entry spells C:\Ruby40-x64\bin with backslashes, and a host tree spelled + # that way is not the C:/Ruby40-x64/... that Gem.path and a gem's base_dir + # name, so neither IronRubyHostGemFilter nor load_plugins recognised it. + prefix = File.expand_path(File.dirname(File.dirname(exe))) gems = File.join(prefix, "lib", "ruby", "gems", ruby_api_version) next unless File.directory?(gems) next if seen[gems] @@ -226,6 +230,21 @@ def self.path paths.path | [default_dir] | host_ruby_dirs end + ## + # RubyGems' plugins, from every directory on Gem.path but the host trees. A + # host CRuby's plugins/ holds the stubs its gems' installs wrote, and they + # load that CRuby's code into IronRuby: its rdoc's rubygems_plugin.rb loaded + # rdoc 7.0.4 from the host over the rdoc vendored here ("already initialized + # constant RDoc::VERSION") and hooked it into every `gem install`, which then + # failed generating the documentation. The host trees are for gems' files; + # the plugins IronRuby runs are the ones installed for IronRuby. + + def self.load_plugins + (path - host_ruby_dirs).each do |gem_path| + load_plugin_files Gem::Util.glob_files_in_dir("*#{Gem.plugin_suffix_pattern}", plugindir(gem_path)) + end + end + ## # RbConfig.ruby names the `ironruby` apphost, which starts without the -X:StdLib # argument that gives it Ruby 4's standard library. RubyGems and From 17b5bbfaeb42329fa2b54721c91108bc9c50cba4 Mon Sep 17 00:00:00 2001 From: Largo Date: Wed, 28 Oct 2026 22:59:46 +0100 Subject: [PATCH 4/6] IronRuby no longer uses a CRuby's gems IronRuby offered the gem directory of a CRuby 4.0 found on the PATH for third-party gems, with a filter that kept CRuby's copies of the gems IronRuby ships out. It still loaded CRuby's code into IronRuby: with CRuby on the PATH, `igem install` loaded CRuby's rdoc plugin over the rdoc shipped here and failed generating documentation. The previous commit filtered the plugins as well; this one removes the host trees instead, so nothing of a CRuby installation is read at all. Gem.path and Gem.default_path are IronRuby's own directories only - user_dir, GEM_HOME, GEM_PATH and default_dir. Gone with the host trees: Gem.host_ruby_dirs, the load_plugins override, IronRubyHostGemFilter and Gem.ironruby_own_gem_names, which only the filter used, and IRONRUBY_NO_HOST_GEMS. A gem IronRuby needs is installed for it, with igem or Bundler. With C:\Ruby40-x64\bin on the PATH: Gem.path lists no CRuby directory, no rdoc is loaded by the plugins, rake is IronRuby's 13.3.1, and `igem install file_exists` installs with no warning. --- Docs/releases/v4.0.3.md | 17 +- README.md | 9 +- .../ruby/4.0/rubygems/defaults/ironruby.rb | 194 +++--------------- Util/install-bundled-gems.rb | 3 +- 4 files changed, 43 insertions(+), 180 deletions(-) diff --git a/Docs/releases/v4.0.3.md b/Docs/releases/v4.0.3.md index 5db9d0b21..0f4daed39 100644 --- a/Docs/releases/v4.0.3.md +++ b/Docs/releases/v4.0.3.md @@ -37,6 +37,12 @@ gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby with no script and no `-e`, and stdin a terminal, `ironruby` is `iirb`. From a pipe or a file it still reads the program from stdin, as `ruby` does, and so do `ironruby -`, `-n`, `-p` and `-c`. +- **IronRuby no longer uses a CRuby's gems.** It used to offer the gem directory of a CRuby + 4.0 found on the PATH for third-party gems. That loaded CRuby's code into IronRuby: with + CRuby on the PATH, `igem install` loaded CRuby's rdoc plugin over the rdoc IronRuby ships + ("already initialized constant RDoc::VERSION") and then failed generating the gem's + documentation. IronRuby now reads only its own gem directories; a gem it needs is + installed for it, with `igem install` or Bundler. `IRONRUBY_NO_HOST_GEMS` is gone with it. ### Fixes @@ -51,11 +57,6 @@ gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby `Errno::ENOTTY` from `IO#winsize`: IronRuby's io/console asked only termios for the terminal size, which Windows does not have. It now asks the console, as CRuby does, and raises `Errno::EBADF` off a console, as CRuby does on Windows. -- **`igem install` with CRuby on the PATH.** IronRuby offers a host CRuby's gem directory - for third-party gems, and RubyGems loaded that CRuby's plugins from it too: its rdoc - 7.0.4 hook, over the rdoc IronRuby ships ("already initialized constant RDoc::VERSION"), - which then failed generating the gem's documentation with `uninitialized constant - Ripper::Filter`. The host's plugins are no longer loaded, and `require 'ripper'` gives - `Ripper::Filter`, as in CRuby. On Windows the host directory was also spelled with - backslashes, so the filter that keeps CRuby's copies of the gems IronRuby ships (rake, - minitest, ...) out of the way never matched there; it does now. +- **`require 'ripper'` gives `Ripper::Filter`**, as in CRuby. rdoc's Ruby parser subclasses + it, and generating a gem's documentation failed with `uninitialized constant + Ripper::Filter`. diff --git a/README.md b/README.md index 1ae5b287f..295f614a1 100644 --- a/README.md +++ b/README.md @@ -280,12 +280,11 @@ versions CRuby 4.0.6 bundles, so `require "rexml/document"` and `ir -S rake` wor every machine, with or without a CRuby next to IronRuby. [`Util/install-bundled-gems.rb`](Util/install-bundled-gems.rb) installs them from a CRuby source tarball's `gems/*.gem` and lists the ones left out and why (the C extensions IronRuby -implements itself, and the ones that need `rbs`). A host CRuby 4.0's gem directory is still -offered for third-party gems, but never for a gem IronRuby ships, and its RubyGems plugins -are not loaded; `IRONRUBY_NO_HOST_GEMS=1` turns it off entirely. +implements itself, and the ones that need `rbs`). IronRuby never uses the gems of a CRuby +installed on the same machine: a gem it needs is installed for IronRuby, with `igem install` +or Bundler. -A C extension is never built: there is no `ruby.h` to compile against, and the gem whose -extension is missing is often in a read-only host CRuby tree. Such a gem is left alone - +A C extension is never built: there is no `ruby.h` to compile against. Such a gem is left alone - its `.rb` files load, a real `require` of the `.so` raises `LoadError`, and the pure-Ruby fallback most of them carry is used instead. diff --git a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb index 1a7220759..cf1638d61 100644 --- a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb +++ b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb @@ -38,74 +38,16 @@ def self.user_dir end ## - # Gem trees belonging to a matching host CRuby installation. + # No host CRuby's gems. # - # IronRuby is a drop-in for the CRuby release whose standard library it - # vendors, and pure-Ruby gems installed for that CRuby load unchanged here. - # Rather than force every user to set GEM_PATH, look for a `ruby` on PATH and - # offer its /lib/ruby/gems/ read-only, when the API - # version matches ours. Set IRONRUBY_NO_HOST_GEMS=1 to opt out. - # - # What these trees may provide, and what they may not - # ---------------------------------------------------- - # They are a convenience for *third-party* gems only. Nothing IronRuby ships - # may come from them: not a default gem (the gemspecs in - # specifications/default, see default_specifications_dir) and not a bundled - # gem (the gems Util/install-bundled-gems.rb installs into default_dir - - # rake, rexml, minitest, csv, logger, ...). IronRubyHostGemFilter at the end - # of this file drops the host's copy of every gem whose name IronRuby's own - # gem home has, whatever its version, so `require "rexml/document"` and - # `ir -S rake` resolve to the same files on every machine: the ones in this - # tree, or a newer release the user installed *for IronRuby*. - # - # That used to be otherwise. Before the bundled gems were shipped here, a - # machine with CRuby 4.0 installed got rake 13 and rexml 3.4 from the host, - # and one without it - Windows, CI, most users - got a LoadError or a 1.9-era - # copy, with nothing in between to say which it was. - # - # Why this is still on by default - # ------------------------------- - # Turning it into an opt-in was considered and not done. With the filter it - # can no longer change what the standard library is, so what is left is the - # convenience - a Gemfile or a script that uses a gem the host CRuby already - # has works without installing it again for IronRuby - and the ruby-bench, - # Rails and popular-gem setups on this project's machines are built on it. - # IRONRUBY_NO_HOST_GEMS=1 is what makes a run behave like a machine with no - # CRuby at all, and is how the self-containment check is run. - # - # Bundler narrows GEM_HOME and GEM_PATH to its bundle_path on every command, - # which would hide these trees and default_dir alike; Gem.path below keeps - # both. - - def self.host_ruby_dirs # :nodoc: - return @host_ruby_dirs if defined?(@host_ruby_dirs) && @host_ruby_dirs - - @host_ruby_dirs = [] - return @host_ruby_dirs if ENV["IRONRUBY_NO_HOST_GEMS"] - - seen = {} - (ENV["PATH"] || "").split(File::PATH_SEPARATOR).each do |dir| - next if dir.empty? - - exe = File.join(dir, "ruby#{RbConfig::CONFIG["EXEEXT"]}") - next unless File.file?(exe) - - # Expanded, as Gem::PathSupport expands Gem.path's entries: on Windows a PATH - # entry spells C:\Ruby40-x64\bin with backslashes, and a host tree spelled - # that way is not the C:/Ruby40-x64/... that Gem.path and a gem's base_dir - # name, so neither IronRubyHostGemFilter nor load_plugins recognised it. - prefix = File.expand_path(File.dirname(File.dirname(exe))) - gems = File.join(prefix, "lib", "ruby", "gems", ruby_api_version) - next unless File.directory?(gems) - next if seen[gems] - - seen[gems] = true - @host_ruby_dirs << gems - end - @host_ruby_dirs - rescue StandardError - @host_ruby_dirs = [] - end + # IronRuby reads only its own gem trees: the user's (user_dir), GEM_HOME and + # GEM_PATH, and default_dir with the gems it ships. It used to also offer a + # CRuby 4.0 found on PATH - its /lib/ruby/gems/4.0.0 - for third-party + # gems, filtered so that none IronRuby ships came from there. That still + # loaded that CRuby's code into IronRuby in ways no filter caught: its + # RubyGems plugins (rdoc's hooked rdoc 7.0.4 into every `gem install`), and + # gems installed for one interpreter running on the other. A gem IronRuby + # needs is installed for IronRuby, with `igem install` or Bundler. ## # Where the default gems' specifications live. @@ -209,40 +151,23 @@ def self.default_path path << user_dir if user_home && File.exist?(user_home) path << default_dir path << vendor_dir if vendor_dir && File.directory?(vendor_dir) - path.concat(host_ruby_dirs) path end ## - # IronRuby's own gem home and the host trees stay on Gem.path even when - # something narrows GEM_HOME and GEM_PATH - which Bundler does on every - # command, pinning both to its bundle_path (and GEM_PATH to nothing at all - # when a path is configured). On CRuby that loses little, because - # bundle_path defaults to the very directory the bundled gems are in; here it - # would hide them, and a Gemfile that names rake or minitest would go to - # rubygems.org for a gem this tree already has. default_dir holds the - # bundled gems IronRuby ships (Util/install-bundled-gems.rb) and the host - # trees are filtered (IronRubyHostGemFilter), so both are read-only extra - # sources, like a vendor directory, and appending them is always safe. They - # go last, so a gem installed into GEM_HOME or GEM_PATH is found first. + # IronRuby's own gem home stays on Gem.path even when something narrows + # GEM_HOME and GEM_PATH - which Bundler does on every command, pinning both to + # its bundle_path (and GEM_PATH to nothing at all when a path is configured). + # On CRuby that loses little, because bundle_path defaults to the very + # directory the bundled gems are in; here it would hide them, and a Gemfile + # that names rake or minitest would go to rubygems.org for a gem this tree + # already has. default_dir holds the bundled gems IronRuby ships + # (Util/install-bundled-gems.rb), so it is a read-only extra source, like a + # vendor directory, and appending it is always safe. It goes last, so a gem + # installed into GEM_HOME or GEM_PATH is found first. def self.path - paths.path | [default_dir] | host_ruby_dirs - end - - ## - # RubyGems' plugins, from every directory on Gem.path but the host trees. A - # host CRuby's plugins/ holds the stubs its gems' installs wrote, and they - # load that CRuby's code into IronRuby: its rdoc's rubygems_plugin.rb loaded - # rdoc 7.0.4 from the host over the rdoc vendored here ("already initialized - # constant RDoc::VERSION") and hooked it into every `gem install`, which then - # failed generating the documentation. The host trees are for gems' files; - # the plugins IronRuby runs are the ones installed for IronRuby. - - def self.load_plugins - (path - host_ruby_dirs).each do |gem_path| - load_plugin_files Gem::Util.glob_files_in_dir("*#{Gem.plugin_suffix_pattern}", plugindir(gem_path)) - end + paths.path | [default_dir] end ## @@ -315,13 +240,13 @@ def self.ironruby_default_gem_names # :nodoc: end ## - # Lazy loading of the host CRuby's default gemspecs. + # Lazy loading of the default gemspecs. # # Upstream's Gem::Specification.load_defaults evaluates every gemspec in # Gem.default_specifications_dir at boot, purely to feed # Gem.register_default_spec - which only wants, per gem, the list of paths # that `require` should map to it. On CRuby that directory holds a handful - # of specs and the eval is compiled C; here it is the host's 46 default + # of specs and the eval is compiled C; here it is IronRuby's own default # gemspecs, and evaluating them is by far the largest single cost of # `require "rubygems"`. # @@ -340,7 +265,7 @@ def self.ironruby_default_gem_names # :nodoc: ## # The standard library this process is running out of. A source tree can be # checked out many times over - every worktree on this box shares one home - # directory and one host CRuby - so it is part of the cache's identity: the + # directory - so it is part of the cache's identity: the # recorded feature paths point into *this* tree, and requiring another # worktree's copy of rubygems/dependency.rb would be a real bug. @@ -514,11 +439,9 @@ def self.load_defaults # :nodoc: # RubyGems builds a gem's extensions lazily, the first time something # activates it - installing a gem builds its dependencies' extensions too. # On IronRuby that can only fail: there is no ruby.h and no CRuby ABI to - # compile against. Worse, the gem whose extension is missing is usually one - # in the *host* CRuby's tree (which is offered read-only, see - # host_ruby_dirs), so the attempt writes a half-built Makefile and a - # gem_make.out into someone else's installation before it fails, and takes - # the whole `gem install` down with it. + # compile against. The attempt writes a half-built Makefile and a + # gem_make.out into the gem's directory before it fails, and takes the whole + # `gem install` down with it. # # Doing nothing leaves the gem exactly as it is: its .rb files load, the # missing .so raises a plain LoadError if something really reaches for it, @@ -657,8 +580,8 @@ def ironruby_provided_library? # :nodoc: # running Ruby, and Gem::Dependency#matching_specs then rejects it. On # IronRuby a C extension is never built, so that rule would make every gem # with an extension - and every gem that merely depends on one - impossible - # to activate: parser depends on racc, and racc's .so exists only for the - # host CRuby. Most such gems carry a pure-Ruby fallback that the extension + # to activate: parser depends on racc, and racc's .so exists only for + # CRuby. Most such gems carry a pure-Ruby fallback that the extension # merely accelerates (racc/parser.rb rescues the LoadError of racc/cparse), # and the ones that do not fail at require time with a plain LoadError for # the extension file, which is a better answer than refusing to resolve. @@ -718,65 +641,4 @@ def full_require_paths end BasicSpecification.prepend(IronRubyProvidedGem) - - ## - # A host CRuby's copy of a gem IronRuby ships is never used. - # - # host_ruby_dirs offers a matching CRuby's gem tree read-only, which is right - # for third-party pure-Ruby gems. It also offers that CRuby's *installed* - # bigdecimal, json, psych and bundler, though, and an installed gem sorts - # ahead of a default gem of the same version - so the host's bigdecimal-4.0.1 - # shadowed the one in Src/Libraries, and, worse, the host's bundler shadowed - # the bundler shipped here: a Gemfile.lock saying `BUNDLED WITH 4.0.12` made - # Bundler::SelfManager switch to the host's 4.0.12, an upstream release with - # none of IronRuby's carve-outs, which then dropped every extension gem from - # its index and aborted the bundle. - # - # The same goes for the bundled gems in default_dir, for a quieter reason: the - # host's rake-13.4.2 is newer than the rake-13.3.1 shipped here, so on a - # machine with CRuby it would win activation, and `ir -S rake` would run a - # different rake there than everywhere else. - # - # So drop the host's copy of every gem IronRuby has its own of - a default - # gem, or any gem in its own gem home - from the index entirely. Only the - # host trees are filtered: a gem the user installed *for IronRuby* over a - # shipped one is still an upgrade and still wins, exactly as on CRuby. - # - # Defined ahead of rubygems/specification_record.rb (which reopens the class) - # and prepended, so the real definition cannot overwrite it. - - ## - # The names of the gems in IronRuby's own gem home, default and installed: - # the ones a host tree may not provide. Read from file names, so that - # nothing is evaluated. - - def self.ironruby_own_gem_names # :nodoc: - @ironruby_own_gem_names ||= begin - installed = Dir.glob(File.join(default_dir, "specifications", "*.gemspec")).map do |file| - File.basename(file, ".gemspec")[/\A(.+)-[^-]+\z/, 1] - end - (ironruby_default_gem_names + installed.compact).uniq - end - rescue StandardError - @ironruby_own_gem_names = ironruby_default_gem_names - end - - class SpecificationRecord; end - - module IronRubyHostGemFilter # :nodoc: - private - - def installed_stubs(pattern) - stubs = super - host = Gem.host_ruby_dirs - return stubs if host.empty? - - names = Gem.ironruby_own_gem_names - return stubs if names.empty? - - stubs.reject {|stub| host.include?(stub.base_dir) && names.include?(stub.name) } - end - end - - SpecificationRecord.prepend(IronRubyHostGemFilter) end diff --git a/Util/install-bundled-gems.rb b/Util/install-bundled-gems.rb index f4e6772a0..2fe144d55 100644 --- a/Util/install-bundled-gems.rb +++ b/Util/install-bundled-gems.rb @@ -25,7 +25,8 @@ # interpreter into its default gem home, which `require` finds by activating # them. IronRuby used to have 1.9-era copies of most of them on $LOAD_PATH - # rake 0.8.7, rexml 3.1.7.3, MiniTest 1.6 - and got the modern ones only on a -# machine that also had CRuby 4.0 installed, through Gem.host_ruby_dirs. With +# machine that also had CRuby 4.0 installed, through that CRuby's gem tree +# (which IronRuby no longer reads at all). With # these installed here the modern gem is always present, on every machine and # on Windows, and the 1.9 copies are gone. # From f53132b46439fa41b00dfb3b81e93a9bea33309d Mon Sep 17 00:00:00 2001 From: Largo Date: Wed, 28 Oct 2026 23:06:13 +0100 Subject: [PATCH 5/6] gem install: --no-document by default, rdoc's hooks for --document Gem.platform_defaults gives every gem command --no-document, as TruffleRuby's rubygems/defaults/truffleruby.rb does: generating ri and rdoc takes longer than installing most gems and is rarely read. JRuby leaves RubyGems' default. rdoc ships in IronRuby's standard library, not as a gem, so no install wrote the plugin stub that registers its hooks on CRuby, and --document did nothing. Gem.done_installing and Gem.pre_uninstall now register RDoc::RubyGemsHook.generate and .remove, which require rdoc only when they run and read --document / --no-document themselves. In a scratch GEM_HOME: `igem install file_exists` builds no documentation; with --document it builds the ri documentation; `igem uninstall file_exists` removes it. --- Docs/releases/v4.0.3.md | 4 +++ .../ruby/4.0/rubygems/defaults/ironruby.rb | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Docs/releases/v4.0.3.md b/Docs/releases/v4.0.3.md index 0f4daed39..1352aaacd 100644 --- a/Docs/releases/v4.0.3.md +++ b/Docs/releases/v4.0.3.md @@ -43,6 +43,10 @@ gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby ("already initialized constant RDoc::VERSION") and then failed generating the gem's documentation. IronRuby now reads only its own gem directories; a gem it needs is installed for it, with `igem install` or Bundler. `IRONRUBY_NO_HOST_GEMS` is gone with it. +- **`igem install` builds no documentation unless asked**, as on TruffleRuby: generating ri + and rdoc takes longer than installing most gems. `igem install --document` (or + `gem: --document` in `~/.gemrc`) builds it with the rdoc IronRuby ships, and uninstalling + the gem removes it again. ### Fixes diff --git a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb index cf1638d61..ad4cb7d51 100644 --- a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb +++ b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb @@ -170,6 +170,32 @@ def self.path paths.path | [default_dir] end + ## + # No documentation unless asked for, as on TruffleRuby: generating ri and + # rdoc takes longer than installing most gems, and is rarely read. `gem + # install --document` (or a "gem: --document" line in ~/.gemrc) still builds + # it, through the hooks below. + + def self.platform_defaults + { "gem" => "--no-document" } + end + + # rdoc ships in IronRuby's standard library rather than as a gem, so no + # install wrote the plugin stub that registers its hooks on CRuby (rdoc's + # rubygems_plugin.rb, through /plugins). These register the same + # two, and load rdoc only when one runs. RDoc::RubyGemsHook.generate reads + # the install's --document / --no-document itself. ::RDoc, because inside + # Gem, RDoc is Gem::RDoc - rubygems/rdoc.rb's alias for the hook class. + done_installing do |installer, specs| + require "rdoc/rubygems_hook" + ::RDoc::RubyGemsHook.generate(installer, specs) + end + + pre_uninstall do |uninstaller| + require "rdoc/rubygems_hook" + ::RDoc::RubyGemsHook.remove(uninstaller) + end + ## # RbConfig.ruby names the `ironruby` apphost, which starts without the -X:StdLib # argument that gives it Ruby 4's standard library. RubyGems and From e17868764be899668def40fe9ca4640ab51652a4 Mon Sep 17 00:00:00 2001 From: Largo Date: Wed, 28 Oct 2026 23:11:56 +0100 Subject: [PATCH 6/6] Review: console streams through #dup, no irb from the current directory, -x reads stdin - RubyIO#ConsoleStreamType looked one buffer down for the console stream. #dup of a console stream on Windows cannot dup(2) the descriptor and wraps the original's buffered stream in one of its own, so STDOUT.dup.winsize raised EBADF and STDOUT.dup.tty? was false. It now unwraps every RubyBufferedStream; IO#tty? reads the same property. - `ironruby` at a terminal took FindMainFileFromPath("irb") when that file existed, and FindMainFileFromPath hands back a name it did not find unchanged - so with irb in neither RUBYPATH nor PATH, a file called irb in the current directory ran. Only an irb found in RUBYPATH or PATH runs now; otherwise stdin is read. - -x selects a program from stdin after its #!ruby line, so it keeps the stdin path too, as -n, -p and -c do. In a real console: STDOUT.dup.winsize and a #reopen-ed STDERR give [30, 120] and tty? is true; `ironruby -x` runs the program after "#!ruby" from typed input; ir.exe with no RUBYPATH, in a directory holding a file named irb, reads stdin. --- Docs/releases/v4.0.3.md | 2 +- Src/Ruby/Builtins/RubyIO.cs | 11 +++++++++-- Src/Ruby/Hosting/RubyOptionsParser.cs | 11 +++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Docs/releases/v4.0.3.md b/Docs/releases/v4.0.3.md index 1352aaacd..e441f778d 100644 --- a/Docs/releases/v4.0.3.md +++ b/Docs/releases/v4.0.3.md @@ -36,7 +36,7 @@ gh attestation verify ironruby-4.0.3-win-x64-setup.exe --repo Largo/ironruby - **`ironruby` at a terminal starts irb**, as `python` starts its interactive interpreter: with no script and no `-e`, and stdin a terminal, `ironruby` is `iirb`. From a pipe or a file it still reads the program from stdin, as `ruby` does, and so do `ironruby -`, - `-n`, `-p` and `-c`. + `-n`, `-p`, `-c` and `-x`. - **IronRuby no longer uses a CRuby's gems.** It used to offer the gem directory of a CRuby 4.0 found on the PATH for third-party gems. That loaded CRuby's code into IronRuby: with CRuby on the PATH, `igem install` loaded CRuby's rdoc plugin over the rdoc IronRuby ships diff --git a/Src/Ruby/Builtins/RubyIO.cs b/Src/Ruby/Builtins/RubyIO.cs index 9642753e8..2b5854dea 100644 --- a/Src/Ruby/Builtins/RubyIO.cs +++ b/Src/Ruby/Builtins/RubyIO.cs @@ -218,8 +218,15 @@ public ConsoleStreamType? ConsoleStreamType { if (Closed) { return null; } - var stream = GetStream(); - var console = stream.BaseStream as ConsoleStream; + // A copy that could not dup(2) the descriptor - #dup of a console stream on + // Windows - wraps the original's buffered stream in one of its own, so the + // console may be more than one buffer down. + Stream stream = GetStream().BaseStream; + RubyBufferedStream buffered; + while ((buffered = stream as RubyBufferedStream) != null) { + stream = buffered.BaseStream; + } + var console = stream as ConsoleStream; return console != null ? console.StreamType : (ConsoleStreamType?)null; } } diff --git a/Src/Ruby/Hosting/RubyOptionsParser.cs b/Src/Ruby/Hosting/RubyOptionsParser.cs index fdc00c459..d6038fcfb 100644 --- a/Src/Ruby/Hosting/RubyOptionsParser.cs +++ b/Src/Ruby/Hosting/RubyOptionsParser.cs @@ -847,12 +847,14 @@ protected override void AfterParse() { // had been given, and $0 is "-" (MRI, JRuby); so does `ironruby` when stdin is a pipe // or a file. At a terminal it starts irb instead, as `python` starts its interactive // interpreter there: `ironruby` alone is then `iirb`, the -S irb that ironruby.cmd and - // ironruby find in RUBYPATH. An explicit "-", and -n, -p and -c, which are about + // ironruby find in RUBYPATH. An explicit "-", and -n, -p, -c and -x, which are about // stdin, still read it. ironrubyw, which has no console, reads nothing and exits, as - // rubyw does. + // rubyw does. Only an irb found in RUBYPATH or PATH runs: FindMainFileFromPath hands + // back a name it did not find unchanged, and that would be a file called irb in the + // current directory. if (!ConsoleOptions.Exit && ConsoleOptions.Command == null && ConsoleOptions.FileName == null) { string irb = StartsInteractive() ? FindMainFileFromPath("irb") : null; - if (irb != null && Platform.FileExists(irb)) { + if (irb != null && irb != "irb" && Platform.FileExists(irb)) { ConsoleOptions.FileName = irb; SetupOptionsForMainFile(); } else { @@ -867,7 +869,8 @@ protected override void AfterParse() { /// console on Windows, which .NET tells apart from NUL), and no option asked for stdin. /// private bool StartsInteractive() { - if (LanguageSetup.Options.ContainsKey("LoopOverInput") || LanguageSetup.Options.ContainsKey("CheckSyntaxOnly")) { + if (LanguageSetup.Options.ContainsKey("LoopOverInput") || LanguageSetup.Options.ContainsKey("CheckSyntaxOnly") || + LanguageSetup.Options.ContainsKey("SkipToRubyShebang")) { return false; } try {