Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/zjit-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
rustup install ${{ matrix.rust_version }} --profile minimal
rustup default ${{ matrix.rust_version }}

- uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
- uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/zjit-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:
ruby-version: '3.1'
bundler: none

- uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
- uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
Expand Down
14 changes: 14 additions & 0 deletions doc/file/timestamps.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ Each of these methods returns the modification time for an entry as a Time objec
- File::Stat#mtime.
- Pathname#mtime.

The modification time (along with the access time) may also be updated explicitly:

- File::lutime.
- File::utime.
- Pathname#lutime.
- Pathname#utime.

## Access \Time

The access time for an entry is the time the entry last read.
Expand All @@ -64,6 +71,13 @@ Each of these methods returns the access time for an entry as a Time object:
- File::Stat#atime.
- Pathname#atime.

The access time (along with the modification time) may also be updated explicitly:

- File::lutime.
- File::utime.
- Pathname#lutime.
- Pathname#utime.

## Metadata-Change \Time

The metadata-change time for an entry is the time the entry last read.
Expand Down
2 changes: 1 addition & 1 deletion doc/string/split.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ and trailing empty strings are included:

When +limit+ is negative,
there is no limit on the size of the array,
and trailing empty strings are omitted:
and trailing empty strings are included:

'abracadabra'.split('', -1) # => ["a", "b", "r", "a", "c", "a", "d", "a", "b", "r", "a", ""]
'abracadabra'.split('a', -1) # => ["", "br", "c", "d", "br", ""]
Expand Down
1 change: 1 addition & 0 deletions ext/json/parser/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -2412,6 +2412,7 @@ static VALUE cResumableParser_parse(VALUE self)
complete = false;
if (RTEST(rb_ivar_get(rb_errinfo(), rb_intern("@eos")))) {
complete = false; // is an EOS error
rb_set_errinfo(Qnil);
} else {
rb_jump_tag(status); // reraise
}
Expand Down
78 changes: 66 additions & 12 deletions file.c
Original file line number Diff line number Diff line change
Expand Up @@ -3374,14 +3374,55 @@ rb_file_s_utime(int argc, VALUE *argv, VALUE _)
#if defined(HAVE_UTIMES) && (defined(HAVE_LUTIMES) || (defined(HAVE_UTIMENSAT) && defined(AT_SYMLINK_NOFOLLOW)))

/*
* :markup: markdown
*
* call-seq:
* File.lutime(atime, mtime, file_name, ...) -> integer
* File.lutime(atime, mtime, *paths) -> path_count
*
* Like File#utime, but does not follow symbolic links,
* and therefore changes the times of the entries given by `paths`,
* regardless of whether they are symbolic links;
* returns the number of `paths` given:
*
* ```ruby
* # Create a file and a link to it.
* file_path = 't.tmp'
* File.write(file_path, '')
* link_path = 'link'
* File.symlink(file_path, link_path)
* # Take snapshots of both.
* file_stat = File.stat(file_path)
* link_stat = File.lstat(link_path)
* # Fetch access times and modification times of both.
* file_stat.atime # => 2026-06-15 10:45:11.376753268 -0500
* file_stat.mtime # => 2026-06-15 10:44:47.335854904 -0500
* link_stat.atime # => 2026-06-15 10:44:59.788801128 -0500
* link_stat.mtime # => 2026-06-15 10:44:49.367845961 -0500
* # Update access time and modification time of the link.
* time = Time.now # => 2026-06-15 10:48:57.847422496 -0500
* File.lutime(time, time, link_path)
* # Take fresh snapshots of both.
* file_stat = File.stat(file_path)
* link_stat = File.lstat(link_path)
* # Fetch access time and modification time of file (not changed).
* file_stat.atime # => 2026-06-15 10:45:11.376753268 -0500
* file_stat.mtime # => 2026-06-15 10:44:47.335854904 -0500
* # Fetch access time and modification time of link (changed).
* link_stat.atime # => 2026-06-15 10:49:27.119146136 -0500
* link_stat.mtime # => 2026-06-15 10:48:57.847422496 -0500
* # Clean up.
* File.delete(file_path)
* File.delete(link_path)
* ```
*
* Arguments `atime` and `mtime` may be Time objects (as above).
*
* Either or both may be integers;
* when an integer `i` is passed, `Time.new(i)` is used.
*
* Either or both may be `nil`, in which case `Time.now` is used.
*
* Sets the access and modification times of each named file to the
* first two arguments. If a file is a symlink, this method acts upon
* the link itself as opposed to its referent; for the inverse
* behavior, see File.utime. Returns the number of file
* names in the argument list.
* See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/

static VALUE
Expand Down Expand Up @@ -3427,15 +3468,28 @@ syserr_fail2_in(const char *func, int e, VALUE s1, VALUE s2)

#ifdef HAVE_LINK
/*
* :markup: markdown

* call-seq:
* File.link(old_name, new_name) -> 0
* File.link(path, new_path) -> 0
*
* Not available on some systems.
*
* Creates a new entry at `new_path` for the existing entry at `path`
* using a [hard link](https://en.wikipedia.org/wiki/Hard_link):
*
* Creates a new name for an existing file using a hard link. Will not
* overwrite <i>new_name</i> if it already exists (raising a subclass
* of SystemCallError). Not available on all platforms.
* ```ruby
* File.write('doc/t.tmp', 'foo')
* File.link('doc/t.tmp', 'lib/u.tmp')
* File.read('lib/u.tmp') # => "foo"
* File.write('lib/u.tmp', 'bar')
* File.read('doc/t.tmp') # => "bar"
* File.delete('doc/t.tmp')
* File.read('lib/u.tmp') # => "bar"
* File.delete('lib/u.tmp')
* ```
*
* File.link("testfile", ".testfile") #=> 0
* IO.readlines(".testfile")[0] #=> "This is line one\n"
* Raises an exception if the entry at `new_path` exists.
*/

