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) \