diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 9a9c2be282a000..193c4a8eba9c30 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -196,6 +196,29 @@ jobs:
RUBY_TESTOPTS: -j${{ env.TEST_JOBS || 4 }}
timeout-minutes: 70
+ - run: nmake binary-package
+ if: ${{ matrix.test_task == 'check' }}
+ timeout-minutes: 10
+
+ - name: Verify binary package
+ shell: pwsh
+ if: ${{ matrix.test_task == 'check' }}
+ run: |
+ $zip = Get-Item ruby-*-mswin*.zip
+ $dest = Join-Path $env:RUNNER_TEMP "binpkg"
+ if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
+ New-Item -ItemType Directory $dest | Out-Null
+ & "$env:SystemRoot\System32\tar.exe" -x -f $zip.FullName -C $dest
+ if ((Get-ChildItem $dest).Count -ne 1) { throw "zip must contain a single root directory" }
+ $root = (Get-ChildItem $dest)[0].FullName
+ if (!(Test-Path "$root\bin\ruby.exe")) { throw "bin\ruby.exe not found" }
+ if (Get-ChildItem "$root\bin" -Filter "vcruntime140*.dll") { throw "the VC runtime must not be bundled" }
+ if (!(Get-ChildItem "$root\LICENSES" -ErrorAction SilentlyContinue)) { throw "LICENSES missing" }
+ $env:PATH = "$env:SystemRoot\System32"
+ & "$root\bin\ruby.exe" -v -ropenssl -rfiddle -rpsych -rzlib -e 'abort "configure_args has an absolute path: #{RbConfig::CONFIG[%q(configure_args)]}" if RbConfig::CONFIG[%q(configure_args)] =~ /\b[A-Za-z]:[\/\\]/; puts %q(binary package OK)'
+ if ($LASTEXITCODE -ne 0) { throw "smoke test failed" }
+ timeout-minutes: 5
+
- uses: ./.github/actions/slack
with:
label: Windows ${{ matrix.os }} / ${{ matrix.test_task || 'check' }}
diff --git a/bootstraptest/test_fiber.rb b/bootstraptest/test_fiber.rb
index ae809a59366779..d71fb5b9f0ed3d 100644
--- a/bootstraptest/test_fiber.rb
+++ b/bootstraptest/test_fiber.rb
@@ -42,3 +42,22 @@
assert_normal_exit %q{
Thread.new { Fiber.current.kill }.join
}
+
+# fiber_current() must not read a stale (cached) TLS execution context after a
+# fiber's coroutine migrates between native threads under the M:N scheduler.
+# Needs >= 2 Ractors so coroutines actually migrate; sleep drives the migration.
+assert_equal 'ok', %q{
+ Warning[:experimental] = false
+ 2.times.map do
+ Ractor.new do
+ 200.times do
+ f = Fiber.new { Fiber.yield; 1 }
+ f.resume
+ sleep(0.0003) # block -> M:N native-thread migration
+ f.resume rescue nil
+ end
+ :ok
+ end
+ end.each(&:value)
+ :ok
+}
diff --git a/cont.c b/cont.c
index 6bb61e5ee84be2..77ed342e2438f9 100644
--- a/cont.c
+++ b/cont.c
@@ -2187,7 +2187,9 @@ fiber_t_alloc(VALUE fiber_value, unsigned int blocking)
static inline rb_fiber_t*
fiber_current(void)
{
- rb_execution_context_t *ec = GET_EC();
+ /* Called right after a coroutine transfer: an inlined GET_EC() may read a
+ * TLS pointer cached before the NT migration, so force a fresh load. */
+ rb_execution_context_t *ec = rb_current_ec_noinline();
return ec->fiber_ptr;
}
diff --git a/pathname.c b/pathname.c
index b12f64037392da..13ff83ff764d7e 100644
--- a/pathname.c
+++ b/pathname.c
@@ -133,11 +133,45 @@ same_paths(VALUE self, VALUE a, VALUE b)
}
/*
- * Predicate method for root directories. Returns +true+ if the
- * pathname consists of consecutive slashes.
+ * :markup: markdown
+ *
+ * call-seq:
+ * root? -> true or false
+ *
+ * Returns whether the path in `self` points to a root directory.
+ *
+ * On a non-Windows system, a root directory path is one whose name begins
+ * with one or more slash characters (`'/'):
+ *
+ * ```ruby
+ * Pathname('/').root? # => true
+ * Pathname('////').root? # => true
+ * Pathname('/usr').root? # => false
+ * Pathname('foo').root? # => false
+ * ```
+ *
+ * Does not resolve dot directories:
+ *
+ * ```ruby
+ * Pathname('/usr/.').root? # => false
+ * Pathname('/usr/..').root? # => false
+ * ```
+ *
+ * On a Windows system, a root directory path is one whose name begins as above,
+ * or with a device letter followed by a colon character (`':'`)
+ * and one or more slash characters (`'/'):
+ *
+ * ```ruby
+ * Pathname('/').root? # => true
+ * Pathname('////').root? # => true
+ * Pathname('C:/').root? # => true
+ * Pathname('C:////').root? # => true
+ * Pathname('c:/').root? # => true
+ * Pathname('H:/').root? # => true
+ * Pathname('C:/m').root? # => false
+ * Pathname('C:').root? # => false
+ * ```
*
- * It doesn't access the filesystem. So it may return +false+ for some
- * pathnames which points to roots such as /usr/...
*/
static VALUE
path_root_p(VALUE self)
diff --git a/pathname_builtin.rb b/pathname_builtin.rb
index 17c924cf8e41db..f9b2b47ff3ff74 100644
--- a/pathname_builtin.rb
+++ b/pathname_builtin.rb
@@ -2132,13 +2132,73 @@ def readable_real?() FileTest.readable_real?(@path) end
# See FileTest.setuid?.
def setuid?() FileTest.setuid?(@path) end
- # See FileTest.setgid?.
+ # :markup: markdown
+ #
+ # call-seq:
+ # setgid? -> true or false
+ #
+ # Returns whether the [setgid bit](https://en.wikipedia.org/wiki/Setuid) is set
+ # in the permissions for the entry at the path in `self`:
+ #
+ # ```ruby
+ # # Create a file and get its permissions and setgid? setting.
+ # pn = Pathname('doc/t.tmp')
+ # pn.write('foo')
+ # mode = pn.stat.mode.to_s(8) # => "100664"
+ # pn.setgid? # => false
+ # # Set the bit.
+ # pn.chmod(0o2644)
+ # mode = pn.stat.mode.to_s(8) # => "102644"
+ # pn.setgid? # => true
+ # pn.delete # Clean up.
+ # ```
+ #
+ # On Windows, the bit is never set; the method always returns `false`.
def setgid?() FileTest.setgid?(@path) end
- # See FileTest.size.
+ # :markup: markdown
+ #
+ # call-seq:
+ # size -> integer
+ #
+ # Returns the size of the entry at the path in `self`:
+ #
+ # ```ruby
+ # Pathname('README.md').size # => 3469
+ # Pathname('doc').size # => 4096
+ # pn = Pathname('doc/t.tmp')
+ # pn.write('')
+ # pn.size # => 0
+ # pd.delete # Clean up.
+ # ```
+ #
+ # Raises an exception if the entry does not exist.
+ #
def size() FileTest.size(@path) end
- # See FileTest.size?.
+ # :markup: markdown
+ #
+ # call-seq:
+ # size? -> integer or nil
+ #
+ # If the file or directory entry at the path in `self` exists,
+ # returns its size if non-zero, or `nil` if zero:
+ #
+ # ```ruby
+ # pn = Pathname('doc/t.tmp')
+ # pn.write('foo')
+ # pn.size? # => 3
+ # pn.write('')
+ # pn.size? # => nil
+ # ```
+ #
+ # Returns `nil` if the entry does not exist:
+ #
+ # ```ruby
+ # pn.delete
+ # pn.size? # => nil
+ # ```
+ #
def size?() FileTest.size?(@path) end
# See FileTest.sticky?.
def sticky?() FileTest.sticky?(@path) end
@@ -2296,7 +2356,23 @@ def each_entry(&block) # :yield: pathname
# Argument `permissions` is ignored on Windows.
def mkdir(...) Dir.mkdir(@path, ...) end
- # See Dir.rmdir. Remove the referenced directory.
+ # :markup: markdown
+ #
+ # call-seq:
+ # rmdir -> 0
+ #
+ # Deletes the directory at the path in +self+; returns `0`:
+ #
+ # ```ruby
+ # pn = Pathname('doc/foo')
+ # pn.mkdir
+ # pn.rmdir
+ # ```
+ #
+ # Raises an exception if the directory is not empty,
+ # or if the path does not point to a directory.
+ #
+ # Use method #rmtree to delete the entire filetree at the path.
def rmdir() Dir.rmdir(@path) end
# :markup: markdown
diff --git a/struct.c b/struct.c
index 72a5fa7e3d22e9..606208e87268e5 100644
--- a/struct.c
+++ b/struct.c
@@ -829,7 +829,9 @@ struct_alloc(VALUE klass)
VALUE flags = T_STRUCT;
- if (n > 0 && rb_gc_size_allocatable_p(embedded_size)) {
+ const long embed_len_max = RSTRUCT_EMBED_LEN_MASK >> RSTRUCT_EMBED_LEN_SHIFT;
+
+ if (n > 0 && n <= embed_len_max && rb_gc_size_allocatable_p(embedded_size)) {
flags |= n << RSTRUCT_EMBED_LEN_SHIFT;
if (RCLASS_MAX_IV_COUNT(klass) == 0) {
// We set the flag before calling `NEWOBJ_OF` in case a NEWOBJ tracepoint does
diff --git a/thread_pthread.c b/thread_pthread.c
index 1dc68318495f20..e0190093287976 100644
--- a/thread_pthread.c
+++ b/thread_pthread.c
@@ -1664,10 +1664,16 @@ rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr)
ractor_sched_lock(vm, cr);
{
// running_cnt
- vm->ractor.sched.barrier_waiting_cnt++;
- RUBY_DEBUG_LOG("waiting_cnt:%u serial:%u", vm->ractor.sched.barrier_waiting_cnt, barrier_serial);
-
- ractor_sched_barrier_join_signal_locked(vm);
+ /* A not-yet-running thread (blocking/terminating, joined only to
+ * sync) must not be counted, or barrier_waiting_cnt > running_cnt-1. */
+ if (cr->threads.sched.is_running) {
+ vm->ractor.sched.barrier_waiting_cnt++;
+ RUBY_DEBUG_LOG("waiting_cnt:%u serial:%u", vm->ractor.sched.barrier_waiting_cnt, barrier_serial);
+ ractor_sched_barrier_join_signal_locked(vm);
+ }
+ else {
+ RUBY_DEBUG_LOG("join without counting (not running) serial:%u", barrier_serial);
+ }
ractor_sched_barrier_join_wait_locked(vm, cr->threads.sched.running);
}
ractor_sched_unlock(vm, cr);
diff --git a/tool/binary-package.rb b/tool/binary-package.rb
new file mode 100644
index 00000000000000..a86d12b71b1805
--- /dev/null
+++ b/tool/binary-package.rb
@@ -0,0 +1,89 @@
+#!/usr/bin/ruby
+# Assemble a relocatable binary package from a staged installation on
+# Windows (mswin). Invoked by the `binary-package` target in
+# win32/Makefile.sub; not intended to be run manually.
+
+require 'optparse'
+require 'fileutils'
+
+stage = name = srcdir = vcpkgdir = output = nil
+opt = OptionParser.new
+opt.on('--stage=DIR') {|v| stage = v}
+opt.on('--name=NAME') {|v| name = v}
+opt.on('--srcdir=DIR') {|v| srcdir = v}
+opt.on('--vcpkg-dir=DIR') {|v| vcpkgdir = v}
+opt.on('--output=FILE') {|v| output = v}
+opt.parse!(ARGV)
+abort(opt.to_s) unless stage and name and srcdir and vcpkgdir and output
+
+stage = File.expand_path(stage)
+output = File.expand_path(output)
+# Normalize so backslashes are not treated as glob escapes below.
+srcdir = srcdir.tr("\\", "/")
+vcpkgdir = vcpkgdir.tr("\\", "/")
+
+# Find the installed prefix inside the stage. DESTDIR staging
+# reproduces the prefix below the stage directory (with the drive
+# letter stripped), so descend while there is a single directory
+# until bin/ appears.
+root = stage
+until File.directory?(File.join(root, "bin"))
+ entries = Dir.children(root)
+ unless entries.size == 1 and File.directory?(dir = File.join(root, entries[0]))
+ abort "#{$0}: could not locate installed prefix under #{stage}"
+ end
+ root = dir
+end
+abort "#{$0}: prefix must not be the stage root" if root == stage
+
+bindir = File.join(root, "bin")
+
+# Bundle the vcpkg runtime DLLs next to ruby.exe, where they are
+# found first by the DLL search order.
+dlls = Dir.glob(File.join(vcpkgdir, "bin", "*.dll"))
+dlls.reject! {|f| File.basename(f, ".dll") == "readline"}
+abort "#{$0}: no DLLs in #{vcpkgdir}/bin; run `nmake install-vcpkg' first" if dlls.empty?
+dlls.each {|f| FileUtils.cp(f, bindir)}
+
+# The VC runtime (vcruntime140*.dll) is deliberately not bundled.
+# App-local copies are never serviced by Windows Update; the package
+# assumes the VC++ Redistributable is installed.
+# https://bugs.ruby-lang.org/issues/22180
+
+# Collect license terms of everything the package redistributes.
+licdir = File.join(root, "LICENSES")
+FileUtils.mkdir_p(File.join(licdir, "ruby"))
+%w[COPYING COPYING.ja BSDL LEGAL].each do |f|
+ src = File.join(srcdir, f)
+ FileUtils.cp(src, File.join(licdir, "ruby", f)) if File.exist?(src)
+end
+copyrights = Dir.glob(File.join(vcpkgdir, "share", "*", "copyright"))
+abort "#{$0}: no copyright files in #{vcpkgdir}/share" if copyrights.empty?
+copyrights.each do |f|
+ FileUtils.cp(f, File.join(licdir, "#{File.basename(File.dirname(f))}.txt"))
+end
+
+# Scrub build-machine specific paths from the recorded configure
+# arguments. mkmf applies $configure_args (notably --with-opt-dir) to
+# every extension build, so a leftover absolute path breaks or taints
+# gem compilation on the destination machine.
+rbconfigs = Dir.glob(File.join(root, "lib/ruby/*/*/rbconfig.rb"))
+abort "#{$0}: rbconfig.rb not found under #{root}" if rbconfigs.empty?
+rbconfigs.each do |file|
+ src = File.binread(file)
+ src.sub!(/^\s*CONFIG\["configure_args"\]\s*=\s*"\K.*(?=")/) do |args|
+ args.scan(/\\".*?\\"|\S+/).grep_v(%r{\b[A-Za-z]:[/\\]}).join(" ")
+ end or abort "#{$0}: configure_args not found in #{file}"
+ File.binwrite(file, src)
+end
+
+# Rename the prefix directory to the package name and archive it with
+# bsdtar, which ships with Windows 10 and later. Prefer the inbox
+# tar.exe; a GNU tar earlier in PATH cannot write zip with -a.
+pkgdir = File.join(stage, name)
+File.rename(root, pkgdir) unless root == pkgdir
+FileUtils.rm_f(output)
+tar = File.join(ENV["SystemRoot"] || "C:/Windows", "System32", "tar.exe")
+tar = "tar" unless File.exist?(tar)
+system(tar, "-a", "-c", "-f", output, "-C", stage, name, exception: true)
+puts "packaged #{output}"
diff --git a/win32/Makefile.sub b/win32/Makefile.sub
index cd0ba933dafc3b..619faf6663cae4 100644
--- a/win32/Makefile.sub
+++ b/win32/Makefile.sub
@@ -614,6 +614,20 @@ prepare-vcpkg::
if not %%~nI == readline mklink %%~nxI %%I \
)
+!ifndef VCPKG_INSTALLED
+VCPKG_INSTALLED = $(srcdir)/vcpkg_installed/$(VCPKG_DEFAULT_TRIPLET)
+!endif
+BINARY_PACKAGE_NAME = $(RUBY_BASE_NAME)-$(RUBY_PROGRAM_VERSION)-$(arch)
+BINARY_PACKAGE_STAGE = binary-package-stage
+
+binary-package:
+ @if exist $(BINARY_PACKAGE_STAGE)\. rd /s /q $(BINARY_PACKAGE_STAGE)
+ @$(MAKE) DESTDIR="$(MAKEDIR)\$(BINARY_PACKAGE_STAGE)" install-nodoc
+ $(RUNRUBY) "$(tooldir)/binary-package.rb" --stage=$(BINARY_PACKAGE_STAGE) \
+ --name=$(BINARY_PACKAGE_NAME) --srcdir="$(srcdir)" \
+ --vcpkg-dir="$(VCPKG_INSTALLED)" \
+ --output=$(BINARY_PACKAGE_NAME).zip
+
.PHONY: reconfig
reconfig $(MKFILES): $(srcdir)/common.mk $(srcdir)/version.h \
$(hdrdir)/ruby/version.h $(ABI_VERSION_HDR) \