diff --git a/.github/workflows/compilers.yml b/.github/workflows/compilers.yml index 7877ff7cee3769..2ed14d5b0acde5 100644 --- a/.github/workflows/compilers.yml +++ b/.github/workflows/compilers.yml @@ -114,12 +114,12 @@ jobs: with_gcc: 'clang-23 -flto=auto' optflags: '-O2' timeout-minutes: 30 - if: ${{ matrix.shard == '18' }} - - { uses: *compilers, name: 'clang 23', with: { tag: 'clang-23' }, timeout-minutes: 8, if: "${{ matrix.shard == '18' }}" } - - { uses: *compilers, name: 'clang 22', with: { tag: 'clang-22' }, timeout-minutes: 8, if: "${{ matrix.shard == '22' }}" } - - { uses: *compilers, name: 'clang 21', with: { tag: 'clang-21' }, timeout-minutes: 8, if: "${{ matrix.shard == '22' }}" } - - { uses: *compilers, name: '-O0', with: { optflags: '-O0 -march=x86-64 -mtune=generic' }, timeout-minutes: 8, if: "${{ matrix.shard == '22' }}" } - - { uses: *compilers, name: 'YJIT_FORCE_ENABLE', with: { cppflags: '-DYJIT_FORCE_ENABLE' }, timeout-minutes: 8, if: "${{ matrix.shard == '22' }}" } + if: ${{ matrix.shard == '20' }} + - { uses: *compilers, name: 'clang 23', with: { tag: 'clang-23' }, timeout-minutes: 8, if: "${{ matrix.shard == '20' }}" } + - { uses: *compilers, name: 'clang 22', with: { tag: 'clang-22' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } + - { uses: *compilers, name: 'clang 21', with: { tag: 'clang-21' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } + - { uses: *compilers, name: '-O0', with: { optflags: '-O0 -march=x86-64 -mtune=generic' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } + - { uses: *compilers, name: 'YJIT_FORCE_ENABLE', with: { cppflags: '-DYJIT_FORCE_ENABLE' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } - name: 'GCC 15 LTO' uses: *compilers @@ -129,98 +129,98 @@ jobs: optflags: '-O2' enable_shared: false timeout-minutes: 10 - if: ${{ matrix.shard == '23' }} - - { uses: *compilers, name: 'GCC 15', with: { tag: 'gcc-15' }, timeout-minutes: 8, if: "${{ matrix.shard == '23' }}" } - - { uses: *compilers, name: 'GCC 14', with: { tag: 'gcc-14' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } - - { uses: *compilers, name: 'GCC 13', with: { tag: 'gcc-13' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } - - { uses: *compilers, name: 'GCC 12', with: { tag: 'gcc-12' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } - - { uses: *compilers, name: 'UNIVERSAL_PARSER', with: { cppflags: '-DUNIVERSAL_PARSER' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } + if: ${{ matrix.shard == '21' }} + - { uses: *compilers, name: 'GCC 15', with: { tag: 'gcc-15' }, timeout-minutes: 8, if: "${{ matrix.shard == '21' }}" } + - { uses: *compilers, name: 'GCC 14', with: { tag: 'gcc-14' }, timeout-minutes: 8, if: "${{ matrix.shard == '13' }}" } + - { uses: *compilers, name: 'GCC 13', with: { tag: 'gcc-13' }, timeout-minutes: 8, if: "${{ matrix.shard == '13' }}" } + - { uses: *compilers, name: 'GCC 12', with: { tag: 'gcc-12' }, timeout-minutes: 8, if: "${{ matrix.shard == '13' }}" } + - { uses: *compilers, name: 'UNIVERSAL_PARSER', with: { cppflags: '-DUNIVERSAL_PARSER' }, timeout-minutes: 8, if: "${{ matrix.shard == '13' }}" } - - { uses: *compilers, name: 'clang 20', with: { tag: 'clang-20' }, timeout-minutes: 8, if: "${{ matrix.shard == '13' }}" } - - { uses: *compilers, name: 'clang 19', with: { tag: 'clang-19' }, timeout-minutes: 8, if: "${{ matrix.shard == '13' }}" } - - { uses: *compilers, name: 'clang 18', with: { tag: 'clang-18' }, timeout-minutes: 8, if: "${{ matrix.shard == '13' }}" } - - { uses: *compilers, name: 'clang 17', with: { tag: 'clang-17' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } - - { uses: *compilers, name: 'clang 16', with: { tag: 'clang-16' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } - - { uses: *compilers, name: 'clang 15', with: { tag: 'clang-15' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } - - { uses: *compilers, name: 'clang 14', with: { tag: 'clang-14' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } + - { uses: *compilers, name: 'clang 20', with: { tag: 'clang-20' }, timeout-minutes: 8, if: "${{ matrix.shard == '14' }}" } + - { uses: *compilers, name: 'clang 19', with: { tag: 'clang-19' }, timeout-minutes: 8, if: "${{ matrix.shard == '14' }}" } + - { uses: *compilers, name: 'clang 18', with: { tag: 'clang-18' }, timeout-minutes: 8, if: "${{ matrix.shard == '14' }}" } + - { uses: *compilers, name: 'clang 17', with: { tag: 'clang-17' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } + - { uses: *compilers, name: 'clang 16', with: { tag: 'clang-16' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } + - { uses: *compilers, name: 'clang 15', with: { tag: 'clang-15' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } + - { uses: *compilers, name: 'clang 14', with: { tag: 'clang-14' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } - - { uses: *compilers, name: 'clang 13', with: { tag: 'clang-13' }, timeout-minutes: 8, if: "${{ matrix.shard == '19' }}" } - - { uses: *compilers, name: 'clang 12', with: { tag: 'clang-12' }, timeout-minutes: 8, if: "${{ matrix.shard == '19' }}" } - - { uses: *compilers, name: 'clang 11', with: { tag: 'clang-11' }, timeout-minutes: 8, if: "${{ matrix.shard == '19' }}" } - - { uses: *compilers, name: 'clang 10', with: { tag: 'clang-10' }, timeout-minutes: 8, if: "${{ matrix.shard == '19' }}" } + - { uses: *compilers, name: 'clang 13', with: { tag: 'clang-13' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } + - { uses: *compilers, name: 'clang 12', with: { tag: 'clang-12' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } + - { uses: *compilers, name: 'clang 11', with: { tag: 'clang-11' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } + - { uses: *compilers, name: 'clang 10', with: { tag: 'clang-10' }, timeout-minutes: 8, if: "${{ matrix.shard == '07' }}" } # llvm-objcopy<=9 doesn't have --wildcard. It compiles, but leaves Rust symbols in libyjit.o and fail `make test-leaked-globals`. - - { uses: *compilers, name: 'clang 9', with: { tag: 'clang-9', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '24' }}" } - - { uses: *compilers, name: 'clang 8', with: { tag: 'clang-8', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '24' }}" } - - { uses: *compilers, name: 'clang 7', with: { tag: 'clang-7', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '24' }}" } + - { uses: *compilers, name: 'clang 9', with: { tag: 'clang-9', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '23' }}" } + - { uses: *compilers, name: 'clang 8', with: { tag: 'clang-8', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '23' }}" } + - { uses: *compilers, name: 'clang 7', with: { tag: 'clang-7', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '23' }}" } # -Wno-strict-prototypes is necessary with clang-15 or newer, since # older autoconf prior to 2.72 generate functions without prototype. - - { uses: *compilers, name: 'C99', with: { CFLAGS: '-std=c99 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } - - { uses: *compilers, name: 'C11', with: { CFLAGS: '-std=c11 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } - - { uses: *compilers, name: 'C17', with: { CFLAGS: '-std=c17 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } - - { uses: *compilers, name: 'C23', with: { CFLAGS: '-std=c2x -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } - - { uses: *compilers, name: 'C++98', with: { CXXFLAGS: '-std=c++98 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '20' }}" } - - { uses: *compilers, name: 'C++11', with: { CXXFLAGS: '-std=c++11 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '20' }}" } - - { uses: *compilers, name: 'C++14', with: { CXXFLAGS: '-std=c++14 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '20' }}" } + - { uses: *compilers, name: 'C99', with: { CFLAGS: '-std=c99 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } + - { uses: *compilers, name: 'C11', with: { CFLAGS: '-std=c11 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } + - { uses: *compilers, name: 'C17', with: { CFLAGS: '-std=c17 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } + - { uses: *compilers, name: 'C23', with: { CFLAGS: '-std=c2x -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } + - { uses: *compilers, name: 'C++98', with: { CXXFLAGS: '-std=c++98 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '22' }}" } + - { uses: *compilers, name: 'C++11', with: { CXXFLAGS: '-std=c++11 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '22' }}" } + - { uses: *compilers, name: 'C++14', with: { CXXFLAGS: '-std=c++14 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '22' }}" } - - { uses: *compilers, name: 'C++20', with: { CXXFLAGS: '-std=c++20 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } - - { uses: *compilers, name: 'C++23', with: { CXXFLAGS: '-std=c++23 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } - - { uses: *compilers, name: 'C++26', with: { CXXFLAGS: '-std=c++26 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } - - { uses: *compilers, name: 'gmp', with: { append_configure: '--with-gmp', test_all: 'ruby/test_bignum.rb', test_spec: '/github/workspace/src/spec/ruby/core/integer' }, timeout-minutes: 8, if: "${{ matrix.shard == '10' }}" } - - { uses: *compilers, name: 'jemalloc', with: { append_configure: '--with-jemalloc' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } - - { uses: *compilers, name: 'valgrind', with: { append_configure: '--with-valgrind' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } - - { uses: *compilers, name: 'coroutine=ucontext', with: { append_configure: '--with-coroutine=ucontext' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } - - { uses: *compilers, name: 'coroutine=pthread', with: { append_configure: '--with-coroutine=pthread' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } + - { uses: *compilers, name: 'C++20', with: { CXXFLAGS: '-std=c++20 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } + - { uses: *compilers, name: 'C++23', with: { CXXFLAGS: '-std=c++23 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } + - { uses: *compilers, name: 'C++26', with: { CXXFLAGS: '-std=c++26 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } + - { uses: *compilers, name: 'gmp', with: { append_configure: '--with-gmp', test_all: 'ruby/test_bignum.rb', test_spec: '/github/workspace/src/spec/ruby/core/integer' }, timeout-minutes: 8, if: "${{ matrix.shard == '08' }}" } + - { uses: *compilers, name: 'jemalloc', with: { append_configure: '--with-jemalloc' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } + - { uses: *compilers, name: 'valgrind', with: { append_configure: '--with-valgrind' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } + - { uses: *compilers, name: 'coroutine=ucontext', with: { append_configure: '--with-coroutine=ucontext' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } + - { uses: *compilers, name: 'coroutine=pthread', with: { append_configure: '--with-coroutine=pthread' }, timeout-minutes: 8, if: "${{ matrix.shard == '12' }}" } - - { uses: *compilers, name: 'disable-jit', with: { append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } - - { uses: *compilers, name: 'disable-yjit', with: { append_configure: '--disable-yjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } - - { uses: *compilers, name: 'disable-zjit', with: { append_configure: '--disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } - - { uses: *compilers, name: 'disable-dln', with: { append_configure: '--disable-dln' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } - - { uses: *compilers, name: 'disable-rubygems', with: { append_configure: '--disable-rubygems' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } - - { uses: *compilers, name: 'RUBY_DEVEL', with: { append_configure: '--enable-devel' }, timeout-minutes: 8, if: "${{ matrix.shard == '14' }}" } - - { uses: *compilers, name: 'OPT_THREADED_CODE=0', with: { cppflags: '-DOPT_THREADED_CODE=0' }, timeout-minutes: 8, if: "${{ matrix.shard == '14' }}" } - - { uses: *compilers, name: 'OPT_THREADED_CODE=1', with: { cppflags: '-DOPT_THREADED_CODE=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '14' }}" } - - { uses: *compilers, name: 'OPT_THREADED_CODE=2', with: { cppflags: '-DOPT_THREADED_CODE=2' }, timeout-minutes: 8, if: "${{ matrix.shard == '14' }}" } + - { uses: *compilers, name: 'disable-jit', with: { append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } + - { uses: *compilers, name: 'disable-yjit', with: { append_configure: '--disable-yjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } + - { uses: *compilers, name: 'disable-zjit', with: { append_configure: '--disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } + - { uses: *compilers, name: 'disable-dln', with: { append_configure: '--disable-dln' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } + - { uses: *compilers, name: 'disable-rubygems', with: { append_configure: '--disable-rubygems' }, timeout-minutes: 8, if: "${{ matrix.shard == '03' }}" } + - { uses: *compilers, name: 'RUBY_DEVEL', with: { append_configure: '--enable-devel' }, timeout-minutes: 8, if: "${{ matrix.shard == '06' }}" } + - { uses: *compilers, name: 'OPT_THREADED_CODE=0', with: { cppflags: '-DOPT_THREADED_CODE=0' }, timeout-minutes: 8, if: "${{ matrix.shard == '06' }}" } + - { uses: *compilers, name: 'OPT_THREADED_CODE=1', with: { cppflags: '-DOPT_THREADED_CODE=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '06' }}" } + - { uses: *compilers, name: 'OPT_THREADED_CODE=2', with: { cppflags: '-DOPT_THREADED_CODE=2' }, timeout-minutes: 8, if: "${{ matrix.shard == '06' }}" } - - { uses: *compilers, name: 'NDEBUG', with: { cppflags: '-DNDEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '15' }}" } - - { uses: *compilers, name: 'RUBY_DEBUG', with: { cppflags: '-DRUBY_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '15' }}" } - - { uses: *compilers, name: 'CCAN_LIST_DEBUG', with: { cppflags: '-DCCAN_LIST_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '15' }}" } - - { uses: *compilers, name: 'CPDEBUG=-1', with: { cppflags: '-DCPDEBUG=-1' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } - - { uses: *compilers, name: 'ENC_DEBUG', with: { cppflags: '-DENC_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } - - { uses: *compilers, name: 'VM_DEBUG_BP_CHECK', with: { cppflags: '-DVM_DEBUG_BP_CHECK' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } - - { uses: *compilers, name: 'VM_DEBUG_VERIFY_METHOD_CACHE', with: { cppflags: '-DVM_DEBUG_VERIFY_METHOD_CACHE' }, timeout-minutes: 8, if: "${{ matrix.shard == '04' }}" } + - { uses: *compilers, name: 'NDEBUG', with: { cppflags: '-DNDEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '18' }}" } + - { uses: *compilers, name: 'RUBY_DEBUG', with: { cppflags: '-DRUBY_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '18' }}" } + - { uses: *compilers, name: 'CCAN_LIST_DEBUG', with: { cppflags: '-DCCAN_LIST_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '18' }}" } + - { uses: *compilers, name: 'CPDEBUG=-1', with: { cppflags: '-DCPDEBUG=-1' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } + - { uses: *compilers, name: 'ENC_DEBUG', with: { cppflags: '-DENC_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } + - { uses: *compilers, name: 'VM_DEBUG_BP_CHECK', with: { cppflags: '-DVM_DEBUG_BP_CHECK' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } + - { uses: *compilers, name: 'VM_DEBUG_VERIFY_METHOD_CACHE', with: { cppflags: '-DVM_DEBUG_VERIFY_METHOD_CACHE' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } - - { uses: *compilers, name: 'ID_TABLE_DEBUG', with: { cppflags: '-DID_TABLE_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '17' }}" } - - { uses: *compilers, name: 'SYMBOL_DEBUG', with: { cppflags: '-DSYMBOL_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '17' }}" } - - { uses: *compilers, name: 'RGENGC_CHECK_MODE', with: { cppflags: '-DRGENGC_CHECK_MODE' }, timeout-minutes: 8, if: "${{ matrix.shard == '06' }}" } - - { uses: *compilers, name: 'USE_EMBED_CI=0', with: { cppflags: '-DUSE_EMBED_CI=0' }, timeout-minutes: 8, if: "${{ matrix.shard == '06' }}" } - - { uses: *compilers, name: 'USE_FLONUM=0', with: { cppflags: '-DUSE_FLONUM=0', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '06' }}" } + - { uses: *compilers, name: 'ID_TABLE_DEBUG', with: { cppflags: '-DID_TABLE_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '24' }}" } + - { uses: *compilers, name: 'SYMBOL_DEBUG', with: { cppflags: '-DSYMBOL_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '24' }}" } + - { uses: *compilers, name: 'RGENGC_CHECK_MODE', with: { cppflags: '-DRGENGC_CHECK_MODE' }, timeout-minutes: 8, if: "${{ matrix.shard == '19' }}" } + - { uses: *compilers, name: 'USE_EMBED_CI=0', with: { cppflags: '-DUSE_EMBED_CI=0' }, timeout-minutes: 8, if: "${{ matrix.shard == '19' }}" } + - { uses: *compilers, name: 'USE_FLONUM=0', with: { cppflags: '-DUSE_FLONUM=0', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '19' }}" } - - { uses: *compilers, name: 'USE_LAZY_LOAD', with: { cppflags: '-DUSE_LAZY_LOAD' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } - - { uses: *compilers, name: 'USE_RUBY_DEBUG_LOG=1', with: { cppflags: '-DUSE_RUBY_DEBUG_LOG=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } - - { uses: *compilers, name: 'USE_DEBUG_COUNTER', with: { cppflags: '-DUSE_DEBUG_COUNTER=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } - - { uses: *compilers, name: 'SHARABLE_MIDDLE_SUBSTRING', with: { cppflags: '-DSHARABLE_MIDDLE_SUBSTRING=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '05' }}" } - - { uses: *compilers, name: 'DEBUG_FIND_TIME_NUMGUESS', with: { cppflags: '-DDEBUG_FIND_TIME_NUMGUESS' }, timeout-minutes: 8, if: "${{ matrix.shard == '16' }}" } - - { uses: *compilers, name: 'DEBUG_INTEGER_PACK', with: { cppflags: '-DDEBUG_INTEGER_PACK' }, timeout-minutes: 8, if: "${{ matrix.shard == '16' }}" } - - { uses: *compilers, name: 'RGENGC_PROFILE', with: { cppflags: '-DRGENGC_PROFILE' }, timeout-minutes: 8, if: "${{ matrix.shard == '16' }}" } + - { uses: *compilers, name: 'USE_LAZY_LOAD', with: { cppflags: '-DUSE_LAZY_LOAD' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } + - { uses: *compilers, name: 'USE_RUBY_DEBUG_LOG=1', with: { cppflags: '-DUSE_RUBY_DEBUG_LOG=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } + - { uses: *compilers, name: 'USE_DEBUG_COUNTER', with: { cppflags: '-DUSE_DEBUG_COUNTER=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } + - { uses: *compilers, name: 'SHARABLE_MIDDLE_SUBSTRING', with: { cppflags: '-DSHARABLE_MIDDLE_SUBSTRING=1' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } + - { uses: *compilers, name: 'DEBUG_FIND_TIME_NUMGUESS', with: { cppflags: '-DDEBUG_FIND_TIME_NUMGUESS' }, timeout-minutes: 8, if: "${{ matrix.shard == '15' }}" } + - { uses: *compilers, name: 'DEBUG_INTEGER_PACK', with: { cppflags: '-DDEBUG_INTEGER_PACK' }, timeout-minutes: 8, if: "${{ matrix.shard == '15' }}" } + - { uses: *compilers, name: 'RGENGC_PROFILE', with: { cppflags: '-DRGENGC_PROFILE' }, timeout-minutes: 8, if: "${{ matrix.shard == '15' }}" } - - { uses: *compilers, name: 'GC_DEBUG', with: { cppflags: '-DGC_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '21' }}" } - - { uses: *compilers, name: 'GC_ENABLE_LAZY_SWEEP=0', with: { cppflags: '-DGC_ENABLE_LAZY_SWEEP=0' }, timeout-minutes: 8, if: "${{ matrix.shard == '21' }}" } - - { uses: *compilers, name: 'GC_PROFILE_DETAIL_MEMORY', with: { cppflags: '-DGC_PROFILE_DETAIL_MEMORY' }, timeout-minutes: 8, if: "${{ matrix.shard == '21' }}" } - - { uses: *compilers, name: 'GC_PROFILE_MORE_DETAIL', with: { cppflags: '-DGC_PROFILE_MORE_DETAIL' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } - - { uses: *compilers, name: 'MALLOC_ALLOCATED_SIZE_CHECK', with: { cppflags: '-DMALLOC_ALLOCATED_SIZE_CHECK' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } - - { uses: *compilers, name: 'RGENGC_ESTIMATE_OLDMALLOC', with: { cppflags: '-DRGENGC_ESTIMATE_OLDMALLOC' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } - - { uses: *compilers, name: 'C++17', with: { CXXFLAGS: '-std=c++17 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '09' }}" } + - { uses: *compilers, name: 'GC_DEBUG', with: { cppflags: '-DGC_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '17' }}" } + - { uses: *compilers, name: 'GC_ENABLE_LAZY_SWEEP=0', with: { cppflags: '-DGC_ENABLE_LAZY_SWEEP=0' }, timeout-minutes: 8, if: "${{ matrix.shard == '17' }}" } + - { uses: *compilers, name: 'GC_PROFILE_DETAIL_MEMORY', with: { cppflags: '-DGC_PROFILE_DETAIL_MEMORY' }, timeout-minutes: 8, if: "${{ matrix.shard == '17' }}" } + - { uses: *compilers, name: 'GC_PROFILE_MORE_DETAIL', with: { cppflags: '-DGC_PROFILE_MORE_DETAIL' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } + - { uses: *compilers, name: 'MALLOC_ALLOCATED_SIZE_CHECK', with: { cppflags: '-DMALLOC_ALLOCATED_SIZE_CHECK' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } + - { uses: *compilers, name: 'RGENGC_ESTIMATE_OLDMALLOC', with: { cppflags: '-DRGENGC_ESTIMATE_OLDMALLOC' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } + - { uses: *compilers, name: 'C++17', with: { CXXFLAGS: '-std=c++17 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } - - { uses: *compilers, name: 'GCC 11', with: { tag: 'gcc-11' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } - - { uses: *compilers, name: 'GCC 10', with: { tag: 'gcc-10' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } - - { uses: *compilers, name: 'GCC 9', with: { tag: 'gcc-9' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } - - { uses: *compilers, name: 'GCC 8', with: { tag: 'gcc-8' }, timeout-minutes: 8, if: "${{ matrix.shard == '11' }}" } - - { uses: *compilers, name: 'GCC 7', with: { tag: 'gcc-7' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } - - { uses: *compilers, name: 'ext/Setup', with: { static_exts: 'etc json/* */escape' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } - - { uses: *compilers, name: 'enable-mkmf-verbose', with: { append_configure: '--enable-mkmf-verbose' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } - - { uses: *compilers, name: 'HASH_DEBUG', with: { cppflags: '-DHASH_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } - - { uses: *compilers, name: 'clang 6', with: { tag: 'clang-6.0', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '01' }}" } + - { uses: *compilers, name: 'GCC 11', with: { tag: 'gcc-11' }, timeout-minutes: 8, if: "${{ matrix.shard == '16' }}" } + - { uses: *compilers, name: 'GCC 10', with: { tag: 'gcc-10' }, timeout-minutes: 8, if: "${{ matrix.shard == '16' }}" } + - { uses: *compilers, name: 'GCC 9', with: { tag: 'gcc-9' }, timeout-minutes: 8, if: "${{ matrix.shard == '16' }}" } + - { uses: *compilers, name: 'GCC 8', with: { tag: 'gcc-8' }, timeout-minutes: 8, if: "${{ matrix.shard == '16' }}" } + - { uses: *compilers, name: 'GCC 7', with: { tag: 'gcc-7' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } + - { uses: *compilers, name: 'ext/Setup', with: { static_exts: 'etc json/* */escape' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } + - { uses: *compilers, name: 'enable-mkmf-verbose', with: { append_configure: '--enable-mkmf-verbose' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } + - { uses: *compilers, name: 'HASH_DEBUG', with: { cppflags: '-DHASH_DEBUG' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } + - { uses: *compilers, name: 'clang 6', with: { tag: 'clang-6.0', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 8, if: "${{ matrix.shard == '02' }}" } compilemax: diff --git a/compile.c b/compile.c index 13509af55af39f..52f64ce0ae1e93 100644 --- a/compile.c +++ b/compile.c @@ -5406,7 +5406,7 @@ compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int meth * - It contains key-value pairs. So we need to take every two elements. * We can assume that the length is always even. * - * - Merging is done by a method call (id_core_hash_merge_ptr). + * - Merging is done by a method call (id_core_hash_merge_bang_ptr). * Sometimes we need to insert the receiver, so "anchor" is needed. * In addition, a method call is much slower than concatarray. * So it pays only when the subsequence is really long. @@ -5436,7 +5436,7 @@ compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int meth ADD_INSN1(ret, line_node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); \ ADD_INSN(ret, line_node, swap); \ APPEND_LIST(ret, anchor); \ - ADD_SEND(ret, line_node, id_core_hash_merge_ptr, INT2FIX(stack_len + 1)); \ + ADD_SEND(ret, line_node, id_core_hash_merge_bang_ptr, INT2FIX(stack_len + 1)); \ } \ INIT_ANCHOR(anchor); \ first_chunk = stack_len = 0; \ @@ -5482,7 +5482,7 @@ compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int meth ADD_INSN1(ret, line_node, putobject, hash); - ADD_SEND(ret, line_node, id_core_hash_merge_kwd, INT2FIX(2)); + ADD_SEND(ret, line_node, id_core_hash_merge_bang_kwd, INT2FIX(2)); } RB_OBJ_WRITTEN(iseq, Qundef, hash); } @@ -5556,7 +5556,7 @@ compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int meth NO_CHECK(COMPILE(ret, "keyword splat", kw)); - ADD_SEND(ret, line_node, id_core_hash_merge_kwd, INT2FIX(2)); + ADD_SEND(ret, line_node, id_core_hash_merge_bang_kwd, INT2FIX(2)); } } @@ -10217,7 +10217,7 @@ compile_super(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i ADD_INSN1(args, node, putobject, ID2SYM(id)); ADD_GETLOCAL(args, node, idx, lvar_level); } - ADD_SEND(args, node, id_core_hash_merge_ptr, INT2FIX(i * 2 + 1)); + ADD_SEND(args, node, id_core_hash_merge_bang_ptr, INT2FIX(i * 2 + 1)); flag |= VM_CALL_KW_SPLAT| VM_CALL_KW_SPLAT_MUT; } else if (local_body->param.flags.has_kwrest) { diff --git a/defs/id.def b/defs/id.def index 7848c797dcdf0b..26957a793d55c6 100644 --- a/defs/id.def +++ b/defs/id.def @@ -98,7 +98,10 @@ firstline, predefined = __LINE__+1, %[\ core#define_singleton_method core#set_postexe core#hash_merge_ptr + core#hash_merge_bang_ptr core#hash_merge_kwd + core#hash_merge_bang_kwd + core#hash_coerce core#raise core#sprintf diff --git a/gc/default/default.c b/gc/default/default.c index a89ef3b9e6c111..368b1a00c8982b 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -9518,9 +9518,6 @@ rb_gc_impl_prepare_heap(void *objspace_ptr) { rb_objspace_t *objspace = objspace_ptr; - size_t orig_total_slots = objspace_available_slots(objspace); - size_t orig_allocatable_bytes = objspace->heap_pages.allocatable_bytes; - rb_gc_impl_each_objects(objspace, gc_set_candidate_object_i, objspace_ptr); double orig_max_free_slots = gc_params.heap_free_slots_max_ratio; @@ -9534,11 +9531,15 @@ rb_gc_impl_prepare_heap(void *objspace_ptr) heap_pages_free_unused_pages(objspace_ptr); GC_ASSERT(heap_pages_freeable_pages == 0); GC_ASSERT(objspace->empty_pages_count == 0); - objspace->heap_pages.allocatable_bytes = orig_allocatable_bytes; - size_t total_slots = objspace_available_slots(objspace); - if (orig_total_slots > total_slots) { - objspace->heap_pages.allocatable_bytes += (orig_total_slots - total_slots) * heaps[0].slot_size; + // Process.warmup is meant to be called at the end of the boot sequence, which is commonly allocation + // heavy and result in GC limits raising significantly, but it's not indicative of the limits needed + // for runtime. + // Recompute the allocatable_bytes limit based on `gc_params.heap_init_bytes`. + GC_ASSERT(objspace->heap_pages.allocatable_bytes == 0); + for (int i = 0; i < HEAP_COUNT; i++) { + rb_heap_t *heap = &heaps[i]; + heap_allocatable_bytes_expand(objspace, heap, heap->empty_slots, heap->total_slots, heap->slot_size); } #if defined(HAVE_MALLOC_TRIM) && !defined(RUBY_ALTERNATIVE_MALLOC_HEADER) diff --git a/hash.c b/hash.c index 417e7a5023e127..eede4d192108be 100644 --- a/hash.c +++ b/hash.c @@ -1565,10 +1565,19 @@ copy_compare_by_id(VALUE hash, VALUE basis) return hash; } +static VALUE +hash_new_capa(VALUE klass, size_t capa) +{ + return hash_init_capa(hash_alloc_capa(klass, 0, Qnil, capa, false), capa); +} + VALUE rb_hash_new_capa(long capa) { - return hash_init_capa(hash_alloc_capa(rb_cHash, 0, Qnil, capa, false), capa); + if (capa < 0) { + rb_raise(rb_eArgError, "negative hash size (or size too big)"); + } + return hash_new_capa(rb_cHash, capa); } VALUE @@ -1641,6 +1650,17 @@ hash_dup(VALUE hash, VALUE klass, VALUE flags) return hash_copy(dup, hash); } +static VALUE +hash_dup_capa(VALUE hash, size_t capa) +{ + VALUE ret = hash_alloc_capa(rb_cHash, 0, Qnil, capa, false); + if (capa > RHASH_AR_TABLE_MAX_SIZE) { + RHASH_SET_ST_FLAG(ret); + } + hash_copy(ret, hash); + return ret; +} + VALUE rb_hash_dup(VALUE hash) { @@ -1855,6 +1875,7 @@ rb_hash_init(rb_execution_context_t *ec, VALUE hash, VALUE capa_value, VALUE ifn } static VALUE rb_hash_to_a(VALUE hash); +static VALUE hash_new_with_bulk_insert(VALUE klass, long argc, const VALUE *argv); /* * call-seq: @@ -1901,17 +1922,19 @@ rb_hash_s_create(int argc, VALUE *argv, VALUE klass) if (argc == 1) { tmp = rb_hash_s_try_convert(Qnil, argv[0]); if (!NIL_P(tmp)) { - if (!RHASH_EMPTY_P(tmp) && rb_hash_compare_by_id_p(tmp)) { + if (RHASH_EMPTY_P(tmp)) { + return hash_new_capa(klass, 0); + } + + if (rb_hash_compare_by_id_p(tmp)) { /* hash_copy for non-empty hash will copy compare_by_identity flag, but we don't want it copied. Work around by converting hash to flattened array and using that. */ tmp = rb_hash_to_a(tmp); } else { - hash = hash_alloc(klass); - if (!RHASH_EMPTY_P(tmp)) - hash_copy(hash, tmp); - return hash; + hash = hash_alloc_capa(klass, 0, Qnil, RHASH_SIZE(tmp), false); + return hash_copy(hash, tmp); } } else { @@ -1919,9 +1942,11 @@ rb_hash_s_create(int argc, VALUE *argv, VALUE klass) } if (!NIL_P(tmp)) { - long i; + if (RARRAY_LEN(tmp) == 0) { + return hash_new_capa(klass, 0); + } - hash = hash_alloc(klass); + long i; for (i = 0; i < RARRAY_LEN(tmp); ++i) { VALUE e = RARRAY_AREF(tmp, i); VALUE v = rb_check_array_type(e); @@ -1931,6 +1956,18 @@ rb_hash_s_create(int argc, VALUE *argv, VALUE klass) rb_raise(rb_eArgError, "wrong element type %s at %ld (expected array)", rb_builtin_class_name(e), i); } + + if (i == 0) { + switch (RARRAY_LEN(v)) { + case 2: + hash = hash_new_capa(klass, RARRAY_LEN(tmp)); + break; + case 1: + hash = hash_new_capa(klass, RARRAY_LEN(tmp) / 1); + break; + } + } + switch (RARRAY_LEN(v)) { default: rb_raise(rb_eArgError, "invalid number of elements (%ld for 1..2)", @@ -1949,8 +1986,7 @@ rb_hash_s_create(int argc, VALUE *argv, VALUE klass) rb_raise(rb_eArgError, "odd number of arguments for Hash"); } - hash = hash_alloc(klass); - rb_hash_bulk_insert(argc, argv, hash); + hash = hash_new_with_bulk_insert(klass, argc, argv); hash_verify(hash); return hash; } @@ -5213,14 +5249,46 @@ rb_hash_bulk_insert(long argc, const VALUE *argv, VALUE hash) } } +static VALUE +hash_new_with_bulk_insert(VALUE klass, long argc, const VALUE *argv) +{ + VALUE val = hash_new_capa(klass, argc / 2); + rb_hash_bulk_insert(argc, argv, val); + return val; +} + VALUE rb_hash_new_with_bulk_insert(long argc, const VALUE *argv) { - VALUE val = rb_hash_new_capa(argc / 2); + return hash_new_with_bulk_insert(rb_cHash, argc, argv); +} + +VALUE +rb_hash_merge2_bulk(VALUE hash, long argc, const VALUE *argv, bool dup) +{ + VALUE val = hash; + if (dup) { + // This is used to build literal hashes and keyword arguments, + // we can assume duplicate keys are very rare. + val = hash_dup_capa(val, RHASH_SIZE(val) + argc / 2); + } rb_hash_bulk_insert(argc, argv, val); return val; } +VALUE +rb_hash_merge2(VALUE h1, VALUE h2, bool dup) +{ + VALUE val = h1; + if (dup) { + // This is used to build literal hashes and keyword arguments, + // we can assume duplicate keys are very rare. + val = hash_dup_capa(val, RHASH_SIZE(val) + RHASH_SIZE(h2)); + } + rb_hash_foreach(h2, rb_hash_update_i, val); + return val; +} + #undef USE_ORIGENVIRON #if !defined(_WIN32) && !(defined(HAVE_SETENV) && defined(HAVE_UNSETENV)) # define USE_ORIGENVIRON 1 diff --git a/include/ruby/io/buffer.h b/include/ruby/io/buffer.h index 88df750056915a..0d6b52e2698e82 100644 --- a/include/ruby/io/buffer.h +++ b/include/ruby/io/buffer.h @@ -92,8 +92,9 @@ VALUE rb_io_buffer_free(VALUE self); // not exactly one. VALUE rb_io_buffer_free_locked(VALUE self); -// Access the internal buffer and flags. Validates the pointers. -// The points may not remain valid if the source buffer is manipulated. +// Access the internal buffer and flags. Validates the pointers. If the returned +// base is NULL, the returned size is always zero. +// The pointers may not remain valid if the source buffer is manipulated. // Consider using rb_io_buffer_lock if needed. enum rb_io_buffer_flags rb_io_buffer_get_bytes(VALUE self, void **base, size_t *size); void rb_io_buffer_get_bytes_for_reading(VALUE self, const void **base, size_t *size); diff --git a/internal/hash.h b/internal/hash.h index acc81978d9004b..3f7331ab265ef9 100644 --- a/internal/hash.h +++ b/internal/hash.h @@ -111,6 +111,8 @@ int rb_hash_stlike_foreach_with_replace(VALUE hash, st_foreach_check_callback_fu RUBY_SYMBOL_EXPORT_END VALUE rb_hash_new_with_bulk_insert(long argc, const VALUE *argv); +VALUE rb_hash_merge2(VALUE h1, VALUE h2, bool dup); +VALUE rb_hash_merge2_bulk(VALUE hash, long argc, const VALUE *argv, bool dup); VALUE rb_hash_resurrect(VALUE hash); int rb_hash_stlike_lookup(VALUE hash, st_data_t key, st_data_t *pval); VALUE rb_hash_keys(VALUE hash); diff --git a/io_buffer.c b/io_buffer.c index e512357db7d7fc..54896098751cec 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -48,6 +48,9 @@ enum { // This is used to validate the flags given by the user. RB_IO_BUFFER_FLAGS_MASK = RB_IO_BUFFER_EXTERNAL | RB_IO_BUFFER_INTERNAL | RB_IO_BUFFER_MAPPED | RB_IO_BUFFER_SHARED | RB_IO_BUFFER_PRIVATE | RB_IO_BUFFER_READONLY, + RB_IO_BUFFER_ALLOCATION_FLAGS = RB_IO_BUFFER_INTERNAL | RB_IO_BUFFER_MAPPED, + RB_IO_BUFFER_MAPPING_FLAGS = RB_IO_BUFFER_SHARED | RB_IO_BUFFER_PRIVATE, + RB_IO_BUFFER_DEBUG = 0, }; @@ -235,7 +238,7 @@ io_buffer_initialize(VALUE self, struct rb_io_buffer *buffer, void *base, size_t } static void -io_buffer_free(struct rb_io_buffer *buffer) +io_buffer_release(struct rb_io_buffer *buffer) { if (buffer->base) { if (buffer->flags & RB_IO_BUFFER_INTERNAL) { @@ -264,9 +267,9 @@ io_buffer_free(struct rb_io_buffer *buffer) #if defined(_WIN32) if (buffer->mapping) { - if (RB_IO_BUFFER_DEBUG) fprintf(stderr, "io_buffer_free:CloseHandle -> %p\n", buffer->mapping); + if (RB_IO_BUFFER_DEBUG) fprintf(stderr, "io_buffer_release:CloseHandle -> %p\n", buffer->mapping); if (!CloseHandle(buffer->mapping)) { - fprintf(stderr, "io_buffer_free:GetLastError -> %lu\n", GetLastError()); + fprintf(stderr, "io_buffer_release:GetLastError -> %lu\n", GetLastError()); } buffer->mapping = NULL; } @@ -309,7 +312,7 @@ rb_io_buffer_type_free(void *_buffer) { struct rb_io_buffer *buffer = _buffer; - io_buffer_free(buffer); + io_buffer_release(buffer); } static size_t @@ -383,6 +386,24 @@ io_buffer_extract_flags(VALUE argument) return flags & RB_IO_BUFFER_FLAGS_MASK; } +static inline enum rb_io_buffer_flags +io_buffer_flags_for_map(enum rb_io_buffer_flags flags) +{ + if (flags & RB_IO_BUFFER_INTERNAL) { + rb_raise(rb_eArgError, "IO::Buffer::INTERNAL can't be used with IO::Buffer.map!"); + } + + if (flags & RB_IO_BUFFER_EXTERNAL) { + rb_raise(rb_eArgError, "IO::Buffer::EXTERNAL can't be used with IO::Buffer.map!"); + } + + if ((flags & RB_IO_BUFFER_MAPPING_FLAGS) == RB_IO_BUFFER_MAPPING_FLAGS) { + rb_raise(rb_eArgError, "Flags can't include both IO::Buffer::SHARED and IO::Buffer::PRIVATE!"); + } + + return flags; +} + // Extract an offset argument, which must be a non-negative integer. static inline size_t io_buffer_extract_offset(VALUE argument) @@ -821,6 +842,12 @@ rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags * By default, the buffer is writable and expects the file to be writable. * It is also shared, so several processes can use the same mapping. * + * The mapping mode may be explicitly selected with IO::Buffer::SHARED or + * IO::Buffer::PRIVATE, but the two flags are mutually exclusive. + * IO::Buffer::MAPPED is accepted but redundant because this method always + * creates a mapped buffer. IO::Buffer::INTERNAL and IO::Buffer::EXTERNAL + * cannot be specified. + * * You can pass IO::Buffer::READONLY in +flags+ argument to make a read-only buffer; * this allows to work with files opened only for reading. * Specifying IO::Buffer::PRIVATE in +flags+ creates a private mapping, @@ -927,6 +954,7 @@ io_buffer_map(int argc, VALUE *argv, VALUE klass) if (argc >= 4) { flags = io_buffer_extract_flags(argv[3]); } + flags = io_buffer_flags_for_map(flags); return rb_io_buffer_map(io, size, offset, flags); } @@ -942,8 +970,49 @@ io_flags_for_size(size_t size) return RB_IO_BUFFER_INTERNAL; } +static inline enum rb_io_buffer_flags +io_buffer_flags_for_new(enum rb_io_buffer_flags flags, size_t size) +{ + if (size == 0) { + // A null buffer has no allocation and therefore no allocation flags: + return 0; + } + + if (!(flags & RB_IO_BUFFER_ALLOCATION_FLAGS)) { + if (flags & RB_IO_BUFFER_MAPPING_FLAGS) { + // Mapping properties imply a mapped allocation: + flags |= RB_IO_BUFFER_MAPPED; + } + else { + // No explicit allocation mode was given, so infer one from size: + flags |= io_flags_for_size(size); + } + } + + enum rb_io_buffer_flags allocation = flags & RB_IO_BUFFER_ALLOCATION_FLAGS; + RUBY_ASSERT(allocation != 0); + + if (allocation == RB_IO_BUFFER_ALLOCATION_FLAGS) { + rb_raise(rb_eArgError, "Flags can't include both IO::Buffer::INTERNAL and IO::Buffer::MAPPED!"); + } + + if (flags & RB_IO_BUFFER_EXTERNAL) { + rb_raise(rb_eArgError, "IO::Buffer::EXTERNAL can't be used with IO::Buffer.new!"); + } + + if ((flags & RB_IO_BUFFER_MAPPING_FLAGS) && allocation != RB_IO_BUFFER_MAPPED) { + rb_raise(rb_eArgError, "IO::Buffer::SHARED and IO::Buffer::PRIVATE require IO::Buffer::MAPPED!"); + } + + if ((flags & RB_IO_BUFFER_MAPPING_FLAGS) == RB_IO_BUFFER_MAPPING_FLAGS) { + rb_raise(rb_eArgError, "Flags can't include both IO::Buffer::SHARED and IO::Buffer::PRIVATE!"); + } + + return flags; +} + /* - * call-seq: IO::Buffer.new([size = DEFAULT_SIZE, [flags = 0]]) -> io_buffer + * call-seq: IO::Buffer.new([size = DEFAULT_SIZE, [flags]]) -> io_buffer * * Create a new zero-filled IO::Buffer of +size+ bytes. * By default, the buffer will be _internal_: directly allocated chunk @@ -953,6 +1022,11 @@ io_flags_for_size(size_t size) * on Windows). The behavior can be forced by passing IO::Buffer::MAPPED * as a second parameter. * + * IO::Buffer::SHARED and IO::Buffer::PRIVATE imply IO::Buffer::MAPPED and are + * mutually exclusive. Otherwise, if +flags+ do not include an allocation + * mode, IO::Buffer::INTERNAL or IO::Buffer::MAPPED is inferred from the + * requested size. The two allocation modes are mutually exclusive. + * * buffer = IO::Buffer.new(4) * # => * # # @@ -985,9 +1059,7 @@ rb_io_buffer_initialize(int argc, VALUE *argv, VALUE self) if (argc >= 2) { flags = io_buffer_extract_flags(argv[1]); } - else { - flags |= io_flags_for_size(size); - } + flags = io_buffer_flags_for_new(flags, size); io_buffer_initialize(self, buffer, NULL, size, flags, Qnil); @@ -1318,14 +1390,14 @@ rb_io_buffer_size(VALUE self) /* * call-seq: valid? -> true or false * - * Returns whether the buffer's recorded memory range currently exists within - * its source. A buffer which is not a slice is always valid, including a null - * buffer. + * A buffer which is not a slice is always valid, including a null buffer. + * Only slices can become invalid. * - * A slice can become invalid if its source is freed, transferred, shrunk past - * the slice, or reallocated at a different address. Validity is dynamic: if - * the source later contains the same address range again, the slice becomes - * valid again. + * A slice is valid when its entire recorded memory range is contained within + * its source's current memory range. It can become invalid if its source is + * freed, transferred, shrunk past the slice, or reallocated at a different + * address. Validity is dynamic: if the source later contains the same address + * range again, the slice becomes valid again. * * #valid?, #null? and #empty? describe independent properties. For example, * an invalid slice can still have a non-null address and a non-zero size. @@ -1659,6 +1731,20 @@ rb_io_buffer_locked(VALUE self) return rb_ensure(rb_yield, self, rb_io_buffer_locked_ensure, self); } +VALUE +rb_io_buffer_free(VALUE self) +{ + struct rb_io_buffer *buffer = get_io_buffer(self); + + if (io_buffer_locked(buffer)) { + rb_raise(rb_eIOBufferLockedError, "Buffer is locked!"); + } + + io_buffer_release(buffer); + + return self; +} + /* * call-seq: free -> self * @@ -1667,35 +1753,37 @@ rb_io_buffer_locked(VALUE self) * * for a buffer created from scratch: free memory. * * for a buffer created from string: undo the association. * - * After the buffer is freed, no further operations can be performed on it. + * After releasing any referenced memory, the buffer is reset to a valid, + * empty, null state. It has no backing storage and its size is zero. + * Zero-length operations remain valid, while operations requiring bytes fail + * normal bounds checking. * - * You can resize a freed buffer to re-allocate it. + * You can resize the buffer to allocate new storage. * * buffer = IO::Buffer.for('test') * buffer.free * # => # * - * buffer.get_value(:U8, 0) - * # in `get_value': The buffer is not allocated! (IO::Buffer::AllocationError) + * buffer.null? # => true + * buffer.empty? # => true + * buffer.valid? # => true + * buffer.get_string # => "" * - * buffer.get_string - * # in `get_string': The buffer is not allocated! (IO::Buffer::AllocationError) + * buffer.get_value(:U8, 0) # raises ArgumentError * - * buffer.null? - * # => true + * A frozen buffer cannot be freed, as that would release the memory its + * contents live in: + * + * buffer = IO::Buffer.for('test').freeze + * buffer.free + * # in `free': can't modify frozen IO::Buffer (FrozenError) */ -VALUE -rb_io_buffer_free(VALUE self) +static VALUE +io_buffer_free(VALUE self) { - struct rb_io_buffer *buffer = get_io_buffer(self); + rb_check_frozen(self); - if (io_buffer_locked(buffer)) { - rb_raise(rb_eIOBufferLockedError, "Buffer is locked!"); - } - - io_buffer_free(buffer); - - return self; + return rb_io_buffer_free(self); } VALUE rb_io_buffer_free_locked(VALUE self) @@ -1711,7 +1799,7 @@ VALUE rb_io_buffer_free_locked(VALUE self) } io_buffer_unlock(buffer); - io_buffer_free(buffer); + io_buffer_release(buffer); return self; } @@ -1867,6 +1955,25 @@ io_buffer_slice(int argc, VALUE *argv, VALUE self) return rb_io_buffer_slice(buffer, self, offset, length); } +VALUE +rb_io_buffer_transfer(VALUE self) +{ + struct rb_io_buffer *buffer = get_io_buffer(self); + + if (io_buffer_locked(buffer)) { + rb_raise(rb_eIOBufferLockedError, "Cannot transfer ownership of locked buffer!"); + } + + VALUE instance = rb_io_buffer_type_allocate(rb_class_of(self)); + struct rb_io_buffer *transferred; + TypedData_Get_Struct(instance, struct rb_io_buffer, &rb_io_buffer_type, transferred); + + *transferred = *buffer; + io_buffer_zero(buffer); + + return instance; +} + /* * call-seq: transfer -> new_io_buffer * @@ -1884,24 +1991,20 @@ io_buffer_slice(int argc, VALUE *argv, VALUE self) * # # * buffer.null? * # => true + * + * A frozen buffer cannot transfer ownership, as that would leave it + * uninitialized: + * + * buffer = IO::Buffer.for('test').freeze + * buffer.transfer + * # in `transfer': can't modify frozen IO::Buffer (FrozenError) */ -VALUE -rb_io_buffer_transfer(VALUE self) +static VALUE +io_buffer_transfer(VALUE self) { - struct rb_io_buffer *buffer = get_io_buffer(self); - - if (io_buffer_locked(buffer)) { - rb_raise(rb_eIOBufferLockedError, "Cannot transfer ownership of locked buffer!"); - } + rb_check_frozen(self); - VALUE instance = rb_io_buffer_type_allocate(rb_class_of(self)); - struct rb_io_buffer *transferred; - TypedData_Get_Struct(instance, struct rb_io_buffer, &rb_io_buffer_type, transferred); - - *transferred = *buffer; - io_buffer_zero(buffer); - - return instance; + return rb_io_buffer_transfer(self); } static void @@ -1927,7 +2030,7 @@ io_buffer_resize_copy(VALUE self, struct rb_io_buffer *buffer, size_t size) io_buffer_resize_clear(buffer, resized.base, size); } - io_buffer_free(buffer); + io_buffer_release(buffer); *buffer = resized; } @@ -1993,7 +2096,7 @@ rb_io_buffer_resize(VALUE self, size_t size) } if (size == 0) { - io_buffer_free(buffer); + io_buffer_release(buffer); return; } @@ -2054,11 +2157,13 @@ rb_io_buffer_resize(VALUE self, size_t size) * does not change, a slice can be resized while its source is locked. * * External owning buffers (created with ::for), and locked owning buffers - * cannot be resized. + * cannot be resized. Frozen buffers cannot be resized. */ static VALUE io_buffer_resize(VALUE self, VALUE size) { + rb_check_frozen(self); + rb_io_buffer_resize(self, io_buffer_extract_size(size)); return self; @@ -2095,6 +2200,12 @@ rb_io_buffer_compare(VALUE self, VALUE other) return RB_INT2NUM(1); } + if (size1 == 0) { + return RB_INT2NUM(0); + } + + RUBY_ASSERT(ptr1 != NULL); + RUBY_ASSERT(ptr2 != NULL); return RB_INT2NUM(memcmp(ptr1, ptr2, size1)); } @@ -2808,6 +2919,10 @@ io_buffer_memmove(struct rb_io_buffer *buffer, size_t offset, const void *source rb_raise(rb_eArgError, "The computed source range exceeds the size of the source buffer!"); } + if (length == 0) return; + + RUBY_ASSERT(base != NULL); + RUBY_ASSERT(source_base != NULL); struct io_buffer_memmove_arguments arguments = { .destination = (unsigned char*)base+offset, .source = (unsigned char*)source_base+source_offset, @@ -3072,6 +3187,9 @@ rb_io_buffer_clear(VALUE self, uint8_t value, size_t offset, size_t length) io_buffer_validate_range(buffer, offset, length); + if (length == 0) return; + + RUBY_ASSERT(base != NULL); memset((char*)base + offset, value, length); } @@ -3258,8 +3376,11 @@ rb_io_buffer_read(VALUE self, VALUE io, size_t length, size_t offset) size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); - base = (unsigned char*)base + offset; size = size - offset; + if (size == 0) return SIZET2NUM(0); + + RUBY_ASSERT(base != NULL); + base = (unsigned char*)base + offset; struct io_buffer_read_internal_argument argument = { .descriptor = descriptor, @@ -3280,7 +3401,8 @@ rb_io_buffer_read(VALUE self, VALUE io, size_t length, size_t offset) * If +length+ is not given or +nil+, it defaults to the size of the buffer * minus the offset, i.e. the entire buffer. * - * If +length+ is zero, exactly one read operation will occur. + * If +length+ is zero, exactly one read operation will occur, unless + * there is no available space in the buffer at +offset+. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the * buffer. @@ -3376,8 +3498,11 @@ rb_io_buffer_pread(VALUE self, VALUE io, rb_off_t from, size_t length, size_t of size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); - base = (unsigned char*)base + offset; size = size - offset; + if (size == 0) return SIZET2NUM(0); + + RUBY_ASSERT(base != NULL); + base = (unsigned char*)base + offset; struct io_buffer_pread_internal_argument argument = { .descriptor = descriptor, @@ -3400,7 +3525,8 @@ rb_io_buffer_pread(VALUE self, VALUE io, rb_off_t from, size_t length, size_t of * If +length+ is not given or +nil+, it defaults to the size of the buffer * minus the offset, i.e. the entire buffer. * - * If +length+ is zero, exactly one pread operation will occur. + * If +length+ is zero, exactly one pread operation will occur, + * unless there is no available space in the buffer at +offset+. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the * buffer. @@ -3497,8 +3623,11 @@ rb_io_buffer_write(VALUE self, VALUE io, size_t length, size_t offset) size_t size; io_buffer_get_bytes_for_reading(buffer, &base, &size); - base = (unsigned char*)base + offset; size = size - offset; + if (size == 0) return SIZET2NUM(0); + + RUBY_ASSERT(base != NULL); + base = (const unsigned char*)base + offset; struct io_buffer_write_internal_argument argument = { .descriptor = descriptor, @@ -3519,7 +3648,8 @@ rb_io_buffer_write(VALUE self, VALUE io, size_t length, size_t offset) * If +length+ is not given or +nil+, it defaults to the size of the buffer * minus the offset, i.e. the entire buffer. * - * If +length+ is zero, exactly one write operation will occur. + * If +length+ is zero, exactly one write operation will occur, + * unless there are no available bytes in the buffer at +offset+. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the * buffer. @@ -3608,8 +3738,11 @@ rb_io_buffer_pwrite(VALUE self, VALUE io, rb_off_t from, size_t length, size_t o size_t size; io_buffer_get_bytes_for_reading(buffer, &base, &size); - base = (unsigned char*)base + offset; size = size - offset; + if (size == 0) return SIZET2NUM(0); + + RUBY_ASSERT(base != NULL); + base = (const unsigned char*)base + offset; struct io_buffer_pwrite_internal_argument argument = { .descriptor = descriptor, @@ -3640,7 +3773,8 @@ rb_io_buffer_pwrite(VALUE self, VALUE io, rb_off_t from, size_t length, size_t o * If +length+ is not given or +nil+, it defaults to the size of the buffer * minus the offset, i.e. the entire buffer. * - * If +length+ is zero, exactly one pwrite operation will occur. + * If +length+ is zero, exactly one pwrite operation will occur, + * unless there are no available bytes in the buffer at +offset+. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the * buffer. @@ -4096,6 +4230,9 @@ io_buffer_bit_count(int argc, VALUE *argv, VALUE self) size_t size; io_buffer_get_bytes_for_reading(buffer, &base, &size); + if (length == 0) return SIZET2NUM(0); + + RUBY_ASSERT(base != NULL); size_t count = memory_bit_count((const unsigned char *)base + offset, length); return SIZET2NUM(count); @@ -4240,7 +4377,7 @@ Init_IO_Buffer(void) rb_define_method(rb_cIOBuffer, "size", rb_io_buffer_size, 0); rb_define_method(rb_cIOBuffer, "valid?", rb_io_buffer_valid_p, 0); - rb_define_method(rb_cIOBuffer, "transfer", rb_io_buffer_transfer, 0); + rb_define_method(rb_cIOBuffer, "transfer", io_buffer_transfer, 0); /* Indicates that the memory in the buffer is owned by someone else. See #external? for more details. */ rb_define_const(rb_cIOBuffer, "EXTERNAL", RB_INT2NUM(RB_IO_BUFFER_EXTERNAL)); @@ -4292,7 +4429,7 @@ Init_IO_Buffer(void) rb_define_method(rb_cIOBuffer, "<=>", rb_io_buffer_compare, 1); rb_define_method(rb_cIOBuffer, "resize", io_buffer_resize, 1); rb_define_method(rb_cIOBuffer, "clear", io_buffer_clear, -1); - rb_define_method(rb_cIOBuffer, "free", rb_io_buffer_free, 0); + rb_define_method(rb_cIOBuffer, "free", io_buffer_free, 0); rb_include_module(rb_cIOBuffer, rb_mComparable); diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 070cc095bdef5e..caefd055906483 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -3,11 +3,27 @@ module Bundler module CLI::Common def self.validate_cooldown!(value) - return if value.nil? + # Without the flag the config file and BUNDLE_COOLDOWN decide, and those + # only warn, so a typo left in a config file keeps the command usable. + return warn_invalid_cooldown_setting if value.nil? return if value.is_a?(Integer) && value >= 0 raise InvalidOption, "Expected `--cooldown` to be a non-negative integer, got #{value.inspect}" end + # A cooldown value that cannot be read as a non-negative integer disables + # the cooldown for every source, overriding any per-source `cooldown:` in + # the Gemfile, so say so rather than letting the protection lapse quietly. + def self.warn_invalid_cooldown_setting + value = Bundler.settings.locations(:cooldown).values.first + return if value.nil? + + days = Integer(value.to_s, exception: false) + return if days && !days.negative? + + Bundler.ui.warn "Invalid cooldown value #{value.inspect}, so the cooldown is disabled for all sources. " \ + "Expected a non-negative integer number of days." + end + def self.output_post_install_messages(messages) return if Bundler.settings["ignore_messages"] messages.to_a.each do |name, msg| diff --git a/lib/bundler/cli/doctor/diagnose.rb b/lib/bundler/cli/doctor/diagnose.rb index a878025ddafd80..378ef9bf1755bf 100644 --- a/lib/bundler/cli/doctor/diagnose.rb +++ b/lib/bundler/cli/doctor/diagnose.rb @@ -53,7 +53,7 @@ def dylibs(path) end def bundles_for_gem(spec) - Dir.glob("#{spec.full_gem_path}/**/*.bundle") + SharedHelpers.glob_files_in_dir("**/*.bundle", spec.full_gem_path) end def lookup_with_fiddle(path) diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index e5c9273383885a..637f61c760d558 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -164,11 +164,29 @@ def retrieve_active_spec(definition, current_spec) return active_spec if strict - active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.installable_on_platform?(current_spec.platform) }.sort_by(&:version) - if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1 - active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } + matching_specs(active_spec, current_spec).last + end + + def matching_specs(active_spec, current_spec) + @matching_specs ||= {} + @matching_specs[[active_spec.source, current_spec.name, current_spec.platform]] ||= begin + active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.installable_on_platform?(current_spec.platform) }.sort_by(&:version) + if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1 + active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } + end + active_specs end - active_specs.last + end + + # The newest version the cooldown setting would let bundler adopt right + # now, when the newest overall version is still inside the window. Only a + # version strictly between the installed one and the newest one is an + # adoptable update worth showing. + def newest_out_of_cooldown(active_spec, current_spec) + newest = matching_specs(active_spec, current_spec).reverse_each.find {|spec| cooldown_days_remaining(spec).nil? } + return unless newest + return if newest.version >= active_spec.version || newest.version <= current_spec.version + newest end def print_gems(gems_list) @@ -214,7 +232,11 @@ def print_gem(current_spec, active_spec, dependency, groups) spec_outdated_info += ", released #{release_date}" unless release_date.empty? remaining = cooldown_days_remaining(active_spec) - spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}" if remaining + if remaining + spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}" + adoptable = newest_out_of_cooldown(active_spec, current_spec) + spec_outdated_info += ", newest out of cooldown #{adoptable.version}" if adoptable + end spec_outdated_info += ")" @@ -233,7 +255,11 @@ def gem_column_for(current_spec, active_spec, dependency, groups) current_version = "#{current_spec.version}#{current_spec.git_version}" spec_version = "#{active_spec.version}#{active_spec.git_version}" remaining = cooldown_days_remaining(active_spec) - spec_version += " (cooldown #{remaining}d)" if remaining + if remaining + adoptable = newest_out_of_cooldown(active_spec, current_spec) + adoptable_note = adoptable ? ", #{adoptable.version} out of cooldown" : "" + spec_version += " (cooldown #{remaining}d#{adoptable_note})" + end dependency = dependency.requirement if dependency ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s] @@ -242,7 +268,7 @@ def gem_column_for(current_spec, active_spec, dependency, groups) ret_val end - def cooldown_days_remaining(spec, now = Time.now) + def cooldown_days_remaining(spec, now = cooldown_now) return nil unless spec.respond_to?(:created_at) && spec.created_at return nil unless spec.respond_to?(:remote) && spec.remote days = spec.remote.effective_cooldown @@ -251,6 +277,10 @@ def cooldown_days_remaining(spec, now = Time.now) remaining > 0 ? remaining.ceil : nil end + def cooldown_now + @cooldown_now ||= Time.now + end + def check_for_deployment_mode! return unless Bundler.frozen_bundle? suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any? diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 59514a9d5454d9..b9399a3ccf9515 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -69,7 +69,7 @@ def gemspec(opts = nil) development_group = opts[:development_group] || :development expanded_path = gemfile_root.join(path) - gemspecs = Gem::Util.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) } + gemspecs = SharedHelpers.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) } gemspecs.reject! {|s| s.name != name } if name specs_by_name_and_version = gemspecs.group_by {|s| [s.name, s.version] } diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 02d3ac37028166..5d0485ee558ef0 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -155,10 +155,11 @@ def local_specification_path end def parse_metadata(data) + @created_at = nil + unless data @required_ruby_version = nil @required_rubygems_version = nil - @created_at = nil return end @@ -181,20 +182,31 @@ def parse_metadata(data) when "ruby" @required_ruby_version = Gem::Requirement.new(v) when "created_at" - value = v.is_a?(Array) ? v.last : v - if value.is_a?(String) - @created_at = begin - Time.new(value) - rescue ArgumentError - nil - end - end + @created_at = parse_created_at(v.is_a?(Array) ? v.last : v)&.freeze end end rescue StandardError => e raise GemspecError, "There was an error parsing the metadata for the gem #{name} (#{version}): #{e.class}\n#{e}\nThe metadata was #{data.inspect}" end + # Matches an ISO 8601 time zone designator at the end of a timestamp. + TIME_ZONE_SUFFIX = /(?:Z|z|[+-]\d{2}(?::?\d{2})?)\z/ + private_constant :TIME_ZONE_SUFFIX + + # A timestamp without a time zone offset is read as UTC, because reading + # it as local time would shift the cooldown window by the environment's + # offset. Unparsable values become nil so the cooldown fails open. + def parse_created_at(value) + return unless value.is_a?(String) + + require "time" + begin + Time.iso8601(value.match?(TIME_ZONE_SUFFIX) ? value : "#{value}Z") + rescue ArgumentError + nil + end + end + def build_dependency(name, requirements) Dependency.new(name, requirements) end diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index 5ce0ef6280037b..e3af1b957f357c 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -32,7 +32,7 @@ def gemspec(&block) def initialize(base = nil, name = nil) @base = File.expand_path(base || SharedHelpers.pwd) - gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : Gem::Util.glob_files_in_dir("{,*}.gemspec", @base) + gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : SharedHelpers.glob_files_in_dir("{,*}.gemspec", @base) raise "Unable to determine name from existing gemspec. Use :name => 'gemname' in #install_tasks to manually set it." unless gemspecs.size == 1 @spec_path = gemspecs.first @gemspec = Bundler.load_gemspec(@spec_path) @@ -124,7 +124,7 @@ def rubygem_push(path) end def built_gem_path - Gem::Util.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last + SharedHelpers.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last end def git_push(remote = nil) diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 88f7f67b4bcc4c..0ae7c8b4510af9 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -99,7 +99,11 @@ The per\-source \fBcooldown:\fR keyword in the Gemfile, such as \fBsource "https .IP The CLI flag and this setting apply uniformly to every source, including ones declared with their own \fBcooldown:\fR value\. To keep a private registry permanently exempt while still cooling down public gems, declare \fBsource "https://internal", cooldown: 0\fR in the Gemfile; remember that \fB\-\-cooldown N\fR on the command line will still override it for that single run\. .IP +The value must be a non\-negative integer\. \fB\-\-cooldown\fR rejects anything else outright, while a value coming from this setting or from \fBBUNDLE_COOLDOWN\fR only warns, because a typo left in a config file should not make every command fail\. Such a value disables the cooldown for every source, including sources that declare their own \fBcooldown:\fR in the Gemfile\. +.IP Cooldown filtering depends on the gem server providing a per\-version \fBcreated_at\fR timestamp in the v2 compact\-index format\. Versions without that metadata \- older gem servers, historical entries that predate the v2 cutover on \fBrubygems\.org\fR, or private registries that still emit the v1 format \- are treated as outside the cooldown window and remain resolvable\. If you rely on cooldown for supply\-chain protection, confirm that the gem server emits \fBcreated_at\fR in its \fB/info/\fR responses\. +.IP +A \fBcreated_at\fR timestamp is read as UTC when it carries no time zone offset\. \fBrubygems\.org\fR always sends one, but a third\-party server that omits it would otherwise shift the cooldown window by the offset of whatever machine runs bundler\. .IP "\(bu" 4 \fBdefault_cli_command\fR (\fBBUNDLE_DEFAULT_CLI_COMMAND\fR): The command that running \fBbundle\fR without arguments should run\. Defaults to \fBcli_help\fR since Bundler 4, but can also be \fBinstall\fR which was the previous default\. .IP "\(bu" 4 diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 6e9fedd109afb9..f01f43d709c2c7 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -159,6 +159,13 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). Gemfile; remember that `--cooldown N` on the command line will still override it for that single run. + The value must be a non-negative integer. `--cooldown` rejects + anything else outright, while a value coming from this setting or + from `BUNDLE_COOLDOWN` only warns, because a typo left in a config + file should not make every command fail. Such a value disables the + cooldown for every source, including sources that declare their own + `cooldown:` in the Gemfile. + Cooldown filtering depends on the gem server providing a per-version `created_at` timestamp in the v2 compact-index format. Versions without that metadata - older gem servers, historical entries that @@ -167,6 +174,11 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). window and remain resolvable. If you rely on cooldown for supply-chain protection, confirm that the gem server emits `created_at` in its `/info/` responses. + + A `created_at` timestamp is read as UTC when it carries no time zone + offset. `rubygems.org` always sends one, but a third-party server + that omits it would otherwise shift the cooldown window by the + offset of whatever machine runs bundler. * `default_cli_command` (`BUNDLE_DEFAULT_CLI_COMMAND`): The command that running `bundle` without arguments should run. Defaults to `cli_help` since Bundler 4, but can also be `install` which was the previous diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index c2f8086e241070..d884a1ff9bcae4 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -55,7 +55,7 @@ Only list patch newer versions\. Only list gems specified in your Gemfile, not their dependencies\. .TP \fB\-\-cooldown=\fR -Annotate (rather than hide) versions that are still inside the cooldown window of \fInumber\fR days\. The prose output appends "in cooldown for Nd more days" and the table form adds "(cooldown Nd)" to the Latest column\. See \fBcooldown\fR in bundle\-config(1)\. +Annotate (rather than hide) versions that are still inside the cooldown window of \fInumber\fR days\. The prose output appends "in cooldown for Nd more days" and the table form adds "(cooldown Nd)" to the Latest column\. When a version outside the window is newer than the installed one, the prose output also appends "newest out of cooldown X" and the table form becomes "(cooldown Nd, X out of cooldown)"\. With \fB\-\-filter\-strict\fR (or \fB\-\-patch\fR, \fB\-\-minor\fR, \fB\-\-major\fR, which imply it), "newest" is already the cooldown\-filtered resolved version, so none of these annotations appear\. See \fBcooldown\fR in bundle\-config(1)\. .SH "PATCH LEVEL OPTIONS" See bundle update(1) \fIbundle\-update\.1\.html\fR for details\. .SH "FILTERING OUTPUT" diff --git a/lib/bundler/man/bundle-outdated.1.ronn b/lib/bundler/man/bundle-outdated.1.ronn index e5badac2e99484..468b84d057d412 100644 --- a/lib/bundler/man/bundle-outdated.1.ronn +++ b/lib/bundler/man/bundle-outdated.1.ronn @@ -76,7 +76,13 @@ are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. Annotate (rather than hide) versions that are still inside the cooldown window of days. The prose output appends "in cooldown for Nd more days" and the table form adds "(cooldown Nd)" to - the Latest column. See `cooldown` in bundle-config(1). + the Latest column. When a version outside the window is newer than the + installed one, the prose output also appends "newest out of cooldown + X" and the table form becomes "(cooldown Nd, X out of cooldown)". + With `--filter-strict` (or `--patch`, `--minor`, `--major`, which + imply it), "newest" is already the cooldown-filtered resolved version, + so none of these annotations appear. See `cooldown` in + bundle-config(1). ## PATCH LEVEL OPTIONS diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 510ae09712c295..a164a419354174 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -469,9 +469,9 @@ def cooldown_hint(specs) def cooldown_excluded?(spec) return false unless spec.respond_to?(:created_at) && spec.created_at return false unless spec.respond_to?(:remote) && spec.remote - return false if locked_by_lockfile?(spec) days = spec.remote.effective_cooldown return false if days.nil? || days <= 0 + return false if locked_by_lockfile?(spec) (cooldown_now - spec.created_at) < (days * 86_400) end diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index d821dc022b6e32..b76734f5907bf3 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -141,7 +141,7 @@ def cache(custom_path = nil, local = false) end end - Gem::Util.glob_files_in_dir("*/.git", cache_path.to_s).each do |git_dir| + SharedHelpers.glob_files_in_dir("*/.git", cache_path.to_s).each do |git_dir| FileUtils.rm_rf(git_dir) FileUtils.touch(File.expand_path("../.bundlecache", git_dir)) end @@ -159,13 +159,13 @@ def prune_cache(cache_path) end def clean(dry_run = false) - gem_bins = Gem::Util.glob_files_in_dir("bin/*", Gem.dir) - git_dirs = Gem::Util.glob_files_in_dir("bundler/gems/*", Gem.dir) - git_cache_dirs = Gem::Util.glob_files_in_dir("cache/bundler/git/*", Gem.dir) - gem_dirs = Gem::Util.glob_files_in_dir("gems/*", Gem.dir) - gem_files = Gem::Util.glob_files_in_dir("cache/*.gem", Gem.dir) - gemspec_files = Gem::Util.glob_files_in_dir("specifications/*.gemspec", Gem.dir) - extension_dirs = Gem::Util.glob_files_in_dir("extensions/*/*/*", Gem.dir) + Gem::Util.glob_files_in_dir("bundler/gems/extensions/*/*/*", Gem.dir) + gem_bins = SharedHelpers.glob_files_in_dir("bin/*", Gem.dir) + git_dirs = SharedHelpers.glob_files_in_dir("bundler/gems/*", Gem.dir) + git_cache_dirs = SharedHelpers.glob_files_in_dir("cache/bundler/git/*", Gem.dir) + gem_dirs = SharedHelpers.glob_files_in_dir("gems/*", Gem.dir) + gem_files = SharedHelpers.glob_files_in_dir("cache/*.gem", Gem.dir) + gemspec_files = SharedHelpers.glob_files_in_dir("specifications/*.gemspec", Gem.dir) + extension_dirs = SharedHelpers.glob_files_in_dir("extensions/*/*/*", Gem.dir) + SharedHelpers.glob_files_in_dir("bundler/gems/extensions/*/*/*", Gem.dir) spec_gem_paths = [] # need to keep git sources around spec_git_paths = @definition.spec_git_paths @@ -232,7 +232,7 @@ def clean(dry_run = false) private def prune_gem_cache(resolve, cache_path) - cached = Gem::Util.glob_files_in_dir("*.gem", cache_path.to_s) + cached = SharedHelpers.glob_files_in_dir("*.gem", cache_path.to_s) cached = cached.delete_if do |path| spec = Bundler.rubygems.spec_from_gem path @@ -257,7 +257,7 @@ def prune_gem_cache(resolve, cache_path) end def prune_git_and_path_cache(resolve, cache_path) - cached = Gem::Util.glob_files_in_dir("*/.bundlecache", cache_path.to_s) + cached = SharedHelpers.glob_files_in_dir("*/.bundlecache", cache_path.to_s) cached = cached.delete_if do |path| name = File.basename(File.dirname(path)) @@ -283,7 +283,7 @@ def setup_manpath # Add man/ subdirectories from activated bundles to MANPATH for man(1) manuals = $LOAD_PATH.filter_map do |path| man_subdir = path.sub(/lib$/, "man") - man_subdir unless Dir.glob("man?/", base: man_subdir).empty? + man_subdir unless SharedHelpers.glob_files_in_dir("man?/", man_subdir).empty? end return if manuals.empty? diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 2c22103e3066b3..09cee899e1a5bb 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -232,6 +232,21 @@ def relative_path_to(destination, from: pwd) destination end + # Globs for entries matching +glob+ inside of +base_path+, returning + # absolute paths. Glob metacharacters in +base_path+ are not treated as + # part of the pattern, and matched entries are joined literally, so an + # entry starting with `~` is not expanded into the home directory. + # + # Bundler runs against whatever RubyGems the host provides, so this cannot + # delegate to Gem::Util.glob_files_in_dir, which only gained the literal + # join in RubyGems 4.1. + def glob_files_in_dir(glob, base_path) + expanded_path = nil + Dir.glob(glob, base: base_path).map! do |f| + File.join(expanded_path ||= File.expand_path(base_path), f) + end + end + private def validate_bundle_path diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 2cb6ec3faecb23..ff08ef03281fe5 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -325,7 +325,7 @@ def humanized_ref def serialize_gemspecs_in(destination) destination = destination.expand_path(Bundler.root) if destination.relative? - Gem::Util.glob_files_in_dir(@glob, destination.to_s).each do |spec_path| + SharedHelpers.glob_files_in_dir(@glob, destination.to_s).each do |spec_path| # Evaluate gemspecs and cache the result. Gemspecs # in git might require git or other dependencies. # The gemspecs we cache should already be evaluated. diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 366a23aea725d7..db6c0ff8df728a 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -161,7 +161,7 @@ def load_spec_files if File.directory?(expanded_path) # We sort depth-first since `<<` will override the earlier-found specs - Gem::Util.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| + SharedHelpers.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| next unless spec = load_gemspec(file) spec.source = self diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 8c21864b08280a..22b9ca821cabbb 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -406,7 +406,7 @@ def cached_specs @cached_specs ||= begin idx = Index.new - Gem::Util.glob_files_in_dir("*.gem", cache_path.to_s).each do |gemfile| + SharedHelpers.glob_files_in_dir("*.gem", cache_path.to_s).each do |gemfile| s ||= Bundler.rubygems.spec_from_gem(gemfile) s.source = self idx << s diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb index 3d847424b7aeb3..ca788f82939936 100644 --- a/lib/bundler/source/rubygems/remote.rb +++ b/lib/bundler/source/rubygems/remote.rb @@ -20,10 +20,15 @@ def initialize(uri, cooldown: nil) # Returns the cooldown days that apply to this remote, resolving the # precedence CLI > config > Gemfile per-source. Returns nil if no # cooldown applies. + # + # The resolver asks once per candidate spec, so the settings lookup is + # memoized. That snapshots the value: a Remote created before a + # settings change keeps the old one. Nothing in Bundler changes the + # cooldown setting after the sources are built (the CLI flag is applied + # before the Definition exists), so build a new Remote if you need to. def effective_cooldown - override = Bundler.settings[:cooldown] - return override if override - @cooldown + return @effective_cooldown if defined?(@effective_cooldown) + @effective_cooldown = Bundler.settings[:cooldown] || @cooldown end MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index d4f98718683c3b..d665646317d7b6 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -91,13 +91,21 @@ def files_in(spec) end def files_in_gem(spec) - gem_path = spec.full_gem_path - extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only] - glob = "#{gem_path}#{extra}/**/*" - prefix_re = %r{#{Regexp.escape(gem_path)}/} + gem_path = spec.full_gem_path - Dir[glob].map do |file| - [gem_path, file.sub(prefix_re, "")] + if options[:lib_only] + # raw_require_paths rather than require_paths, since the latter prepends + # the absolute extension_dir, which would escape the glob base + require_paths = spec.raw_require_paths + return [] if require_paths.empty? + + glob = "{#{require_paths.join ","}}/**/*" + else + glob = "**/*" + end + + Dir.glob(glob, base: gem_path).map do |file| + [gem_path, file] end end diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index e6c3c7d0de2a81..1b90b4f63ae706 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -318,7 +318,7 @@ def install_rdoc (!File.exist?(rubygems_doc_dir) || File.writable?(rubygems_doc_dir)) say "Removing old RubyGems RDoc and ri" if @verbose - Dir[File.join(Gem.dir, "doc", "rubygems-[0-9]*")].each do |dir| + Gem::Util.glob_files_in_dir("rubygems-[0-9]*", gem_doc_dir).each do |dir| rm_rf dir end diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb index 0be2b85159f9cc..efaeea77d0dec9 100644 --- a/lib/rubygems/commands/stale_command.rb +++ b/lib/rubygems/commands/stale_command.rb @@ -25,7 +25,7 @@ def execute gem_to_atime = {} Gem::Specification.each do |spec| name = spec.full_name - Dir["#{spec.full_gem_path}/**/*.*"].each do |file| + Gem::Util.glob_files_in_dir("**/*.*", spec.full_gem_path).each do |file| next if File.directory?(file) stat = File.stat(file) gem_to_atime[name] ||= stat.atime diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index 3d63af60487b78..101e9f89ecb0c3 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -121,7 +121,9 @@ class Gem::ConfigFile ## # Number of days a newly published gem version must wait before it is # considered for installation or update (the cooldown period). 0 - # disables the cooldown. + # disables the cooldown. A value that cannot be read as a non-negative + # integer warns and leaves the cooldown disabled, so a typo in the gemrc + # file does not make every command fail. attr_accessor :cooldown diff --git a/lib/rubygems/cooldown.rb b/lib/rubygems/cooldown.rb index 499e08b3be1985..f44958a7bc4d4b 100644 --- a/lib/rubygems/cooldown.rb +++ b/lib/rubygems/cooldown.rb @@ -28,8 +28,15 @@ def self.from_options(options) end def initialize(days, now: Time.now) - @days = days.to_i + # A gemrc value is arbitrary YAML, so it can be any type at all. Anything + # that cannot be read as a non-negative integer leaves the cooldown + # disabled rather than raising out of an unrelated command. + valid = valid_days?(days) + + @days = valid ? days.to_i : 0 @now = now + + Gem::Cooldown.warn_invalid_days(days) unless valid || days.nil? end ## @@ -83,6 +90,27 @@ def self.output_skipped_summary(entries) end end + # Matches an ISO 8601 time zone designator at the end of a timestamp. + TIME_ZONE_SUFFIX = /(?:Z|z|[+-]\d{2}(?::?\d{2})?)\z/ # :nodoc: + private_constant :TIME_ZONE_SUFFIX + + ## + # Parses a +created_at+ timestamp from the compact index. A timestamp + # without a time zone offset is read as UTC, because reading it as local + # time would shift the cooldown window by the environment's offset. + # Returns nil for anything unparsable, so the cooldown fails open. + + def self.parse_created_at(value) + return unless value.is_a?(String) + + require "time" + begin + Time.iso8601(value.match?(TIME_ZONE_SUFFIX) ? value : "#{value}Z") + rescue ArgumentError + nil + end + end + ## # Warns once per process that +source+ did not provide publish times, so # the cooldown cannot be applied to gems from it. @@ -98,4 +126,30 @@ def self.warn_missing_created_at(source) def self.reset_warned_missing_created_at # :nodoc: @warned = nil end + + # Warns once per process that a configured cooldown value cannot be read + # as a non-negative integer, which leaves the cooldown disabled. The + # --cooldown option is validated by the option parser; this catches the + # gemrc path. + + def self.warn_invalid_days(value) # :nodoc: + return if @warned_invalid_days + @warned_invalid_days = true + + Gem::DefaultUserInteraction.ui.alert_warning \ + "Invalid cooldown value #{value.inspect}, so the cooldown is disabled. " \ + "Expected a non-negative integer number of days." + end + + def self.reset_warned_invalid_days # :nodoc: + @warned_invalid_days = nil + end + + private + + def valid_days?(value) + days = Integer(value.to_s, exception: false) + + !days.nil? && !days.negative? + end end diff --git a/lib/rubygems/cooldown_option.rb b/lib/rubygems/cooldown_option.rb index 84195d39f1abaa..b91ae6eab45cf0 100644 --- a/lib/rubygems/cooldown_option.rb +++ b/lib/rubygems/cooldown_option.rb @@ -15,6 +15,11 @@ def add_cooldown_option(group = nil) "the last DAYS days (0 disables the cooldown)"].compact add_option(*args) do |value, options| + if value.negative? + raise Gem::OptionParser::InvalidArgument, + "#{value} (expected a non-negative integer number of days; use 0 to disable the cooldown)" + end + options[:cooldown] = value end end diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb index c45d83832aba9c..7a0d98cb80c5fa 100644 --- a/lib/rubygems/resolver/api_specification.rb +++ b/lib/rubygems/resolver/api_specification.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "../cooldown" + ## # Represents a specification retrieved via the Compact Index API. # @@ -114,13 +116,7 @@ def source # :nodoc: def parse_created_at(value) value = value.first if value.is_a?(Array) - return unless value.is_a?(String) - require "time" - begin - Time.iso8601(value) - rescue ArgumentError - nil - end + Gem::Cooldown.parse_created_at(value) end end diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 3060bfaa3129f1..286a036a5f4be2 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "text" +require_relative "cooldown" ## # A Source knows how to list and fetch gems from a RubyGems marshal index. # @@ -210,14 +211,8 @@ def created_at(name, version, platform = Gem::Platform::RUBY) return unless row value = row[Gem::CompactIndexClient::INFO_REQS].assoc("created_at")&.last&.first - return unless value.is_a?(String) - require "time" - begin - Time.iso8601(value) - rescue ArgumentError - nil - end + Gem::Cooldown.parse_created_at(value) end ## diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index ba8e955ad4e9cb..2d66f3a2da3f9e 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -75,15 +75,19 @@ def self.traverse_parents(directory, &block) end ## - # Globs for files matching +pattern+ inside of +directory+, - # returning absolute paths to the matching files. Unlike a plain - # Dir.glob with an interpolated path, glob metacharacters in - # +base_path+ are not treated as part of the pattern. + # Globs for entries matching +glob+ inside of +base_path+, returning + # absolute paths to the matching files and directories. Unlike a plain + # Dir.glob with an interpolated path, glob metacharacters in +base_path+ + # are not treated as part of the pattern. Matched entries are joined to + # +base_path+ literally, so an entry starting with `~` is not expanded + # into a home directory, and no other normalization is applied to them. def self.glob_files_in_dir(glob, base_path) expanded_path = nil Dir.glob(glob, base: base_path).map! do |f| - File.expand_path(f, expanded_path ||= File.expand_path(base_path)) + # File.join instead of File.expand_path, so that matched entries + # starting with `~` are not expanded into the home directory + File.join(expanded_path ||= File.expand_path(base_path), f) end end diff --git a/prism_compile.c b/prism_compile.c index 81eda24a254dbb..69e7d5354913ce 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -1494,6 +1494,7 @@ pm_compile_hash_elements(rb_iseq_t *iseq, const pm_node_t *node, const pm_node_l int stack_length = 0; bool first_chunk = true; + bool owned_hash = false; // This is an optimization wherein we keep track of whether or not the // previous element was a static literal. If it was, then we do not attempt @@ -1504,21 +1505,27 @@ pm_compile_hash_elements(rb_iseq_t *iseq, const pm_node_t *node, const pm_node_l DECL_ANCHOR(anchor); // Convert pushed elements to a hash, and merge if needed. -#define FLUSH_CHUNK \ - if (stack_length) { \ - if (first_chunk) { \ - PUSH_SEQ(ret, anchor); \ - PUSH_INSN1(ret, location, newhash, INT2FIX(stack_length)); \ - first_chunk = false; \ - } \ - else { \ - PUSH_INSN1(ret, location, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); \ - PUSH_INSN(ret, location, swap); \ - PUSH_SEQ(ret, anchor); \ - PUSH_SEND(ret, location, id_core_hash_merge_ptr, INT2FIX(stack_length + 1)); \ - } \ - INIT_ANCHOR(anchor); \ - stack_length = 0; \ +#define FLUSH_CHUNK \ + if (stack_length) { \ + if (first_chunk) { \ + PUSH_SEQ(ret, anchor); \ + PUSH_INSN1(ret, location, newhash, INT2FIX(stack_length)); \ + first_chunk = false; \ + } \ + else { \ + PUSH_INSN1(ret, location, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); \ + PUSH_INSN(ret, location, swap); \ + PUSH_SEQ(ret, anchor); \ + if (owned_hash) { \ + PUSH_SEND(ret, location, id_core_hash_merge_bang_ptr, INT2FIX(stack_length + 1)); \ + } \ + else { \ + PUSH_SEND(ret, location, id_core_hash_merge_ptr, INT2FIX(stack_length + 1)); \ + owned_hash = true; \ + } \ + } \ + INIT_ANCHOR(anchor); \ + stack_length = 0; \ } for (size_t index = 0; index < elements->size; index++) { @@ -1538,7 +1545,9 @@ pm_compile_hash_elements(rb_iseq_t *iseq, const pm_node_t *node, const pm_node_l size_t count = 1; while (index + count < elements->size && PM_NODE_FLAG_P(elements->nodes[index + count], PM_NODE_FLAG_STATIC_LITERAL)) count++; - if ((first_chunk && stack_length == 0) || count >= min_tmp_hash_length) { + bool first_element = first_chunk && stack_length == 0; + + if (first_element || count >= min_tmp_hash_length) { // The subsequence of elements in this hash is long enough // to merit its own hash. VALUE ary = rb_ary_hidden_new(count); @@ -1564,7 +1573,14 @@ pm_compile_hash_elements(rb_iseq_t *iseq, const pm_node_t *node, const pm_node_l // Emit optimized code. FLUSH_CHUNK; if (first_chunk) { - PUSH_INSN1(ret, location, duphash, hash); + if (count == elements->size) { + // A fully literal hash. + PUSH_INSN1(ret, location, duphash, hash); + } + else { + // Partial hash that will be merged with a newly built one, no need to dup + PUSH_INSN1(ret, location, putobject, rb_obj_reveal(hash, rb_cHash)); + } first_chunk = false; } else { @@ -1611,61 +1627,60 @@ pm_compile_hash_elements(rb_iseq_t *iseq, const pm_node_t *node, const pm_node_l bool last_element = index == elements->size - 1; bool only_element = first_element && last_element; - if (empty_hash) { - if (only_element && argument) { - // **{} appears at the only keyword argument in method call, - // so it won't be modified. - // - // This is only done for method calls and not for literal - // hashes, because literal hashes should always result in a - // new hash. - PUSH_INSN(ret, location, putnil); - } - else if (first_element) { - // **{} appears as the first keyword argument, so it may be - // modified. We need to create a fresh hash object. - PUSH_INSN1(ret, location, newhash, INT2FIX(0)); + if (only_element) { + if (argument) { + if (empty_hash) { + // **{} appears at the only keyword argument in method call + // We can substitute it for `**nil`. + PUSH_INSN(ret, location, putnil); + } + else { + // ** is the only element. + // Since we're in a method call, we can use it directly. + // This will be not be flagged as mutable. + if (shareability == 0) { + PM_COMPILE_NOT_POPPED(element); + } + else { + pm_compile_shareable_constant_value(iseq, element, shareability, path, ret, scope_node, false); + } + } } - // Any empty keyword splats that are not the first can be - // ignored since merging an empty hash into the existing hash is - // the same as not merging it. - } - else { - if (only_element && argument) { - // ** is only keyword argument in the method call. Use it - // directly. This will be not be flagged as mutable. This is - // only done for method calls and not for literal hashes, - // because literal hashes should always result in a new - // hash. + else { + // {**something} + // We can simply call `core_hash_coerce(something)` to ensure it is coerced + // into a mutable Hash. + PUSH_INSN1(ret, location, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); if (shareability == 0) { PM_COMPILE_NOT_POPPED(element); } else { pm_compile_shareable_constant_value(iseq, element, shareability, path, ret, scope_node, false); } + PUSH_SEND(ret, location, id_core_hash_coerce, INT2FIX(1)); } - else { - // There is more than one keyword argument, or this is not a - // method call. In that case, we need to add an empty hash - // (if first keyword), or merge the hash to the accumulated - // hash (if not the first keyword). + } + else { + if (!first_element) { PUSH_INSN1(ret, location, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); + PUSH_INSN(ret, location, swap); + } - if (first_element) { - PUSH_INSN1(ret, location, newhash, INT2FIX(0)); - } - else { - PUSH_INSN(ret, location, swap); - } + if (shareability == 0) { + PM_COMPILE_NOT_POPPED(element); + } + else { + pm_compile_shareable_constant_value(iseq, element, shareability, path, ret, scope_node, false); + } - if (shareability == 0) { - PM_COMPILE_NOT_POPPED(element); + if (!first_element) { + if (owned_hash) { + PUSH_SEND(ret, location, id_core_hash_merge_bang_kwd, INT2FIX(2)); } else { - pm_compile_shareable_constant_value(iseq, element, shareability, path, ret, scope_node, false); + PUSH_SEND(ret, location, id_core_hash_merge_kwd, INT2FIX(2)); + owned_hash = true; } - - PUSH_SEND(ret, location, id_core_hash_merge_kwd, INT2FIX(2)); } } diff --git a/spec/bundler/bundler/cli_common_spec.rb b/spec/bundler/bundler/cli_common_spec.rb index 015894b3a13e2e..d38dc998c82d26 100644 --- a/spec/bundler/bundler/cli_common_spec.rb +++ b/spec/bundler/bundler/cli_common_spec.rb @@ -19,4 +19,53 @@ expect(message).to match(/Did you mean 'method(|s)' or 'method(|s)'?/) end end + + describe "validate_cooldown!" do + def warning(inspected) + "Invalid cooldown value #{inspected}, so the cooldown is disabled for all sources. " \ + "Expected a non-negative integer number of days." + end + + it "rejects a negative flag value" do + expect { subject.validate_cooldown!(-1) }.to raise_error( + Bundler::InvalidOption, "Expected `--cooldown` to be a non-negative integer, got -1" + ) + end + + it "accepts a non-negative flag value without reading the settings" do + expect(Bundler.ui).not_to receive(:warn) + expect { subject.validate_cooldown!(0) }.not_to raise_error + expect { subject.validate_cooldown!(7) }.not_to raise_error + end + + context "when no flag is given" do + it "warns when the configured value is not a number" do + Bundler.settings.temporary(cooldown: "abc") do + expect(Bundler.ui).to receive(:warn).with(warning('"abc"')).once + subject.validate_cooldown!(nil) + end + end + + it "warns when the configured value is negative" do + Bundler.settings.temporary(cooldown: "-5") do + expect(Bundler.ui).to receive(:warn).with(warning('"-5"')).once + subject.validate_cooldown!(nil) + end + end + + it "warns when the configured value is only partly a number" do + Bundler.settings.temporary(cooldown: "7days") do + expect(Bundler.ui).to receive(:warn).with(warning('"7days"')).once + subject.validate_cooldown!(nil) + end + end + + it "stays quiet for a valid value, an explicit 0, and an unset value" do + expect(Bundler.ui).not_to receive(:warn) + Bundler.settings.temporary(cooldown: "7") { subject.validate_cooldown!(nil) } + Bundler.settings.temporary(cooldown: "0") { subject.validate_cooldown!(nil) } + subject.validate_cooldown!(nil) + end + end + end end diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb index 690f25caa31c50..229ea34dda66f0 100644 --- a/spec/bundler/bundler/endpoint_specification_spec.rb +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -10,6 +10,14 @@ subject(:spec) { described_class.new(name, version, platform, spec_fetcher, dependencies, metadata) } + def with_tz(tz) + orig_tz = ENV["TZ"] + ENV["TZ"] = tz + yield + ensure + ENV["TZ"] = orig_tz + end + describe "#build_dependency" do let(:name) { "foo" } let(:requirement1) { "~> 1.1" } @@ -63,6 +71,24 @@ end end + context "when created_at has no time zone offset" do + let(:metadata) { { "created_at" => "2026-05-12T10:00:00" } } + + it "is interpreted as UTC regardless of the local time zone" do + with_tz("Asia/Tokyo") do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0)) + end + end + end + + context "when created_at has an explicit offset" do + let(:metadata) { { "created_at" => "2026-05-12T10:00:00+02:00" } } + + it "keeps the offset" do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 8, 0, 0)) + end + end + context "when created_at is truncated (older rubygems splits on colons)" do let(:metadata) { { "created_at" => "2026-05-12T10" } } diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb index 5e1aaaa5551109..9ac3603caf11d0 100644 --- a/spec/bundler/bundler/settings_spec.rb +++ b/spec/bundler/bundler/settings_spec.rb @@ -124,6 +124,14 @@ settings.set_local :cooldown, "7" expect(settings[:cooldown]).to be 7 end + + it "coerces an unparsable cooldown to 0 without warning here" do + # The warning lives in CLI::Common so it fires once per command, + # outside any Bundler.ui.silence block. See cli_common_spec.rb. + expect(Bundler.ui).not_to receive(:warn) + settings.set_local :cooldown, "abc" + expect(settings[:cooldown]).to be 0 + end end context "when it's not possible to create the settings directory" do diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb index 27430d4a3bb72e..0fca8dbd672525 100644 --- a/spec/bundler/bundler/source/rubygems/remote_spec.rb +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -203,5 +203,21 @@ def remote(uri) expect(remote(uri_no_auth).effective_cooldown).to eq(14) end end + + it "reads the settings only once, however many candidates ask" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(Bundler.settings).to receive(:[]).with(:cooldown).once.and_return(14) + + expect(r.effective_cooldown).to eq(14) + expect(r.effective_cooldown).to eq(14) + end + + it "memoizes an absent override without re-reading the settings" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(Bundler.settings).to receive(:[]).with(:cooldown).once.and_return(nil) + + expect(r.effective_cooldown).to eq(7) + expect(r.effective_cooldown).to eq(7) + end end end diff --git a/spec/bundler/cache/path_spec.rb b/spec/bundler/cache/path_spec.rb index 42648aea1fc5a4..1cb3047574b79b 100644 --- a/spec/bundler/cache/path_spec.rb +++ b/spec/bundler/cache/path_spec.rb @@ -92,6 +92,31 @@ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist end + it "removes stale entries whose names look like home directory expansions" do + build_lib "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle :cache + + tilde_entry = bundled_app("vendor/cache/~") + # a name no account can have, so a regression cannot resolve it to a real home + tilde_user_entry = bundled_app("vendor/cache/~nonexistent.user") + [tilde_entry, tilde_user_entry].each do |entry| + FileUtils.mkdir_p entry + FileUtils.touch entry.join(".bundlecache") + end + + bundle :cache + + expect(tilde_entry).not_to exist + expect(tilde_user_entry).not_to exist + expect(home).to exist + end + it "does not cache path gems if cache_all is set to false" do build_lib "foo" diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 7cf6dde932b254..b45d50b1f2e1bc 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -108,6 +108,18 @@ s.date = now - (30 * 86_400) end + # an adoptable version sits between the installed one and the + # in-cooldown newest one + build_gem "mid_gem", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "mid_gem", "1.5.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "mid_gem", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + # every published version is inside the cooldown window build_gem "fresh_gem", "0.3.1" do |s| s.date = now - (1 * 86_400) @@ -466,6 +478,151 @@ expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/) end + it "shows the newest out-of-cooldown version next to the in-cooldown newest one" do + gemfile <<-G + source "https://gem.repo3" + gem "mid_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + mid_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mid_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/mid_gem \(newest 2\.0\.0, installed 1\.0\.0.*in cooldown for \d+ more days, newest out of cooldown 1\.5\.0\)/) + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/mid_gem.*2\.0\.0 \(cooldown \d+d, 1\.5\.0 out of cooldown\)/) + end + + it "shows the resolved version without cooldown notes in strict mode" do + gemfile <<-G + source "https://gem.repo3" + gem "mid_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + mid_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mid_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --strict --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + # in strict mode "newest" is the resolved (cooldown-filtered) version + # itself, so the annotations have nothing to add + expect(out).to match(/mid_gem \(newest 1\.5\.0, installed 1\.0\.0/) + expect(out).not_to include("cooldown") + end + + it "shows no out-of-cooldown note when every version is inside the window" do + gemfile <<-G + source "https://gem.repo3" + gem "fresh_gem", "0.3.1" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + fresh_gem (0.3.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + fresh_gem (= 0.3.1) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/fresh_gem.*in cooldown for \d+ more day/) + expect(out).not_to include("out of cooldown") + end + + it "uses the singular form when one cooldown day remains" do + gemfile <<-G + source "https://gem.repo3" + gem "mid_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + mid_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mid_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + # mid_gem 2.0.0 is one day old, so a two-day window leaves one day + bundle "outdated --cooldown 2 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/mid_gem \(newest 2\.0\.0, installed 1\.0\.0.*in cooldown for 1 more day, newest out of cooldown 1\.5\.0\)/) + end + + it "leaves bundle outdated output untouched when cooldown is not enabled" do + gemfile <<-G + source "https://gem.repo3" + gem "mid_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + mid_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mid_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/mid_gem \(newest 2\.0\.0, installed 1\.0\.0/) + expect(out).not_to include("cooldown") + end + it "excludes a locally-installed version that is still within the cooldown window" do system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3 diff --git a/spec/ruby/core/io/buffer/free_spec.rb b/spec/ruby/core/io/buffer/free_spec.rb index 20fb7b901f4f3d..fe3a774201cbd4 100644 --- a/spec/ruby/core/io/buffer/free_spec.rb +++ b/spec/ruby/core/io/buffer/free_spec.rb @@ -69,6 +69,38 @@ buffer.null?.should == true end + ruby_version_is "3.4" do + it "resets the buffer to a valid empty state" do + buffer = IO::Buffer.new(4) + buffer.free + + buffer.null?.should == true + buffer.empty?.should == true + buffer.valid?.should == true + buffer.get_string.should == "" + end + end + + ruby_version_is "4.1" do + it "raises FrozenError without freeing a frozen buffer" do + buffer = IO::Buffer.new(4) + buffer.set_string("test") + buffer.freeze + + -> { buffer.free }.should.raise(FrozenError) + buffer.null?.should == false + buffer.get_string.should == "test" + end + + it "allows internal cleanup after a buffer is frozen inside .for" do + string = +"test" + buffer = IO::Buffer.for(string, &:freeze) + + buffer.null?.should == true + (string << "!").should == "test!" + end + end + it "is disallowed while locked, raising IO::Buffer::LockedError" do buffer = IO::Buffer.new(4) buffer.locked do diff --git a/spec/ruby/core/io/buffer/initialize_spec.rb b/spec/ruby/core/io/buffer/initialize_spec.rb index dba6fddc7994aa..4a7019a521dd2c 100644 --- a/spec/ruby/core/io/buffer/initialize_spec.rb +++ b/spec/ruby/core/io/buffer/initialize_spec.rb @@ -81,12 +81,26 @@ end it "allows extra flags" do - @buffer = IO::Buffer.new(10, IO::Buffer::INTERNAL | IO::Buffer::SHARED | IO::Buffer::READONLY) - @buffer.should.internal? + @buffer = IO::Buffer.new(10, IO::Buffer::MAPPED | IO::Buffer::SHARED | IO::Buffer::READONLY) + @buffer.should.mapped? @buffer.should.shared? @buffer.should.readonly? end + ruby_version_is "4.1" do + it "infers IO::Buffer::MAPPED from IO::Buffer::SHARED" do + @buffer = IO::Buffer.new(10, IO::Buffer::SHARED) + @buffer.should.mapped? + @buffer.should.shared? + end + + it "infers IO::Buffer::MAPPED from IO::Buffer::PRIVATE" do + @buffer = IO::Buffer.new(10, IO::Buffer::PRIVATE) + @buffer.should.mapped? + @buffer.should.private? + end + end + it "ignores flags if size is 0" do @buffer = IO::Buffer.new(0, 0xffff) @buffer.should.null? @@ -103,9 +117,46 @@ @buffer.should.valid? end - it "raises IO::Buffer::AllocationError if neither IO::Buffer::MAPPED nor IO::Buffer::INTERNAL is given" do - -> { IO::Buffer.new(10, IO::Buffer::READONLY) }.should.raise(IO::Buffer::AllocationError, "Could not allocate buffer!") - -> { IO::Buffer.new(10, 0) }.should.raise(IO::Buffer::AllocationError, "Could not allocate buffer!") + ruby_version_is ""..."4.1" do + it "raises IO::Buffer::AllocationError if neither IO::Buffer::MAPPED nor IO::Buffer::INTERNAL is given" do + -> { IO::Buffer.new(10, IO::Buffer::READONLY) }.should.raise(IO::Buffer::AllocationError, "Could not allocate buffer!") + -> { IO::Buffer.new(10, 0) }.should.raise(IO::Buffer::AllocationError, "Could not allocate buffer!") + end + end + + ruby_version_is "4.1" do + it "infers the allocation mode if neither IO::Buffer::MAPPED nor IO::Buffer::INTERNAL is given" do + @buffer = IO::Buffer.new(10, IO::Buffer::READONLY) + @buffer.should.internal? + @buffer.should.readonly? + + @buffer.free + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, 0) + @buffer.should.mapped? + end + + it "raises ArgumentError if both IO::Buffer::MAPPED and IO::Buffer::INTERNAL are given" do + flags = IO::Buffer::INTERNAL | IO::Buffer::MAPPED + -> { IO::Buffer.new(10, flags) }.should.raise(ArgumentError, "Flags can't include both IO::Buffer::INTERNAL and IO::Buffer::MAPPED!") + end + + it "raises ArgumentError if IO::Buffer::EXTERNAL is given" do + flags = IO::Buffer::INTERNAL | IO::Buffer::EXTERNAL + -> { IO::Buffer.new(10, flags) }.should.raise(ArgumentError, "IO::Buffer::EXTERNAL can't be used with IO::Buffer.new!") + end + + it "raises ArgumentError if mapping flags are given with IO::Buffer::INTERNAL" do + flags = IO::Buffer::INTERNAL | IO::Buffer::SHARED + -> { IO::Buffer.new(10, flags) }.should.raise(ArgumentError, "IO::Buffer::SHARED and IO::Buffer::PRIVATE require IO::Buffer::MAPPED!") + + flags = IO::Buffer::INTERNAL | IO::Buffer::PRIVATE + -> { IO::Buffer.new(10, flags) }.should.raise(ArgumentError, "IO::Buffer::SHARED and IO::Buffer::PRIVATE require IO::Buffer::MAPPED!") + end + + it "raises ArgumentError if both IO::Buffer::SHARED and IO::Buffer::PRIVATE are given" do + flags = IO::Buffer::SHARED | IO::Buffer::PRIVATE + -> { IO::Buffer.new(10, flags) }.should.raise(ArgumentError, "Flags can't include both IO::Buffer::SHARED and IO::Buffer::PRIVATE!") + end end it "raises ArgumentError if flags is negative" do diff --git a/spec/ruby/core/io/buffer/map_spec.rb b/spec/ruby/core/io/buffer/map_spec.rb index 93ce90c393f06b..a711271fd54599 100644 --- a/spec/ruby/core/io/buffer/map_spec.rb +++ b/spec/ruby/core/io/buffer/map_spec.rb @@ -284,6 +284,42 @@ def open_big_file_fixture end context "with flags argument" do + ruby_version_is "4.1" do + it "allows the redundant MAPPED flag" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::MAPPED) + + @buffer.should.mapped? + @buffer.should.shared? + end + + it "raises ArgumentError if INTERNAL is specified" do + @file = open_fixture + -> { IO::Buffer.map(@file, nil, 0, IO::Buffer::INTERNAL) }.should.raise( + ArgumentError, + "IO::Buffer::INTERNAL can't be used with IO::Buffer.map!" + ) + end + + it "raises ArgumentError if EXTERNAL is specified" do + @file = open_fixture + -> { IO::Buffer.map(@file, nil, 0, IO::Buffer::EXTERNAL) }.should.raise( + ArgumentError, + "IO::Buffer::EXTERNAL can't be used with IO::Buffer.map!" + ) + end + + it "raises ArgumentError if both SHARED and PRIVATE are specified" do + @file = open_fixture + flags = IO::Buffer::SHARED | IO::Buffer::PRIVATE + + -> { IO::Buffer.map(@file, nil, 0, flags) }.should.raise( + ArgumentError, + "Flags can't include both IO::Buffer::SHARED and IO::Buffer::PRIVATE!" + ) + end + end + context "when READONLY flag is specified" do it "sets readonly flag on the buffer, allowing only reads" do @file = open_fixture diff --git a/spec/ruby/core/io/buffer/private_spec.rb b/spec/ruby/core/io/buffer/private_spec.rb index 6e6afee34ca10f..2222848b14df1c 100644 --- a/spec/ruby/core/io/buffer/private_spec.rb +++ b/spec/ruby/core/io/buffer/private_spec.rb @@ -7,7 +7,7 @@ end it "is true for a buffer created with PRIVATE flag" do - @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::PRIVATE) + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED | IO::Buffer::PRIVATE) @buffer.private?.should == true end diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb index 35ff38145214aa..c0c94ade81e8e0 100644 --- a/spec/ruby/core/io/buffer/resize_spec.rb +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -115,6 +115,18 @@ @buffer.size.should == 1 end + ruby_version_is "4.1" do + it "raises FrozenError without resizing a frozen buffer" do + buffer = IO::Buffer.new(4) + buffer.set_string("test") + buffer.freeze + + -> { buffer.resize(8) }.should.raise(FrozenError) + buffer.size.should == 4 + buffer.get_string.should == "test" + end + end + it "always clears extra memory" do @buffer = IO::Buffer.new(4) @buffer.set_string("test") diff --git a/spec/ruby/core/io/buffer/shared_spec.rb b/spec/ruby/core/io/buffer/shared_spec.rb index 2cc93e6d08c07a..8f97cccc4ad344 100644 --- a/spec/ruby/core/io/buffer/shared_spec.rb +++ b/spec/ruby/core/io/buffer/shared_spec.rb @@ -7,7 +7,7 @@ end it "is true for a buffer created with SHARED flag" do - @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::SHARED) + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED | IO::Buffer::SHARED) @buffer.shared?.should == true end diff --git a/spec/ruby/core/io/buffer/transfer_spec.rb b/spec/ruby/core/io/buffer/transfer_spec.rb index 3bc08998dde737..82e1380aab6f94 100644 --- a/spec/ruby/core/io/buffer/transfer_spec.rb +++ b/spec/ruby/core/io/buffer/transfer_spec.rb @@ -81,6 +81,18 @@ @buffer.null?.should == false end + ruby_version_is "4.1" do + it "raises FrozenError without transferring a frozen buffer" do + buffer = IO::Buffer.new(4) + buffer.set_string("test") + buffer.freeze + + -> { buffer.transfer }.should.raise(FrozenError) + buffer.null?.should == false + buffer.get_string.should == "test" + end + end + it "is disallowed while locked, raising IO::Buffer::LockedError" do @buffer = IO::Buffer.new(4) @buffer.locked do diff --git a/spec/ruby/optional/capi/hash_spec.rb b/spec/ruby/optional/capi/hash_spec.rb index 842bc82b970901..dc36eefa7ce19a 100644 --- a/spec/ruby/optional/capi/hash_spec.rb +++ b/spec/ruby/optional/capi/hash_spec.rb @@ -59,8 +59,16 @@ @s.rb_hash_new_capa(3) {}.default_proc.should == nil end - it "raises RuntimeError when negative index is provided" do - -> { @s.rb_hash_new_capa(-1) }.should.raise(RuntimeError, "st_table too big") + ruby_version_is ""..."4.1" do + it "raises RuntimeError when negative index is provided" do + -> { @s.rb_hash_new_capa(-1) }.should.raise(RuntimeError, "st_table too big") + end + end + + ruby_version_is "4.1" do + it "raises ArgumentError when negative index is provided" do + -> { @s.rb_hash_new_capa(-1) }.should.raise(ArgumentError, "negative hash size (or size too big)") + end end end diff --git a/test/ruby/test_allocation.rb b/test/ruby/test_allocation.rb index 7047eddec70112..ae55c5fc637ee3 100644 --- a/test/ruby/test_allocation.rb +++ b/test/ruby/test_allocation.rb @@ -70,7 +70,11 @@ def self.num_allocations #{checks} - assert_empty(failures) + if failures.empty? + assert true + else + assert false, failures.join("\\n") + end RUBY end diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 78a34bcc0307cf..1e6baac2710242 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -125,6 +125,74 @@ def test_new_readonly end end + def test_new_invalid_flags + assert_raise(ArgumentError) do + IO::Buffer.new(128, IO::Buffer::EXTERNAL) + end + + assert_raise(ArgumentError) do + IO::Buffer.new(128, IO::Buffer::INTERNAL | IO::Buffer::MAPPED) + end + + assert_raise(ArgumentError) do + IO::Buffer.new(128, IO::Buffer::INTERNAL | IO::Buffer::SHARED) + end + + assert_raise(ArgumentError) do + IO::Buffer.new(128, IO::Buffer::INTERNAL | IO::Buffer::PRIVATE) + end + + assert_raise(ArgumentError) do + IO::Buffer.new(128, IO::Buffer::MAPPED | IO::Buffer::SHARED | IO::Buffer::PRIVATE) + end + end + + def test_new_infers_allocation_mode_with_flags + internal = IO::Buffer.new(128, IO::Buffer::READONLY) + assert_predicate internal, :internal? + assert_predicate internal, :readonly? + + mapped = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::READONLY) + assert_predicate mapped, :mapped? + assert_predicate mapped, :readonly? + + shared = IO::Buffer.new(128, IO::Buffer::SHARED) + assert_predicate shared, :mapped? + assert_predicate shared, :shared? + + private_buffer = IO::Buffer.new(128, IO::Buffer::PRIVATE) + assert_predicate private_buffer, :mapped? + assert_predicate private_buffer, :private? + end + + def test_map_invalid_flags + File.open(__FILE__) do |file| + assert_raise(ArgumentError) do + IO::Buffer.map(file, nil, 0, IO::Buffer::INTERNAL) + end + + assert_raise(ArgumentError) do + IO::Buffer.map(file, nil, 0, IO::Buffer::EXTERNAL) + end + + assert_raise(ArgumentError) do + IO::Buffer.map(file, nil, 0, IO::Buffer::SHARED | IO::Buffer::PRIVATE) + end + end + end + + def test_map_allows_redundant_mapped_flag + buffer = File.open(__FILE__) do |file| + IO::Buffer.map(file, nil, 0, IO::Buffer::MAPPED | IO::Buffer::READONLY) + end + + assert_predicate buffer, :mapped? + assert_predicate buffer, :shared? + assert_predicate buffer, :readonly? + ensure + buffer&.free + end + def test_file_mapped buffer = File.open(__FILE__) {|file| IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)} assert_equal File.size(__FILE__), buffer.size @@ -404,6 +472,79 @@ def test_resize_invalidated_slice end end + def test_resize_after_free + buffer = IO::Buffer.new(4) + buffer.set_string("test") + buffer.free + assert_predicate buffer, :null? + + buffer.resize(8) + assert_equal 8, buffer.size + refute_predicate buffer, :null? + end + + def test_free_frozen + buffer = IO::Buffer.new(4) + buffer.set_string("test") + buffer.freeze + + assert_raise(FrozenError) do + buffer.free + end + + refute_predicate buffer, :null? + assert_equal "test", buffer.get_string + end + + def test_free_frozen_external + string = "Hello World".freeze + buffer = IO::Buffer.for(string) + buffer.freeze + + assert_raise(FrozenError) do + buffer.free + end + + refute_predicate buffer, :null? + assert_equal string, buffer.get_string + end + + def test_free_frozen_in_block + string = +"Hello World" + + buffer = IO::Buffer.for(string, &:freeze) + assert_predicate buffer, :null? + + # The string is no longer locked by the buffer: + assert_equal "Hello World!", string << "!" + end + + def test_resize_frozen + buffer = IO::Buffer.new(4) + buffer.set_string("test") + buffer.freeze + + assert_raise(FrozenError) do + buffer.resize(8) + end + + assert_equal 4, buffer.size + assert_equal "test", buffer.get_string + end + + def test_resize_frozen_external + string = "Hello World".freeze + buffer = IO::Buffer.for(string) + buffer.freeze + + assert_raise(FrozenError) do + buffer.resize(8) + end + + assert_equal string.bytesize, buffer.size + assert_equal string, buffer.get_string + end + def test_compare_same_size buffer1 = IO::Buffer.new(1) assert_equal buffer1, buffer1 @@ -427,9 +568,11 @@ def test_compare_different_size def test_compare_zero_length buffer1 = IO::Buffer.new(0) buffer2 = IO::Buffer.new(1) + buffer3 = IO::Buffer.new(0) assert_negative buffer1 <=> buffer2 assert_positive buffer2 <=> buffer1 + assert_equal 0, buffer1 <=> buffer3 end def test_slice @@ -535,6 +678,32 @@ def test_transfer_in_block assert_equal "Ciao! World", hello end + def test_transfer_frozen + buffer = IO::Buffer.new(4) + buffer.set_string("test") + buffer.freeze + + assert_raise(FrozenError) do + buffer.transfer + end + + refute_predicate buffer, :null? + assert_equal "test", buffer.get_string + end + + def test_transfer_frozen_external + string = "Hello World".freeze + buffer = IO::Buffer.for(string) + buffer.freeze + + assert_raise(FrozenError) do + buffer.transfer + end + + refute_predicate buffer, :null? + assert_equal string, buffer.get_string + end + def test_counted_locking buffer = IO::Buffer.new(128) @@ -887,6 +1056,13 @@ def test_clear assert_raise(ArgumentError) {buffer.clear(0, SIZE_MAX-7, 10)} end + def test_clear_zero_length + buffer = IO::Buffer.new(0) + + assert_same buffer, buffer.clear + assert_predicate buffer, :empty? + end + def test_invalidation input, output = IO.pipe @@ -1020,6 +1196,31 @@ def test_write_with_length_and_offset io.close! end + def test_zero_length_io + io = Tempfile.new + + assert_zero_length_io = proc do |buffer, offset = 0| + assert_equal 0, buffer.read(io, 0, offset) + assert_equal 0, buffer.pread(io, 0, 0, offset) + assert_equal 0, buffer.write(io, 0, offset) + assert_equal 0, buffer.pwrite(io, 0, 0, offset) + end + + buffer = IO::Buffer.new(0) + assert_predicate buffer, :null? + assert_zero_length_io.call(buffer) + + IO::Buffer.for("") do |buffer| + refute_predicate buffer, :null? + assert_zero_length_io.call(buffer) + end + + buffer = IO::Buffer.new(8) + assert_zero_length_io.call(buffer, buffer.size) + ensure + io.close! + end + def test_pread io = Tempfile.new io.write("Hello World") diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb index 1fb19c61326a7f..576f219dac295d 100644 --- a/test/ruby/test_keyword.rb +++ b/test/ruby/test_keyword.rb @@ -224,6 +224,8 @@ def self.skws(*a, **kw); [a, kw] end assert_equal([[], {}], skws(*nil, **nil)) assert_equal({}, {**nil}) + assert_equal({}, {**nil, **nil}) + assert_equal({}, {**nil, **nil, **nil}) assert_equal({a: 1}, {a: 1, **nil}) assert_equal({a: 1}, {**nil, a: 1}) end @@ -497,6 +499,7 @@ def self.method_missing(_, **kw) kw end def self.yo(*a, **kw) = kw assert_equal_not_same kw, yo(**kw) assert_equal_not_same kw, yo(**kw, **kw) + assert_equal_not_same kw, yo(**kw, **kw, **kw, **kw) singleton_class.send(:remove_method, :yo) def self.yo(opts) = opts @@ -4187,7 +4190,7 @@ def initialize(d:, **h) end end - assert_raise_with_message(TypeError, /expected Hash/, bug13015) do + assert_raise_with_message(TypeError, /no implicit conversion of Array into Hash/, bug13015) do klass.new(d: 4) end end diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index dbb53600ba650a..73360ced5c848c 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -545,6 +545,7 @@ def teardown if defined? Gem::Cooldown Gem::Cooldown.reset_warned_missing_created_at + Gem::Cooldown.reset_warned_invalid_days end Dir.chdir @current_dir diff --git a/test/rubygems/test_gem_commands_contents_command.rb b/test/rubygems/test_gem_commands_contents_command.rb index 049afe8a014ad1..a67ecfd63469b8 100644 --- a/test/rubygems/test_gem_commands_contents_command.rb +++ b/test/rubygems/test_gem_commands_contents_command.rb @@ -92,6 +92,47 @@ def test_execute_lib_only assert_equal "", @ui.error end + def test_files_in_gem_with_glob_metacharacters_in_install_path + base_dir = File.join @tempdir, "dir[1]" + spec = util_spec "foo" + spec.loaded_from = File.join base_dir, "specifications", spec.spec_name + + write_file File.join(spec.full_gem_path, "lib", "foo.rb") + + files = @cmd.files_in_gem spec + + assert_includes files, [spec.full_gem_path, "lib/foo.rb"] + end + + def test_files_in_gem_lib_only_stays_inside_gem_dir_with_extensions + @cmd.options[:lib_only] = true + + spec = util_spec "foo" do |s| + s.extensions = %w[ext/foo/extconf.rb] + end + + write_file File.join(spec.full_gem_path, "lib", "foo.rb") + write_file File.join(spec.extension_dir, "foo.so") + + files = @cmd.files_in_gem spec + + assert_includes files, [spec.full_gem_path, "lib/foo.rb"] + files.each do |_prefix, relative| + refute File.absolute_path?(relative), "#{relative} escapes the gem directory" + end + end + + def test_files_in_gem_lib_only_without_require_paths + @cmd.options[:lib_only] = true + + spec = util_spec "foo" + spec.require_paths = [] + + write_file File.join(spec.full_gem_path, "lib", "foo.rb") + + assert_empty @cmd.files_in_gem(spec) + end + def test_execute_missing_single @cmd.options[:args] = %w[foo] diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index 6ae389db2fba49..a99afbfef74e7f 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -855,6 +855,14 @@ def test_cooldown_option assert_equal 7, @cmd.options[:cooldown] end + def test_cooldown_option_negative + e = assert_raise Gem::OptionParser::InvalidArgument do + @cmd.handle_options %w[--cooldown -7 a] + end + + assert_match "--cooldown", e.message + end + def test_execute_with_invalid_gem_file FileUtils.touch("a.gem") diff --git a/test/rubygems/test_gem_commands_stale_command.rb b/test/rubygems/test_gem_commands_stale_command.rb index ea7493b418801c..556b803f4e2326 100644 --- a/test/rubygems/test_gem_commands_stale_command.rb +++ b/test/rubygems/test_gem_commands_stale_command.rb @@ -40,4 +40,25 @@ def test_execute_sorts assert_equal("#{foo_bar.name}-#{foo_bar.version}", lines[0].split.first) assert_equal("#{bar_baz.name}-#{bar_baz.version}", lines[1].split.first) end + + def test_execute_with_glob_metacharacters_in_gem_path + gemhome2 = File.join(@tempdir, "gemhome[2]") + Gem.use_paths gemhome2 + + foo = util_spec "foo" do |gem| + gem.files = %w[lib/foo.rb] + end + install_specs foo + + filename = File.join(gemhome2, "gems", foo.full_name, "lib", "foo.rb") + FileUtils.mkdir_p File.dirname filename + FileUtils.touch filename + + use_ui @stub_ui do + @cmd.execute + end + + listed_gems = @stub_ui.output.split("\n").map {|line| line.split.first } + assert_includes listed_gems, "#{foo.name}-#{foo.version}" + end end diff --git a/test/rubygems/test_gem_cooldown.rb b/test/rubygems/test_gem_cooldown.rb index 704b6c169f26f0..fe2ea4afd3cbb8 100644 --- a/test/rubygems/test_gem_cooldown.rb +++ b/test/rubygems/test_gem_cooldown.rb @@ -42,6 +42,85 @@ def test_from_options Gem.configuration.cooldown = orig_cooldown end + def test_invalid_days_warns_once_and_fails_open + use_ui @ui do + refute Gem::Cooldown.new("abc").active? + refute Gem::Cooldown.new("abc").active? + end + + assert_equal 1, @ui.error.scan("Invalid cooldown value").size + assert_match 'Invalid cooldown value "abc", so the cooldown is disabled.', @ui.error + assert_match "Expected a non-negative integer number of days.", @ui.error + end + + def test_negative_days_warns_and_fails_open + use_ui @ui do + refute Gem::Cooldown.new(-5).active? + end + + assert_match "Invalid cooldown value -5", @ui.error + end + + def test_partly_numeric_days_warns_and_fails_open + use_ui @ui do + refute Gem::Cooldown.new("7days").active? + end + + assert_match 'Invalid cooldown value "7days"', @ui.error + end + + def test_non_numeric_type_warns_instead_of_raising + use_ui @ui do + [true, [7], :sym].each do |value| + refute Gem::Cooldown.new(value).active? + end + end + + assert_match "Invalid cooldown value true", @ui.error + end + + def test_valid_days_do_not_warn + use_ui @ui do + Gem::Cooldown.new 7 + Gem::Cooldown.new 0 + Gem::Cooldown.new "3" + Gem::Cooldown.new nil + end + + assert_empty @ui.error + end + + def test_parse_created_at_without_offset_is_utc + with_tz "Asia/Tokyo" do + assert_equal Time.utc(2026, 6, 5, 10, 30, 45), + Gem::Cooldown.parse_created_at("2026-06-05T10:30:45") + end + end + + def test_parse_created_at_keeps_explicit_offset + assert_equal Time.utc(2026, 6, 5, 8, 30, 45), + Gem::Cooldown.parse_created_at("2026-06-05T10:30:45+02:00") + + assert_equal Time.utc(2026, 6, 5, 10, 30, 45), + Gem::Cooldown.parse_created_at("2026-06-05T10:30:45Z") + end + + def test_parse_created_at_invalid + assert_nil Gem::Cooldown.parse_created_at("not a timestamp") + assert_nil Gem::Cooldown.parse_created_at("2026") + assert_nil Gem::Cooldown.parse_created_at("2026-06-05T10") + assert_nil Gem::Cooldown.parse_created_at(nil) + assert_nil Gem::Cooldown.parse_created_at(7) + end + + def with_tz(tz) + orig_tz = ENV["TZ"] + ENV["TZ"] = tz + yield + ensure + ENV["TZ"] = orig_tz + end + def test_warn_missing_created_at_warns_once source = Gem::Source.new @gem_repo diff --git a/test/rubygems/test_gem_resolver_api_specification.rb b/test/rubygems/test_gem_resolver_api_specification.rb index a58dd75de05f00..44d2ee254a37e1 100644 --- a/test/rubygems/test_gem_resolver_api_specification.rb +++ b/test/rubygems/test_gem_resolver_api_specification.rb @@ -75,6 +75,46 @@ def test_initialize_created_at_non_iso8601 assert_nil spec.created_at end + def test_initialize_created_at_without_offset_is_utc + set = Gem::Resolver::APISet.new + data = { + name: "rails", + number: "3.0.3", + platform: "ruby", + dependencies: [], + requirements: { created_at: ["2026-06-05T10:30:45"] }, + } + + with_tz("Asia/Tokyo") do + spec = Gem::Resolver::APISpecification.new set, data + + assert_equal Time.utc(2026, 6, 5, 10, 30, 45), spec.created_at + end + end + + def test_initialize_created_at_keeps_explicit_offset + set = Gem::Resolver::APISet.new + data = { + name: "rails", + number: "3.0.3", + platform: "ruby", + dependencies: [], + requirements: { created_at: ["2026-06-05T10:30:45+02:00"] }, + } + + spec = Gem::Resolver::APISpecification.new set, data + + assert_equal Time.utc(2026, 6, 5, 8, 30, 45), spec.created_at + end + + def with_tz(tz) + orig_tz = ENV["TZ"] + ENV["TZ"] = tz + yield + ensure + ENV["TZ"] = orig_tz + end + def test_fetch_development_dependencies specs = spec_fetcher do |fetcher| fetcher.spec "rails", "3.0.3" do |s| diff --git a/test/rubygems/test_gem_util.rb b/test/rubygems/test_gem_util.rb index 9688d066db2425..2270ac08b0f15b 100644 --- a/test/rubygems/test_gem_util.rb +++ b/test/rubygems/test_gem_util.rb @@ -62,6 +62,20 @@ def test_glob_files_in_dir assert_equal expected_paths.sort, files_with_relative_base.sort end + def test_glob_files_in_dir_with_leading_tilde_entries + FileUtils.mkdir_p "j" + FileUtils.touch File.join("j", "~") + FileUtils.touch File.join("j", "~k.rb") + + expected_paths = [ + File.join(@tempdir, "j", "~"), + File.join(@tempdir, "j", "~k.rb"), + ] + + files = Gem::Util.glob_files_in_dir("*", File.join(@tempdir, "j")) + assert_equal expected_paths.sort, files.sort + end + def test_correct_for_windows_path path = "/C:/WINDOWS/Temp/gems" assert_equal "C:/WINDOWS/Temp/gems", Gem::Util.correct_for_windows_path(path) diff --git a/vm.c b/vm.c index f0bebc00bd7ada..97d2731002fe72 100644 --- a/vm.c +++ b/vm.c @@ -4236,40 +4236,113 @@ m_core_set_postexe(VALUE self) return Qnil; } -static VALUE core_hash_merge_kwd(VALUE hash, VALUE kw); +static VALUE +core_hash_merge(VALUE hash, long argc, const VALUE *argv, bool dup) +{ + if (NIL_P(hash)) { + hash = rb_cHash_empty_frozen; + } + else { + hash = rb_to_hash_type(hash); + Check_Type(hash, T_HASH); + } + + return rb_hash_merge2_bulk(hash, argc, argv, dup); +} static VALUE -core_hash_merge(VALUE hash, long argc, const VALUE *argv) +m_core_hash_merge_ptr(int argc, VALUE *argv, VALUE recv) { - Check_Type(hash, T_HASH); - VM_ASSERT(argc % 2 == 0); - rb_hash_bulk_insert(argc, argv, hash); + VALUE hash = argv[0]; + VM_ASSERT(argc % 2 == 1); + + REWIND_CFP(hash = core_hash_merge(hash, argc - 1, argv + 1, true)); + return hash; } static VALUE -m_core_hash_merge_ptr(int argc, VALUE *argv, VALUE recv) +m_core_hash_merge_bang_ptr(int argc, VALUE *argv, VALUE recv) { VALUE hash = argv[0]; + VM_ASSERT(argc % 2 == 1); - REWIND_CFP(hash = core_hash_merge(hash, argc-1, argv+1)); + REWIND_CFP(hash = core_hash_merge(hash, argc - 1, argv + 1, false)); return hash; } -static int -kwmerge_i(VALUE key, VALUE value, VALUE hash) +static VALUE +core_hash_merge_kwd(VALUE hash, VALUE kw, bool dup) { - rb_hash_aset(hash, key, value); - return ST_CONTINUE; + kw = rb_to_hash_type(kw); + if (NIL_P(hash)) { + return dup ? rb_hash_resurrect(kw) : kw; + } + else { + hash = rb_to_hash_type(hash); + Check_Type(hash, T_HASH); + return rb_hash_merge2(hash, kw, dup); + } } static VALUE m_core_hash_merge_kwd(VALUE recv, VALUE hash, VALUE kw) { - if (!NIL_P(kw)) { - REWIND_CFP(hash = core_hash_merge_kwd(hash, kw)); + // We don't own `hash` so we can't mutate it, nor just return it. + if (NIL_P(kw)) { + if (NIL_P(hash)) { + // If we knew that we're dealing with keyword arguments, and not a hash literal, + // we could return nil here. + return rb_hash_new(); + } + + hash = rb_hash_resurrect(hash); + } + else { + REWIND_CFP(hash = core_hash_merge_kwd(hash, kw, true)); + } + VM_ASSERT(CLASS_OF(hash)); + return hash; +} + +static VALUE +m_core_hash_merge_bang_kwd(VALUE recv, VALUE hash, VALUE kw) +{ + // We own `hash` we should mutate it in place if possible. + if (NIL_P(kw)) { + if (NIL_P(hash)) { + hash = rb_hash_new(); + } + else { + hash = rb_hash_resurrect(hash); + } + } + else { + REWIND_CFP(hash = core_hash_merge_kwd(hash, kw, false)); + } + VM_ASSERT(CLASS_OF(hash)); + return hash; +} + +static VALUE +core_hash_coerce(VALUE hash) +{ + if (NIL_P(hash)) { + return rb_hash_new(); + } + VALUE new_hash = rb_to_hash_type(hash); + if (new_hash == hash) { + new_hash = rb_hash_dup(new_hash); } + return new_hash; +} + +static VALUE +m_core_hash_coerce(VALUE recv, VALUE hash) +{ + REWIND_CFP(hash = core_hash_coerce(hash)); + VM_ASSERT(CLASS_OF(hash)); return hash; } @@ -4291,13 +4364,6 @@ m_core_ensure_shareable(VALUE recv, VALUE obj, VALUE name) return rb_ractor_ensure_shareable(obj, name); } -static VALUE -core_hash_merge_kwd(VALUE hash, VALUE kw) -{ - rb_hash_foreach(rb_to_hash_type(kw), kwmerge_i, hash); - return hash; -} - extern VALUE *rb_gc_stack_start; extern size_t rb_gc_stack_maxsize; @@ -4466,7 +4532,10 @@ Init_VM(void) rb_define_method_id(klass, id_core_undef_method, m_core_undef_method, 2); rb_define_method_id(klass, id_core_set_postexe, m_core_set_postexe, 0); rb_define_method_id(klass, id_core_hash_merge_ptr, m_core_hash_merge_ptr, -1); + rb_define_method_id(klass, id_core_hash_merge_bang_ptr, m_core_hash_merge_bang_ptr, -1); rb_define_method_id(klass, id_core_hash_merge_kwd, m_core_hash_merge_kwd, 2); + rb_define_method_id(klass, id_core_hash_merge_bang_kwd, m_core_hash_merge_bang_kwd, 2); + rb_define_method_id(klass, id_core_hash_coerce, m_core_hash_coerce, 1); rb_define_method_id(klass, id_core_raise, f_raise, -1); rb_define_method_id(klass, id_core_sprintf, f_sprintf, -1); rb_define_method_id(klass, idProc, f_proc, 0); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index ed819e7a34a65f..4591ac70fbfd97 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2754,17 +2754,13 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :a@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v15:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1008)) - v17:HashExact = NewHash - PatchPoint NoEPEscape(test) - v22:BasicObject = Send v15, :core#hash_merge_kwd, v17, v10 # SendFallbackReason: Uncategorized(opt_send_without_block) - v24:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1008)) - v27:StaticSymbol[:b] = Const Value(VALUE(0x1010)) - v29:Fixnum[1] = Const Value(1) - v31:BasicObject = Send v24, :core#hash_merge_ptr, v22, v27, v29 # SendFallbackReason: Uncategorized(opt_send_without_block) - v33:BasicObject = Send v9, :foo, v31 # SendFallbackReason: Uncategorized(opt_send_without_block) + v16:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1008)) + v19:StaticSymbol[:b] = Const Value(VALUE(0x1010)) + v21:Fixnum[1] = Const Value(1) + v23:BasicObject = Send v16, :core#hash_merge_ptr, v10, v19, v21 # SendFallbackReason: Uncategorized(opt_send_without_block) + v25:BasicObject = Send v9, :foo, v23 # SendFallbackReason: Uncategorized(opt_send_without_block) CheckInterrupts - Return v33 + Return v25 "); }