static VALUE
Expand Down
27 changes: 24 additions & 3 deletions lib/bundler/installer/parallel_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,31 @@ def failed_specs
end

def install_with_worker
installed_specs = {}
enqueue_specs(installed_specs)
with_jobserver do
installed_specs = {}
enqueue_specs(installed_specs)

process_specs(installed_specs) until finished_installing?
process_specs(installed_specs) until finished_installing?
end
end

def with_jobserver
r, w = IO.pipe
r.close_on_exec = false
w.close_on_exec = false
w.write("*" * @size)

old_makeflags = ENV["MAKEFLAGS"]
ENV["MAKEFLAGS"] = [old_makeflags, "--jobserver-auth=#{r.fileno},#{w.fileno}"].compact.join(" ")

yield
ensure
# Restore MAKEFLAGS before closing the pipe so a close failure can't
# leave the process with descriptors that point at a closed pipe.
old_makeflags ? ENV["MAKEFLAGS"] = old_makeflags : ENV.delete("MAKEFLAGS")

r&.close
w&.close
end

def install_serially
Expand Down
35 changes: 34 additions & 1 deletion lib/bundler/rubygems_gem_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

module Bundler
class RubyGemsGemInstaller < Gem::Installer
# Cap how many jobserver slots a single gem's `make` may grab so that one
# gem with many recipes doesn't starve the others sharing the pool. Beyond
# a handful of jobs the extra parallelism rarely pays off in practice.
MAX_JOBS_PER_GEM = 3

def check_executable_overwrite(filename)
# Bundler needs to install gems regardless of binstub overwriting
end
Expand Down Expand Up @@ -124,10 +129,18 @@ def generate_bin_script(filename, bindir)
end

def build_jobs
Bundler.settings[:jobs] || super
@jobserver_read_io&.read_nonblock(MAX_JOBS_PER_GEM, @jobserver_tokens)
acquired_jobs = @jobserver_tokens.empty? ? nil : @jobserver_tokens.size

acquired_jobs || Bundler.settings[:jobs] || super
rescue IO::WaitReadable, EOFError
1
end

def build_extensions
@jobserver_tokens = +""
@jobserver_read_io, @jobserver_write_io = connect_to_jobserver

extension_cache_path = options[:bundler_extension_cache_path]
extension_dir = spec.extension_dir
unless extension_cache_path && extension_dir
Expand All @@ -151,6 +164,11 @@ def build_extensions
FileUtils.cp_r extension_dir, extension_cache_path
end
end
ensure
unless @jobserver_tokens.empty?
@jobserver_write_io.write(@jobserver_tokens)
@jobserver_write_io.flush
end
end

def spec
Expand All @@ -167,6 +185,21 @@ def gem_checksum

private

def connect_to_jobserver
return unless ENV["MAKEFLAGS"]
# We append our own --jobserver-auth, so read the last one. Otherwise a
# parent jobserver's descriptors (e.g. `bundle install` run under
# `make -j`) would be picked up instead of the pool ParallelInstaller created.
read_fd, write_fd = ENV["MAKEFLAGS"].scan(/--jobserver-auth=(\d+),(\d+)/).last

return unless read_fd && write_fd

# Pass explicit modes. On POSIX, IO.new detects the descriptor's access
# mode, but on Windows it can't, so the write end would default to read
# mode and raise "IOError: not opened for writing" when releasing slots.
[IO.new(read_fd.to_i, "r", autoclose: false), IO.new(write_fd.to_i, "w", autoclose: false)]
end

def prepare_extension_build(extension_dir)
SharedHelpers.filesystem_access(extension_dir, :create) do
FileUtils.mkdir_p extension_dir
Expand Down
3 changes: 2 additions & 1 deletion lib/rubygems/ext/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = [
# nmake doesn't support parallel build
unless is_nmake
have_make_arguments = make_program.size > 1
n_jobs ||= 0

if !have_make_arguments && !ENV["MAKEFLAGS"] && n_jobs
if !have_make_arguments && n_jobs > 1 && !ENV["MAKEFLAGS"]&.match(/-j\d*(\s|\Z)/)
make_program << "-j#{n_jobs}"
end
end
Expand Down
74 changes: 70 additions & 4 deletions pathname_builtin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1436,7 +1436,28 @@ def fnmatch?(pattern, ...) File.fnmatch?(pattern, @path, ...) end
# Returns <tt>'unknown'</tt> if the type cannot be determined.
def ftype() File.ftype(@path) end

# See <tt>File.link</tt>. Creates a hard link.
# :markup: markdown
#
# call-seq:
# make_link(path) -> 0
#
# Not available on some systems.
#
# Creates a new entry at the path in `self` for the existing entry at `path`
# using a [hard link](https://en.wikipedia.org/wiki/Hard_link):
#
# ```ruby
# File.write('doc/t.tmp', 'foo')
# Pathname('lib/u.tmp').make_link('doc/t.tmp')
# File.read('lib/u.tmp') # => "foo"
# File.write('lib/u.tmp', 'bar')
# File.read('doc/t.tmp') # => "bar"
# File.delete('doc/t.tmp')
# File.read('lib/u.tmp') # => "bar"
# File.delete('lib/u.tmp')
# ```
#
# Raises an exception if the entry at the path in `self` exists.
def make_link(old) File.link(old, @path) end

# See <tt>File.open</tt>. Opens the file for reading or writing.
Expand Down Expand Up @@ -1489,11 +1510,56 @@ def truncate(length) File.truncate(@path, length) end
# See <tt>File.utime</tt>. Update the access and modification times.
def utime(atime, mtime) File.utime(atime, mtime, @path) end

# Update the access and modification times of the file.
# :markup: markdown
#
# call-seq:
# lutime(atime, mtime) -> 1
#
# Like Pathname#utime, but does not follow symbolic links,
# and therefore changes the times of the entry in `self`,
# regardless of whether it is a symbolic link:
#
# ```ruby
# # Create a file and a link to it.
# file_path = 't.tmp'
# link_path = 'link'
# File.write(file_path, '')
# File.symlink(file_path, link_path)
# # Take snapshots of both.
# file_stat = File.stat(file_path)
# link_stat = File.lstat(link_path)
# # Fetch access times and modification times of both.
# file_stat.atime # => 2026-06-15 11:03:29.600373255 -0500
# file_stat.mtime # => 2026-06-15 11:03:22.247352211 -0500
# link_stat.atime # => 2026-06-15 11:03:29.251372254 -0500
# link_stat.mtime # => 2026-06-15 11:03:26.66436484 -0500
# # Update access time and modification time of the link.
# pn = Pathname(link_path)
# time = Time.now # => 2026-06-15 11:08:07.384287523 -0500
# pn.lutime(time, time)
# # Take fresh snapshots of both.
# file_stat = File.stat(file_path)
# link_stat = File.lstat(link_path)
# # Fetch access time and modification time of file (not changed).
# file_stat.atime # => 2026-06-15 11:03:29.600373255 -0500
# file_stat.mtime # => 2026-06-15 11:03:22.247352211 -0500
# # Fetch access time and modification time of link (changed).
# link_stat.atime # => 2026-06-15 11:08:29.847301399 -0500
# link_stat.mtime # => 2026-06-15 11:08:07.384287523 -0500
# # Clean up.
# File.delete(file_path)
# File.delete(link_path)
# ```
#
# Arguments `atime` and `mtime` may be Time objects (as above).
#
# Either or both may be integers;
# when an integer `i` is passed, `Time.new(i)` is used.
#
# Either or both may be `nil`, in which case `Time.now` is used.
#
# Same as Pathname#utime, but does not follow symbolic links.
# See {File System Timestamps}[rdoc-ref:file/timestamps.md].
#
# See File.lutime.
def lutime(atime, mtime) File.lutime(atime, mtime, @path) end

# call-seq:
Expand Down
Loading