diff --git a/.gdbinit b/.gdbinit index 63cd7064766117..1806196a42abb8 100644 --- a/.gdbinit +++ b/.gdbinit @@ -156,8 +156,12 @@ define rp else if ($flags & RUBY_T_MASK) == RUBY_T_HASH printf "%sT_HASH%s: ", $color_type, $color_end, - if ((struct RHash *)($arg0))->ntbl - printf "len=%ld ", ((struct RHash *)($arg0))->ntbl->num_entries + if (((struct RHash *)($arg0))->basic->flags & RHASH_ST_TABLE_FLAG) + printf "st len=%ld ", ((struct RHash *)($arg0))->as.st->num_entries + else + printf "li len=%ld bound=%ld ", \ + ((((struct RHash *)($arg0))->basic->flags & RHASH_ARRAY_LEN_MASK) >> RHASH_ARRAY_LEN_SHIFT), \ + ((((struct RHash *)($arg0))->basic->flags & RHASH_ARRAY_BOUND_MASK) >> RHASH_ARRAY_BOUND_SHIFT) end print (struct RHash *)($arg0) else diff --git a/.gitignore b/.gitignore index 9849721d2a66f3..e03e17e9d1cc14 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.a *.bak *.bc +*.bundle *.dSYM *.dmyh *.dylib @@ -20,6 +21,8 @@ *.rej *.s *.sav +*.sl +*.so *.swp *.yarb *~ @@ -92,6 +95,7 @@ lcov*.info /libruby*.* /miniprelude.c /miniruby +/mjit_build_dir.c /newdate.rb /newline.c /newver.rb @@ -139,6 +143,10 @@ lcov*.info /enc/jis/props.h /enc/unicode/data +# /coroutine/ +!/coroutine/**/*.s +/coroutine/**/.time + # /enc/trans/ /enc/trans/*.c /enc/trans/*.def diff --git a/.travis.yml b/.travis.yml index c9ad5322265ac1..f00efeb8c7da7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,9 +17,16 @@ # is also a good place to look at. language: c -dist: trusty + +dist: xenial + +osx_image: xcode10.1 sudo: false + +git: + quiet: true + addons: apt: config: @@ -48,160 +55,319 @@ addons: - libffi - openssl@1.1 - zlib + - ccache + +cache: + ccache: true + directories: + - $HOME/config_2nd + - $HOME/.downloaded-cache + +env: + global: + - CONFIGURE_TTY=no + - CCACHE_COMPILERCHECK=none + - CCACHE_NOCOMPRESS=1 + - CCACHE_MAXSIZE=512Mi + - >- + NPROC="`case ${TRAVIS_OS_NAME} in + osx) sysctl -n hw.activecpu ;; + linux) nproc ;; + esac`" + # JOBS and SETARCH are overridden when necessary; see below. + - JOBS=-j$((1+${NPROC})) + - SETARCH= + - RUBY_PREFIX=/tmp/ruby-prefix + - GEMS_FOR_TEST='timezone tzinfo' + +.org.ruby-lang.ci.matrix-definitions: + + - &cron-only + if: (type = cron) AND (branch = trunk) AND (fork = false) + + - &make-test-only + script: + - $SETARCH make -s test TESTOPTS="${TESTOPTS=$JOBS -q --tty=no}" + + - &linux + os: linux + compiler: gcc-8 + + - &osx + os: osx + compiler: clang + before_install: + - /usr/local/opt/openssl@1.1/bin/openssl version + + # -------- + + - &x86_64-linux + name: x86_64-linux + <<: *linux + + - &jemalloc + name: --with-jemalloc + <<: *linux + <<: *cron-only + env: + - CONFIG_FLAG='--with-gmp --with-jemalloc --with-valgrind' + + - &VM_CHECK_MODE + name: VM_CHECK_MODE=3 + <<: *linux + <<: *cron-only + <<: *make-test-only + env: + - cppflags=-DVM_CHECK_MODE=0x0003 + + - &FIBER_USE_sjlj + name: FIBER_USE_NATIVE=0 + <<: *linux + <<: *cron-only + env: + - cppflags=-DFIBER_USE_NATIVE=0 + + - &TOKEN_THREADED_CODE + name: TOKEN_THREADED_CODE + <<: *linux + <<: *cron-only + <<: *make-test-only + env: + - cppflags=-DOPT_THREADED_CODE=1 + + - &CALL_THREADED_CODE + name: CALL_THREADED_CODE + <<: *linux + <<: *cron-only + <<: *make-test-only + env: + - cppflags=-DOPT_THREADED_CODE=2 + + - &NO_THREADED_CODE + name: NO_THREADED_CODE + <<: *linux + <<: *cron-only + <<: *make-test-only + env: + - cppflags=-DOPT_THREADED_CODE=3 + + - &ASAN + name: -fsanitize=address + <<: *linux + #<<: *cron-only + <<: *make-test-only + compiler: clang + env: + - ASAN_OPTIONS=detect_leaks=0 + - cflags='-march=native -fsanitize=address -fno-omit-frame-pointer' + - debugflags=-ggdb3 + - optflags=-O1 + - LD=clang + - LDFLAGS=-fsanitize=address + + - &MSAN + name: -fsanitize=memory + <<: *linux + #<<: *cron-only + <<: *make-test-only + compiler: clang + env: + - cflags='-fsanitize=memory -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer' + - optflags=-O1 + - LD=clang + - LDFLAGS=-fsanitize=memory + - CONFIG_FLAG=--with-out-ext=openssl + + - &UBSAN + name: -fsanitize=undefined + <<: *linux + #<<: *cron-only + <<: *make-test-only + compiler: clang + env: + - cflags='-fsanitize=undefined,integer,nullability -fno-omit-frame-pointer' + - cppflags=-DUNALIGNED_WORD_ACCESS=0 + - debugflags=-ggdb3 + - optflags='-O1 -march=native' + - LD=clang + - LDFLAGS=-fsanitize=undefined,integer,nullability + + - &i686-linux + name: i686-linux + <<: *linux + sudo: required + env: + - GCC_FLAGS=-m32 + - debugflags=-g0 + - SETARCH='setarch i686 --verbose --3gb' + addons: + apt: + config: + retries: true + update: true + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-8-multilib + - libffi-dev:i386 + - libffi6:i386 + - libgdbm-dev:i386 + - libgdbm3:i386 + - libncurses5-dev:i386 + - libncurses5:i386 + - libncursesw5-dev:i386 + - libreadline6-dev:i386 + - libreadline6:i386 + - libssl-dev:i386 + - libssl1.0.0:i386 + - linux-libc-dev:i386 + - zlib1g-dev:i386 + - zlib1g:i386 + + - &pedanticism + name: -ansi -pedantic + <<: *linux + <<: *make-test-only + compiler: clang + env: + - GCC_FLAGS='-ansi -Werror=pedantic -pedantic-errors -std=iso9899:1990' + - CONFIG_FLAG= + - JOBS= + - >- + warnflags=' + -Wall + -Wextra + -Werror=declaration-after-statement + -Werror=deprecated-declarations + -Werror=division-by-zero + -Werror=extra-tokens + -Werror=implicit-function-declaration + -Werror=implicit-int + -Werror=long-long + -Werror=pointer-arith + -Werror=shorten-64-to-32 + -Werror=write-strings + -Wmissing-noreturn + -Wno-constant-logical-operand + -Wno-missing-field-initializers + -Wno-overlength-strings + -Wno-parentheses-equality + -Wno-self-assign + -Wno-tautological-compare + -Wno-unused-local-typedef + -Wno-unused-parameter + -Wunused-variable + ' + - LDFLAGS=-Wno-unused-command-line-argument + + - &rubyspec + name: ruby/spec on Ruby 2.3 # to ensure version guards are correctly added + <<: *linux + language: ruby + rvm: 2.3.8 + addons: + apt: + packages: + before_install: + install: + before_script: chmod -R u+w spec/ruby + # -j randomly hangs. Using -fs to make sure we can know problematic spec on failure. + script: ruby -C spec/ruby ../mspec/bin/mspec -fs . + + - &x86_64-darwin17 + name: x86_64-darwin17 + <<: *osx + env: + - CONFIG_FLAG=--with-opt-dir=/usr/local/opt/openssl@1.1:/usr/local/opt/zlib + - TEST_ALL_OPTS="$JOBS -q --tty=no --excludes=\$(TESTSDIR)/excludes/_travis/osx" + + - &universal-darwin17 + name: uinversal.x86_64h-darwin17 + <<: *osx + <<: *cron-only + <<: *make-test-only + env: + - CONFIG_FLAG=--with-arch=x86_64h,x86_64,i386 + - TEST_ALL_OPTS="$JOBS -q --tty=no --excludes=\$(TESTSDIR)/excludes/_travis/osx" matrix: include: # to reduce time for finishing all jobs, run the slowest osx build first. - - name: x86_64-darwin17 - os: osx - compiler: clang - env: - - "CONFIG_FLAG='--with-opt-dir=/usr/local/opt/openssl@1.1:/usr/local/opt/zlib'" - - "JOBS=" # osx build randomly fails with `-j` - # `-v` prints too long outputs, but sometimes it osx build hangs and we can't know which test is bad without `-v`. - - "TEST_ALL_TESTOPTS='-v --color=never --job-status=normal --excludes=$(TESTSDIR)/excludes/_travis'" - before_install: - # Bare "brew update" nukes everything. - # These steps are very carefully chosen to avoid breaking things. - - brew_core_dir=/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/ - - git -C $brew_core_dir fetch -q origin - - git -C $brew_core_dir checkout -q 25590710e3f7af9cfa173c5cd3eb41a0532107b6 - - HOMEBREW_NO_AUTO_UPDATE=1 brew upgrade openssl@1.1 - - /usr/local/opt/openssl@1.1/bin/openssl version - - name: x86_64-linux --with-jemalloc - os: linux - compiler: gcc - env: - - "CONFIG_FLAG='--with-gmp --with-jemalloc --with-valgrind'" - - "JOBS=-j`nproc`" - - name: x86_64-linux - os: linux - compiler: gcc-8 - env: - - "JOBS=-j`nproc`" - # - name: uinversal.i386-darwin17 - # os: osx - # compiler: clang - # env: - # - "CONFIG_FLAG='--with-arch=i386'" - # - "JOBS=\"-j`sysctl -n hw.activecpu`\"" - # - name: uinversal.x86_64h-darwin17 - # os: osx - # compiler: clang - # env: - # - "CONFIG_FLAG='--with-arch=x86_64h'" - # - "JOBS=\"-j`sysctl -n hw.activecpu`\"" - - name: i686-linux - os: linux - sudo: required - compiler: "'gcc-8 -m32'" - env: - - "CONFIG_FLAG='debugflags=-g0'" - - "JOBS=-j`nproc`" - - "SETARCH='setarch i686 --verbose --3gb'" - addons: - apt: - config: - retries: true - update: true - sources: - - ubuntu-toolchain-r-test - packages: - - gcc-8-multilib - - libffi-dev:i386 - - libffi6:i386 - - libgdbm-dev:i386 - - libgdbm3:i386 - - libncurses5-dev:i386 - - libncurses5:i386 - - libncursesw5-dev:i386 - - libreadline6-dev:i386 - - libreadline6:i386 - - libssl-dev:i386 - - libssl1.0.0:i386 - - linux-libc-dev:i386 - - zlib1g-dev:i386 - - zlib1g:i386 - - name: pedanticism - os: linux - compiler: "'clang -ansi -Werror=pedantic -pedantic-errors -std=iso9899:1990'" - env: - - "CONFIG_FLAG=" - - "JOBS=" - # construct warnflags (using bashism...) - before_install: | - warnflags_array=( - -Wall - -Wextra - -Werror=declaration-after-statement - -Werror=deprecated-declarations - -Werror=division-by-zero - -Werror=extra-tokens - -Werror=implicit-function-declaration - -Werror=implicit-int - -Werror=long-long - -Werror=pointer-arith - -Werror=shorten-64-to-32 - -Werror=write-strings - -Wmissing-noreturn - -Wno-constant-logical-operand - -Wno-extended-offsetof - -Wno-missing-field-initializers - -Wno-overlength-strings - -Wno-parentheses-equality - -Wno-self-assign - -Wno-tautological-compare - -Wno-unused-local-typedef - -Wno-unused-parameter - -Wunused-variable - ) - CONFIG_FLAG_ARRAY=( - warnflags="${warnflags_array[*]}" - LDFLAGS=-Wno-unused-command-line-argument) - - os: linux - language: ruby - rvm: 2.3.8 - name: Running ruby/spec on 2.3 to ensure version guards are correctly added - addons: - apt: - packages: - before_install: - install: - before_script: chmod -R u+w spec/ruby - # -j randomly hangs. Using -fs to make sure we can know problematic spec on failure. - script: ruby -C spec/ruby ../mspec/bin/mspec -fs . + - <<: *x86_64-darwin17 + - <<: *x86_64-linux + - <<: *i686-linux + - <<: *jemalloc + - <<: *pedanticism + - <<: *ASAN + - <<: *MSAN + - <<: *UBSAN + - <<: *VM_CHECK_MODE + - <<: *FIBER_USE_sjlj + - <<: *TOKEN_THREADED_CODE + - <<: *CALL_THREADED_CODE + - <<: *NO_THREADED_CODE + - <<: *rubyspec allow_failures: - - name: pedanticism + - name: uinversal.x86_64h-darwin17 + - name: -fsanitize=address + - name: -fsanitize=memory + - name: -fsanitize=undefined fast_finish: true before_script: - - "echo JOBS=$JOBS SETARCH=${SETARCH=}" - - "$SETARCH uname -a" - - "$SETARCH uname -r" - - "rm -fr .ext autom4te.cache" - - "echo $TERM" + - echo JOBS=${JOBS} SETARCH=${SETARCH} + - $SETARCH uname -a + - $SETARCH uname -r + - rm -fr .ext autom4te.cache + - echo $TERM + - |- + [ -d ~/.downloaded-cache ] || + mkdir ~/.downloaded-cache + - ln -s ~/.downloaded-cache - "> config.status" - - "sed -f tool/prereq.status Makefile.in common.mk > Makefile" - - "make update-config_files" - - "make touch-unicode-files" - - "make -s $JOBS srcs UNICODE_FILES=." - - "rm config.status Makefile rbconfig.rb .rbconfig.time" - - "mkdir build config_1st config_2nd" - - "chmod -R a-w ." - - "chmod u+w build config_1st config_2nd" - - "cd build" - - "$SETARCH ../configure -C --disable-install-doc --prefix=/tmp/ruby-prefix --with-gcc=\"$CC\" $CONFIG_FLAG \"${CONFIG_FLAG_ARRAY[@]}\"" - - "cp -pr config.cache config.status .ext/include ../config_1st" - - "$SETARCH make reconfig" - - "cp -pr config.cache config.status .ext/include ../config_2nd" - - "(cd .. && exec diff -ru config_1st config_2nd)" - - "$SETARCH make -s $JOBS && make install" + - sed -f tool/prereq.status Makefile.in common.mk > Makefile + - make -s $JOBS update-download + - make -s $JOBS srcs + - rm config.status Makefile rbconfig.rb .rbconfig.time + - |- + if [ -d ~/config_2nd ]; then + cp -pr ~/config_2nd build + else + mkdir build + fi + - mkdir config_1st config_2nd + - chmod -R a-w . + - chmod -R u+w build config_1st config_2nd + - cd build + - ccache --show-stats + - |- + case "$CC" in + gcc*) CC="ccache $CC${GCC_FLAGS:+ }$GCC_FLAGS -fno-diagnostics-color";; + clang*) CC="ccache $CC${GCC_FLAGS:+ }$GCC_FLAGS -fno-color-diagnostics";; + esac + - |- + [ ! -f config.cache ] || + [ "$CC" = "`sed -n s/^ac_cv_prog_CC=//p config.cache`" ] || + (set -x; exec rm config.cache) + - $SETARCH ../configure -C --disable-install-doc --prefix=$RUBY_PREFIX $CONFIG_FLAG + - cp -pr config.cache config.status .ext/include ../config_1st + - $SETARCH make reconfig + - cp -pr config.cache config.status .ext/include ../config_2nd + - (cd .. && exec diff -ru config_1st config_2nd) + - chmod u+w .. + - rm -r ~/config_2nd + - mv ../config_2nd ~ + - chmod u-w .. + - $SETARCH make -s $JOBS && make install + - ccache --show-stats + - |- + [ -z "${GEMS_FOR_TEST}" ] || + $RUBY_PREFIX/bin/gem install --no-document $GEMS_FOR_TEST script: - - "$SETARCH make -s test TESTOPTS=--color=never" - - "export TEST_ALL_TESTOPTS=\"${TEST_ALL_TESTOPTS:- -q --color=never --job-status=normal}\"" - - "$SETARCH make -s $JOBS test-all -o exts TESTOPTS=\"$TEST_ALL_TESTOPTS\"" - - "$SETARCH make -s $JOBS test-spec MSPECOPT=-fs" # not using `-j` because sometimes `mspec -j` silently dies + - $SETARCH make -s test TESTOPTS="${TESTOPTS=$JOBS -q --tty=no}" + - $SETARCH make -s test-all -o exts TESTOPTS="${TEST_ALL_OPTS=$TESTOPTS}" + - $SETARCH make -s test-spec MSPECOPT=-ff # not using `-j` because sometimes `mspec -j` silently dies # Branch matrix. Not all branches are Travis-ready so we limit branches here. branches: diff --git a/LEGAL b/LEGAL index 04797516910f68..4ff1726cbaae48 100644 --- a/LEGAL +++ b/LEGAL @@ -848,3 +848,33 @@ test/rubygems: IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +lib/bundler: +lib/bundler.rb: +lib/bundler.gemspec: +spec/bundler: +man/bundle-*,gemfile.*: + + Portions copyright (c) 2010 Andre Arko + Portions copyright (c) 2009 Engine Yard + + MIT License + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile.in b/Makefile.in index 73f0608544a674..ac8652ea1db8dc 100644 --- a/Makefile.in +++ b/Makefile.in @@ -16,6 +16,7 @@ top_srcdir = $(srcdir) hdrdir = $(srcdir)/include PLATFORM_DIR = @PLATFORM_DIR@ +CC_WRAPPER = @XCC_WRAPPER@ CC = @CC@ CPP = @CPP@ LD = @LD@ @@ -99,6 +100,7 @@ LIBS = @LIBS@ $(EXTLIBS) MISSING = @LIBOBJS@ @ALLOCA@ ENABLE_SHARED = @ENABLE_SHARED@ LDSHARED = @LIBRUBY_LDSHARED@ +DLDSHARED = @DLDSHARED@ DLDFLAGS = @LIBRUBY_DLDFLAGS@ $(XLDFLAGS) $(ARCH_FLAG) SOLIBS = @SOLIBS@ ENABLE_DEBUG_ENV = @ENABLE_DEBUG_ENV@ @@ -141,6 +143,9 @@ XRUBY_RUBYLIBDIR = @XRUBY_RUBYLIBDIR@ XRUBY_RUBYHDRDIR = @XRUBY_RUBYHDRDIR@ BOOTSTRAPRUBY = @BOOTSTRAPRUBY@ +COROUTINE_H = @X_FIBER_COROUTINE_H@ +COROUTINE_OBJ = $(COROUTINE_H:.h=.@OBJEXT@) + #### End of system configuration section. #### MAJOR= @MAJOR@ @@ -239,7 +244,7 @@ DESTDIR = @DESTDIR@ configure_args = @configure_args@ #### End of variables -.SUFFIXES: .inc .h .c .y .i .$(DTRACE_EXT) +.SUFFIXES: .inc .h .c .y .i .$(ASMEXT) .$(DTRACE_EXT) all: @@ -338,12 +343,13 @@ uncommon.mk: $(srcdir)/common.mk .PHONY: reconfig reconfig-args = $(srcdir)/$(CONFIGURE) $(configure_args) config.status-args = ./config.status --recheck -reconfig-exec-0 = test -t 1 && { CONFIGURE_TTY=yes; export CONFIGURE_TTY; }; exec 3>&1; exit `exec 4>&1; { "$$@" 3>&- 4>&-; echo $$? 1>&4; } | fgrep -v '(cached)' 1>&3 3>&- 4>&-` +reconfig-exec-0 = test -t 1 && { : $${CONFIGURE_TTY=yes}; export CONFIGURE_TTY; }; exec 3>&1; exit `exec 4>&1; { "$$@" 3>&- 4>&-; echo $$? 1>&4; } | fgrep -v '(cached)' 1>&3 3>&- 4>&-` reconfig-exec-1 = set -x; "$$@" reconfig config.status: $(srcdir)/$(CONFIGURE) $(srcdir)/enc/Makefile.in \ $(srcdir)/include/ruby/version.h @PWD= MINIRUBY="$(MINIRUBY)"; export MINIRUBY; \ + warnflags="@warnflags@"; export warnflags; \ set $(SHELL) $($@-args); $(reconfig-exec-$(V)) $(srcdir)/$(CONFIGURE): $(srcdir)/configure.ac $(srcdir)/aclocal.m4 @@ -401,11 +407,11 @@ $(srcdir)/enc/jis/props.h: enc/jis/props.kwd @$(ECHO) compiling $< $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $< -.s.@OBJEXT@: +.$(ASMEXT).@OBJEXT@: @$(ECHO) assembling $< - $(Q) $(AS) $(ASFLAGS) -o $@ $< + $(Q) $(CC) $(ASFLAGS) -o $@ -c $< -.c.S: +.c.$(ASMEXT): @$(ECHO) translating $< $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -S $< @@ -443,6 +449,7 @@ clean-local:: $(Q)$(RM) $(MJIT_MIN_HEADER) $(MJIT_MIN_HEADER:.h=)$(MJIT_HEADER_SUFFIX:%=*).h $(Q)$(RM) $(MJIT_HEADER_INSTALL_DIR)/rb_mjit_min_header-*.h $(Q)$(RM) $(TIMESTAMPDIR)/$(MJIT_HEADER:.h=)$(MJIT_HEADER_SUFFIX).time mjit_config.h + $(Q)$(RM) -r mjit_build_dir.* -$(Q) $(RMDIRS) $(MJIT_HEADER_INSTALL_DIR) 2> $(NULL) || exit 0 # DTrace static library hacks described here: @@ -455,7 +462,7 @@ clean-local:: $(Q)$(RM) ext/extinit.c ext/extinit.$(OBJEXT) ext/ripper/y.output \ enc/encinit.c enc/encinit.$(OBJEXT) -$(Q)$(RM) $(pkgconfig_DATA) - -$(Q)$(RMALL) exe/ ruby-runner.h *.dSYM + -$(Q)$(RMALL) exe/ ruby-runner.$(OBJEXT) ruby-runner.h *.dSYM distclean-local:: $(Q)$(RM) ext/config.cache $(RBCONFIG) Doxyfile @@ -512,6 +519,8 @@ ext/extinit.$(OBJEXT): ext/extinit.c $(SETUP) enc/encinit.$(OBJEXT): enc/encinit.c $(SETUP) +cont.$(OBJEXT): $(COROUTINE_H) + test-bundled-gems-run: $(Q) set -e; while read gem _; do \ echo testing $$gem gem && \ @@ -553,6 +562,10 @@ update-simplecov: update-coverage: update-simplecov update-simplecov-html update-doclie +update-known-errors: + errno --list | cut -d' ' -f1 | sort -u - $(srcdir)/defs/known_errors.def | \ + $(IFCHANGE) $(srcdir)/defs/known_errors.def - + INSNS = opt_sc.inc optinsn.inc optunifs.inc insns.inc insns_info.inc \ vmtc.inc vm.inc mjit_compile.inc @@ -576,55 +589,34 @@ un-runnable: mjit_config.h: $(ECHO) making $@ @{ \ - quote() { \ - printf "#define $$1"; shift; \ - $${1+printf} $${1+' "%s"'$$sep} $${1+"$$@"}; \ - echo; \ - }; \ - parse_arch_flag() { \ - while [ "$$#" -gt 0 ]; do \ - case "$$1" in \ - -arch) \ - shift; \ - archs="$${archs:+$$archs }$$1"; \ - shift; \ - ;; \ - *) \ - arch_flag="$${arch_flag:+$${arch_flag} }$$1"; \ - shift; \ - ;; \ - esac; \ - done; \ - }; \ + . $(srcdir)/tool/mjit_archflag.sh; \ + parse_arch_flags "$(UNIVERSAL_ARCHNAMES)" $(ARCH_FLAG); \ test "$(Q)" = @ || set -x; \ - archs="$(UNIVERSAL_ARCHNAMES)"; \ - arch_flag=""; \ - parse_arch_flag $(ARCH_FLAG); \ - need_mjit_archflag="$${archs}$${arch_flag}"; \ echo '#ifndef RUBY_MJIT_CONFIG_H'; \ echo '#define RUBY_MJIT_CONFIG_H 1'; \ echo; \ sep=; \ - quote MJIT_BUILD_DIR "`$(CHDIR) . && pwd`"; \ quote MJIT_MIN_HEADER_NAME "/$(MJIT_HEADER_INSTALL_DIR)/$(MJIT_MIN_HEADER_NAME)"; \ sep=,; \ quote "MJIT_CC_COMMON " $(MJIT_CC); \ - quote "MJIT_CFLAGS $${need_mjit_archflag:+ MJIT_ARCHFLAG}" $(MJIT_CFLAGS); \ + quote "MJIT_CFLAGS MJIT_ARCHFLAG" $(MJIT_CFLAGS); \ quote "MJIT_OPTFLAGS " $(MJIT_OPTFLAGS); \ quote "MJIT_DEBUGFLAGS " $(MJIT_DEBUGFLAGS); \ quote "MJIT_LDSHARED " $(MJIT_LDSHARED); \ - quote "MJIT_DLDFLAGS $${need_mjit_archflag:+ MJIT_ARCHFLAG}" $(MJIT_DLDFLAGS); \ + quote "MJIT_DLDFLAGS MJIT_ARCHFLAG" $(MJIT_DLDFLAGS); \ quote "MJIT_LIBS " $(LIBRUBYARG_SHARED); \ - $${archs:+echo} $${archs:+'#if 0'}; \ - for arch in $$archs; do \ - echo "#elif defined __$${arch%=*}__"; \ - quote " MJIT_ARCHFLAG " -arch $${arch%=*}; \ - done; \ - $${archs:+echo} $${archs:+'#else'}; \ - if [ "$${arch_flag}" != "" ]; then \ - quote " MJIT_ARCHFLAG " $${arch_flag}; \ - fi; \ - $${archs:+echo} $${archs:+'#endif'}; \ + quote 'PRELOADENV "@PRELOADENV@"'; \ + indent=$${archs:+' '}; \ + define_arch_flags; \ echo; \ echo '#endif /* RUBY_MJIT_CONFIG_H */'; \ } > $@ + +yes-test-almost yes-test-all: mjit_build_dir.$(SOEXT) +mjit_build_dir.$(SOEXT): $(MJIT_MIN_HEADER) + $(ECHO) making $@ + $(Q) { \ + echo 'const char MJIT_BUILD_DIR[] = "'"`$(CHDIR) . && pwd`"'";'; \ + } > $(@:.$(SOEXT)=.c) + $(Q) cat $(@:.$(SOEXT)=.c) + $(Q) $(DLDSHARED) $(MJIT_DLDFLAGS) $(ARCH_FLAG) $(CFLAGS) $(CPPFLAGS) $(@:.$(SOEXT)=.c) $(OUTFLAG)$@ diff --git a/NEWS b/NEWS index 722ef2520ac98e..189916a04a5310 100644 --- a/NEWS +++ b/NEWS @@ -16,7 +16,11 @@ sufficient information, see the ChangeLog file or Redmine * $SAFE is a process global state and we can set 0 again. [Feature #14250] -* refinements take place at block passing. [Feature #14223] +* refinements takes place at block passing. [Feature #14223] + +* refinements takes place at Kernel#public_send. [Feature #15326] + +* refinements takes place at Kernel#respond_to?. [Feature #15327] * +else+ without +rescue+ now causes a syntax error. [EXPERIMENTAL] @@ -39,7 +43,7 @@ sufficient information, see the ChangeLog file or Redmine * Print exception backtrace and error message in reverse order when the exception is not caught and STDOUT is unchanged and a tty. [Feature #8661] -* Print `cause` of the exception if the exception is not caught and printed +* Print +cause+ of the exception if the exception is not caught and printed its backtraces and error message [Feature #8257] === Core classes updates (outstanding ones only) @@ -82,6 +86,12 @@ sufficient information, see the ChangeLog file or Redmine [Enumerable] + [New methods] + + * Enumerable#chain returns an enumerator object that iterates over the + elements of the receiver and then those of each argument in sequence. + [Feature #15144] + [Modified methods] * Enumerable#to_h now maps elements to new keys and values by the block if @@ -91,6 +101,14 @@ sufficient information, see the ChangeLog file or Redmine * Enumerable#filter is a new alias for Enumerable#select. [Feature #13784] +[Enumerator] + + [New methods] + + * Enumerator#+ returns an enumerator object that iterates over the + elements of the receiver and then those of the other operand. + [Feature #15144] + [Enumerator::ArithmeticSequence] * This is a new class to represent a generator of an arithmetic sequence, @@ -98,6 +116,12 @@ sufficient information, see the ChangeLog file or Redmine for representing what is similar to Python's slice. You can get an instance of this class from Numeric#step and Range#step. +[Enumerator::Chain] + + * This is a new class to represent a chain of enumerables that works as a + single enumerator, generated by such methods as Enumerable#chain and + Enumerator#+. + [Enumerator::Lazy] [Aliased methods] @@ -168,8 +192,14 @@ sufficient information, see the ChangeLog file or Redmine [New options] - * KeyError#initialize accepts +:receiver+ and +:key+ options to - set receiver and key in Ruby code. [Feature #14313] + * KeyError.new accepts +:receiver+ and +:key+ options to set receiver and + key in Ruby code. [Feature #14313] + +[Method] + + [New methods] + + * added Method#<< and Method#>> for Proc composition. [Feature #6284] [Module] @@ -184,15 +214,21 @@ sufficient information, see the ChangeLog file or Redmine [New options] - * NameError#initialize accepts +:receiver+ option to set receiver - in Ruby code. [Feature #14313] + * NameError.new accepts +:receiver+ option to set receiver in Ruby + code. [Feature #14313] + +[NilClass] + + [New methods] + + * NilClass#=~ is added for compatibility. [Feature #15231] [NoMethodError] [New options] - * NoMethodError#initialize accepts +:receiver+ option to set receiver - in Ruby code. [Feature #14313] + * NoMethodError.new accepts +:receiver+ option to set receiver in Ruby + code. [Feature #14313] [Numeric] @@ -203,6 +239,10 @@ sufficient information, see the ChangeLog file or Redmine [Proc] + [New methods] + + * added Proc#<< and Proc#>> for Proc composition. [Feature #6284] + [Incompatible changes] * Proc#call doesn't change $SAFE any more. [Feature #14250] @@ -226,16 +266,19 @@ sufficient information, see the ChangeLog file or Redmine * Range#step now returns an instance of Enumerator::ArithmeticSequence class rather than one of Enumerator class. -[RubyVM::AST] +[RubyVM::AbstractSyntaxTree] [New methods] - * RubyVM::AST.parse parses a given string and returns AST + * RubyVM::AbstractSyntaxTree.parse parses a given string and returns AST nodes. [experimental] - * RubyVM::AST.parse_file parses a given file and returns AST + * RubyVM::AbstractSyntaxTree.parse_file parses a given file and returns AST nodes. [experimental] + * RubyVM::AbstractSyntaxTree.of returns AST nodes of the given proc or method. + [experimental] + [String] [New features] @@ -269,6 +312,12 @@ sufficient information, see the ChangeLog file or Redmine === Stdlib updates (outstanding ones only) +[Bundler] + + * Add Bundler to Standard Library. [Feature #12733] + + * Use 1.17.1. It's latest stable version. + [Coverage] A oneshot_lines mode is added. [Feature #15022] @@ -311,9 +360,15 @@ sufficient information, see the ChangeLog file or Redmine [New methods] - * Matrix#antisymmetric? + * Matrix#antisymmetric? / #skew_symmetric? + + * Matrix#map! / #collect! [Feature #14151] + + * Matrix#[]= + + * Vector#map! / #collect! - * Matrix#reflexive? + * Vector#[]= [Net] @@ -387,16 +442,20 @@ sufficient information, see the ChangeLog file or Redmine === Compatibility issues (excluding feature bug fixes) +[Dir] + + * Dir.glob with '\0'-separated pattern list will be deprecated, + and is now warned. [Feature #14643] + [File] * File.read, File.binread, File.write, File.binwrite, File.foreach, and File.readlines do not invoke external commands even if the path starts with the pipe character '|'. [Feature #14245] -[Dir] +[Object] - * Dir.glob with '\0'-separated pattern list will be deprecated, - and is now warned. [Feature #14643] + * Object#=~ is deprecated. [Feature #15231] === Stdlib compatibility issues (excluding feature bug fixes) @@ -454,6 +513,11 @@ sufficient information, see the ChangeLog file or Redmine * timer thread is eliminated for platforms with POSIX timers [Misc #14937] +* Transient Heap (theap) is supported. [Bug #14858] [Feature #14989] + theap is managed heap for short-living memory objects. For example, + making small and short-living Hash object is x2 faster. With rdoc benchmark, + we maesured 6-7% performance improvement. + === Miscellaneous changes * On macOS, shared libraries no longer include a full version number of Ruby diff --git a/README.md b/README.md index 099bc1435edfdc..1e76fda4f06a03 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -[![Build Status](https://travis-ci.org/ruby/ruby.svg)](https://travis-ci.org/ruby/ruby) +[![Build Status](https://travis-ci.org/ruby/ruby.svg?branch=trunk)](https://travis-ci.org/ruby/ruby) [![Build status](https://ci.appveyor.com/api/projects/status/0sy8rrxut4o0k960/branch/trunk?svg=true)](https://ci.appveyor.com/project/ruby/ruby/branch/trunk) +[![wercker status](https://app.wercker.com/status/e5e7e1704f62b76525022aa424aef6ef/s/trunk "wercker status")](https://app.wercker.com/project/byKey/e5e7e1704f62b76525022aa424aef6ef) # What's Ruby diff --git a/addr2line.c b/addr2line.c index 30e7509f4ec540..a11d32b73d1efc 100644 --- a/addr2line.c +++ b/addr2line.c @@ -10,6 +10,7 @@ #if defined(__clang__) #pragma clang diagnostic ignored "-Wpedantic" +#pragma clang diagnostic ignored "-Wgcc-compat" #elif defined(__GNUC__) #pragma GCC diagnostic ignored "-Wpedantic" #endif @@ -65,7 +66,11 @@ void *alloca(); #endif #ifdef HAVE_MACH_O_LOADER_H +# include +# include # include +# include +# include #endif #ifdef USE_ELF @@ -1519,10 +1524,10 @@ debug_info_read(DebugInfoReader *reader, int num_traces, void **traces, /* enumerate abbrev */ for (;;) { DebugInfoValue v = {{}}; - //ptrdiff_t pos = reader->p - reader->p0; + /* ptrdiff_t pos = reader->p - reader->p0; */ if (!di_read_record(reader, &v)) break; - //fprintf(stderr,"\n%d:%tx: AT:%lx FORM:%lx\n",__LINE__,pos,v.at,v.form); - //div_inspect(&v); + /* fprintf(stderr,"\n%d:%tx: AT:%lx FORM:%lx\n",__LINE__,pos,v.at,v.form); */ + /* div_inspect(&v); */ switch (v.at) { case DW_AT_name: line.sname = get_cstr_value(&v); @@ -1542,10 +1547,10 @@ debug_info_read(DebugInfoReader *reader, int num_traces, void **traces, goto skip_die; case DW_AT_inline: /* 1 or 3 */ - break; // goto skip_die; + break; /* goto skip_die; */ case DW_AT_abstract_origin: read_abstract_origin(reader, v.as.uint64, &line); - break; //goto skip_die; + break; /* goto skip_die; */ } } /* ranges_inspect(reader, &ranges); */ @@ -1819,11 +1824,16 @@ static uintptr_t fill_lines(int num_traces, void **traces, int check_debuglink, obj_info_t **objp, line_info_t *lines, int offset) { +# ifdef __LP64__ +# define LP(x) x##_64 +# else +# define LP(x) x +# endif int fd; off_t filesize; - char *file, *p; + char *file, *p = NULL; obj_info_t *obj = *objp; - struct mach_header_64 *header; + struct LP(mach_header) *header; uintptr_t dladdr_fbase = 0; { @@ -1834,6 +1844,7 @@ fill_lines(int num_traces, void **traces, int check_debuglink, size_t basesize = size - (base - binary_filename); s += size; max -= size; + p = s; size = strlcpy(s, ".dSYM/Contents/Resources/DWARF/", max); if (size == 0) goto fail; s += size; @@ -1841,12 +1852,17 @@ fill_lines(int num_traces, void **traces, int check_debuglink, if (max <= basesize) goto fail; memcpy(s, base, basesize); s[basesize] = 0; - } - fd = open(binary_filename, O_RDONLY); - if (fd < 0) { - goto fail; + fd = open(binary_filename, O_RDONLY); + if (fd < 0) { + *p = 0; /* binary_filename becomes original file name */ + fd = open(binary_filename, O_RDONLY); + if (fd < 0) { + goto fail; + } + } } + filesize = lseek(fd, 0, SEEK_END); if (filesize < 0) { int e = errno; @@ -1875,19 +1891,56 @@ fill_lines(int num_traces, void **traces, int check_debuglink, obj->mapped = file; obj->mapped_size = (size_t)filesize; - header = (struct mach_header_64 *)file; - if (header->magic != MH_MAGIC_64) { - /* TODO: universal binaries */ - kprintf("'%s' is not a 64-bit Mach-O file!\n",binary_filename); + header = (struct LP(mach_header) *)file; + if (header->magic == LP(MH_MAGIC)) { + /* non universal binary */ + p = file; + } + else if (header->magic == FAT_CIGAM) { + struct fat_header *fat = (struct fat_header *)file; + char *q = file + sizeof(*fat); + uint32_t nfat_arch = __builtin_bswap32(fat->nfat_arch); + /* fprintf(stderr,"%d: fat:%s %d\n",__LINE__, binary_filename,nfat_arch); */ + for (uint32_t i = 0; i < nfat_arch; i++) { + struct fat_arch *arch = (struct fat_arch *)q; + cpu_type_t cputype = __builtin_bswap32(arch->cputype); + cpu_subtype_t cpusubtype = __builtin_bswap32(arch->cpusubtype); + uint32_t offset = __builtin_bswap32(arch->offset); + /* fprintf(stderr,"%d: fat %d %x/%x %x/%x\n",__LINE__, i, _mh_execute_header.cputype,_mh_execute_header.cpusubtype, cputype,cpusubtype); */ + if (_mh_execute_header.cputype == cputype && + (_mh_execute_header.cpusubtype & ~CPU_SUBTYPE_MASK) == cpusubtype) { + p = file + offset; + file = p; + header = (struct LP(mach_header) *)p; + if (header->magic == LP(MH_MAGIC)) { + goto found_mach_header; + } + break; + } + q += sizeof(*arch); + } + kprintf("'%s' is not a Mach-O universal binary file!\n",binary_filename); + close(fd); + goto fail; + } + else { + kprintf("'%s' is not a " +# ifdef __LP64__ + "64" +# else + "32" +# endif + "-bit Mach-O file!\n",binary_filename); close(fd); goto fail; } +found_mach_header: + p += sizeof(*header); - p = file + sizeof(struct mach_header_64); for (uint32_t i = 0; i < (uint32_t)header->ncmds; i++) { struct load_command *lcmd = (struct load_command *)p; switch (lcmd->cmd) { - case LC_SEGMENT_64: + case LP(LC_SEGMENT): { static const char *debug_section_names[] = { "__debug_abbrev", @@ -1896,15 +1949,15 @@ fill_lines(int num_traces, void **traces, int check_debuglink, "__debug_ranges", "__debug_str" }; - struct segment_command_64 *scmd = (struct segment_command_64 *)lcmd; + struct LP(segment_command) *scmd = (struct LP(segment_command) *)lcmd; if (strcmp(scmd->segname, "__TEXT") == 0) { obj->vmaddr = scmd->vmaddr; } else if (strcmp(scmd->segname, "__DWARF") == 0) { - p += sizeof(struct segment_command_64); + p += sizeof(struct LP(segment_command)); for (uint64_t i = 0; i < scmd->nsects; i++) { - struct section_64 *sect = (struct section_64 *)p; - p += sizeof(struct section_64); + struct LP(section) *sect = (struct LP(section) *)p; + p += sizeof(struct LP(section)); for (int j=0; j < DWARF_SECTION_COUNT; j++) { struct dwarf_section *s = obj_dwarf_section_at(obj, j); @@ -1924,23 +1977,39 @@ fill_lines(int num_traces, void **traces, int check_debuglink, } break; -#if 0 case LC_SYMTAB: { - struct symtab_command *c = (struct symtab_command *)lcmd; - struct nlist_64 *nl = (struct nlist *)(file + c->symoff); - char *strtab = file + c->stroff; + struct symtab_command *cmd = (struct symtab_command *)lcmd; + struct LP(nlist) *nl = (struct LP(nlist) *)(file + cmd->symoff); + char *strtab = file + cmd->stroff, *sname; uint32_t j; - kprintf("[%2d]: %x/symtab %lx\n", i, c->cmd, p); - for (j = 0; j < c->nsyms; j++) { - struct nlist_64 *e = &nl[j]; - if (!(e->n_type & N_STAB)) continue; - /* if (e->n_type != N_FUN) continue; */ - kprintf("[%2d][%4d]: %02x/%x/%x: %s %lx\n", i, j, - e->n_type,e->n_sect,e->n_desc,strtab+e->n_un.n_strx,e->n_value); + uintptr_t saddr; + /* kprintf("[%2d]: %x/symtab %p\n", i, cmd->cmd, p); */ + for (j = 0; j < cmd->nsyms; j++) { + uintptr_t symsize, d; + struct LP(nlist) *e = &nl[j]; + /* kprintf("[%2d][%4d]: %02x/%x/%x: %s %llx\n", i, j, e->n_type,e->n_sect,e->n_desc,strtab+e->n_un.n_strx,e->n_value); */ + if (e->n_type != N_FUN) continue; + if (e->n_sect) { + saddr = (uintptr_t)e->n_value + obj->base_addr - obj->vmaddr; + sname = strtab + e->n_un.n_strx; + /* kprintf("[%2d][%4d]: %02x/%x/%x: %s %llx\n", i, j, e->n_type,e->n_sect,e->n_desc,strtab+e->n_un.n_strx,e->n_value); */ + continue; + } + for (int k = offset; k < num_traces; k++) { + d = (uintptr_t)traces[k] - saddr; + symsize = e->n_value; + /* kprintf("%lx %lx %lx\n",saddr,symsize,traces[k]); */ + if (lines[k].line > 0 || d > (uintptr_t)symsize) + continue; + /* fill symbol name and addr from .symtab */ + if (!lines[k].sname) lines[k].sname = sname; + lines[k].saddr = saddr; + lines[k].path = obj->path; + lines[k].base_addr = obj->base_addr; + } } } -#endif } p += lcmd->cmdsize; } @@ -2101,6 +2170,8 @@ rb_dump_backtrace_with_lines(int num_traces, void **traces) path = info.dli_fname; obj->path = path; lines[i].path = path; + lines[i].sname = info.dli_sname; + lines[i].saddr = (uintptr_t)info.dli_saddr; strlcpy(binary_filename, path, PATH_MAX); if (fill_lines(num_traces, traces, 1, &obj, lines, i) == (uintptr_t)-1) break; diff --git a/appveyor.yml b/appveyor.yml index 90dc6ee2460a54..3bf1568cd93ed2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,6 +16,7 @@ environment: - build: vs vs: 140 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + GEMS_FOR_TEST: "timezone tzinfo" notifications: - provider: Webhook url: @@ -89,13 +90,14 @@ for: - nmake -l - nmake install-nodoc - \usr\bin\ruby -v -e "p :locale => Encoding.find('locale'), :filesystem => Encoding.find('filesystem')" + - if not "%GEMS_FOR_TEST%" == "" \usr\bin\gem install --no-document %GEMS_FOR_TEST% test_script: - set /a JOBS=%NUMBER_OF_PROCESSORS% - nmake -l "TESTOPTS=-v -q" btest - nmake -l "TESTOPTS=-v -q" test-basic - - nmake -l "TESTOPTS=-q -j%JOBS% --subprocess-timeout-scale=3.0 --exclude win32ole --exclude test_syntax --exclude test_open-uri --exclude test_bundled_ca" test-all - # separately execute tests that may crash worker without -j. - - nmake -l "TESTOPTS=-v --subprocess-timeout-scale=3.0" test-all TESTS="../test/win32ole ../test/ruby/test_syntax.rb ../test/open-uri/test_open-uri.rb ../test/rubygems/test_bundled_ca.rb" + - nmake -l "TESTOPTS=-q --subprocess-timeout-scale=3.0 --excludes=../test/excludes/_appveyor -j%JOBS% --exclude win32ole --exclude test_bignum --exclude test_syntax --exclude test_open-uri --exclude test_bundled_ca" test-all + # separately execute tests without -j which may crash worker with -j. + - nmake -l "TESTOPTS=-v --subprocess-timeout-scale=3.0 --excludes=../test/excludes/_appveyor" test-all TESTS="../test/win32ole ../test/ruby/test_bignum.rb ../test/ruby/test_syntax.rb ../test/open-uri/test_open-uri.rb ../test/rubygems/test_bundled_ca.rb" - nmake -l test-spec MSPECOPT=-fs # not using `-j` because sometimes `mspec -j` silently dies on Windows - matrix: @@ -134,9 +136,10 @@ for: - mingw32-make -j%JOBS% up - mingw32-make -j%JOBS% - mingw32-make DESTDIR=../install install-nodoc + - if not "%GEMS_FOR_TEST%" == "" ..\install\bin\gem install --no-document %GEMS_FOR_TEST% test_script: - mingw32-make test - - mingw32-make test-all TESTOPTS="--retry --job-status=normal --show-skip --subprocess-timeout-scale=1.5 -j %JOBS% --exclude win32ole --exclude test_open-uri" - # separately execute tests that may crash worker without -j. - - mingw32-make test-all TESTOPTS="--retry --job-status=normal --show-skip --subprocess-timeout-scale=1.5" TESTS="../ruby/test/win32ole ../ruby/test/open-uri/test_open-uri.rb" + - mingw32-make test-all TESTOPTS="--retry --job-status=normal --show-skip --subprocess-timeout-scale=1.5 --excludes=../ruby/test/excludes/_appveyor -j %JOBS% --exclude win32ole --exclude test_open-uri" + # separately execute tests without -j which may crash worker with -j. + - mingw32-make test-all TESTOPTS="--retry --job-status=normal --show-skip --subprocess-timeout-scale=1.5 --excludes=../ruby/test/excludes/_appveyor" TESTS="../ruby/test/win32ole ../ruby/test/open-uri/test_open-uri.rb" - mingw32-make test-spec MSPECOPT=-fs # not using `-j` because sometimes `mspec -j` silently dies on Windows diff --git a/array.c b/array.c index 83a5ff41f56137..38ed1753bd83ae 100644 --- a/array.c +++ b/array.c @@ -14,12 +14,14 @@ #include "ruby/encoding.h" #include "ruby/util.h" #include "ruby/st.h" -#include "internal.h" #include "probes.h" #include "id.h" #include "debug_counter.h" +#include "gc.h" +#include "transient_heap.h" +#include "internal.h" -#ifndef ARRAY_DEBUG +#if !ARRAY_DEBUG # define NDEBUG #endif #include "ruby_assert.h" @@ -42,17 +44,21 @@ VALUE rb_cArray; #define ARY_HEAP_PTR(a) (assert(!ARY_EMBED_P(a)), RARRAY(a)->as.heap.ptr) #define ARY_HEAP_LEN(a) (assert(!ARY_EMBED_P(a)), RARRAY(a)->as.heap.len) +#define ARY_HEAP_CAPA(a) (assert(!ARY_EMBED_P(a)), RARRAY(a)->as.heap.aux.capa) + #define ARY_EMBED_PTR(a) (assert(ARY_EMBED_P(a)), RARRAY(a)->as.ary) #define ARY_EMBED_LEN(a) \ (assert(ARY_EMBED_P(a)), \ (long)((RBASIC(a)->flags >> RARRAY_EMBED_LEN_SHIFT) & \ (RARRAY_EMBED_LEN_MASK >> RARRAY_EMBED_LEN_SHIFT))) -#define ARY_HEAP_SIZE(a) (assert(!ARY_EMBED_P(a)), assert(ARY_OWNS_HEAP_P(a)), RARRAY(a)->as.heap.aux.capa * sizeof(VALUE)) +#define ARY_HEAP_SIZE(a) (assert(!ARY_EMBED_P(a)), assert(ARY_OWNS_HEAP_P(a)), ARY_HEAP_CAPA(a) * sizeof(VALUE)) #define ARY_OWNS_HEAP_P(a) (!FL_TEST((a), ELTS_SHARED|RARRAY_EMBED_FLAG)) #define FL_SET_EMBED(a) do { \ assert(!ARY_SHARED_P(a)); \ FL_SET((a), RARRAY_EMBED_FLAG); \ + RARY_TRANSIENT_UNSET(a); \ + ary_verify(a); \ } while (0) #define FL_UNSET_EMBED(ary) FL_UNSET((ary), RARRAY_EMBED_FLAG|RARRAY_EMBED_LEN_MASK) #define FL_SET_SHARED(ary) do { \ @@ -102,7 +108,7 @@ VALUE rb_cArray; } while (0) #define ARY_CAPA(ary) (ARY_EMBED_P(ary) ? RARRAY_EMBED_LEN_MAX : \ - ARY_SHARED_ROOT_P(ary) ? RARRAY_LEN(ary) : RARRAY(ary)->as.heap.aux.capa) + ARY_SHARED_ROOT_P(ary) ? RARRAY_LEN(ary) : ARY_HEAP_CAPA(ary)) #define ARY_SET_CAPA(ary, n) do { \ assert(!ARY_EMBED_P(ary)); \ assert(!ARY_SHARED_P(ary)); \ @@ -130,11 +136,82 @@ VALUE rb_cArray; } while (0) #define FL_SET_SHARED_ROOT(ary) do { \ assert(!ARY_EMBED_P(ary)); \ + assert(!RARRAY_TRANSIENT_P(ary)); \ FL_SET((ary), RARRAY_SHARED_ROOT_FLAG); \ } while (0) #define ARY_SET(a, i, v) RARRAY_ASET((assert(!ARY_SHARED_P(a)), (a)), (i), (v)) + +#if ARRAY_DEBUG +#define ary_verify(ary) ary_verify_(ary, __FILE__, __LINE__) + +static VALUE +ary_verify_(VALUE ary, const char *file, int line) +{ + assert(RB_TYPE_P(ary, T_ARRAY)); + + if (FL_TEST(ary, ELTS_SHARED)) { + VALUE root = RARRAY(ary)->as.heap.aux.shared; + const VALUE *ptr = ARY_HEAP_PTR(ary); + const VALUE *root_ptr = RARRAY_CONST_PTR_TRANSIENT(root); + long len = ARY_HEAP_LEN(ary), root_len = RARRAY_LEN(root); + assert(FL_TEST(root, RARRAY_SHARED_ROOT_FLAG)); + assert(root_ptr <= ptr && ptr + len <= root_ptr + root_len); + ary_verify(root); + } + else if (ARY_EMBED_P(ary)) { + assert(!RARRAY_TRANSIENT_P(ary)); + assert(!ARY_SHARED_P(ary)); + assert(RARRAY_LEN(ary) <= RARRAY_EMBED_LEN_MAX); + } + else { +#if 1 + const VALUE *ptr = RARRAY_CONST_PTR_TRANSIENT(ary); + long i, len = RARRAY_LEN(ary); + volatile VALUE v; + if (len > 1) len = 1; /* check only HEAD */ + for (i=0; ias.heap.ptr, VALUE, new_capa, old_capa); + } + ary_verify(ary); +} + +#if USE_TRANSIENT_HEAP +static inline void +rb_ary_transient_heap_evacuate_(VALUE ary, int transient, int promote) +{ + if (transient) { + VALUE *new_ptr; + const VALUE *old_ptr = ARY_HEAP_PTR(ary); + long capa = ARY_HEAP_CAPA(ary); + long len = ARY_HEAP_LEN(ary); + + if (ARY_SHARED_ROOT_P(ary)) { + capa = len; + } + + assert(ARY_OWNS_HEAP_P(ary)); + assert(RARRAY_TRANSIENT_P(ary)); + assert(!ARY_PTR_USING_P(ary)); + + if (promote) { + new_ptr = ALLOC_N(VALUE, capa); + RARY_TRANSIENT_UNSET(ary); + } + else { + new_ptr = ary_heap_alloc(ary, capa); + } + + MEMCPY(new_ptr, old_ptr, VALUE, capa); + /* do not use ARY_SET_PTR() because they assert !frozen */ + RARRAY(ary)->as.heap.ptr = new_ptr; + } + + ary_verify(ary); +} + +void +rb_ary_transient_heap_evacuate(VALUE ary, int promote) +{ + rb_ary_transient_heap_evacuate_(ary, RARRAY_TRANSIENT_P(ary), promote); +} + +void +rb_ary_detransient(VALUE ary) +{ + assert(RARRAY_TRANSIENT_P(ary)); + rb_ary_transient_heap_evacuate_(ary, TRUE, TRUE); +} +#else +void +rb_ary_detransient(VALUE ary) +{ + /* do nothing */ +} +#endif + static void ary_resize_capa(VALUE ary, long capacity) { assert(RARRAY_LEN(ary) <= capacity); assert(!OBJ_FROZEN(ary)); assert(!ARY_SHARED_P(ary)); + if (capacity > RARRAY_EMBED_LEN_MAX) { if (ARY_EMBED_P(ary)) { long len = ARY_EMBED_LEN(ary); - VALUE *ptr = ALLOC_N(VALUE, (capacity)); + VALUE *ptr = ary_heap_alloc(ary, capacity); + MEMCPY(ptr, ARY_EMBED_PTR(ary), VALUE, len); FL_UNSET_EMBED(ary); ARY_SET_PTR(ary, ptr); ARY_SET_HEAP_LEN(ary, len); } else { - SIZED_REALLOC_N(RARRAY(ary)->as.heap.ptr, VALUE, capacity, RARRAY(ary)->as.heap.aux.capa); + ary_heap_realloc(ary, capacity); } - ARY_SET_CAPA(ary, (capacity)); + ARY_SET_CAPA(ary, capacity); } else { if (!ARY_EMBED_P(ary)) { - long len = RARRAY_LEN(ary); - const VALUE *ptr = RARRAY_CONST_PTR(ary); + long len = ARY_HEAP_LEN(ary); + long old_capa = ARY_HEAP_CAPA(ary); + const VALUE *ptr = ARY_HEAP_PTR(ary); - if (len > capacity) len = capacity; + if (len > capacity) len = capacity; MEMCPY((VALUE *)RARRAY(ary)->as.ary, ptr, VALUE, len); + ary_heap_free_ptr(ary, ptr, old_capa); + FL_SET_EMBED(ary); ARY_SET_LEN(ary, len); - ruby_sized_xfree((VALUE *)ptr, RARRAY(ary)->as.heap.aux.capa); } } + + ary_verify(ary); } static inline void ary_shrink_capa(VALUE ary) { long capacity = ARY_HEAP_LEN(ary); - long old_capa = RARRAY(ary)->as.heap.aux.capa; + long old_capa = ARY_HEAP_CAPA(ary); assert(!ARY_SHARED_P(ary)); assert(old_capa >= capacity); - if (old_capa > capacity) - SIZED_REALLOC_N(RARRAY(ary)->as.heap.ptr, VALUE, capacity, old_capa); + if (old_capa > capacity) ary_heap_realloc(ary, capacity); + + ary_verify(ary); } static void @@ -253,6 +456,8 @@ ary_double_capa(VALUE ary, long min) } new_capa += min; ary_resize_capa(ary, new_capa); + + ary_verify(ary); } static void @@ -308,6 +513,7 @@ static inline void rb_ary_modify_check(VALUE ary) { rb_check_frozen(ary); + ary_verify(ary); } void @@ -317,6 +523,9 @@ rb_ary_modify(VALUE ary) if (ARY_SHARED_P(ary)) { long shared_len, len = RARRAY_LEN(ary); VALUE shared = ARY_SHARED(ary); + + ary_verify(shared); + if (len <= RARRAY_EMBED_LEN_MAX) { const VALUE *ptr = ARY_HEAP_PTR(ary); FL_UNSET_SHARED(ary); @@ -326,9 +535,9 @@ rb_ary_modify(VALUE ary) ARY_SET_EMBED_LEN(ary, len); } else if (ARY_SHARED_OCCUPIED(shared) && len > ((shared_len = RARRAY_LEN(shared))>>1)) { - long shift = RARRAY_CONST_PTR(ary) - RARRAY_CONST_PTR(shared); + long shift = RARRAY_CONST_PTR_TRANSIENT(ary) - RARRAY_CONST_PTR_TRANSIENT(shared); FL_UNSET_SHARED(ary); - ARY_SET_PTR(ary, RARRAY_CONST_PTR(shared)); + ARY_SET_PTR(ary, RARRAY_CONST_PTR_TRANSIENT(shared)); ARY_SET_CAPA(ary, shared_len); RARRAY_PTR_USE(ary, ptr, { MEMMOVE(ptr, ptr+shift, VALUE, len); @@ -337,8 +546,8 @@ rb_ary_modify(VALUE ary) rb_ary_decrement_share(shared); } else { - VALUE *ptr = ALLOC_N(VALUE, len); - MEMCPY(ptr, RARRAY_CONST_PTR(ary), VALUE, len); + VALUE *ptr = ary_heap_alloc(ary, len); + MEMCPY(ptr, ARY_HEAP_PTR(ary), VALUE, len); rb_ary_unshare(ary); ARY_SET_CAPA(ary, len); ARY_SET_PTR(ary, ptr); @@ -346,6 +555,7 @@ rb_ary_modify(VALUE ary) rb_gc_writebarrier_remember(ary); } + ary_verify(ary); } static VALUE @@ -362,9 +572,12 @@ ary_ensure_room_for_push(VALUE ary, long add_len) if (new_len > RARRAY_EMBED_LEN_MAX) { VALUE shared = ARY_SHARED(ary); if (ARY_SHARED_OCCUPIED(shared)) { - if (RARRAY_CONST_PTR(ary) - RARRAY_CONST_PTR(shared) + new_len <= RARRAY_LEN(shared)) { + if (ARY_HEAP_PTR(ary) - RARRAY_CONST_PTR_TRANSIENT(shared) + new_len <= RARRAY_LEN(shared)) { rb_ary_modify_check(ary); - return shared; + + ary_verify(ary); + ary_verify(shared); + return shared; } else { /* if array is shared, then it is likely it participate in push/shift pattern */ @@ -373,11 +586,13 @@ ary_ensure_room_for_push(VALUE ary, long add_len) if (new_len > capa - (capa >> 6)) { ary_double_capa(ary, new_len); } + ary_verify(ary); return ary; } } } - rb_ary_modify(ary); + ary_verify(ary); + rb_ary_modify(ary); } else { rb_ary_modify_check(ary); @@ -387,6 +602,7 @@ ary_ensure_room_for_push(VALUE ary, long add_len) ary_double_capa(ary, new_len); } + ary_verify(ary); return ary; } @@ -459,7 +675,7 @@ ary_new(VALUE klass, long capa) ary = ary_alloc(klass); if (capa > RARRAY_EMBED_LEN_MAX) { - ptr = ALLOC_N(VALUE, capa); + ptr = ary_heap_alloc(ary, capa); FL_UNSET_EMBED(ary); ARY_SET_PTR(ary, ptr); ARY_SET_CAPA(ary, capa); @@ -523,7 +739,9 @@ rb_ary_new_from_values(long n, const VALUE *elts) VALUE rb_ary_tmp_new(long capa) { - return ary_new(0, capa); + VALUE ary = ary_new(0, capa); + rb_ary_transient_heap_evacuate(ary, TRUE); + return ary; } VALUE @@ -532,6 +750,7 @@ rb_ary_tmp_new_fill(long capa) VALUE ary = ary_new(0, capa); ary_memfill(ary, 0, capa, Qnil); ARY_SET_LEN(ary, capa); + rb_ary_transient_heap_evacuate(ary, TRUE); return ary; } @@ -539,8 +758,13 @@ void rb_ary_free(VALUE ary) { if (ARY_OWNS_HEAP_P(ary)) { - RB_DEBUG_COUNTER_INC(obj_ary_ptr); - ruby_sized_xfree((void *)ARY_HEAP_PTR(ary), ARY_HEAP_SIZE(ary)); + if (RARRAY_TRANSIENT_P(ary)) { + RB_DEBUG_COUNTER_INC(obj_ary_transient); + } + else { + RB_DEBUG_COUNTER_INC(obj_ary_ptr); + ary_heap_free(ary); + } } else { RB_DEBUG_COUNTER_INC(obj_ary_embed); @@ -563,13 +787,15 @@ ary_discard(VALUE ary) { rb_ary_free(ary); RBASIC(ary)->flags |= RARRAY_EMBED_FLAG; - RBASIC(ary)->flags &= ~RARRAY_EMBED_LEN_MASK; + RBASIC(ary)->flags &= ~(RARRAY_EMBED_LEN_MASK | RARRAY_TRANSIENT_FLAG); } static VALUE ary_make_shared(VALUE ary) { assert(!ARY_EMBED_P(ary)); + ary_verify(ary); + if (ARY_SHARED_P(ary)) { return ARY_SHARED(ary); } @@ -577,6 +803,7 @@ ary_make_shared(VALUE ary) return ary; } else if (OBJ_FROZEN(ary)) { + rb_ary_transient_heap_evacuate(ary, TRUE); ary_shrink_capa(ary); FL_SET_SHARED_ROOT(ary); ARY_SET_SHARED_NUM(ary, 1); @@ -584,18 +811,25 @@ ary_make_shared(VALUE ary) } else { long capa = ARY_CAPA(ary), len = RARRAY_LEN(ary); + const VALUE *ptr; NEWOBJ_OF(shared, struct RArray, 0, T_ARRAY | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0)); - FL_UNSET_EMBED(shared); + rb_ary_transient_heap_evacuate(ary, TRUE); + ptr = ARY_HEAP_PTR(ary); + + FL_UNSET_EMBED(shared); ARY_SET_LEN((VALUE)shared, capa); - ARY_SET_PTR((VALUE)shared, RARRAY_CONST_PTR(ary)); - ary_mem_clear((VALUE)shared, len, capa - len); + ARY_SET_PTR((VALUE)shared, ptr); + ary_mem_clear((VALUE)shared, len, capa - len); FL_SET_SHARED_ROOT(shared); ARY_SET_SHARED_NUM((VALUE)shared, 1); FL_SET_SHARED(ary); ARY_SET_SHARED(ary, (VALUE)shared); OBJ_FREEZE(shared); - return (VALUE)shared; + + ary_verify((VALUE)shared); + ary_verify(ary); + return (VALUE)shared; } } @@ -606,7 +840,7 @@ ary_make_substitution(VALUE ary) if (len <= RARRAY_EMBED_LEN_MAX) { VALUE subst = rb_ary_new2(len); - ary_memcpy(subst, 0, len, RARRAY_CONST_PTR(ary)); + ary_memcpy(subst, 0, len, RARRAY_CONST_PTR_TRANSIENT(ary)); ARY_SET_EMBED_LEN(subst, len); return subst; } @@ -729,8 +963,8 @@ rb_ary_initialize(int argc, VALUE *argv, VALUE ary) rb_ary_modify(ary); if (argc == 0) { - if (ARY_OWNS_HEAP_P(ary) && RARRAY_CONST_PTR(ary) != 0) { - ruby_sized_xfree((void *)RARRAY_CONST_PTR(ary), ARY_HEAP_SIZE(ary)); + if (ARY_OWNS_HEAP_P(ary) && ARY_HEAP_PTR(ary) != NULL) { + ary_heap_free(ary); } rb_ary_unshare_safe(ary); FL_SET_EMBED(ary); @@ -837,7 +1071,7 @@ ary_make_partial(VALUE ary, VALUE klass, long offset, long len) if (len <= RARRAY_EMBED_LEN_MAX) { VALUE result = ary_alloc(klass); - ary_memcpy(result, 0, len, RARRAY_CONST_PTR(ary) + offset); + ary_memcpy(result, 0, len, RARRAY_CONST_PTR_TRANSIENT(ary) + offset); ARY_SET_EMBED_LEN(result, len); return result; } @@ -846,12 +1080,15 @@ ary_make_partial(VALUE ary, VALUE klass, long offset, long len) FL_UNSET_EMBED(result); shared = ary_make_shared(ary); - ARY_SET_PTR(result, RARRAY_CONST_PTR(ary)); + ARY_SET_PTR(result, RARRAY_CONST_PTR_TRANSIENT(ary)); ARY_SET_LEN(result, RARRAY_LEN(ary)); rb_ary_set_shared(result, shared); ARY_INCREASE_PTR(result, offset); ARY_SET_LEN(result, len); + + ary_verify(shared); + ary_verify(result); return result; } } @@ -910,12 +1147,13 @@ ary_take_first_or_last(int argc, const VALUE *argv, VALUE ary, enum ary_take_pos VALUE rb_ary_push(VALUE ary, VALUE item) { - long idx = RARRAY_LEN(ary); + long idx = RARRAY_LEN((ary_verify(ary), ary)); VALUE target_ary = ary_ensure_room_for_push(ary, 1); RARRAY_PTR_USE(ary, ptr, { RB_OBJ_WRITE(target_ary, &ptr[idx], item); }); ARY_SET_LEN(ary, idx + 1); + ary_verify(ary); return ary; } @@ -967,6 +1205,7 @@ rb_ary_pop(VALUE ary) } --n; ARY_SET_LEN(ary, n); + ary_verify(ary); return RARRAY_AREF(ary, n); } @@ -1000,6 +1239,7 @@ rb_ary_pop_m(int argc, VALUE *argv, VALUE ary) rb_ary_modify_check(ary); result = ary_take_first_or_last(argc, argv, ary, ARY_TAKE_LAST); ARY_INCREASE_LEN(ary, -RARRAY_LEN(result)); + ary_verify(ary); return result; } @@ -1018,6 +1258,7 @@ rb_ary_shift(VALUE ary) MEMMOVE(ptr, ptr+1, VALUE, len-1); }); /* WB: no new reference */ ARY_INCREASE_LEN(ary, -1); + ary_verify(ary); return top; } assert(!ARY_EMBED_P(ary)); /* ARY_EMBED_LEN_MAX < ARY_DEFAULT_SIZE */ @@ -1031,6 +1272,8 @@ rb_ary_shift(VALUE ary) ARY_INCREASE_PTR(ary, 1); /* shift ptr */ ARY_INCREASE_LEN(ary, -1); + ary_verify(ary); + return top; } @@ -1101,6 +1344,7 @@ rb_ary_behead(VALUE ary, long n) } ARY_INCREASE_LEN(ary, -n); + ary_verify(ary); return ary; } @@ -1120,8 +1364,8 @@ ary_ensure_room_for_unshift(VALUE ary, int argc) VALUE shared = ARY_SHARED(ary); capa = RARRAY_LEN(shared); if (ARY_SHARED_OCCUPIED(shared) && capa > new_len) { - head = RARRAY_CONST_PTR(ary); - sharedp = RARRAY_CONST_PTR(shared); + head = RARRAY_CONST_PTR_TRANSIENT(ary); + sharedp = RARRAY_CONST_PTR_TRANSIENT(shared); goto makeroom_if_need; } } @@ -1134,11 +1378,13 @@ ary_ensure_room_for_unshift(VALUE ary, int argc) /* use shared array for big "queues" */ if (new_len > ARY_DEFAULT_SIZE * 4) { - /* make a room for unshifted items */ + ary_verify(ary); + + /* make a room for unshifted items */ capa = ARY_CAPA(ary); ary_make_shared(ary); - head = sharedp = RARRAY_CONST_PTR(ary); + head = sharedp = RARRAY_CONST_PTR_TRANSIENT(ary); goto makeroom; makeroom_if_need: if (head - sharedp < argc) { @@ -1151,6 +1397,8 @@ ary_ensure_room_for_unshift(VALUE ary, int argc) } ARY_SET_PTR(ary, head - argc); assert(ARY_SHARED_OCCUPIED(ARY_SHARED(ary))); + + ary_verify(ary); return ARY_SHARED(ary); } else { @@ -1159,6 +1407,7 @@ ary_ensure_room_for_unshift(VALUE ary, int argc) MEMMOVE(ptr + argc, ptr, VALUE, len); }); + ary_verify(ary); return ary; } } @@ -1576,7 +1825,7 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) } { - const VALUE *optr = RARRAY_CONST_PTR(ary); + const VALUE *optr = RARRAY_CONST_PTR_TRANSIENT(ary); rofs = (rptr >= optr && rptr < optr + olen) ? rptr - optr : -1; } @@ -1589,7 +1838,7 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) len = beg + rlen; ary_mem_clear(ary, olen, beg - olen); if (rlen > 0) { - if (rofs != -1) rptr = RARRAY_CONST_PTR(ary) + rofs; + if (rofs != -1) rptr = RARRAY_CONST_PTR_TRANSIENT(ary) + rofs; ary_memcpy0(ary, beg, rlen, rptr, target_ary); } ARY_SET_LEN(ary, len); @@ -1613,7 +1862,7 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) ARY_SET_LEN(ary, alen); } if (rlen > 0) { - if (rofs != -1) rptr = RARRAY_CONST_PTR(ary) + rofs; + if (rofs != -1) rptr = RARRAY_CONST_PTR_TRANSIENT(ary) + rofs; /* give up wb-protected ary */ MEMMOVE(RARRAY_PTR(ary) + beg, rptr, VALUE, rlen); } @@ -1673,11 +1922,12 @@ rb_ary_resize(VALUE ary, long len) } else { if (olen > len + ARY_DEFAULT_SIZE) { - SIZED_REALLOC_N(RARRAY(ary)->as.heap.ptr, VALUE, len, RARRAY(ary)->as.heap.aux.capa); + ary_heap_realloc(ary, len); ARY_SET_CAPA(ary, len); } ARY_SET_HEAP_LEN(ary, len); } + ary_verify(ary); return ary; } @@ -1738,7 +1988,7 @@ rb_ary_aset(int argc, VALUE *argv, VALUE ary) /* check if idx is Range */ range: rpl = rb_ary_to_ary(argv[argc-1]); - rb_ary_splice(ary, beg, len, RARRAY_CONST_PTR(rpl), RARRAY_LEN(rpl)); + rb_ary_splice(ary, beg, len, RARRAY_CONST_PTR_TRANSIENT(rpl), RARRAY_LEN(rpl)); RB_GC_GUARD(rpl); return argv[argc-1]; } @@ -1820,7 +2070,7 @@ VALUE rb_ary_each(VALUE ary) { long i; - + ary_verify(ary); RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); for (i=0; i 0) { - const VALUE *p1 = RARRAY_CONST_PTR(ary); - VALUE *p2 = (VALUE *)RARRAY_CONST_PTR(dup) + len - 1; + const VALUE *p1 = RARRAY_CONST_PTR_TRANSIENT(ary); + VALUE *p2 = (VALUE *)RARRAY_CONST_PTR_TRANSIENT(dup) + len - 1; do *p2-- = *p1++; while (--len > 0); } ARY_SET_LEN(dup, RARRAY_LEN(ary)); @@ -2382,7 +2635,7 @@ rb_ary_rotate_m(int argc, VALUE *argv, VALUE ary) rotated = rb_ary_new2(len); if (len > 0) { cnt = rotate_count(cnt, len); - ptr = RARRAY_CONST_PTR(ary); + ptr = RARRAY_CONST_PTR_TRANSIENT(ary); len -= cnt; ary_memcpy(rotated, 0, len, ptr + cnt); ary_memcpy(rotated, len, cnt, ptr); @@ -2482,7 +2735,6 @@ rb_ary_sort_bang(VALUE ary) VALUE tmp = ary_make_substitution(ary); /* only ary refers tmp */ struct ary_sort_data data; long len = RARRAY_LEN(ary); - RBASIC_CLEAR_CLASS(tmp); data.ary = tmp; data.cmp_opt.opt_methods = 0; @@ -2515,21 +2767,22 @@ rb_ary_sort_bang(VALUE ary) rb_ary_unshare(ary); } else { - ruby_sized_xfree((void *)ARY_HEAP_PTR(ary), ARY_HEAP_SIZE(ary)); + ary_heap_free(ary); } - ARY_SET_PTR(ary, RARRAY_CONST_PTR(tmp)); + ARY_SET_PTR(ary, ARY_HEAP_PTR(tmp)); ARY_SET_HEAP_LEN(ary, len); - ARY_SET_CAPA(ary, RARRAY_LEN(tmp)); + ARY_SET_CAPA(ary, ARY_HEAP_LEN(tmp)); } /* tmp was lost ownership for the ptr */ FL_UNSET(tmp, FL_FREEZE); FL_SET_EMBED(tmp); ARY_SET_EMBED_LEN(tmp, 0); FL_SET(tmp, FL_FREEZE); - } + } /* tmp will be GC'ed. */ RBASIC_SET_CLASS_RAW(tmp, rb_cArray); /* rb_cArray must be marked */ } + ary_verify(ary); return ary; } @@ -2757,7 +3010,7 @@ rb_ary_collect(VALUE ary) RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); collect = rb_ary_new2(RARRAY_LEN(ary)); for (i = 0; i < RARRAY_LEN(ary); i++) { - rb_ary_push(collect, rb_yield_force_blockarg(RARRAY_AREF(ary, i))); + rb_ary_push(collect, rb_yield(RARRAY_AREF(ary, i))); } return collect; } @@ -2833,7 +3086,7 @@ append_values_at_single(VALUE result, VALUE ary, long olen, VALUE idx) /* check if idx is Range */ else if (rb_range_beg_len(idx, &beg, &len, olen, 1)) { if (len > 0) { - const VALUE *const src = RARRAY_CONST_PTR(ary); + const VALUE *const src = RARRAY_CONST_PTR_TRANSIENT(ary); const long end = beg + len; const long prevlen = RARRAY_LEN(result); if (beg < olen) { @@ -2886,18 +3139,22 @@ rb_ary_values_at(int argc, VALUE *argv, VALUE ary) * call-seq: * ary.select {|item| block} -> new_ary * ary.select -> Enumerator + * ary.filter {|item| block} -> new_ary + * ary.filter -> Enumerator * * Returns a new array containing all elements of +ary+ * for which the given +block+ returns a true value. * * If no block is given, an Enumerator is returned instead. * - * [1,2,3,4,5].select {|num| num.even? } #=> [2, 4] + * [1,2,3,4,5].select {|num| num.even? } #=> [2, 4] * - * a = %w{ a b c d e f } - * a.select {|v| v =~ /[aeiou]/} #=> ["a", "e"] + * a = %w[ a b c d e f ] + * a.select {|v| v =~ /[aeiou]/ } #=> ["a", "e"] * * See also Enumerable#select. + * + * Array#filter is an alias for Array#select. */ static VALUE @@ -2962,8 +3219,10 @@ select_bang_ensure(VALUE a) /* * call-seq: - * ary.select! {|item| block } -> ary or nil - * ary.select! -> Enumerator + * ary.select! {|item| block } -> ary or nil + * ary.select! -> Enumerator + * ary.filter! {|item| block } -> ary or nil + * ary.filter! -> Enumerator * * Invokes the given block passing in successive elements from +self+, * deleting elements for which the block returns a +false+ value. @@ -2972,10 +3231,11 @@ select_bang_ensure(VALUE a) * * If changes were made, it will return +self+, otherwise it returns +nil+. * - * See also Array#keep_if - * * If no block is given, an Enumerator is returned instead. * + * See also Array#keep_if. + * + * Array#filter! is an alias for Array#select!. */ static VALUE @@ -2997,14 +3257,15 @@ rb_ary_select_bang(VALUE ary) * ary.keep_if -> Enumerator * * Deletes every element of +self+ for which the given block evaluates to - * +false+. - * - * See also Array#select! + * +false+, and returns +self+. * * If no block is given, an Enumerator is returned instead. * - * a = %w{ a b c d e f } - * a.keep_if {|v| v =~ /[aeiou]/} #=> ["a", "e"] + * a = %w[ a b c d e f ] + * a.keep_if {|v| v =~ /[aeiou]/ } #=> ["a", "e"] + * a #=> ["a", "e"] + * + * See also Array#select!. */ static VALUE @@ -3075,6 +3336,7 @@ rb_ary_delete(VALUE ary, VALUE item) ary_resize_smaller(ary, i2); + ary_verify(ary); return v; } @@ -3119,7 +3381,7 @@ rb_ary_delete_at(VALUE ary, long pos) MEMMOVE(ptr+pos, ptr+pos+1, VALUE, len-pos-1); }); ARY_INCREASE_LEN(ary, -1); - + ary_verify(ary); return del; } @@ -3187,7 +3449,7 @@ rb_ary_slice_bang(int argc, VALUE *argv, VALUE ary) len = orig_len - pos; } if (len == 0) return rb_ary_new2(0); - arg2 = rb_ary_new4(len, RARRAY_CONST_PTR(ary)+pos); + arg2 = rb_ary_new4(len, RARRAY_CONST_PTR_TRANSIENT(ary)+pos); RBASIC_SET_CLASS(arg2, rb_obj_class(ary)); rb_ary_splice(ary, pos, len, 0, 0); return arg2; @@ -3223,7 +3485,8 @@ ary_reject(VALUE orig, VALUE result) for (i = 0; i < RARRAY_LEN(orig); i++) { VALUE v = RARRAY_AREF(orig, i); - if (!RTEST(rb_yield(v))) { + + if (!RTEST(rb_yield(v))) { rb_ary_push(result, v); } } @@ -3252,7 +3515,6 @@ static VALUE ary_reject_bang(VALUE ary) { struct select_bang_arg args; - rb_ary_modify_check(ary); args.ary = ary; args.len[0] = args.len[1] = 0; @@ -3326,6 +3588,7 @@ rb_ary_reject(VALUE ary) static VALUE rb_ary_delete_if(VALUE ary) { + ary_verify(ary); RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); ary_reject_bang(ary); return ary; @@ -3335,7 +3598,8 @@ static VALUE take_i(RB_BLOCK_CALL_FUNC_ARGLIST(val, cbarg)) { VALUE *args = (VALUE *)cbarg; - if (args[1]-- == 0) rb_iter_break(); + if (args[1] == 0) rb_iter_break(); + else args[1]--; if (argc > 1) val = rb_ary_new4(argc, argv); rb_ary_push(args[0], val); return Qnil; @@ -3504,14 +3768,14 @@ rb_ary_replace(VALUE copy, VALUE orig) VALUE shared = 0; if (ARY_OWNS_HEAP_P(copy)) { - RARRAY_PTR_USE(copy, ptr, ruby_sized_xfree(ptr, ARY_HEAP_SIZE(copy))); + ary_heap_free(copy); } else if (ARY_SHARED_P(copy)) { shared = ARY_SHARED(copy); FL_UNSET_SHARED(copy); } FL_SET_EMBED(copy); - ary_memcpy(copy, 0, RARRAY_LEN(orig), RARRAY_CONST_PTR(orig)); + ary_memcpy(copy, 0, RARRAY_LEN(orig), RARRAY_CONST_PTR_TRANSIENT(orig)); if (shared) { rb_ary_decrement_share(shared); } @@ -3520,16 +3784,17 @@ rb_ary_replace(VALUE copy, VALUE orig) else { VALUE shared = ary_make_shared(orig); if (ARY_OWNS_HEAP_P(copy)) { - RARRAY_PTR_USE(copy, ptr, ruby_sized_xfree(ptr, ARY_HEAP_SIZE(copy))); + ary_heap_free(copy); } else { rb_ary_unshare_safe(copy); } FL_UNSET_EMBED(copy); - ARY_SET_PTR(copy, RARRAY_CONST_PTR(orig)); - ARY_SET_LEN(copy, RARRAY_LEN(orig)); + ARY_SET_PTR(copy, ARY_HEAP_PTR(orig)); + ARY_SET_LEN(copy, ARY_HEAP_LEN(orig)); rb_ary_set_shared(copy, shared); } + ary_verify(copy); return copy; } @@ -3547,16 +3812,20 @@ VALUE rb_ary_clear(VALUE ary) { rb_ary_modify_check(ary); - ARY_SET_LEN(ary, 0); if (ARY_SHARED_P(ary)) { if (!ARY_EMBED_P(ary)) { rb_ary_unshare(ary); FL_SET_EMBED(ary); + ARY_SET_EMBED_LEN(ary, 0); } } - else if (ARY_DEFAULT_SIZE * 2 < ARY_CAPA(ary)) { - ary_resize_capa(ary, ARY_DEFAULT_SIZE * 2); + else { + ARY_SET_LEN(ary, 0); + if (ARY_DEFAULT_SIZE * 2 < ARY_CAPA(ary)) { + ary_resize_capa(ary, ARY_DEFAULT_SIZE * 2); + } } + ary_verify(ary); return ary; } @@ -3689,8 +3958,8 @@ rb_ary_plus(VALUE x, VALUE y) len = xlen + ylen; z = rb_ary_new2(len); - ary_memcpy(z, 0, xlen, RARRAY_CONST_PTR(x)); - ary_memcpy(z, xlen, ylen, RARRAY_CONST_PTR(y)); + ary_memcpy(z, 0, xlen, RARRAY_CONST_PTR_TRANSIENT(x)); + ary_memcpy(z, xlen, ylen, RARRAY_CONST_PTR_TRANSIENT(y)); ARY_SET_LEN(z, len); return z; } @@ -3700,7 +3969,7 @@ ary_append(VALUE x, VALUE y) { long n = RARRAY_LEN(y); if (n > 0) { - rb_ary_splice(x, RARRAY_LEN(x), 0, RARRAY_CONST_PTR(y), n); + rb_ary_splice(x, RARRAY_LEN(x), 0, RARRAY_CONST_PTR_TRANSIENT(y), n); } return x; } @@ -3742,6 +4011,7 @@ rb_ary_concat_multi(int argc, VALUE *argv, VALUE ary) ary_append(ary, args); } + ary_verify(ary); return ary; } @@ -3796,16 +4066,16 @@ rb_ary_times(VALUE ary, VALUE times) ary2 = ary_new(rb_obj_class(ary), len); ARY_SET_LEN(ary2, len); - ptr = RARRAY_CONST_PTR(ary); + ptr = RARRAY_CONST_PTR_TRANSIENT(ary); t = RARRAY_LEN(ary); if (0 < t) { ary_memcpy(ary2, 0, t, ptr); while (t <= len/2) { - ary_memcpy(ary2, t, t, RARRAY_CONST_PTR(ary2)); + ary_memcpy(ary2, t, t, RARRAY_CONST_PTR_TRANSIENT(ary2)); t *= 2; } if (t < len) { - ary_memcpy(ary2, t, len-t, RARRAY_CONST_PTR(ary2)); + ary_memcpy(ary2, t, len-t, RARRAY_CONST_PTR_TRANSIENT(ary2)); } } out: @@ -3891,6 +4161,7 @@ recursive_equal(VALUE ary1, VALUE ary2, int recur) if (recur) return Qtrue; /* Subtle! */ + /* rb_equal() can evacuate ptrs */ p1 = RARRAY_CONST_PTR(ary1); p2 = RARRAY_CONST_PTR(ary2); len1 = RARRAY_LEN(ary1); @@ -3903,8 +4174,8 @@ recursive_equal(VALUE ary1, VALUE ary2, int recur) return Qfalse; if (len1 < i) return Qtrue; - p1 = RARRAY_CONST_PTR(ary1) + i; - p2 = RARRAY_CONST_PTR(ary2) + i; + p1 = RARRAY_CONST_PTR(ary1) + i; + p2 = RARRAY_CONST_PTR(ary2) + i; } else { return Qfalse; @@ -3941,7 +4212,7 @@ rb_ary_equal(VALUE ary1, VALUE ary2) return rb_equal(ary2, ary1); } if (RARRAY_LEN(ary1) != RARRAY_LEN(ary2)) return Qfalse; - if (RARRAY_CONST_PTR(ary1) == RARRAY_CONST_PTR(ary2)) return Qtrue; + if (RARRAY_CONST_PTR_TRANSIENT(ary1) == RARRAY_CONST_PTR_TRANSIENT(ary2)) return Qtrue; return rb_exec_recursive_paired(recursive_equal, ary1, ary2, ary2); } @@ -3972,7 +4243,7 @@ rb_ary_eql(VALUE ary1, VALUE ary2) if (ary1 == ary2) return Qtrue; if (!RB_TYPE_P(ary2, T_ARRAY)) return Qfalse; if (RARRAY_LEN(ary1) != RARRAY_LEN(ary2)) return Qfalse; - if (RARRAY_CONST_PTR(ary1) == RARRAY_CONST_PTR(ary2)) return Qtrue; + if (RARRAY_CONST_PTR_TRANSIENT(ary1) == RARRAY_CONST_PTR_TRANSIENT(ary2)) return Qtrue; return rb_exec_recursive_paired(recursive_eql, ary1, ary2, ary2); } @@ -4166,11 +4437,11 @@ static inline void ary_recycle_hash(VALUE hash) { assert(RBASIC_CLASS(hash) == 0); - if (RHASH(hash)->ntbl) { - st_table *tbl = RHASH(hash)->ntbl; + if (RHASH_TABLE_P(hash)) { + st_table *tbl = RHASH_ST_TABLE(hash); st_free_table(tbl); + RHASH_CLEAR(hash); } - rb_gc_force_recycle(hash); } /* @@ -4213,7 +4484,7 @@ rb_ary_diff(VALUE ary1, VALUE ary2) hash = ary_make_hash(ary2); for (i=0; ibody.root) { + rb_ast_dispose(ast); + rb_exc_raise(GET_EC()->errinfo); + } + + return ast_new_internal(ast, (NODE *)ast->body.root); +} + /* * call-seq: - * RubyVM::AST.parse(string) -> RubyVM::AST::Node + * RubyVM::AbstractSyntaxTree.parse(string) -> RubyVM::AbstractSyntaxTree::Node * * Parses the given string into an abstract syntax tree, * returning the root node of that tree. * * SyntaxError is raised if the given string is invalid syntax. * - * RubyVM::AST.parse("x = 1 + 2") - * # => # + * RubyVM::AbstractSyntaxTree.parse("x = 1 + 2") + * # => # */ static VALUE rb_ast_s_parse(VALUE module, VALUE str) { - VALUE obj; - rb_ast_t *ast = 0; + return rb_ast_parse_str(str); +} - const VALUE parser = rb_parser_new(); +static VALUE +rb_ast_parse_str(VALUE str) +{ + rb_ast_t *ast = 0; str = rb_check_string_type(str); - rb_parser_set_context(parser, NULL, 0); - ast = rb_parser_compile_string_path(parser, rb_str_new_cstr("no file name"), str, 1); - - if (!ast->body.root) { - rb_ast_dispose(ast); - rb_exc_raise(GET_EC()->errinfo); - } - - obj = ast_new_internal(ast, (NODE *)ast->body.root); - - return obj; + ast = rb_parser_compile_string_path(ast_parse_new(), Qnil, str, 1); + return ast_parse_done(ast); } /* * call-seq: - * RubyVM::AST.parse_file(pathname) -> RubyVM::AST::Node + * RubyVM::AbstractSyntaxTree.parse_file(pathname) -> RubyVM::AbstractSyntaxTree::Node * * Reads the file from pathname, then parses it like ::parse, * returning the root node of the abstract syntax tree. @@ -95,34 +111,145 @@ rb_ast_s_parse(VALUE module, VALUE str) * SyntaxError is raised if pathname's contents are not * valid Ruby syntax. * - * RubyVM::AST.parse_file("my-app/app.rb") - * # => # + * RubyVM::AbstractSyntaxTree.parse_file("my-app/app.rb") + * # => # */ static VALUE rb_ast_s_parse_file(VALUE module, VALUE path) { - VALUE obj, f; + return rb_ast_parse_file(path); +} + +static VALUE +rb_ast_parse_file(VALUE path) +{ + VALUE f; rb_ast_t *ast = 0; rb_encoding *enc = rb_utf8_encoding(); - const VALUE parser = rb_parser_new(); - FilePathValue(path); f = rb_file_open_str(path, "r"); rb_funcall(f, rb_intern("set_encoding"), 2, rb_enc_from_encoding(enc), rb_str_new_cstr("-")); - rb_parser_set_context(parser, NULL, 0); - ast = rb_parser_compile_file_path(parser, path, f, 1); - + ast = rb_parser_compile_file_path(ast_parse_new(), Qnil, f, 1); rb_io_close(f); + return ast_parse_done(ast); +} - if (!ast->body.root) { - rb_ast_dispose(ast); - rb_exc_raise(GET_EC()->errinfo); +static VALUE +lex_array(VALUE array, int index) +{ + VALUE str = rb_ary_entry(array, index); + if (!NIL_P(str)) { + StringValue(str); + if (!rb_enc_asciicompat(rb_enc_get(str))) { + rb_raise(rb_eArgError, "invalid source encoding"); + } + } + return str; +} + +static VALUE +rb_ast_parse_array(VALUE array) +{ + rb_ast_t *ast = 0; + + array = rb_check_array_type(array); + ast = rb_parser_compile_generic(ast_parse_new(), lex_array, Qnil, array, 1); + return ast_parse_done(ast); +} + +static VALUE node_children(rb_ast_t*, NODE*); + +static VALUE +node_find(VALUE self, const int node_id) +{ + VALUE ary; + long i; + struct ASTNodeData *data; + TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data); + + if (nd_node_id(data->node) == node_id) return self; + + ary = node_children(data->ast, data->node); + + for (i = 0; i < RARRAY_LEN(ary); i++) { + VALUE child = RARRAY_AREF(ary, i); + + if (CLASS_OF(child) == rb_cNode) { + VALUE result = node_find(child, node_id); + if (RTEST(result)) return result; + } } - obj = ast_new_internal(ast, (NODE *)ast->body.root); + return Qnil; +} - return obj; +extern VALUE rb_e_script; + +static VALUE +script_lines(VALUE path) +{ + VALUE hash, lines; + ID script_lines; + CONST_ID(script_lines, "SCRIPT_LINES__"); + if (!rb_const_defined_at(rb_cObject, script_lines)) return Qnil; + hash = rb_const_get_at(rb_cObject, script_lines); + if (!RB_TYPE_P(hash, T_HASH)) return Qnil; + lines = rb_hash_lookup(hash, path); + if (!RB_TYPE_P(lines, T_ARRAY)) return Qnil; + return lines; +} + +/* + * call-seq: + * RubyVM::AbstractSyntaxTree.of(proc) -> RubyVM::AbstractSyntaxTree::Node + * RubyVM::AbstractSyntaxTree.of(method) -> RubyVM::AbstractSyntaxTree::Node + * + * Returns AST nodes of the given proc or method. + * + * RubyVM::AbstractSyntaxTree.of(proc {1 + 2}) + * # => # + * + * def hello + * puts "hello, world" + * end + * + * RubyVM::AbstractSyntaxTree.of(method(:hello)) + * # => # + */ +static VALUE +rb_ast_s_of(VALUE module, VALUE body) +{ + VALUE path, node, lines; + int node_id; + const rb_iseq_t *iseq = NULL; + + if (rb_obj_is_proc(body)) { + iseq = vm_proc_iseq(body); + + if (!rb_obj_is_iseq((VALUE)iseq)) { + iseq = NULL; + } + } + else { + iseq = rb_method_iseq(body); + } + + if (!iseq) return Qnil; + + path = rb_iseq_path(iseq); + node_id = iseq->body->location.node_id; + if (!NIL_P(lines = script_lines(path))) { + node = rb_ast_parse_array(lines); + } + else if (RSTRING_LEN(path) == 2 && memcmp(RSTRING_PTR(path), "-e", 2) == 0) { + node = rb_ast_parse_str(rb_e_script); + } + else { + node = rb_ast_parse_file(path); + } + + return node_find(node, node_id); } static VALUE @@ -146,7 +273,7 @@ node_type_to_str(NODE *node) * * Returns the type of this node as a string. * - * root = RubyVM::AST.parse("x = 1 + 2") + * root = RubyVM::AbstractSyntaxTree.parse("x = 1 + 2") * root.type # => "NODE_SCOPE" * call = root.children[2] * call.type # => "NODE_OPCALL" @@ -605,20 +732,21 @@ void Init_ast(void) { /* - * AST provides methods to parse Ruby code into + * AbstractSyntaxTree provides methods to parse Ruby code into * abstract syntax trees. The nodes in the tree - * are instances of RubyVM::AST::Node. + * are instances of RubyVM::AbstractSyntaxTree::Node. */ - rb_mAST = rb_define_module_under(rb_cRubyVM, "AST"); + rb_mAST = rb_define_module_under(rb_cRubyVM, "AbstractSyntaxTree"); /* - * RubyVM::AST::Node instances are created by parse methods in - * RubyVM::AST. + * RubyVM::AbstractSyntaxTree::Node instances are created by parse methods in + * RubyVM::AbstractSyntaxTree. */ rb_cNode = rb_define_class_under(rb_mAST, "Node", rb_cObject); rb_undef_alloc_func(rb_cNode); rb_define_singleton_method(rb_mAST, "parse", rb_ast_s_parse, 1); rb_define_singleton_method(rb_mAST, "parse_file", rb_ast_s_parse_file, 1); + rb_define_singleton_method(rb_mAST, "of", rb_ast_s_of, 1); rb_define_method(rb_cNode, "type", rb_ast_node_type, 0); rb_define_method(rb_cNode, "first_lineno", rb_ast_node_first_lineno, 0); rb_define_method(rb_cNode, "first_column", rb_ast_node_first_column, 0); diff --git a/benchmark/fiber_chain.rb b/benchmark/fiber_chain.rb new file mode 100755 index 00000000000000..7e0a7f9d45cb4b --- /dev/null +++ b/benchmark/fiber_chain.rb @@ -0,0 +1,40 @@ +# Check performance of fiber creation and transfer. + +def make_link(previous) + Fiber.new do + while message = previous.resume + Fiber.yield(message) + end + end +end + +def make_chain(length, &block) + chain = Fiber.new(&block) + + (length - 1).times do + chain = make_link(chain) + end + + return chain +end + +def run_benchmark(length, repeats, message = :hello) + chain = nil + + chain = make_chain(length) do + while true + Fiber.yield(message) + end + end + + repeats.times do + abort "invalid result" unless chain.resume == message + end +end + +n = (ARGV[0] || 1000).to_i +m = (ARGV[1] || 1000).to_i + +5.times do + run_benchmark(n, m) +end diff --git a/bignum.c b/bignum.c index e53b2bd2c17723..b3046f20173fc3 100644 --- a/bignum.c +++ b/bignum.c @@ -385,6 +385,7 @@ bdigitdbl2bary(BDIGIT *ds, size_t n, BDIGIT_DBL num) static int bary_cmp(const BDIGIT *xds, size_t xn, const BDIGIT *yds, size_t yn) { + size_t i; BARY_TRUNC(xds, xn); BARY_TRUNC(yds, yn); @@ -393,11 +394,12 @@ bary_cmp(const BDIGIT *xds, size_t xn, const BDIGIT *yds, size_t yn) if (xn > yn) return 1; - while (xn-- && xds[xn] == yds[xn]) - ; - if (xn == (size_t)-1) + for (i = 0; i < xn; i++) + if (xds[xn - i - 1] != yds[yn - i - 1]) + break; + if (i == xn) return 0; - return xds[xn] < yds[xn] ? -1 : 1; + return xds[xn - i - 1] < yds[yn - i - 1] ? -1 : 1; } static BDIGIT @@ -418,15 +420,16 @@ bary_small_lshift(BDIGIT *zds, const BDIGIT *xds, size_t n, int shift) static void bary_small_rshift(BDIGIT *zds, const BDIGIT *xds, size_t n, int shift, BDIGIT higher_bdigit) { + size_t i; BDIGIT_DBL num = 0; assert(0 <= shift && shift < BITSPERDIG); num = BIGUP(higher_bdigit); - while (n--) { - BDIGIT x = xds[n]; + for (i = 0; i < n; i++) { + BDIGIT x = xds[n - i - 1]; num = (num | x) >> shift; - zds[n] = BIGLO(num); + zds[n - i - 1] = BIGLO(num); num = BIGUP(x); } } @@ -445,8 +448,9 @@ bary_zero_p(const BDIGIT *xds, size_t xn) static void bary_neg(BDIGIT *ds, size_t n) { - while (n--) - ds[n] = BIGLO(~ds[n]); + size_t i; + for (i = 0; i < n; i++) + ds[n - i - 1] = BIGLO(~ds[n - i - 1]); } static int @@ -616,8 +620,12 @@ static int bytes_2comp(unsigned char *buf, size_t len) { size_t i; - for (i = 0; i < len; i++) - buf[i] = ~buf[i]; + for (i = 0; i < len; i++) { + signed char c = buf[i]; + signed int d = ~c; + unsigned int e = d & 0xFF; + buf[i] = e; + } for (i = 0; i < len; i++) { buf[i]++; if (buf[i] != 0) @@ -1439,7 +1447,9 @@ bary_add_one(BDIGIT *ds, size_t n) { size_t i; for (i = 0; i < n; i++) { - ds[i] = BIGLO(ds[i]+1); + BDIGIT_DBL n = ds[i]; + n += 1; + ds[i] = BIGLO(n); if (ds[i] != 0) return 0; } @@ -1507,15 +1517,16 @@ bigdivrem_mulsub(BDIGIT *zds, size_t zn, BDIGIT x, const BDIGIT *yds, size_t yn) i = 0; do { - BDIGIT_DBL ee; + BDIGIT_DBL_SIGNED ee; t2 += (BDIGIT_DBL)yds[i] * x; ee = num - BIGLO(t2); - num = (BDIGIT_DBL)zds[i] + ee; + num = (BDIGIT_DBL_SIGNED)zds[i] + ee; if (ee) zds[i] = BIGLO(num); num = BIGDN(num); t2 = BIGDN(t2); } while (++i < yn); - num += zds[i] - t2; /* borrow from high digit; don't update */ + num -= (BDIGIT_DBL_SIGNED)t2; + num += (BDIGIT_DBL_SIGNED)zds[yn]; /* borrow from high digit; don't update */ return num; } @@ -2583,10 +2594,9 @@ bigdivrem_single1(BDIGIT *qds, const BDIGIT *xds, size_t xn, BDIGIT x_higher_bdi size_t i; BDIGIT_DBL t2; t2 = x_higher_bdigit; - i = xn; - while (i--) { - t2 = BIGUP(t2) + xds[i]; - qds[i] = (BDIGIT)(t2 / y); + for (i = 0; i < xn; i++) { + t2 = BIGUP(t2) + xds[xn - i - 1]; + qds[xn - i - 1] = (BDIGIT)(t2 / y); t2 %= y; } return (BDIGIT)t2; @@ -5083,6 +5093,9 @@ rb_big2str(VALUE x, int base) static unsigned long big2ulong(VALUE x, const char *type) { +#if SIZEOF_LONG > SIZEOF_BDIGIT + size_t i; +#endif size_t len = BIGNUM_LEN(x); unsigned long num; BDIGIT *ds; @@ -5097,9 +5110,9 @@ big2ulong(VALUE x, const char *type) num = (unsigned long)ds[0]; #else num = 0; - while (len--) { + for (i = 0; i < len; i++) { num <<= BITSPERDIG; - num += (unsigned long)ds[len]; /* overflow is already checked */ + num += (unsigned long)ds[len - i - 1]; /* overflow is already checked */ } #endif return num; @@ -5141,6 +5154,9 @@ rb_big2long(VALUE x) static unsigned LONG_LONG big2ull(VALUE x, const char *type) { +#if SIZEOF_LONG_LONG > SIZEOF_BDIGIT + size_t i; +#endif size_t len = BIGNUM_LEN(x); unsigned LONG_LONG num; BDIGIT *ds = BDIGITS(x); @@ -5153,9 +5169,9 @@ big2ull(VALUE x, const char *type) num = (unsigned LONG_LONG)ds[0]; #else num = 0; - while (len--) { + for (i = 0; i < len; i++) { num = BIGUP(num); - num += ds[len]; + num += ds[len - i - 1]; } #endif return num; @@ -5262,8 +5278,13 @@ big2dbl(VALUE x) } } if (carry) { - dl &= BDIGMAX << bits; - dl = BIGLO(dl + ((BDIGIT)1 << bits)); + BDIGIT mask = BDIGMAX; + BDIGIT bit = 1; + mask <<= bits; + bit <<= bits; + dl &= mask; + dl += bit; + dl = BIGLO(dl); if (!dl) d += 1; } } @@ -6032,12 +6053,15 @@ rb_big_divide(VALUE x, VALUE y, ID op) } else if (RB_FLOAT_TYPE_P(y)) { if (op == '/') { - return DBL2NUM(rb_big2dbl(x) / RFLOAT_VALUE(y)); + double dx = rb_big2dbl(x); + return rb_flo_div_flo(DBL2NUM(dx), y); } else { + VALUE v; double dy = RFLOAT_VALUE(y); if (dy == 0.0) rb_num_zerodiv(); - return rb_dbl2big(rb_big2dbl(x) / dy); + v = rb_big_divide(x, y, '/'); + return rb_dbl2big(RFLOAT_VALUE(v)); } } else { @@ -6170,6 +6194,7 @@ double rb_big_fdiv_double(VALUE x, VALUE y) { double dx, dy; + VALUE v; dx = big2dbl(x); if (FIXNUM_P(y)) { @@ -6190,7 +6215,8 @@ rb_big_fdiv_double(VALUE x, VALUE y) else { return NUM2DBL(rb_num_coerce_bin(x, y, rb_intern("fdiv"))); } - return dx / dy; + v = rb_flo_div_flo(DBL2NUM(dx), DBL2NUM(dy)); + return NUM2DBL(v); } VALUE @@ -6211,7 +6237,7 @@ rb_big_pow(VALUE x, VALUE y) if (RB_FLOAT_TYPE_P(y)) { d = RFLOAT_VALUE(y); if ((BIGNUM_NEGATIVE_P(x) && !BIGZEROP(x))) { - return rb_dbl_complex_polar(pow(-rb_big2dbl(x), d), d); + return rb_dbl_complex_polar_pi(pow(-rb_big2dbl(x), d), d); } } else if (RB_BIGNUM_TYPE_P(y)) { diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000000000..1a0b06b005ee0e --- /dev/null +++ b/bin/bundle @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# +# This file was generated by RubyGems. +# +# The application 'bundler' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = ">= 0.a" + +str = ARGV.first +if str + str = str.b[/\A_(.*)_\z/, 1] + if str and Gem::Version.correct?(str) + version = str + ARGV.shift + end +end + +if Gem.respond_to?(:activate_bin_path) +load Gem.activate_bin_path('bundler', 'bundle', version) +else +gem "bundler", version +load Gem.bin_path("bundler", "bundle", version) +end diff --git a/bin/bundler b/bin/bundler new file mode 100755 index 00000000000000..e15eb39ed70c10 --- /dev/null +++ b/bin/bundler @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# +# This file was generated by RubyGems. +# +# The application 'bundler' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = ">= 0.a" + +str = ARGV.first +if str + str = str.b[/\A_(.*)_\z/, 1] + if str and Gem::Version.correct?(str) + version = str + ARGV.shift + end +end + +if Gem.respond_to?(:activate_bin_path) +load Gem.activate_bin_path('bundler', 'bundler', version) +else +gem "bundler", version +load Gem.bin_path("bundler", "bundler", version) +end diff --git a/bin/irb b/bin/irb index c64ee85fbdcd41..ae6d358c9db038 100755 --- a/bin/irb +++ b/bin/irb @@ -1,11 +1,27 @@ #!/usr/bin/env ruby # -# irb.rb - interactive ruby -# $Release Version: 0.9.6 $ -# $Revision$ -# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# This file was generated by RubyGems. # +# The application 'irb' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = ">= 0.a" -require "irb" +str = ARGV.first +if str + str = str.b[/\A_(.*)_\z/, 1] + if str and Gem::Version.correct?(str) + version = str + ARGV.shift + end +end -IRB.start(__FILE__) +if Gem.respond_to?(:activate_bin_path) +load Gem.activate_bin_path('irb', 'irb', version) +else +gem "irb", version +load Gem.bin_path("irb", "irb", version) +end diff --git a/bin/rdoc b/bin/rdoc index aaa23292dfd1ec..8fa948cddb2cd0 100755 --- a/bin/rdoc +++ b/bin/rdoc @@ -1,44 +1,27 @@ #!/usr/bin/env ruby # -# RDoc: Documentation tool for source code -# (see lib/rdoc/rdoc.rb for more information) +# This file was generated by RubyGems. +# +# The application 'rdoc' is installed as part of a gem, and +# this file is here to facilitate running it. # -# Copyright (c) 2003 Dave Thomas -# Released under the same terms as Ruby -begin - gem 'rdoc' -rescue NameError => e # --disable-gems - raise unless e.name == :gem -rescue Gem::LoadError -end +require 'rubygems' -require 'rdoc/rdoc' +version = ">= 0.a" -begin - r = RDoc::RDoc.new - r.document ARGV -rescue Errno::ENOSPC - $stderr.puts 'Ran out of space creating documentation' - $stderr.puts - $stderr.puts 'Please free up some space and try again' -rescue SystemExit - raise -rescue Exception => e - if $DEBUG_RDOC then - $stderr.puts e.message - $stderr.puts "#{e.backtrace.join "\n\t"}" - $stderr.puts - elsif Interrupt === e then - $stderr.puts - $stderr.puts 'Interrupted' - else - $stderr.puts "uh-oh! RDoc had a problem:" - $stderr.puts e.message - $stderr.puts - $stderr.puts "run with --debug for full backtrace" +str = ARGV.first +if str + str = str.b[/\A_(.*)_\z/, 1] + if str and Gem::Version.correct?(str) + version = str + ARGV.shift end - - exit 1 end +if Gem.respond_to?(:activate_bin_path) +load Gem.activate_bin_path('rdoc', 'rdoc', version) +else +gem "rdoc", version +load Gem.bin_path("rdoc", "rdoc", version) +end diff --git a/bin/ri b/bin/ri index 7fbed0c09900f5..0cc2f73bb69f61 100755 --- a/bin/ri +++ b/bin/ri @@ -1,12 +1,27 @@ #!/usr/bin/env ruby +# +# This file was generated by RubyGems. +# +# The application 'rdoc' is installed as part of a gem, and +# this file is here to facilitate running it. +# -begin - gem 'rdoc' -rescue NameError => e # --disable-gems - raise unless e.name == :gem -rescue Gem::LoadError -end +require 'rubygems' + +version = ">= 0.a" -require 'rdoc/ri/driver' +str = ARGV.first +if str + str = str.b[/\A_(.*)_\z/, 1] + if str and Gem::Version.correct?(str) + version = str + ARGV.shift + end +end -RDoc::RI::Driver.run ARGV +if Gem.respond_to?(:activate_bin_path) +load Gem.activate_bin_path('rdoc', 'ri', version) +else +gem "rdoc", version +load Gem.bin_path("rdoc", "ri", version) +end diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb index a5d5d219cf95a4..feb8ada5c146a4 100755 --- a/bootstraptest/runner.rb +++ b/bootstraptest/runner.rb @@ -385,7 +385,7 @@ def assert_finish(timeout_seconds, testsrc, message = '') if IO.select([io], nil, nil, diff) begin io.read_nonblock(1024) - rescue Errno::EAGAIN, EOFError + rescue Errno::EAGAIN, IO::WaitReadable, EOFError break end while true end diff --git a/class.c b/class.c index d140e8adea0812..5785cee84a2369 100644 --- a/class.c +++ b/class.c @@ -1804,11 +1804,10 @@ NORETURN(static void unknown_keyword_error(VALUE hash, const ID *table, int keyw static void unknown_keyword_error(VALUE hash, const ID *table, int keywords) { - st_table *tbl = rb_hash_tbl_raw(hash); int i; for (i = 0; i < keywords; i++) { st_data_t key = ID2SYM(table[i]); - st_delete(tbl, &key, NULL); + rb_hash_stlike_delete(hash, &key, NULL); } rb_keyword_error("unknown", rb_hash_keys(hash)); } @@ -1855,7 +1854,7 @@ rb_extract_keywords(VALUE *orighash) *orighash = 0; return hash; } - st_foreach(rb_hash_tbl_raw(hash), separate_symbol, (st_data_t)&arg); + rb_hash_foreach(hash, separate_symbol, (st_data_t)&arg); if (arg.kwdhash) { if (arg.nonsymkey != Qundef) { rb_raise(rb_eArgError, "non-symbol key in keyword arguments: %+"PRIsVALUE, @@ -1876,8 +1875,8 @@ rb_get_kwargs(VALUE keyword_hash, const ID *table, int required, int optional, V #define extract_kwarg(keyword, val) \ (key = (st_data_t)(keyword), values ? \ - st_delete(rb_hash_tbl_raw(keyword_hash), &key, (val)) : \ - st_lookup(rb_hash_tbl_raw(keyword_hash), key, (val))) + rb_hash_stlike_delete(keyword_hash, &key, (val)) : \ + rb_hash_stlike_lookup(keyword_hash, key, (val))) if (NIL_P(keyword_hash)) keyword_hash = 0; diff --git a/common.mk b/common.mk index 42584f65a2129e..6ab6c2850085cf 100644 --- a/common.mk +++ b/common.mk @@ -2,7 +2,7 @@ bin: $(PROGRAM) $(WPROGRAM) lib: $(LIBRUBY) dll: $(LIBRUBY_SO) -.SUFFIXES: .inc .h .c .y .i .$(DTRACE_EXT) +.SUFFIXES: .inc .h .c .y .i .$(ASMEXT) .$(DTRACE_EXT) # V=0 quiet, V=1 verbose. other values don't work. V = 0 @@ -130,6 +130,7 @@ COMMONOBJS = array.$(OBJEXT) \ thread.$(OBJEXT) \ time.$(OBJEXT) \ transcode.$(OBJEXT) \ + transient_heap.$(OBJEXT) \ util.$(OBJEXT) \ variable.$(OBJEXT) \ version.$(OBJEXT) \ @@ -137,6 +138,7 @@ COMMONOBJS = array.$(OBJEXT) \ vm_backtrace.$(OBJEXT) \ vm_dump.$(OBJEXT) \ vm_trace.$(OBJEXT) \ + $(COROUTINE_OBJ) \ $(DTRACE_OBJ) \ $(BUILTIN_ENCOBJS) \ $(BUILTIN_TRANSOBJS) \ @@ -342,8 +344,12 @@ $(STATIC_RUBY)$(EXEEXT): $(MAINOBJ) $(DLDOBJS) $(EXTOBJS) $(LIBRUBY_A) $(PURIFY) $(CC) $(MAINOBJ) $(DLDOBJS) $(LIBRUBY_A) $(MAINLIBS) $(EXTLIBS) $(LIBS) $(OUTFLAG)$@ $(LDFLAGS) $(XLDFLAGS) ruby.imp: $(COMMONOBJS) - $(Q)$(NM) -Pgp $(COMMONOBJS) | \ - awk 'BEGIN{print "#!"}; $$2~/^[BDT]$$/&&$$1!~/^(Init_|ruby_static_id_|.*_threadptr_|rb_ec_\.)/{print $$1}' | \ + $(Q){ \ + $(NM) -Pgp $(COMMONOBJS) | \ + awk 'BEGIN{print "#!"}; $$2~/^[BDT]$$/&&$$1!~/^$(SYMBOL_PREFIX)(Init_|InitVM_|ruby_static_id_|.*_threadptr_|rb_ec_)|^\./{print $$1}'; \ + ($(CHDIR) $(srcdir) && \ + exec sed -n '/^MJIT_FUNC_EXPORTED/!d;N;s/.*\n\(rb_[a-zA-Z_0-9]*\).*/$(SYMBOL_PREFIX)\1/p' cont.c gc.c thread*c vm*.c) \ + } | \ sort -u -o $@ install: install-$(INSTALLDOC) @@ -536,15 +542,15 @@ post-install-gem:: rdoc: PHONY main @echo Generating RDoc documentation - $(Q) $(XRUBY) "$(srcdir)/bin/rdoc" --root "$(srcdir)" --page-dir "$(srcdir)/doc" --encoding=UTF-8 --no-force-update --all --ri --op "$(RDOCOUT)" $(RDOCFLAGS) "$(srcdir)" + $(Q) $(XRUBY) "$(srcdir)/libexec/rdoc" --root "$(srcdir)" --page-dir "$(srcdir)/doc" --encoding=UTF-8 --no-force-update --all --ri --op "$(RDOCOUT)" $(RDOCFLAGS) "$(srcdir)" html: PHONY main @echo Generating RDoc HTML files - $(Q) $(XRUBY) "$(srcdir)/bin/rdoc" --root "$(srcdir)" --page-dir "$(srcdir)/doc" --encoding=UTF-8 --no-force-update --all --op "$(HTMLOUT)" $(RDOCFLAGS) "$(srcdir)" + $(Q) $(XRUBY) "$(srcdir)/libexec/rdoc" --root "$(srcdir)" --page-dir "$(srcdir)/doc" --encoding=UTF-8 --no-force-update --all --op "$(HTMLOUT)" $(RDOCFLAGS) "$(srcdir)" rdoc-coverage: PHONY main @echo Generating RDoc coverage report - $(Q) $(XRUBY) "$(srcdir)/bin/rdoc" --root "$(srcdir)" --encoding=UTF-8 --all --quiet -C $(RDOCFLAGS) "$(srcdir)" + $(Q) $(XRUBY) "$(srcdir)/libexec/rdoc" --root "$(srcdir)" --encoding=UTF-8 --all --quiet -C $(RDOCFLAGS) "$(srcdir)" RDOCBENCHOUT=/tmp/rdocbench @@ -577,7 +583,8 @@ clean: clean-ext clean-enc clean-golf clean-docs clean-extout clean-local clean- clean-local:: clean-runnable $(Q)$(RM) $(OBJS) $(MINIOBJS) $(MAINOBJ) $(LIBRUBY_A) $(LIBRUBY_SO) $(LIBRUBY) $(LIBRUBY_ALIASES) $(Q)$(RM) $(PROGRAM) $(WPROGRAM) miniruby$(EXEEXT) dmyext.$(OBJEXT) dmyenc.$(OBJEXT) $(ARCHFILE) .*.time - $(Q)$(RM) y.tab.c y.output encdb.h transdb.h config.log rbconfig.rb $(ruby_pc) probes.h probes.$(OBJEXT) probes.stamp ruby-glommed.$(OBJEXT) + $(Q)$(RM) y.tab.c y.output encdb.h transdb.h config.log rbconfig.rb $(ruby_pc) + $(Q)$(RM) probes.h probes.$(OBJEXT) probes.stamp ruby-glommed.$(OBJEXT) ruby.imp $(Q)$(RM) GNUmakefile.old Makefile.old $(arch)-fake.rb bisect.sh $(ENC_TRANS_D) -$(Q) $(RMDIR) enc/jis enc/trans enc 2> $(NULL) || exit 0 clean-runnable:: PHONY @@ -615,12 +622,26 @@ distclean-spec: clean-spec distclean-rubyspec: distclean-spec realclean:: realclean-ext realclean-local realclean-enc realclean-golf realclean-extout -realclean-local:: distclean-local +realclean-local:: distclean-local realclean-srcs-local + +clean-srcs:: clean-srcs-local clean-srcs-ext +realclean-srcs:: realclean-srcs-local realclean-srcs-ext + +clean-srcs-local:: $(Q)$(RM) parse.c parse.h lex.c enc/trans/newline.c revision.h - $(Q)$(RM) id.c id.h probes.dmyh + $(Q)$(RM) id.c id.h probes.dmyh probes.h + $(Q)$(RM) encdb.h transdb.h verconf.h ruby-runner.h + $(Q)$(RM) mjit_build_dir.c mjit_config.h rb_mjit_header.h + $(Q)$(RM) $(MJIT_MIN_HEADER) $(MJIT_MIN_HEADER:.h=)$(MJIT_HEADER_SUFFIX:%=*).h + +realclean-srcs-local:: clean-srcs-local $(Q)$(CHDIR) $(srcdir) && $(exec) $(RM) parse.c parse.h lex.c enc/trans/newline.c $(PRELUDES) revision.h $(Q)$(CHDIR) $(srcdir) && $(exec) $(RM) id.c id.h probes.dmyh $(Q)$(CHDIR) $(srcdir) && $(exec) $(RM) configure aclocal.m4 tool/config.guess tool/config.sub gems/*.gem + +clean-srcs-ext:: +realclean-srcs-ext:: clean-srcs-ext + realclean-ext:: PHONY realclean-golf: distclean-golf $(Q)$(RM) $(GOLFPRELUDES) @@ -705,12 +726,12 @@ $(arch)-fake.rb: $(srcdir)/template/fake.rb.in $(srcdir)/tool/generic_erb.rb ver btest: $(TEST_RUNNABLE)-btest no-btest: PHONY yes-btest: fake miniruby$(EXEEXT) PHONY - $(Q)$(exec) $(BOOTSTRAPRUBY) "$(srcdir)/bootstraptest/runner.rb" --ruby="$(BTESTRUBY) $(RUN_OPTS)" $(OPTS) $(TESTOPTS) + $(Q)$(exec) $(BOOTSTRAPRUBY) "$(srcdir)/bootstraptest/runner.rb" --ruby="$(BTESTRUBY) $(RUN_OPTS)" $(OPTS) $(TESTOPTS) $(BTESTS) btest-ruby: $(TEST_RUNNABLE)-btest-ruby no-btest-ruby: PHONY yes-btest-ruby: prog PHONY - $(Q)$(exec) $(RUNRUBY) "$(srcdir)/bootstraptest/runner.rb" --ruby="$(PROGRAM) -I$(srcdir)/lib $(RUN_OPTS)" -q $(OPTS) $(TESTOPTS) + $(Q)$(exec) $(RUNRUBY) "$(srcdir)/bootstraptest/runner.rb" --ruby="$(PROGRAM) -I$(srcdir)/lib $(RUN_OPTS)" -q $(OPTS) $(TESTOPTS) $(BTESTS) test-basic: $(TEST_RUNNABLE)-test-basic no-test-basic: PHONY @@ -833,7 +854,7 @@ $(PLATFORM_D): $(Q) $(MAKEDIRS) $(PLATFORM_DIR) $(@D) @exit > $@ -exe/$(PROGRAM): ruby-runner.c ruby-runner.h exe/.time miniruby$(EXEEXT) +exe/$(PROGRAM): ruby-runner.c ruby-runner.h exe/.time miniruby$(EXEEXT) {$(VPATH)}config.h $(Q) $(CC) $(CFLAGS) $(CPPFLAGS) -DRUBY_INSTALL_NAME=$(@F) $(COUTFLAG)ruby-runner.$(OBJEXT) -c $(CSRCFLAG)$(srcdir)/ruby-runner.c $(Q) $(PURIFY) $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(OUTFLAG)$@ ruby-runner.$(OBJEXT) $(LIBS) $(Q) $(POSTLINK) @@ -846,7 +867,7 @@ exe/$(PROGRAM): ruby-runner.c ruby-runner.h exe/.time miniruby$(EXEEXT) $(@F) $(@D) exe/.time: - $(Q) $(MAKEDIRS) exe $(@D) + $(Q) $(MAKEDIRS) $(@D) @exit > $@ $(BUILTIN_ENCOBJS) $(BUILTIN_TRANSOBJS): $(ENC_TRANS_D) @@ -889,6 +910,12 @@ strstr.$(OBJEXT): {$(VPATH)}strstr.c nt.$(OBJEXT): {$(VPATH)}nt.c ia64.$(OBJEXT): {$(VPATH)}ia64.s $(CC) $(CFLAGS) -c $< +.coroutine_obj $(COROUTINE_OBJ): \ + {$(VPATH)}$(COROUTINE_H:.h=).$(ASMEXT) \ + $(COROUTINE_H:/Context.h=/.time) +$(COROUTINE_H:/Context.h=/.time): + $(Q) $(MAKEDIRS) $(@D) + @exit > $@ ### @@ -953,13 +980,20 @@ EXT_SRCS = $(srcdir)/ext/ripper/ripper.c \ $(srcdir)/ext/rbconfig/sizeof/sizes.c \ $(srcdir)/ext/rbconfig/sizeof/limits.c \ $(srcdir)/ext/socket/constdefs.c \ + $(srcdir)/ext/etc/constdefs.h \ # EXT_SRCS srcs-ext: $(EXT_SRCS) +realclean-srcs-ext:: + $(Q)$(RM) $(EXT_SRCS) -srcs-extra: $(srcdir)/ext/json/parser/parser.c \ - $(srcdir)/ext/date/zonetab.h \ - $(empty) +EXTRA_SRCS = $(srcdir)/ext/json/parser/parser.c \ + $(srcdir)/ext/date/zonetab.h \ + $(empty) + +srcs-extra: $(EXTRA_SRCS) +realclean-srcs-extra:: + $(Q)$(RM) $(EXTRA_SRCS) LIB_SRCS = $(srcdir)/lib/unicode_normalize/tables.rb @@ -1049,7 +1083,8 @@ preludes: {$(VPATH)}miniprelude.c preludes: {$(srcdir)}golf_prelude.c $(srcdir)/revision.h: - @exit > $@ + $(Q)$(gnumake:yes=#) $(RM) $(@F) + $(Q)$(gnumake:yes=#) exit > $@ || exit > $(@F) $(REVISION_H): $(srcdir)/version.h $(srcdir)/tool/file2lastrev.rb $(REVISION_FORCE) -$(Q) $(BASERUBY) $(srcdir)/tool/file2lastrev.rb -q --revision.h "$(srcdir)" > revision.tmp @@ -1095,6 +1130,12 @@ $(srcdir)/ext/socket/constdefs.c: $(srcdir)/ext/socket/depend $(exec) $(MAKE) -f - $(mflags) \ Q=$(Q) ECHO=$(ECHO) top_srcdir=../.. srcdir=. VPATH=../.. RUBY="$(BASERUBY)" +$(srcdir)/ext/etc/constdefs.h: $(srcdir)/ext/etc/depend + $(Q) $(CHDIR) $(@D) && \ + sed '/AUTOGENERATED/q' depend | \ + $(exec) $(MAKE) -f - $(mflags) \ + Q=$(Q) ECHO=$(ECHO) top_srcdir=../.. srcdir=. VPATH=../.. RUBY="$(BASERUBY)" + ## run: fake miniruby$(EXEEXT) PHONY @@ -1181,7 +1222,7 @@ update-mspec: update-rubyspec: update-config_files: PHONY - $(Q) $(BASERUBY) -C "$(srcdir)" tool/downloader.rb -d tool -e gnu \ + $(Q) $(BASERUBY) -C "$(srcdir)" tool/downloader.rb -d tool --cache-dir=$(CACHE_DIR) -e gnu \ config.guess config.sub update-gems: PHONY @@ -1208,11 +1249,11 @@ extract-gems: PHONY update-bundled_gems: PHONY $(Q) $(RUNRUBY) -rrubygems \ -pla \ - -e '$$_=Gem::SpecFetcher.fetcher.detect(:latest) {'"|s|" \ - -e 'if s.platform=="ruby"&&s.name==$$F[0]' \ - -e 'break [s.name, s.version, *$$F[2..-1]].join(" ")' \ - -e 'end' \ + -e '(gem,src), = Gem::SpecFetcher.fetcher.detect(:latest) {'"|s|" \ + -e 's.platform=="ruby"&&s.name==$$F[0]' \ -e '}' \ + -e 'gem = src.fetch_spec(gem)' \ + -e '$$_ = [gem.name, gem.version, gem.metadata["source_code_uri"]||gem.homepage].join(" ")' \ "$(srcdir)/gems/bundled_gems" | \ "$(IFCHANGE)" "$(srcdir)/gems/bundled_gems" - @@ -1222,7 +1263,7 @@ test-bundled-gems-fetch: $(PREP) $(Q) $(BASERUBY) -C $(srcdir)/gems ../tool/fetch-bundled_gems.rb src bundled_gems test-bundled-gems-prepare: test-bundled-gems-precheck test-bundled-gems-fetch - $(XRUBY) -C "$(srcdir)" bin/gem install --no-ri --no-rdoc \ + $(XRUBY) -C "$(srcdir)" bin/gem install --no-document \ --install-dir .bundle --conservative "bundler" "minitest:~> 5" 'test-unit' 'rake' 'hoe' 'yard' 'pry' 'packnga' PREPARE_BUNDLED_GEMS = test-bundled-gems-prepare @@ -1231,6 +1272,21 @@ yes-test-bundled-gems: test-bundled-gems-run no-test-bundled-gems: test-bundled-gems-run: $(PREPARE_BUNDLED_GEMS) +test-bundler-precheck: $(arch)-fake.rb programs + +yes-test-bundler-prepare: test-bundler-precheck + $(XRUBY) -C "$(srcdir)" bin/gem install --no-document \ + --install-dir .bundle --conservative "rspec:~> 3.5" + +RSPECOPTS = --format progress +BUNDLER_SPECS = +test-bundler: $(TEST_RUNNABLE)-test-bundler +yes-test-bundler: yes-test-bundler-prepare + $(gnumake_recursive)$(Q) \ + $(XRUBY) -C $(srcdir) -Ispec/bundler .bundle/bin/rspec \ + --require spec_helper $(RSPECOPTS) spec/bundler/$(BUNDLER_SPECS) +no-test-bundler: + UNICODE_FILES = $(UNICODE_SRC_DATA_DIR)/UnicodeData.txt \ $(UNICODE_SRC_DATA_DIR)/CompositionExclusions.txt \ $(UNICODE_SRC_DATA_DIR)/NormalizationTest.txt \ @@ -1247,13 +1303,18 @@ UNICODE_PROPERTY_FILES = \ $(UNICODE_SRC_DATA_DIR)/PropertyValueAliases.txt \ $(UNICODE_SRC_DATA_DIR)/Scripts.txt \ $(UNICODE_SRC_DATA_DIR)/auxiliary/GraphemeBreakProperty.txt \ + $(UNICODE_SRC_DATA_DIR)/auxiliary/GraphemeBreakTest.txt \ $(empty) UNICODE_EMOJI_FILES = \ $(UNICODE_SRC_EMOJI_DATA_DIR)/emoji-data.txt \ + $(UNICODE_SRC_EMOJI_DATA_DIR)/emoji-sequences.txt \ + $(UNICODE_SRC_EMOJI_DATA_DIR)/emoji-test.txt \ + $(UNICODE_SRC_EMOJI_DATA_DIR)/emoji-variation-sequences.txt \ + $(UNICODE_SRC_EMOJI_DATA_DIR)/emoji-zwj-sequences.txt \ $(empty) -update-unicode: $(UNICODE_FILES) +update-unicode: $(UNICODE_FILES) $(UNICODE_PROPERTY_FILES) CACHE_DIR = $(srcdir)/.downloaded-cache UNICODE_DOWNLOAD = \ @@ -1402,6 +1463,7 @@ help: PHONY " test-all: all ruby tests [TESTOPTS=-j4 TESTS=]" \ " test-spec: run the Ruby spec suite" \ " test-rubyspec: same as test-spec" \ + " test-bundler: run the Bundler spec" \ " test-bundled-gems: run the test suite of bundled gems" \ " up: update local copy and autogenerated files" \ " benchmark: benchmark this ruby and COMPARE_RUBY." \ @@ -1445,6 +1507,7 @@ array.$(OBJEXT): {$(VPATH)}probes.h array.$(OBJEXT): {$(VPATH)}ruby_assert.h array.$(OBJEXT): {$(VPATH)}st.h array.$(OBJEXT): {$(VPATH)}subst.h +array.$(OBJEXT): {$(VPATH)}transient_heap.h array.$(OBJEXT): {$(VPATH)}util.h ast.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h ast.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h @@ -1791,6 +1854,7 @@ enum.$(OBJEXT): {$(VPATH)}oniguruma.h enum.$(OBJEXT): {$(VPATH)}st.h enum.$(OBJEXT): {$(VPATH)}subst.h enum.$(OBJEXT): {$(VPATH)}symbol.h +enum.$(OBJEXT): {$(VPATH)}transient_heap.h enum.$(OBJEXT): {$(VPATH)}util.h enumerator.$(OBJEXT): $(hdrdir)/ruby/ruby.h enumerator.$(OBJEXT): $(top_srcdir)/include/ruby.h @@ -1935,6 +1999,7 @@ gc.$(OBJEXT): {$(VPATH)}subst.h gc.$(OBJEXT): {$(VPATH)}thread.h gc.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h gc.$(OBJEXT): {$(VPATH)}thread_native.h +gc.$(OBJEXT): {$(VPATH)}transient_heap.h gc.$(OBJEXT): {$(VPATH)}util.h gc.$(OBJEXT): {$(VPATH)}vm_core.h gc.$(OBJEXT): {$(VPATH)}vm_debug.h @@ -2032,14 +2097,20 @@ io.$(OBJEXT): {$(VPATH)}intern.h io.$(OBJEXT): {$(VPATH)}internal.h io.$(OBJEXT): {$(VPATH)}io.c io.$(OBJEXT): {$(VPATH)}io.h +io.$(OBJEXT): {$(VPATH)}method.h io.$(OBJEXT): {$(VPATH)}missing.h +io.$(OBJEXT): {$(VPATH)}node.h io.$(OBJEXT): {$(VPATH)}onigmo.h io.$(OBJEXT): {$(VPATH)}oniguruma.h +io.$(OBJEXT): {$(VPATH)}ruby_assert.h io.$(OBJEXT): {$(VPATH)}ruby_atomic.h io.$(OBJEXT): {$(VPATH)}st.h io.$(OBJEXT): {$(VPATH)}subst.h io.$(OBJEXT): {$(VPATH)}thread.h +io.$(OBJEXT): {$(VPATH)}thread_native.h io.$(OBJEXT): {$(VPATH)}util.h +io.$(OBJEXT): {$(VPATH)}vm_core.h +io.$(OBJEXT): {$(VPATH)}vm_opts.h iseq.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h iseq.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h iseq.$(OBJEXT): $(CCAN_DIR)/list/list.h @@ -2803,6 +2874,7 @@ struct.$(OBJEXT): {$(VPATH)}struct.c struct.$(OBJEXT): {$(VPATH)}subst.h struct.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h struct.$(OBJEXT): {$(VPATH)}thread_native.h +struct.$(OBJEXT): {$(VPATH)}transient_heap.h struct.$(OBJEXT): {$(VPATH)}vm_core.h struct.$(OBJEXT): {$(VPATH)}vm_debug.h struct.$(OBJEXT): {$(VPATH)}vm_opts.h @@ -2898,6 +2970,15 @@ transcode.$(OBJEXT): {$(VPATH)}st.h transcode.$(OBJEXT): {$(VPATH)}subst.h transcode.$(OBJEXT): {$(VPATH)}transcode.c transcode.$(OBJEXT): {$(VPATH)}transcode_data.h +transient_heap.$(OBJEXT): $(hdrdir)/ruby/ruby.h +transient_heap.$(OBJEXT): $(top_srcdir)/include/ruby.h +transient_heap.$(OBJEXT): {$(VPATH)}debug_counter.h +transient_heap.$(OBJEXT): {$(VPATH)}gc.h +transient_heap.$(OBJEXT): {$(VPATH)}internal.h +transient_heap.$(OBJEXT): {$(VPATH)}ruby_assert.h +transient_heap.$(OBJEXT): {$(VPATH)}transient_heap.c +transient_heap.$(OBJEXT): {$(VPATH)}transient_heap.h +transient_heap.$(OBJEXT): {$(VPATH)}vm_debug.h util.$(OBJEXT): $(hdrdir)/ruby/ruby.h util.$(OBJEXT): $(top_srcdir)/include/ruby.h util.$(OBJEXT): {$(VPATH)}config.h @@ -2940,6 +3021,7 @@ variable.$(OBJEXT): {$(VPATH)}st.h variable.$(OBJEXT): {$(VPATH)}subst.h variable.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h variable.$(OBJEXT): {$(VPATH)}thread_native.h +variable.$(OBJEXT): {$(VPATH)}transient_heap.h variable.$(OBJEXT): {$(VPATH)}util.h variable.$(OBJEXT): {$(VPATH)}variable.c variable.$(OBJEXT): {$(VPATH)}vm_core.h diff --git a/compile.c b/compile.c index ba6924e713700f..9d9560c3b527d6 100644 --- a/compile.c +++ b/compile.c @@ -370,7 +370,7 @@ NORETURN(static void append_compile_error(rb_iseq_t *iseq, int line, const char #endif static void -append_compile_error(rb_iseq_t *iseq, int line, const char *fmt, ...) +append_compile_error(const rb_iseq_t *iseq, int line, const char *fmt, ...) { VALUE err_info = ISEQ_COMPILE_DATA(iseq)->err_info; VALUE file = rb_iseq_path(iseq); @@ -567,6 +567,8 @@ APPEND_ELEM(ISEQ_ARG_DECLARE LINK_ANCHOR *const anchor, LINK_ELEMENT *before, LI #define APPEND_ELEM(anchor, before, elem) APPEND_ELEM(iseq, (anchor), (before), (elem)) #endif +#define ISEQ_LAST_LINE(iseq) (ISEQ_COMPILE_DATA(iseq)->last_line) + static int iseq_add_mark_object_compile_time(const rb_iseq_t *iseq, VALUE v) { @@ -1393,7 +1395,8 @@ get_local_var_idx(const rb_iseq_t *iseq, ID id) int idx = get_dyna_var_idx_at_raw(iseq->body->local_iseq, id); if (idx < 0) { - rb_bug("get_local_var_idx: %d", idx); + COMPILE_ERROR(iseq, ISEQ_LAST_LINE(iseq), + "get_local_var_idx: %d", idx); } return idx; @@ -1403,6 +1406,7 @@ static int get_dyna_var_idx(const rb_iseq_t *iseq, ID id, int *level, int *ls) { int lv = 0, idx = -1; + const rb_iseq_t *const topmost_iseq = iseq; while (iseq) { idx = get_dyna_var_idx_at_raw(iseq, id); @@ -1414,7 +1418,8 @@ get_dyna_var_idx(const rb_iseq_t *iseq, ID id, int *level, int *ls) } if (idx < 0) { - rb_bug("get_dyna_var_idx: -1"); + COMPILE_ERROR(topmost_iseq, ISEQ_LAST_LINE(topmost_iseq), + "get_dyna_var_idx: -1"); } *level = lv; @@ -1510,7 +1515,7 @@ iseq_calc_param_size(rb_iseq_t *iseq) body->param.size = body->param.lead_num + body->param.opt_num; } else { - rb_bug("unreachable"); + UNREACHABLE; } } else { @@ -1649,7 +1654,7 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons opt_table = ALLOC_N(VALUE, i+1); - MEMCPY(opt_table, RARRAY_CONST_PTR(labels), VALUE, i+1); + MEMCPY(opt_table, RARRAY_CONST_PTR_TRANSIENT(labels), VALUE, i+1); for (j = 0; j < i+1; j++) { opt_table[j] &= ~1; } @@ -2022,7 +2027,9 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) if (ISEQ_LINE_COVERAGE(iseq) && (events & RUBY_EVENT_COVERAGE_LINE) && !(rb_get_coverage_mode() & COVERAGE_TARGET_ONESHOT_LINES)) { int line = iobj->insn_info.line_no; - RARRAY_ASET(ISEQ_LINE_COVERAGE(iseq), line - 1, INT2FIX(0)); + if (line >= 1) { + RARRAY_ASET(ISEQ_LINE_COVERAGE(iseq), line - 1, INT2FIX(0)); + } } if (ISEQ_BRANCH_COVERAGE(iseq) && (events & RUBY_EVENT_COVERAGE_BRANCH)) { while (RARRAY_LEN(ISEQ_PC2BRANCHINDEX(iseq)) <= code_index) { @@ -2154,7 +2161,10 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) unsigned int ic_index = FIX2UINT(operands[j]); IC ic = (IC)&body->is_entries[ic_index]; if (UNLIKELY(ic_index >= body->is_size)) { - rb_bug("iseq_set_sequence: ic_index overflow: index: %d, size: %d", ic_index, body->is_size); + BADINSN_DUMP(anchor, &iobj->link, 0); + COMPILE_ERROR(iseq, iobj->insn_info.line_no, + "iseq_set_sequence: ic_index overflow: index: %d, size: %d", + ic_index, body->is_size); } generated_iseq[code_index + 1 + j] = (VALUE)ic; break; @@ -2297,14 +2307,14 @@ iseq_set_exception_table(rb_iseq_t *iseq) struct iseq_catch_table_entry *entry; tlen = (int)RARRAY_LEN(ISEQ_COMPILE_DATA(iseq)->catch_table_ary); - tptr = RARRAY_CONST_PTR(ISEQ_COMPILE_DATA(iseq)->catch_table_ary); + tptr = RARRAY_CONST_PTR_TRANSIENT(ISEQ_COMPILE_DATA(iseq)->catch_table_ary); if (tlen > 0) { struct iseq_catch_table *table = xmalloc(iseq_catch_table_bytes(tlen)); table->size = tlen; for (i = 0; i < table->size; i++) { - ptr = RARRAY_CONST_PTR(tptr[i]); + ptr = RARRAY_CONST_PTR_TRANSIENT(tptr[i]); entry = &table->entries[i]; entry->type = (enum catch_type)(ptr[0] & 0xffff); entry->start = label_get_position((LABEL *)(ptr[1] & ~1)); @@ -3837,7 +3847,7 @@ enum compile_array_type_t { }; static inline int -static_literal_node_p(const NODE *node) +static_literal_node_p(const NODE *node, const rb_iseq_t *iseq) { node = node->nd_head; switch (nd_type(node)) { @@ -3846,13 +3856,19 @@ static_literal_node_p(const NODE *node) case NODE_TRUE: case NODE_FALSE: return TRUE; + case NODE_STR: + if (ISEQ_COMPILE_DATA(iseq)->option->frozen_string_literal) { + return TRUE; + } else { + return FALSE; + } default: return FALSE; } } static inline VALUE -static_literal_value(const NODE *node) +static_literal_value(const NODE *node, rb_iseq_t *iseq) { node = node->nd_head; switch (nd_type(node)) { @@ -3862,6 +3878,17 @@ static_literal_value(const NODE *node) return Qtrue; case NODE_FALSE: return Qfalse; + case NODE_STR: + if (ISEQ_COMPILE_DATA(iseq)->option->debug_frozen_string_literal || RTEST(ruby_debug)) { + VALUE lit; + VALUE debug_info = rb_ary_new_from_args(2, rb_iseq_path(iseq), INT2FIX((int)nd_line(node))); + lit = rb_str_dup(node->nd_lit); + rb_ivar_set(lit, id_debug_created_info, rb_obj_freeze(debug_info)); + return rb_str_freeze(lit); + } + else { + return rb_fstring(node->nd_lit); + } default: return node->nd_lit; } @@ -3911,7 +3938,7 @@ compile_array(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node_ro } break; } - if (opt_p && !static_literal_node_p(node)) { + if (opt_p && !static_literal_node_p(node, iseq)) { opt_p = 0; } @@ -3933,15 +3960,15 @@ compile_array(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node_ro node = start_node; while (node != end_node) { - rb_ary_push(ary, static_literal_value(node)); + rb_ary_push(ary, static_literal_value(node, iseq)); node = node->nd_next; } while (node && node->nd_next && - static_literal_node_p(node) && - static_literal_node_p(node->nd_next)) { + static_literal_node_p(node, iseq) && + static_literal_node_p(node->nd_next, iseq)) { VALUE elem[2]; - elem[0] = static_literal_value(node); - elem[1] = static_literal_value(node->nd_next); + elem[0] = static_literal_value(node, iseq); + elem[1] = static_literal_value(node->nd_next, iseq); rb_ary_cat(ary, elem, 2); node = node->nd_next->nd_next; len++; @@ -5017,7 +5044,7 @@ compile_case(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_nod INIT_ANCHOR(body_seq); INIT_ANCHOR(cond_seq); - rb_hash_tbl_raw(literals)->type = &cdhash_type; + RHASH_TBL_RAW(literals)->type = &cdhash_type; CHECK(COMPILE(head, "case base", node->nd_head)); @@ -6829,9 +6856,9 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, in LABEL *lend = NEW_LABEL(line); int ic_index = body->is_size++; - ADD_INSN2(ret, line, getinlinecache, lend, INT2FIX(ic_index)); + ADD_INSN2(ret, line, opt_getinlinecache, lend, INT2FIX(ic_index)); ADD_INSN1(ret, line, getconstant, ID2SYM(node->nd_vid)); - ADD_INSN1(ret, line, setinlinecache, INT2FIX(ic_index)); + ADD_INSN1(ret, line, opt_setinlinecache, INT2FIX(ic_index)); ADD_LABEL(ret, lend); } else { @@ -7187,7 +7214,7 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, in CHECK(compile_const_prefix(iseq, node, pref, body)); if (LIST_INSN_SIZE_ZERO(pref)) { if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) { - ADD_INSN2(ret, line, getinlinecache, lend, INT2FIX(ic_index)); + ADD_INSN2(ret, line, opt_getinlinecache, lend, INT2FIX(ic_index)); } else { ADD_INSN(ret, line, putnil); @@ -7196,7 +7223,7 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, in ADD_SEQ(ret, body); if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) { - ADD_INSN1(ret, line, setinlinecache, INT2FIX(ic_index)); + ADD_INSN1(ret, line, opt_setinlinecache, INT2FIX(ic_index)); ADD_LABEL(ret, lend); } } @@ -7224,7 +7251,7 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, in /* add cache insn */ if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) { - ADD_INSN2(ret, line, getinlinecache, lend, INT2FIX(ic_index)); + ADD_INSN2(ret, line, opt_getinlinecache, lend, INT2FIX(ic_index)); ADD_INSN(ret, line, pop); } @@ -7232,7 +7259,7 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, in ADD_INSN1(ret, line, getconstant, ID2SYM(node->nd_mid)); if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) { - ADD_INSN1(ret, line, setinlinecache, INT2FIX(ic_index)); + ADD_INSN1(ret, line, opt_setinlinecache, INT2FIX(ic_index)); ADD_LABEL(ret, lend); } @@ -7990,7 +8017,7 @@ iseq_build_from_ary_body(rb_iseq_t *iseq, LINK_ANCHOR *const anchor, int i; VALUE map = rb_hash_new_with_size(RARRAY_LEN(op)/2); - rb_hash_tbl_raw(map)->type = &cdhash_type; + RHASH_TBL_RAW(map)->type = &cdhash_type; op = rb_to_array_type(op); for (i=0; ilocation.base_label, ibf_load_location_str(load, body->location.base_label)); RB_OBJ_WRITE(iseq, &load_body->location.label, ibf_load_location_str(load, body->location.label)); load_body->location.first_lineno = body->location.first_lineno; + load_body->location.node_id = body->location.node_id; load_body->location.code_location = body->location.code_location; load_body->is_entries = ZALLOC_N(union iseq_inline_storage_entry, body->is_size); @@ -9890,6 +9918,7 @@ rb_ibf_load_iseq_complete(rb_iseq_t *iseq) ibf_load_iseq_each(load, iseq, offset); ISEQ_COMPILE_DATA_CLEAR(iseq); FL_UNSET(iseq, ISEQ_NOT_LOADED_YET); + rb_iseq_init_trace(iseq); load->iseq = prev_src_iseq; } @@ -10038,8 +10067,6 @@ rb_iseq_ibf_load(VALUE str) ibf_load_setup(load, loader_obj, str); iseq = ibf_load_iseq(load, 0); - rb_iseq_init_trace(iseq); - RB_GC_GUARD(loader_obj); return iseq; } diff --git a/complex.c b/complex.c index a6e2de60edf39d..7a52a4259e2552 100644 --- a/complex.c +++ b/complex.c @@ -546,7 +546,7 @@ f_complex_polar(VALUE klass, VALUE x, VALUE y) /* returns a Complex or Float of ang*PI-rotated abs */ VALUE -rb_dbl_complex_polar(double abs, double ang) +rb_dbl_complex_polar_pi(double abs, double ang) { double fi; const double fr = modf(ang, &fi); diff --git a/configure.ac b/configure.ac index 70d8ed9cc3b4f9..ed88ec15ff168e 100644 --- a/configure.ac +++ b/configure.ac @@ -252,19 +252,27 @@ AC_SUBST(OUTFLAG) AC_SUBST(COUTFLAG) AC_SUBST(CSRCFLAG) +: ${MJIT_CC=$CC} +AS_IF([test "x$cross_compiling" = xno], [ + AC_PATH_PROG([MJIT_CC], ${MJIT_CC}) + AS_CASE([$target_os], + [*mingw*], [command -v cygpath > /dev/null && MJIT_CC=`cygpath -ma $MJIT_CC`]) + shift 2 + MJIT_CC="$MJIT_CC${1+ }$*" +]) + AS_CASE(["$build_os"], [darwin1*.*], [ # Xcode linker warns for deprecated architecture and wrongly # installed TBD files. CC_WRAPPER="" - AS_IF([! $CC -E -xc - </dev/null 2>&1], [ - @%:@if defined __APPLE_CC__ && defined __clang_major__ && __clang_major__ >= 10 - @%:@error ignore linker warnings - @%:@endif -SRC + echo 'int main(void) {return 0;}' > conftest.c + AS_IF([$CC -framework Foundation -o conftest conftest.c 2>&1 | + grep '^ld: warning: text-based stub file' >/dev/null], [ CC_WRAPPER=`cd -P "$srcdir/tool" && pwd`/darwin-cc CC="$CC_WRAPPER $CC" ]) + rm -fr conftest* ]) cc_version= @@ -465,46 +473,48 @@ AC_ARG_ENABLE(werror, [particular_werror_flags=$enableval]) rb_cv_warnflags="$warnflags" -AS_IF([test "$GCC:${warnflags+set}:no" = yes::no], [ +AS_CASE(["$GCC:${warnflags+set}:${extra_warnflags:+set}:"], +[yes::*|yes:*:set:], [# GCC && (!warnflags || extra_warnflags) AS_IF([test $gcc_major -ge 4], [ - extra_warning=-Werror=extra-tokens - ], [ - extra_warning= + extra_warnflags="$extra_warnflags -Werror=extra-tokens" ]) AS_IF([test $gcc_major -ge 5 -a $gcc_major -le 6], [ - extra_warning="$extra_warning -Wno-maybe-uninitialized" + extra_warnflags="$extra_warnflags -Wno-maybe-uninitialized" ]) # ICC doesn't support -Werror= AS_IF([test $icc_version -gt 0], [ particular_werror_flags=no ]) - for wflag in -Wno-unused-parameter -Wno-long-long \ - -diag-disable=175,188,2259 \ - -Wno-missing-field-initializers \ - -Wno-tautological-compare \ - -Wno-parentheses-equality \ - -Wno-constant-logical-operand \ - -Wno-self-assign \ - -Wno-cast-function-type \ - -Wunused-variable \ - -Werror=implicit-int \ - -Werror=pointer-arith \ - -Werror=write-strings \ + for wflag in \ -Werror=declaration-after-statement \ - -Werror=shorten-64-to-32 \ - -Werror=implicit-function-declaration \ - -Werror=division-by-zero \ -Werror=deprecated-declarations \ + -Werror=division-by-zero \ + -Werror=duplicated-cond \ + -Werror=implicit-function-declaration \ + -Werror=implicit-int \ -Werror=misleading-indentation \ + -Werror=pointer-arith \ + -Werror=restrict \ + -Werror=shorten-64-to-32 \ + -Werror=write-strings \ + -Wimplicit-fallthrough=0 \ + -Wmissing-noreturn \ + -Wno-cast-function-type \ + -Wno-constant-logical-operand \ + -Wno-long-long \ + -Wno-missing-field-initializers \ -Wno-overlength-strings \ -Wno-packed-bitfield-compat \ - -Wsuggest-attribute=noreturn \ + -Wno-parentheses-equality \ + -Wno-self-assign \ + -Wno-tautological-compare \ + -Wno-unused-parameter \ + -Wno-unused-value \ -Wsuggest-attribute=format \ - -Wmissing-noreturn \ - -Wimplicit-fallthrough=0 \ - -Werror=duplicated-cond \ - -Werror=restrict \ - $extra_warning \ + -Wsuggest-attribute=noreturn \ + -Wunused-variable \ + -diag-disable=175,188,2259 \ + $extra_warnflags \ ; do AS_IF([test "$particular_werror_flags" != yes], [ wflag=`echo x$wflag | sed 's/^x-Werror=/-W/;s/^x//'` @@ -948,9 +958,12 @@ main() ac_cv_func_clock_gettime=yes ac_cv_func_clock_getres=yes ac_cv_func_malloc_usable_size=no + ac_cv_type_off_t=yes + ac_cv_sizeof_off_t=8 AS_IF([test "$target_cpu" = x64], [ - ac_cv_func___builtin_setjmp=no + ac_cv_func___builtin_setjmp=yes ac_cv_func_round=no + rb_cv_fiber_coroutine=yes ]) ac_cv_func_tgamma=no rb_cv_negative_time_t=yes @@ -991,6 +1004,11 @@ main() AS_CASE(["$target_cpu"], [powerpc64*], [ ac_cv_func___builtin_setjmp=no ]) + # With gcc-8's -fcf-protection, MJIT's __builtin_longjmp fails. + AS_CASE(["$CC $CFLAGS "], [*" -fcf-protection "*], [cf_protection=yes], [cf_protection=no]) + AS_IF([test "$cf_protection" = yes], [ + ac_cv_func___builtin_setjmp=no + ]) ], [ LIBS="-lm $LIBS"]) : ${ORIG_LIBS=$LIBS} @@ -1023,6 +1041,8 @@ AC_CHECK_HEADERS(malloc_np.h) AC_CHECK_HEADERS(net/socket.h) AC_CHECK_HEADERS(process.h) AC_CHECK_HEADERS(pwd.h) +AC_CHECK_HEADERS(sanitizer/asan_interface.h) +AC_CHECK_HEADERS(sanitizer/msan_interface.h) AC_CHECK_HEADERS(setjmpex.h) AC_CHECK_HEADERS(stdalign.h) AC_CHECK_HEADERS(sys/attr.h) @@ -1127,7 +1147,6 @@ mv confdefs1.h confdefs.h cat largefile.h >> confdefs.h AS_CASE(["$target_os"], - [mingw*], [ac_cv_type_off_t=yes;ac_cv_sizeof_off_t=8], [aix*], [ AS_CASE(["$target_cpu:$ac_cv_sys_large_files"], [ppc64:*|powerpc64:*], [], @@ -1352,6 +1371,9 @@ RUBY_FUNC_ATTRIBUTE(__deprecated__("by "@%:@n), DEPRECATED_BY(n,x), rb_cv_func_d RUBY_TYPE_ATTRIBUTE(__deprecated__ mesg, DEPRECATED_TYPE(mesg,x), rb_cv_type_deprecated) RUBY_FUNC_ATTRIBUTE(__noinline__, NOINLINE) RUBY_FUNC_ATTRIBUTE(__always_inline__, ALWAYS_INLINE) +RUBY_FUNC_ATTRIBUTE(__no_sanitize__(san), NO_SANITIZE(san, x), rb_cv_func_no_sanitize) +RUBY_FUNC_ATTRIBUTE(__no_sanitize_address__, NO_SANITIZE_ADDRESS) +RUBY_FUNC_ATTRIBUTE(__no_address_safety_analysis__, NO_ADDRESS_SAFETY_ANALYSIS) RUBY_FUNC_ATTRIBUTE(__warn_unused_result__, WARN_UNUSED_RESULT) RUBY_FUNC_ATTRIBUTE(__unused__, MAYBE_UNUSED) RUBY_FUNC_ATTRIBUTE(__error__ mesg, ERRORFUNC(mesg,x), rb_cv_func___error__) @@ -1619,34 +1641,6 @@ AS_IF([test $rb_cv_stack_end_address != no], [ AC_DEFINE_UNQUOTED(STACK_END_ADDRESS, $rb_cv_stack_end_address) ]) -# posix_memalign(memptr, alignment, size) implemented for OpenBSD 4.8 doesn't work if alignment > MALLOC_PAGESIZE. -# [ruby-core:42158] https://bugs.ruby-lang.org/issues/5901 -# OpenBSD 5.2 fixed the problem. (src/lib/libc/stdlib/malloc.c:1.142) -# MirOS #10semel has the problem but fixed in the repository. (src/lib/libc/stdlib/malloc.c:1.9) -AS_CASE(["$target_os"], -[openbsd*|mirbsd*], [ - AC_CACHE_CHECK(for heap align log on openbsd, rb_cv_page_size_log, - [rb_cv_page_size_log=no - for page_log in 12 13; do - AC_TRY_RUN([ -#include -#include - -int -main() { - if ((int)log2((double)sysconf(_SC_PAGESIZE)) != $page_log) return 1; - return 0; -} - ], - rb_cv_page_size_log="$page_log"; break) - done]) - AS_IF([test $rb_cv_page_size_log != no], [ - AC_DEFINE_UNQUOTED(HEAP_ALIGN_LOG, $rb_cv_page_size_log) - ], [ - AC_DEFINE_UNQUOTED(HEAP_ALIGN_LOG, 12) - ]) -]) - dnl Checks for library functions. AC_TYPE_GETGROUPS AC_TYPE_SIGNAL @@ -1980,6 +1974,7 @@ AS_IF([test x$rb_cv_builtin___builtin_choose_expr = xyes], [ ]) ]) RUBY_CHECK_BUILTIN_FUNC(__builtin_types_compatible_p, [__builtin_types_compatible_p(int, int)]) +RUBY_CHECK_BUILTIN_FUNC(__builtin_trap, [__builtin_trap()]) AS_IF([test "$ac_cv_func_qsort_r" != no], [ AC_CACHE_CHECK(whether qsort_r is GNU version, rb_cv_gnu_qsort_r, @@ -2304,6 +2299,40 @@ AS_IF([test "${universal_binary-no}" = yes ], [ AC_DEFINE_UNQUOTED(STACK_GROW_DIRECTION, $dir) ]) +AC_ARG_ENABLE(fiber-coroutine, + AS_HELP_STRING([--disable-fiber-coroutine], [disable native coroutine implementation for fiber]), + [rb_cv_fiber_coroutine=$enableval]) +AS_CASE(["$rb_cv_fiber_coroutine"], [yes|''], [ + AC_MSG_CHECKING(native coroutine implementation for ${target_cpu}-${target_os}) + AS_CASE(["$target_cpu-$target_os"], + [x*64-darwin*], [ + rb_cv_fiber_coroutine=amd64 + ], + [x*64-linux], [ + AS_CASE(["$ac_cv_sizeof_voidp"], + [8], [ rb_cv_fiber_coroutine=amd64 ], + [4], [ rb_cv_fiber_coroutine=x86 ], + [*], [ rb_cv_fiber_coroutine= ] + ) + ], + [*86-linux], [ + rb_cv_fiber_coroutine=x86 + ], + [x64-mingw32], [ + rb_cv_fiber_coroutine=win64 + ], + [*], [ + rb_cv_fiber_coroutine= + ] + ) + AC_MSG_RESULT(${rb_cv_fiber_coroutine:-no}) +]) +AS_IF([test "${rb_cv_fiber_coroutine:-no}" != no], [ + COROUTINE_H=coroutine/$rb_cv_fiber_coroutine/Context.h + AC_DEFINE_UNQUOTED(FIBER_USE_COROUTINE, ["$COROUTINE_H"]) + AC_SUBST(X_FIBER_COROUTINE_H, [$COROUTINE_H]) +]) + AS_IF([test x"$enable_pthread" = xyes], [ for pthread_lib in thr pthread pthreads c c_r root; do AC_CHECK_LIB($pthread_lib, pthread_create, @@ -2533,7 +2562,7 @@ AC_SUBST(DLDFLAGS)dnl AC_SUBST(ARCH_FLAG)dnl AC_SUBST(MJIT_HEADER_FLAGS)dnl AC_SUBST(MJIT_HEADER_INSTALL_DIR)dnl -AC_SUBST(MJIT_CC, [${MJIT_CC-'$(CC)'}])dnl +AC_SUBST(MJIT_CC)dnl AC_SUBST(MJIT_CFLAGS, [${MJIT_CFLAGS-"-w ${orig_cflags}"}])dnl AC_SUBST(MJIT_OPTFLAGS, [${MJIT_OPTFLAGS-'$(optflags)'}])dnl AC_SUBST(MJIT_DEBUGFLAGS, [${MJIT_DEBUGFLAGS-'$(debugflags)'}])dnl @@ -2644,6 +2673,7 @@ AS_IF([test "$with_dln_a_out" != yes], [ ]) rb_cv_dlopen=yes], [darwin*], [ : ${LDSHARED='$(CC) -dynamic -bundle'} + : ${DLDSHARED='$(CC) -dynamiclib'} : ${LDFLAGS=""} : ${LIBPATHENV=DYLD_FALLBACK_LIBRARY_PATH} : ${PRELOADENV=DYLD_INSERT_LIBRARIES} @@ -2654,8 +2684,8 @@ AS_IF([test "$with_dln_a_out" != yes], [ XLDFLAGS="${linker_flag}"'-bE:$(ARCHFILE)'" ${linker_flag}-brtl" XLDFLAGS="$XLDFLAGS ${linker_flag}-blibpath:${prefix}/lib:${LIBPATH:-/usr/lib:/lib}" : ${ARCHFILE="ruby.imp"} - TRY_LINK='$(CC) $(LDFLAGS) -oconftest $(INCFLAGS) -I$(hdrdir) $(CPPFLAGS)' - TRY_LINK="$TRY_LINK"' $(CFLAGS) $(src) $(LIBPATH) $(LOCAL_LIBS) $(LIBS)' + TRY_LINK='$(CC) -oconftest $(INCFLAGS) -I$(hdrdir) $(CPPFLAGS)' + TRY_LINK="$TRY_LINK"' $(CFLAGS) $(src) $(LIBPATH) $(LDFLAGS) $(LOCAL_LIBS) $(LIBS)' : ${LIBPATHENV=LIBPATH} rb_cv_dlopen=yes], [nto-qnx*], [ DLDFLAGS="$DLDFLAGS -L/lib -L/usr/lib -L/usr/local/lib" @@ -3056,7 +3086,7 @@ AC_ARG_WITH(soname, [RUBY_SO_NAME='$(RUBY_BASE_NAME)']) ]) -LIBRUBY_LDSHARED=$LDSHARED +LIBRUBY_LDSHARED=${DLDSHARED=${LDSHARED}} LIBRUBY_DLDFLAGS=$DLDFLAGS LIBRUBY_SO='lib$(RUBY_SO_NAME).$(SOEXT).$(RUBY_PROGRAM_VERSION)' LIBRUBY_SONAME='lib$(RUBY_SO_NAME).$(SOEXT).$(RUBY_API_VERSION)' @@ -3149,7 +3179,6 @@ AS_CASE("$enable_shared", [yes], [ LIBRUBY_SO='lib$(RUBY_SO_NAME).$(SOEXT)' LIBRUBY_SONAME='$(LIBRUBY_SO)' LIBRUBY_ALIASES='lib$(RUBY_INSTALL_NAME).$(SOEXT)' - LIBRUBY_LDSHARED='$(CC) -dynamiclib' AS_IF([test "$load_relative" = yes], [ libprefix="@executable_path/../${libdir_basename}" LIBRUBY_RELATIVE=yes @@ -3325,10 +3354,10 @@ AC_SUBST(CAPITARGET) AS_CASE(["$RDOCTARGET:$CAPITARGET"],[nodoc:nodoc],[INSTALLDOC=nodoc],[INSTALLDOC=all]) AC_SUBST(INSTALLDOC) -AC_ARG_ENABLE(mjit-support, - AS_HELP_STRING([--disable-mjit-support], [disable MJIT features]), +AC_ARG_ENABLE(jit-support, + AS_HELP_STRING([--disable-jit-support], [disable JIT features]), [MJIT_SUPPORT=$enableval - AS_IF([test x"$enable_mjit_support" = "xyes"], + AS_IF([test x"$enable_jit_support" = "xyes"], [AC_DEFINE(USE_MJIT, 1)], [AC_DEFINE(USE_MJIT, 0)])], [MJIT_SUPPORT=yes @@ -3576,6 +3605,7 @@ AC_SUBST(LIBRUBYARG_STATIC) AC_SUBST(LIBRUBYARG_SHARED) AC_SUBST(SOLIBS) AC_SUBST(DLDLIBS) +AC_SUBST(DLDSHARED) AC_SUBST(ENABLE_SHARED) AC_SUBST(MAINLIBS) AC_SUBST(COMMON_LIBS) @@ -3862,12 +3892,14 @@ PACKAGE=$RUBY_BASE_NAME AC_SUBST(PACKAGE) AS_MESSAGE([$PACKAGE library version = $ruby_version]) -AS_CASE(["$build_os"], [darwin1*.*], [ - AS_IF([test x"$CC_WRAPPER" != x], [ - CC="${CC@%:@$CC_WRAPPER }" - CPP="${CPP@%:@$CC_WRAPPER }" - ]) +AS_IF([test x"$CC_WRAPPER" != x], [ + CC='$(CC_WRAPPER) '"${CC@%:@$CC_WRAPPER }" + CPP='$(CC_WRAPPER) '"${CPP@%:@$CC_WRAPPER }" + CC_WRAPPER='$(rubyarchdir)/darwin-cc' + XCC_WRAPPER='$(top_srcdir)/tool/darwin-cc' ]) +AC_SUBST(CC_WRAPPER, '') +AC_SUBST(XCC_WRAPPER) AS_CASE([" $CPP "], [*" $CC "*], [CPP=`echo " $CPP " | sed "s| $CC |"' $(CC) |;s/^ *//;s/ *$//'`]) @@ -3997,7 +4029,7 @@ config_summary "debugflags" "$debugflags" config_summary "warnflags" "$warnflags" config_summary "strip command" "$STRIP" config_summary "install doc" "$install_doc" -config_summary "MJIT support" "$MJIT_SUPPORT" +config_summary "JIT support" "$MJIT_SUPPORT" config_summary "man page type" "$MANTYPE" config_summary "search path" "$search_path" config_summary "static-linked-ext" ${EXTSTATIC:+"yes"} diff --git a/cont.c b/cont.c index 95f2de9b87dd79..142713ad842ab7 100644 --- a/cont.c +++ b/cont.c @@ -26,6 +26,27 @@ * in Proc. of 51th Programming Symposium, pp.21--28 (2010) (in Japanese). */ +/* + Enable FIBER_USE_COROUTINE to make fiber yield/resume much faster by using native assembly implementations. + + rvm install ruby-head-ioquatix-native-fiber --url https://github.com/ioquatix/ruby --branch native-fiber + + # Without libcoro + koyoko% ./build/bin/ruby ./fiber_benchmark.rb 10000 1000 + setup time for 10000 fibers: 0.099961 + execution time for 1000 messages: 19.505909 + + # With libcoro + koyoko% ./build/bin/ruby ./fiber_benchmark.rb 10000 1000 + setup time for 10000 fibers: 0.099268 + execution time for 1000 messages: 8.491746 +*/ + +#ifdef FIBER_USE_COROUTINE +#include FIBER_USE_COROUTINE +#define FIBER_USE_NATIVE 1 +#endif + #if !defined(FIBER_USE_NATIVE) # if defined(HAVE_GETCONTEXT) && defined(HAVE_SETCONTEXT) # if 0 @@ -139,7 +160,7 @@ enum fiber_status { #define FIBER_TERMINATED_P(fib) ((fib)->status == FIBER_TERMINATED) #define FIBER_RUNNABLE_P(fib) (FIBER_CREATED_P(fib) || FIBER_SUSPENDED_P(fib)) -#if FIBER_USE_NATIVE && !defined(_WIN32) +#if FIBER_USE_NATIVE && !defined(FIBER_USE_COROUTINE) && !defined(_WIN32) static inline int fiber_context_create(ucontext_t *context, void (*func)(), void *arg, void *ptr, size_t size) { @@ -158,17 +179,6 @@ fiber_context_create(ucontext_t *context, void (*func)(), void *arg, void *ptr, } #endif -#if FIBER_USE_NATIVE && !defined(_WIN32) -#define MAX_MACHINE_STACK_CACHE 10 -static int machine_stack_cache_index = 0; -typedef struct machine_stack_cache_struct { - void *ptr; - size_t size; -} machine_stack_cache_t; -static machine_stack_cache_t machine_stack_cache[MAX_MACHINE_STACK_CACHE]; -static machine_stack_cache_t terminated_machine_stack; -#endif - struct rb_fiber_struct { rb_context_t cont; VALUE first_proc; @@ -181,9 +191,15 @@ struct rb_fiber_struct { unsigned int transferred : 1; #if FIBER_USE_NATIVE -#ifdef _WIN32 +#if defined(FIBER_USE_COROUTINE) +#define FIBER_ALLOCATE_STACK + coroutine_context context; + void *ss_sp; + size_t ss_size; +#elif defined(_WIN32) void *fib_handle; #else +#define FIBER_ALLOCATE_STACK ucontext_t context; /* Because context.uc_stack.ss_sp and context.uc_stack.ss_size * are not necessarily valid after makecontext() or swapcontext(), @@ -195,6 +211,17 @@ struct rb_fiber_struct { #endif }; +#ifdef FIBER_ALLOCATE_STACK +#define MAX_MACHINE_STACK_CACHE 10 +static int machine_stack_cache_index = 0; +typedef struct machine_stack_cache_struct { + void *ptr; + size_t size; +} machine_stack_cache_t; +static machine_stack_cache_t machine_stack_cache[MAX_MACHINE_STACK_CACHE]; +static machine_stack_cache_t terminated_machine_stack; +#endif + static const char * fiber_status_name(enum fiber_status s) { @@ -250,7 +277,7 @@ fiber_status_set(rb_fiber_t *fib, enum fiber_status s) } void -ec_set_vm_stack(rb_execution_context_t *ec, VALUE *stack, size_t size) +rb_ec_set_vm_stack(rb_execution_context_t *ec, VALUE *stack, size_t size) { ec->vm_stack = stack; ec->vm_stack_size = size; @@ -381,9 +408,22 @@ cont_free(void *ptr) } else { /* fiber */ - const rb_fiber_t *fib = (rb_fiber_t*)cont; + rb_fiber_t *fib = (rb_fiber_t*)cont; +#if defined(FIBER_USE_COROUTINE) + coroutine_destroy(&fib->context); + if (fib->ss_sp != NULL) { + if (fiber_is_root_p(fib)) { + rb_bug("Illegal root fiber parameter"); + } #ifdef _WIN32 - if (!fiber_is_root_p(fib)) { + VirtualFree((void*)fib->ss_sp, 0, MEM_RELEASE); +#else + munmap((void*)fib->ss_sp, fib->ss_size); +#endif + fib->ss_sp = NULL; + } +#elif defined(_WIN32) + if (!fiber_is_root_p(fib)) { /* don't delete root fiber handle */ if (fib->fib_handle) { DeleteFiber(fib->fib_handle); @@ -673,7 +713,7 @@ cont_capture(volatile int *volatile stat) cont->saved_vm_stack.ptr = ALLOC_N(VALUE, ec->vm_stack_size); MEMCPY(cont->saved_vm_stack.ptr, ec->vm_stack, VALUE, ec->vm_stack_size); #endif - ec_set_vm_stack(&cont->saved_ec, NULL, 0); + rb_ec_set_vm_stack(&cont->saved_ec, NULL, 0); cont_save_machine_stack(th, cont); /* backup ensure_list to array for search in another context */ @@ -738,6 +778,10 @@ cont_restore_thread(rb_context_t *cont) ec_switch(th, fib); } + if (th->ec->trace_arg != sec->trace_arg) { + rb_raise(rb_eRuntimeError, "can't call across trace_func"); + } + /* copy vm stack */ #ifdef CAPTURE_JUST_VALID_VM_STACK MEMCPY(th->ec->vm_stack, @@ -760,16 +804,6 @@ cont_restore_thread(rb_context_t *cont) th->ec->ensure_list = sec->ensure_list; th->ec->errinfo = sec->errinfo; - /* trace on -> trace off */ - if (th->ec->trace_arg != NULL && sec->trace_arg == NULL) { - GET_VM()->trace_running--; - } - /* trace off -> trace on */ - else if (th->ec->trace_arg == NULL && sec->trace_arg != NULL) { - GET_VM()->trace_running++; - } - th->ec->trace_arg = sec->trace_arg; - VM_ASSERT(th->ec->vm_stack != NULL); } else { @@ -779,7 +813,13 @@ cont_restore_thread(rb_context_t *cont) } #if FIBER_USE_NATIVE -#ifdef _WIN32 +#if defined(FIBER_USE_COROUTINE) +static COROUTINE +fiber_entry(coroutine_context * from, coroutine_context * to) +{ + rb_fiber_start(); +} +#elif defined(_WIN32) static void fiber_set_stack_location(void) { @@ -797,15 +837,17 @@ fiber_entry(void *arg) fiber_set_stack_location(); rb_fiber_start(); } -#else /* _WIN32 */ - +#else NORETURN(static void fiber_entry(void *arg)); static void fiber_entry(void *arg) { rb_fiber_start(); } +#endif +#endif +#ifdef FIBER_ALLOCATE_STACK /* * FreeBSD require a first (i.e. addr) argument of mmap(2) is not NULL * if MAP_STACK is passed. @@ -823,46 +865,70 @@ static char* fiber_machine_stack_alloc(size_t size) { char *ptr; +#ifdef _WIN32 + DWORD old_protect; +#endif if (machine_stack_cache_index > 0) { - if (machine_stack_cache[machine_stack_cache_index - 1].size == (size / sizeof(VALUE))) { - ptr = machine_stack_cache[machine_stack_cache_index - 1].ptr; - machine_stack_cache_index--; - machine_stack_cache[machine_stack_cache_index].ptr = NULL; - machine_stack_cache[machine_stack_cache_index].size = 0; - } - else{ + if (machine_stack_cache[machine_stack_cache_index - 1].size == (size / sizeof(VALUE))) { + ptr = machine_stack_cache[machine_stack_cache_index - 1].ptr; + machine_stack_cache_index--; + machine_stack_cache[machine_stack_cache_index].ptr = NULL; + machine_stack_cache[machine_stack_cache_index].size = 0; + } else { /* TODO handle multiple machine stack size */ - rb_bug("machine_stack_cache size is not canonicalized"); - } - } - else { - void *page; - STACK_GROW_DIR_DETECTION; + rb_bug("machine_stack_cache size is not canonicalized"); + } + } else { +#ifdef _WIN32 + ptr = VirtualAlloc(0, size, MEM_COMMIT, PAGE_READWRITE); - errno = 0; - ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, FIBER_STACK_FLAGS, -1, 0); - if (ptr == MAP_FAILED) { - rb_raise(rb_eFiberError, "can't alloc machine stack to fiber: %s", ERRNOMSG); - } + if (!ptr) { + rb_raise(rb_eFiberError, "can't allocate machine stack to fiber: %s", ERRNOMSG); + } - /* guard page setup */ - page = ptr + STACK_DIR_UPPER(size - RB_PAGE_SIZE, 0); - if (mprotect(page, RB_PAGE_SIZE, PROT_NONE) < 0) { - rb_raise(rb_eFiberError, "can't set a guard page: %s", ERRNOMSG); - } + if (!VirtualProtect(ptr, RB_PAGE_SIZE, PAGE_READWRITE | PAGE_GUARD, &old_protect)) { + rb_raise(rb_eFiberError, "can't set a guard page: %s", ERRNOMSG); + } +#else + void *page; + STACK_GROW_DIR_DETECTION; + + errno = 0; + ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, FIBER_STACK_FLAGS, -1, 0); + if (ptr == MAP_FAILED) { + rb_raise(rb_eFiberError, "can't alloc machine stack to fiber: %s", ERRNOMSG); + } + + /* guard page setup */ + page = ptr + STACK_DIR_UPPER(size - RB_PAGE_SIZE, 0); + if (mprotect(page, RB_PAGE_SIZE, PROT_NONE) < 0) { + rb_raise(rb_eFiberError, "can't set a guard page: %s", ERRNOMSG); + } +#endif } return ptr; } #endif +#if FIBER_USE_NATIVE static void fiber_initialize_machine_stack_context(rb_fiber_t *fib, size_t size) { rb_execution_context_t *sec = &fib->cont.saved_ec; -#ifdef _WIN32 +#if defined(FIBER_USE_COROUTINE) + char *ptr; + STACK_GROW_DIR_DETECTION; + + ptr = fiber_machine_stack_alloc(size); + fib->ss_sp = ptr; + fib->ss_size = size; + coroutine_initialize(&fib->context, fiber_entry, ptr+size, size); + sec->machine.stack_start = (VALUE*)(ptr + STACK_DIR_UPPER(0, size)); + sec->machine.stack_maxsize = size - RB_PAGE_SIZE; +#elif defined(_WIN32) # if defined(_MSC_VER) && _MSC_VER <= 1200 # define CreateFiberEx(cs, stacksize, flags, entry, param) \ CreateFiber((stacksize), (entry), (param)) @@ -925,19 +991,19 @@ fiber_setcontext(rb_fiber_t *newfib, rb_fiber_t *oldfib) /* restore thread context */ fiber_restore_thread(th, newfib); -#ifndef _WIN32 - if (!newfib->context.uc_stack.ss_sp && th->root_fiber != newfib) { - rb_bug("non_root_fiber->context.uc_stac.ss_sp should not be NULL"); - } -#endif /* swap machine context */ -#ifdef _WIN32 +#if defined(FIBER_USE_COROUTINE) + coroutine_transfer(&oldfib->context, &newfib->context); +#elif defined(_WIN32) SwitchToFiber(newfib->fib_handle); #else + if (!newfib->context.uc_stack.ss_sp && th->root_fiber != newfib) { + rb_bug("non_root_fiber->context.uc_stac.ss_sp should not be NULL"); + } swapcontext(&oldfib->context, &newfib->context); #endif } -#endif +#endif /* FIBER_USE_NATIVE */ NOINLINE(NORETURN(static void cont_restore_1(rb_context_t *))); @@ -1199,7 +1265,7 @@ rollback_ensure_stack(VALUE self,rb_ensure_list_t *current,rb_ensure_entry_t *ta { rb_ensure_list_t *p; rb_ensure_entry_t *entry; - size_t i; + size_t i, j; size_t cur_size; size_t target_size; size_t base_point; @@ -1237,11 +1303,11 @@ rollback_ensure_stack(VALUE self,rb_ensure_list_t *current,rb_ensure_entry_t *ta cur_size--; } /* push ensure stack */ - while (i--) { - func = (VALUE (*)(ANYARGS)) lookup_rollback_func(target[i].e_proc); - if ((VALUE)func != Qundef) { - (*func)(target[i].data2); - } + for (j = 0; j < i; j++) { + func = (VALUE (*)(ANYARGS)) lookup_rollback_func(target[i - j - 1].e_proc); + if ((VALUE)func != Qundef) { + (*func)(target[i - j - 1].data2); + } } } @@ -1425,7 +1491,7 @@ fiber_init(VALUE fibval, VALUE proc) else { vm_stack = ruby_xmalloc(fib_stack_bytes); } - ec_set_vm_stack(sec, vm_stack, fib_stack_bytes / sizeof(VALUE)); + rb_ec_set_vm_stack(sec, vm_stack, fib_stack_bytes / sizeof(VALUE)); sec->cfp = (void *)(sec->vm_stack + sec->vm_stack_size); rb_vm_push_frame(sec, @@ -1532,7 +1598,9 @@ root_fiber_alloc(rb_thread_t *th) fib->cont.self = fibval; #if FIBER_USE_NATIVE -#ifdef _WIN32 +#if defined(FIBER_USE_COROUTINE) + coroutine_initialize(&fib->context, NULL, NULL, 0); +#elif defined(_WIN32) /* setup fib_handle for root Fiber */ if (fib->fib_handle == 0) { if ((fib->fib_handle = ConvertThreadToFiber(0)) == 0) { @@ -1625,11 +1693,11 @@ fiber_store(rb_fiber_t *next_fib, rb_thread_t *th) rb_fiber_t *fib; if (th->ec->fiber_ptr != NULL) { - fib = th->ec->fiber_ptr; + fib = th->ec->fiber_ptr; } else { - /* create root fiber */ - fib = root_fiber_alloc(th); + /* create root fiber */ + fib = root_fiber_alloc(th); } VM_ASSERT(FIBER_RESUMED_P(fib) || FIBER_TERMINATED_P(fib)); @@ -1637,7 +1705,7 @@ fiber_store(rb_fiber_t *next_fib, rb_thread_t *th) #if FIBER_USE_NATIVE if (FIBER_CREATED_P(next_fib)) { - fiber_initialize_machine_stack_context(next_fib, th->vm->default_params.fiber_machine_stack_size); + fiber_initialize_machine_stack_context(next_fib, th->vm->default_params.fiber_machine_stack_size); } #endif @@ -1655,19 +1723,23 @@ fiber_store(rb_fiber_t *next_fib, rb_thread_t *th) /* restored */ #ifdef MAX_MACHINE_STACK_CACHE if (terminated_machine_stack.ptr) { - if (machine_stack_cache_index < MAX_MACHINE_STACK_CACHE) { - machine_stack_cache[machine_stack_cache_index++] = terminated_machine_stack; - } - else { - if (terminated_machine_stack.ptr != fib->cont.machine.stack) { - munmap((void*)terminated_machine_stack.ptr, terminated_machine_stack.size * sizeof(VALUE)); - } - else { - rb_bug("terminated fiber resumed"); - } - } - terminated_machine_stack.ptr = NULL; - terminated_machine_stack.size = 0; + if (machine_stack_cache_index < MAX_MACHINE_STACK_CACHE) { + machine_stack_cache[machine_stack_cache_index++] = terminated_machine_stack; + } + else { + if (terminated_machine_stack.ptr != fib->cont.machine.stack) { +#ifdef _WIN32 + VirtualFree(terminated_machine_stack.ptr, 0, MEM_RELEASE); +#else + munmap((void*)terminated_machine_stack.ptr, terminated_machine_stack.size * sizeof(VALUE)); +#endif + } + else { + rb_bug("terminated fiber resumed"); + } + } + terminated_machine_stack.ptr = NULL; + terminated_machine_stack.size = 0; } #endif /* not _WIN32 */ fib = th->ec->fiber_ptr; @@ -1676,19 +1748,19 @@ fiber_store(rb_fiber_t *next_fib, rb_thread_t *th) #else /* FIBER_USE_NATIVE */ if (ruby_setjmp(fib->cont.jmpbuf)) { - /* restored */ - fib = th->ec->fiber_ptr; - if (fib->cont.argc == -1) rb_exc_raise(fib->cont.value); - if (next_fib->cont.value == Qundef) { - cont_restore_0(&next_fib->cont, &next_fib->cont.value); - VM_UNREACHABLE(fiber_store); - } - return fib->cont.value; + /* restored */ + fib = th->ec->fiber_ptr; + if (fib->cont.argc == -1) rb_exc_raise(fib->cont.value); + if (next_fib->cont.value == Qundef) { + cont_restore_0(&next_fib->cont, &next_fib->cont.value); + VM_UNREACHABLE(fiber_store); + } + return fib->cont.value; } else { - VALUE undef = Qundef; - cont_restore_0(&next_fib->cont, &undef); - VM_UNREACHABLE(fiber_store); + VALUE undef = Qundef; + cont_restore_0(&next_fib->cont, &undef); + VM_UNREACHABLE(fiber_store); } #endif /* FIBER_USE_NATIVE */ } @@ -1777,7 +1849,7 @@ rb_fiber_close(rb_fiber_t *fib) else { ruby_xfree(vm_stack); } - ec_set_vm_stack(ec, NULL, 0); + rb_ec_set_vm_stack(ec, NULL, 0); #if !FIBER_USE_NATIVE /* should not mark machine stack any more */ @@ -1794,9 +1866,13 @@ rb_fiber_terminate(rb_fiber_t *fib, int need_interrupt) VM_ASSERT(FIBER_RESUMED_P(fib)); rb_fiber_close(fib); -#if FIBER_USE_NATIVE && !defined(_WIN32) +#if FIBER_USE_NATIVE +#if defined(FIBER_USE_COROUTINE) + coroutine_destroy(&fib->context); +#elif !defined(_WIN32) fib->context.uc_stack.ss_sp = NULL; #endif + #ifdef MAX_MACHINE_STACK_CACHE /* Ruby must not switch to other thread until storing terminated_machine_stack */ terminated_machine_stack.ptr = fib->ss_sp; @@ -1804,6 +1880,7 @@ rb_fiber_terminate(rb_fiber_t *fib, int need_interrupt) fib->ss_sp = NULL; fib->cont.machine.stack = NULL; fib->cont.machine.stack_size = 0; +#endif #endif ret_fib = return_fiber(); @@ -1833,10 +1910,8 @@ rb_fiber_yield(int argc, const VALUE *argv) } void -rb_fiber_reset_root_local_storage(VALUE thval) +rb_fiber_reset_root_local_storage(rb_thread_t *th) { - rb_thread_t *th = rb_thread_ptr(thval); - if (th->root_fiber && th->root_fiber != th->ec->fiber_ptr) { th->ec->local_storage = th->root_fiber->cont.saved_ec.local_storage; } diff --git a/coroutine/amd64/Context.S b/coroutine/amd64/Context.S new file mode 100644 index 00000000000000..6193993e0374a1 --- /dev/null +++ b/coroutine/amd64/Context.S @@ -0,0 +1,46 @@ +## +## This file is part of the "Coroutine" project and released under the MIT License. +## +## Created by Samuel Williams on 10/5/2018. +## Copyright, 2018, by Samuel Williams. All rights reserved. +## + +.text + +# For older linkers +.globl _coroutine_transfer +_coroutine_transfer: + +.globl coroutine_transfer +coroutine_transfer: + # Save caller state + pushq %rbp + pushq %rbx + pushq %r12 + pushq %r13 + pushq %r14 + pushq %r15 + + # Save caller stack pointer + movq %rsp, (%rdi) + + # Restore callee stack pointer + movq (%rsi), %rsp + + # Restore callee stack + popq %r15 + popq %r14 + popq %r13 + popq %r12 + popq %rbx + popq %rbp + + # Put the first argument into the return value + movq %rdi, %rax + + # We pop the return address and jump to it + ret + +#if defined(__linux__) && defined(__ELF__) +.section .note.GNU-stack,"",%progbits +#endif diff --git a/coroutine/amd64/Context.h b/coroutine/amd64/Context.h new file mode 100644 index 00000000000000..1801c1e2c5d63e --- /dev/null +++ b/coroutine/amd64/Context.h @@ -0,0 +1,59 @@ +/* + * This file is part of the "Coroutine" project and released under the MIT License. + * + * Created by Samuel Williams on 10/5/2018. + * Copyright, 2018, by Samuel Williams. All rights reserved. +*/ + +#pragma once + +#include +#include + +#if __cplusplus +extern "C" { +#endif + +#define COROUTINE __attribute__((noreturn)) void + +const size_t COROUTINE_REGISTERS = 6; + +typedef struct +{ + void **stack_pointer; +} coroutine_context; + +typedef COROUTINE(* coroutine_start)(coroutine_context *from, coroutine_context *self); + +static inline void coroutine_initialize( + coroutine_context *context, + coroutine_start start, + void *stack_pointer, + size_t stack_size +) { + /* Force 16-byte alignment */ + context->stack_pointer = (void**)((uintptr_t)stack_pointer & ~0xF); + + if (!start) { + assert(!context->stack_pointer); + /* We are main coroutine for this thread */ + return; + } + + *--context->stack_pointer = NULL; + *--context->stack_pointer = (void*)start; + + context->stack_pointer -= COROUTINE_REGISTERS; + memset(context->stack_pointer, 0, sizeof(void*) * COROUTINE_REGISTERS); +} + +coroutine_context * coroutine_transfer(coroutine_context * current, coroutine_context * target); + +static inline void coroutine_destroy(coroutine_context * context) +{ + context->stack_pointer = NULL; +} + +#if __cplusplus +} +#endif diff --git a/coroutine/arm32/Context.S b/coroutine/arm32/Context.S new file mode 100644 index 00000000000000..c2b93d0a3464c1 --- /dev/null +++ b/coroutine/arm32/Context.S @@ -0,0 +1,14 @@ +## +## This file is part of the "Coroutine" project and released under the MIT License. +## +## Created by Samuel Williams on 10/5/2018. +## Copyright, 2018, by Samuel Williams. All rights reserved. +## + +.text + +.globl coroutine_transfer +coroutine_transfer: + stmia r1!, {r4-r11,sp,lr} + ldmia r0!, {r4-r11,sp,pc} + bx lr diff --git a/coroutine/arm32/Context.h b/coroutine/arm32/Context.h new file mode 100644 index 00000000000000..60732df7a0f203 --- /dev/null +++ b/coroutine/arm32/Context.h @@ -0,0 +1,56 @@ +/* + * This file is part of the "Coroutine" project and released under the MIT License. + * + * Created by Samuel Williams on 10/5/2018. + * Copyright, 2018, by Samuel Williams. All rights reserved. +*/ + +#pragma once + +#include +#include + +#if __cplusplus +extern "C" { +#endif + +#define COROUTINE __attribute__((noreturn)) void + +const size_t COROUTINE_REGISTERS = 9; + +typedef struct +{ + void **stack_pointer; +} coroutine_context; + +typedef COROUTINE(* coroutine_start)(coroutine_context *from, coroutine_context *self); + +static inline void coroutine_initialize( + coroutine_context *context, + coroutine_start start, + void *stack_pointer, + size_t stack_size +) { + context->stack_pointer = (void**)stack_pointer; + + if (!start) { + assert(!context->stack_pointer); + /* We are main coroutine for this thread */ + return; + } + + *--context->stack_pointer = (void*)start; + + context->stack_pointer -= COROUTINE_REGISTERS; + memset(context->stack_pointer, 0, sizeof(void*) * COROUTINE_REGISTERS); +} + +coroutine_context * coroutine_transfer(coroutine_context * current, coroutine_context * target); + +static inline void coroutine_destroy(coroutine_context * context) +{ +} + +#if __cplusplus +} +#endif diff --git a/coroutine/arm64/Context.S b/coroutine/arm64/Context.S new file mode 100644 index 00000000000000..f6e5f0a6bc83e5 --- /dev/null +++ b/coroutine/arm64/Context.S @@ -0,0 +1,59 @@ +## +## This file is part of the "Coroutine" project and released under the MIT License. +## +## Created by Samuel Williams on 10/5/2018. +## Copyright, 2018, by Samuel Williams. All rights reserved. +## + +.text +.align 2 + +.global coroutine_transfer +coroutine_transfer: + + # Make space on the stack for caller registers + sub sp, sp, 0xb0 + + # Save caller registers + stp d8, d9, [sp, 0x00] + stp d10, d11, [sp, 0x10] + stp d12, d13, [sp, 0x20] + stp d14, d15, [sp, 0x30] + stp x19, x20, [sp, 0x40] + stp x21, x22, [sp, 0x50] + stp x23, x24, [sp, 0x60] + stp x25, x26, [sp, 0x70] + stp x27, x28, [sp, 0x80] + stp x29, x30, [sp, 0x90] + + # Save return address + str x30, [sp, 0xa0] + + # Save stack pointer to x0 (first argument) + mov x2, sp + str x2, [x0, 0] + + # Load stack pointer from x1 (second argument) + ldr x3, [x1, 0] + mov sp, x3 + + # Restore caller registers + ldp d8, d9, [sp, 0x00] + ldp d10, d11, [sp, 0x10] + ldp d12, d13, [sp, 0x20] + ldp d14, d15, [sp, 0x30] + ldp x19, x20, [sp, 0x40] + ldp x21, x22, [sp, 0x50] + ldp x23, x24, [sp, 0x60] + ldp x25, x26, [sp, 0x70] + ldp x27, x28, [sp, 0x80] + ldp x29, x30, [sp, 0x90] + + # Load return address into x4 + ldr x4, [sp, 0xa0] + + # Pop stack frame + add sp, sp, 0xb0 + + # Jump to return address (in x4) + ret x4 diff --git a/coroutine/arm64/Context.h b/coroutine/arm64/Context.h new file mode 100644 index 00000000000000..03b91fd93725b5 --- /dev/null +++ b/coroutine/arm64/Context.h @@ -0,0 +1,57 @@ +/* + * This file is part of the "Coroutine" project and released under the MIT License. + * + * Created by Samuel Williams on 10/5/2018. + * Copyright, 2018, by Samuel Williams. All rights reserved. +*/ + +#pragma once + +#include +#include + +#if __cplusplus +extern "C" { +#endif + +#define COROUTINE __attribute__((noreturn)) void + +const size_t COROUTINE_REGISTERS = 0xb0 / 8; + +typedef struct +{ + void **stack_pointer; +} coroutine_context; + +typedef COROUTINE(* coroutine_start)(coroutine_context *from, coroutine_context *self); + +static inline void coroutine_initialize( + coroutine_context *context, + coroutine_start start, + void *stack_pointer, + size_t stack_size +) { + /* Force 16-byte alignment */ + context->stack_pointer = (void**)((uintptr_t)stack_pointer & ~0xF); + + if (!start) { + assert(!context->stack_pointer); + /* We are main coroutine for this thread */ + return; + } + + context->stack_pointer -= COROUTINE_REGISTERS; + memset(context->stack_pointer, 0, sizeof(void*) * COROUTINE_REGISTERS); + + context->stack_pointer[0xa0 / 8] = (void*)start; +} + +coroutine_context * coroutine_transfer(coroutine_context * current, coroutine_context * target); + +static inline void coroutine_destroy(coroutine_context * context) +{ +} + +#if __cplusplus +} +#endif diff --git a/coroutine/win32/Context.asm b/coroutine/win32/Context.asm new file mode 100644 index 00000000000000..2647ea4bc43cf5 --- /dev/null +++ b/coroutine/win32/Context.asm @@ -0,0 +1,55 @@ +;; +;; This file is part of the "Coroutine" project and released under the MIT License. +;; +;; Created by Samuel Williams on 10/5/2018. +;; Copyright, 2018, by Samuel Williams. All rights reserved. +;; + +.386 +.model flat + +.code + +assume fs:nothing + +; Using fastcall is a big win (and it's the same has how x64 works). +; In coroutine transfer, the arguments are passed in ecx and edx. We don't need +; to touch these in order to pass them to the destination coroutine. + +@coroutine_transfer@8 proc + ; Save the thread information block: + push fs:[0] + push fs:[4] + push fs:[8] + + ; Save caller registers: + push ebp + push ebx + push edi + push esi + + ; Save caller stack pointer: + mov dword ptr [ecx], esp + + ; Restore callee stack pointer: + mov esp, dword ptr [edx] + + ; Restore callee stack: + pop esi + pop edi + pop ebx + pop ebp + + ; Restore the thread information block: + pop fs:[8] + pop fs:[4] + pop fs:[0] + + ; Save the first argument as the return value: + mov eax, dword ptr ecx + + ; Jump to the address on the stack: + ret +@coroutine_transfer@8 endp + +end diff --git a/coroutine/win32/Context.h b/coroutine/win32/Context.h new file mode 100644 index 00000000000000..aa9f17ddab54a3 --- /dev/null +++ b/coroutine/win32/Context.h @@ -0,0 +1,62 @@ +/* + * This file is part of the "Coroutine" project and released under the MIT License. + * + * Created by Samuel Williams on 10/5/2018. + * Copyright, 2018, by Samuel Williams. All rights reserved. +*/ + +#pragma once + +#include +#include + +#if __cplusplus +extern "C" { +#endif + +#define COROUTINE __declspec(noreturn) void __fastcall + +/* This doesn't include thread information block */ +const size_t COROUTINE_REGISTERS = 4; + +typedef struct +{ + void **stack_pointer; +} coroutine_context; + +typedef void(__fastcall * coroutine_start)(coroutine_context *from, coroutine_context *self); + +static inline void coroutine_initialize( + coroutine_context *context, + coroutine_start start, + void *stack_pointer, + size_t stack_size +) { + context->stack_pointer = (void**)stack_pointer; + + if (!start) { + assert(!context->stack_pointer); + /* We are main coroutine for this thread */ + return; + } + + *--context->stack_pointer = (void*)start; + + /* Windows Thread Information Block */ + *--context->stack_pointer = 0; /* fs:[0] */ + *--context->stack_pointer = (void*)stack_pointer; /* fs:[4] */ + *--context->stack_pointer = (void*)((char *)stack_pointer - stack_size); /* fs:[8] */ + + context->stack_pointer -= COROUTINE_REGISTERS; + memset(context->stack_pointer, 0, sizeof(void*) * COROUTINE_REGISTERS); +} + +coroutine_context * __fastcall coroutine_transfer(coroutine_context * current, coroutine_context * target); + +static inline void coroutine_destroy(coroutine_context * context) +{ +} + +#if __cplusplus +} +#endif diff --git a/coroutine/win64/Context.S b/coroutine/win64/Context.S new file mode 100644 index 00000000000000..4b16e0ce8c0316 --- /dev/null +++ b/coroutine/win64/Context.S @@ -0,0 +1,77 @@ +## +## This file is part of the "Coroutine" project and released under the MIT License. +## +## Created by Samuel Williams on 4/11/2018. +## Copyright, 2018, by Samuel Williams. All rights reserved. +## + +.text + +.globl coroutine_transfer +coroutine_transfer: + # Save the thread information block: + pushq %gs:8 + pushq %gs:16 + + # Save caller registers: + pushq %rbp + pushq %rbx + pushq %rdi + pushq %rsi + pushq %r12 + pushq %r13 + pushq %r14 + pushq %r15 + + movaps %xmm15, -168(%rsp) + movaps %xmm14, -152(%rsp) + movaps %xmm13, -136(%rsp) + movaps %xmm12, -120(%rsp) + movaps %xmm11, -104(%rsp) + movaps %xmm10, -88(%rsp) + movaps %xmm9, -72(%rsp) + movaps %xmm8, -56(%rsp) + movaps %xmm7, -40(%rsp) + movaps %xmm6, -24(%rsp) + + # Save caller stack pointer: + mov %rsp, (%rcx) + + # Restore callee stack pointer: + mov (%rdx), %rsp + + movaps -24(%rsp), %xmm6 + movaps -40(%rsp), %xmm7 + movaps -56(%rsp), %xmm8 + movaps -72(%rsp), %xmm9 + movaps -88(%rsp), %xmm10 + movaps -104(%rsp), %xmm11 + movaps -120(%rsp), %xmm12 + movaps -136(%rsp), %xmm13 + movaps -152(%rsp), %xmm14 + movaps -168(%rsp), %xmm15 + + # Restore callee stack: + popq %r15 + popq %r14 + popq %r13 + popq %r12 + popq %rsi + popq %rdi + popq %rbx + popq %rbp + + # Restore the thread information block: + popq %gs:16 + popq %gs:8 + + # Put the first argument into the return value: + mov %rcx, %rax + + # We pop the return address and jump to it: + ret + +.globl coroutine_trampoline +coroutine_trampoline: + # Do not remove this. This forces 16-byte alignment when entering the coroutine. + ret diff --git a/coroutine/win64/Context.asm b/coroutine/win64/Context.asm new file mode 100644 index 00000000000000..59673ffa3eb324 --- /dev/null +++ b/coroutine/win64/Context.asm @@ -0,0 +1,79 @@ +;; +;; This file is part of the "Coroutine" project and released under the MIT License. +;; +;; Created by Samuel Williams on 10/5/2018. +;; Copyright, 2018, by Samuel Williams. All rights reserved. +;; + +.code + +coroutine_transfer proc + ; Save the thread information block: + push qword ptr gs:[8] + push qword ptr gs:[16] + + ; Save caller registers: + push rbp + push rbx + push rdi + push rsi + push r12 + push r13 + push r14 + push r15 + + movaps [rsp - 24], xmm6 + movaps [rsp - 40], xmm7 + movaps [rsp - 56], xmm8 + movaps [rsp - 72], xmm9 + movaps [rsp - 88], xmm10 + movaps [rsp - 104], xmm11 + movaps [rsp - 120], xmm12 + movaps [rsp - 136], xmm13 + movaps [rsp - 152], xmm14 + movaps [rsp - 168], xmm15 + + ; Save caller stack pointer: + mov [rcx], rsp + + ; Restore callee stack pointer: + mov rsp, [rdx] + + movaps xmm15, [rsp - 168] + movaps xmm14, [rsp - 152] + movaps xmm13, [rsp - 136] + movaps xmm12, [rsp - 120] + movaps xmm11, [rsp - 104] + movaps xmm10, [rsp - 88] + movaps xmm9, [rsp - 72] + movaps xmm8, [rsp - 56] + movaps xmm7, [rsp - 40] + movaps xmm6, [rsp - 24] + + ; Restore callee stack: + pop r15 + pop r14 + pop r13 + pop r12 + pop rsi + pop rdi + pop rbx + pop rbp + + ; Restore the thread information block: + pop qword ptr gs:[16] + pop qword ptr gs:[8] + + ; Put the first argument into the return value: + mov rax, rcx + + ; We pop the return address and jump to it: + ret +coroutine_transfer endp + +coroutine_trampoline proc + ; Do not remove this. This forces 16-byte alignment when entering the coroutine. + ret +coroutine_trampoline endp + +end diff --git a/coroutine/win64/Context.h b/coroutine/win64/Context.h new file mode 100644 index 00000000000000..16a8f583ababf5 --- /dev/null +++ b/coroutine/win64/Context.h @@ -0,0 +1,72 @@ +/* + * This file is part of the "Coroutine" project and released under the MIT License. + * + * Created by Samuel Williams on 10/5/2018. + * Copyright, 2018, by Samuel Williams. All rights reserved. +*/ + +#pragma once + +#include +#include + +#if __cplusplus +extern "C" { +#endif + +#define COROUTINE __declspec(noreturn) void + +const size_t COROUTINE_REGISTERS = 8; +const size_t COROUTINE_XMM_REGISTERS = 1+10*2; + +typedef struct +{ + void **stack_pointer; +} coroutine_context; + +typedef void(* coroutine_start)(coroutine_context *from, coroutine_context *self); + +void coroutine_trampoline(); + +static inline void coroutine_initialize( + coroutine_context *context, + coroutine_start start, + void *stack_pointer, + size_t stack_size +) { + /* Force 16-byte alignment */ + context->stack_pointer = (void**)((uintptr_t)stack_pointer & ~0xF); + + if (!start) { + assert(!context->stack_pointer); + /* We are main coroutine for this thread */ + return; + } + + /* Win64 ABI requires space for arguments */ + context->stack_pointer -= 4; + + /* Return address */ + *--context->stack_pointer = 0; + *--context->stack_pointer = (void*)start; + *--context->stack_pointer = (void*)coroutine_trampoline; + + /* Windows Thread Information Block */ + /* *--context->stack_pointer = 0; */ /* gs:[0x00] is not used */ + *--context->stack_pointer = (void*)stack_pointer; /* gs:[0x08] */ + *--context->stack_pointer = (void*)((char *)stack_pointer - stack_size); /* gs:[0x10] */ + + context->stack_pointer -= COROUTINE_REGISTERS; + memset(context->stack_pointer, 0, sizeof(void*) * COROUTINE_REGISTERS); + memset(context->stack_pointer - COROUTINE_XMM_REGISTERS, 0, sizeof(void*) * COROUTINE_XMM_REGISTERS); +} + +coroutine_context * coroutine_transfer(coroutine_context * current, coroutine_context * target); + +static inline void coroutine_destroy(coroutine_context * context) +{ +} + +#if __cplusplus +} +#endif diff --git a/coroutine/x86/Context.S b/coroutine/x86/Context.S new file mode 100644 index 00000000000000..5fc0cccc2479c9 --- /dev/null +++ b/coroutine/x86/Context.S @@ -0,0 +1,39 @@ +## +## This file is part of the "Coroutine" project and released under the MIT License. +## +## Created by Samuel Williams on 3/11/2018. +## Copyright, 2018, by Samuel Williams. All rights reserved. +## + +.text + +.globl coroutine_transfer +coroutine_transfer: + +# For older linkers +.globl _coroutine_transfer +_coroutine_transfer: + + # Save caller registers + pushl %ebp + pushl %ebx + pushl %edi + pushl %esi + + # Save caller stack pointer + movl %esp, (%ecx) + + # Restore callee stack pointer + movl (%edx), %esp + + # Restore callee stack + popl %esi + popl %edi + popl %ebx + popl %ebp + + # Save the first argument as the return value + movl %ecx, %eax + + # Jump to the address on the stack + ret diff --git a/coroutine/x86/Context.h b/coroutine/x86/Context.h new file mode 100644 index 00000000000000..b077227a1dcddd --- /dev/null +++ b/coroutine/x86/Context.h @@ -0,0 +1,59 @@ +/* + * This file is part of the "Coroutine" project and released under the MIT License. + * + * Created by Samuel Williams on 3/11/2018. + * Copyright, 2018, by Samuel Williams. All rights reserved. +*/ + +#pragma once + +#include +#include + +#if __cplusplus +extern "C" { +#endif + +#define COROUTINE __attribute__((noreturn, fastcall)) void + +const size_t COROUTINE_REGISTERS = 4; + +typedef struct +{ + void **stack_pointer; +} coroutine_context; + +typedef COROUTINE(* coroutine_start)(coroutine_context *from, coroutine_context *self) __attribute__((fastcall)); + +static inline void coroutine_initialize( + coroutine_context *context, + coroutine_start start, + void *stack_pointer, + size_t stack_size +) { + /* Force 16-byte alignment */ + context->stack_pointer = (void**)((uintptr_t)stack_pointer & ~0xF); + + if (!start) { + assert(!context->stack_pointer); + /* We are main coroutine for this thread */ + return; + } + + *--context->stack_pointer = NULL; + *--context->stack_pointer = (void*)start; + + context->stack_pointer -= COROUTINE_REGISTERS; + memset(context->stack_pointer, 0, sizeof(void*) * COROUTINE_REGISTERS); +} + +coroutine_context * coroutine_transfer(coroutine_context * current, coroutine_context * target) __attribute__((fastcall)); + +static inline void coroutine_destroy(coroutine_context * context) +{ + context->stack_pointer = NULL; +} + +#if __cplusplus +} +#endif diff --git a/debug_counter.h b/debug_counter.h index 3fcb562f6552cd..b390563a908c8b 100644 --- a/debug_counter.h +++ b/debug_counter.h @@ -141,6 +141,7 @@ RB_DEBUG_COUNTER(gc_major_oldmalloc) * * [attr] * * _ptr: R?? is not embed. * * _embed: R?? is embed. + * * _transient: R?? uses transient heap. * * type specific attr. * * str_shared: str is shared. * * str_nofree: nofree @@ -162,8 +163,9 @@ RB_DEBUG_COUNTER(obj_free) RB_DEBUG_COUNTER(obj_promote) RB_DEBUG_COUNTER(obj_wb_unprotect) -RB_DEBUG_COUNTER(obj_obj_ptr) RB_DEBUG_COUNTER(obj_obj_embed) +RB_DEBUG_COUNTER(obj_obj_transient) +RB_DEBUG_COUNTER(obj_obj_ptr) RB_DEBUG_COUNTER(obj_str_ptr) RB_DEBUG_COUNTER(obj_str_embed) @@ -171,16 +173,23 @@ RB_DEBUG_COUNTER(obj_str_shared) RB_DEBUG_COUNTER(obj_str_nofree) RB_DEBUG_COUNTER(obj_str_fstr) -RB_DEBUG_COUNTER(obj_ary_ptr) RB_DEBUG_COUNTER(obj_ary_embed) +RB_DEBUG_COUNTER(obj_ary_transient) +RB_DEBUG_COUNTER(obj_ary_ptr) RB_DEBUG_COUNTER(obj_hash_empty) RB_DEBUG_COUNTER(obj_hash_under4) RB_DEBUG_COUNTER(obj_hash_ge4) RB_DEBUG_COUNTER(obj_hash_ge8) +RB_DEBUG_COUNTER(obj_hash_array) +RB_DEBUG_COUNTER(obj_hash_st) +RB_DEBUG_COUNTER(obj_hash_transient) + +RB_DEBUG_COUNTER(obj_hash_force_convert) -RB_DEBUG_COUNTER(obj_struct_ptr) RB_DEBUG_COUNTER(obj_struct_embed) +RB_DEBUG_COUNTER(obj_struct_transient) +RB_DEBUG_COUNTER(obj_struct_ptr) RB_DEBUG_COUNTER(obj_regexp_ptr) @@ -219,6 +228,11 @@ RB_DEBUG_COUNTER(heap_xmalloc) RB_DEBUG_COUNTER(heap_xrealloc) RB_DEBUG_COUNTER(heap_xfree) +/* transient_heap */ +RB_DEBUG_COUNTER(theap_alloc) +RB_DEBUG_COUNTER(theap_alloc_fail) +RB_DEBUG_COUNTER(theap_evacuate) + /* load (not implemented yet) */ /* RB_DEBUG_COUNTER(load_files) diff --git a/defs/gmake.mk b/defs/gmake.mk index 8af2106d868250..49934c5f014133 100644 --- a/defs/gmake.mk +++ b/defs/gmake.mk @@ -64,7 +64,7 @@ endif ORDERED_TEST_TARGETS := $(filter $(TEST_TARGETS), \ btest-ruby test-knownbug test-basic \ test-testframework test-ruby test-almost test-all \ - test-spec \ + test-spec test-bundler-prepare test-bundler \ ) prev_test := $(if $(filter test-spec,$(ORDERED_TEST_TARGETS)),test-spec-precheck) $(foreach test,$(ORDERED_TEST_TARGETS), \ @@ -109,7 +109,7 @@ endif STUBPROGRAM = rubystub$(EXEEXT) IGNOREDPATTERNS = %~ .% %.orig %.rej \#%\# SCRIPTBINDIR := $(if $(EXEEXT),,exec/) -SCRIPTPROGRAMS = $(addprefix $(SCRIPTBINDIR),$(addsuffix $(EXEEXT),$(filter-out $(IGNOREDPATTERNS),$(notdir $(wildcard $(srcdir)/bin/*))))) +SCRIPTPROGRAMS = $(addprefix $(SCRIPTBINDIR),$(addsuffix $(EXEEXT),$(filter-out $(IGNOREDPATTERNS),$(notdir $(wildcard $(srcdir)/libexec/*))))) stub: $(STUBPROGRAM) scriptbin: $(SCRIPTPROGRAMS) @@ -184,3 +184,22 @@ $(MJIT_MIN_HEADER): $(mjit_min_headers) $(PREP) $(Q) $(MAKE_LINK) $@ $(MJIT_HEADER_INSTALL_DIR)/$(@F) endif + +# GNU make treat the target as unmodified when its dependents get +# updated but it is not updated, while others may not. +$(srcdir)/revision.h: $(REVISION_H) + +# Query on the generated rdoc +# +# $ make rdoc:Integer#+ +rdoc\:%: PHONY + $(Q)$(RUNRUBY) $(srcdir)/libexec/ri --no-standard-docs --doc-dir=$(RDOCOUT) $(patsubst rdoc:%,%,$@) + +test_%.rb test/%: programs PHONY + +$(Q)$(exec) $(RUNRUBY) "$(srcdir)/test/runner.rb" --ruby="$(RUNRUBY)" $(TEST_EXCLUDES) $(TESTOPTS) $(patsubst test/%,%,$@) + +clean-srcs-ext:: + $(Q)$(RM) $(patsubst $(srcdir)/%,%,$(EXT_SRCS)) + +clean-srcs-extra:: + $(Q)$(RM) $(patsubst $(srcdir)/%,%,$(EXTRA_SRCS)) diff --git a/defs/known_errors.def b/defs/known_errors.def index b9c490d3a2aab1..e9694cfbda684b 100644 --- a/defs/known_errors.def +++ b/defs/known_errors.def @@ -1,148 +1,157 @@ -EPERM -ENOENT -ESRCH -EINTR -EIO -ENXIO E2BIG -ENOEXEC -EBADF -ECHILD -EAGAIN -ENOMEM EACCES -EFAULT -ENOTBLK -EBUSY -EEXIST -EXDEV -ENODEV -ENOTDIR -EISDIR -EINVAL -ENFILE -EMFILE -ENOTTY -ETXTBSY -EFBIG -ENOSPC -ESPIPE -EROFS -EMLINK -EPIPE -EDOM -ERANGE -EDEADLK -ENAMETOOLONG -ENOLCK -ENOSYS -ENOTEMPTY -ELOOP -EWOULDBLOCK -ENOMSG -EIDRM -ECHRNG -EL2NSYNC -EL3HLT -EL3RST -ELNRNG -EUNATCH -ENOCSI -EL2HLT +EADDRINUSE +EADDRNOTAVAIL +EADV +EAFNOSUPPORT +EAGAIN +EALREADY +EAUTH +EBADARCH EBADE +EBADEXEC +EBADF +EBADFD +EBADMACHO +EBADMSG EBADR -EXFULL -ENOANO +EBADRPC EBADRQC EBADSLT -EDEADLOCK EBFONT -ENOSTR -ENODATA -ETIME -ENOSR -ENONET -ENOPKG -EREMOTE -ENOLINK -EADV -ESRMNT +EBUSY +ECANCELED +ECAPMODE +ECHILD +ECHRNG ECOMM -EPROTO -EMULTIHOP -EDOTDOT -EBADMSG -EOVERFLOW -ENOTUNIQ -EBADFD -EREMCHG -ELIBACC -ELIBBAD -ELIBSCN -ELIBMAX -ELIBEXEC -EILSEQ -ERESTART -ESTRPIPE -EUSERS -ENOTSOCK -EDESTADDRREQ -EMSGSIZE -EPROTOTYPE -ENOPROTOOPT -EPROTONOSUPPORT -ESOCKTNOSUPPORT -EOPNOTSUPP -EPFNOSUPPORT -EAFNOSUPPORT -EADDRINUSE -EADDRNOTAVAIL -ENETDOWN -ENETUNREACH -ENETRESET ECONNABORTED -ECONNRESET -ENOBUFS -EISCONN -ENOTCONN -ESHUTDOWN -ETOOMANYREFS -ETIMEDOUT ECONNREFUSED +ECONNRESET +EDEADLK +EDEADLOCK +EDESTADDRREQ +EDEVERR +EDOM +EDOOFUS +EDOTDOT +EDQUOT +EEXIST +EFAULT +EFBIG +EFTYPE EHOSTDOWN EHOSTUNREACH -EALREADY +EHWPOISON +EIDRM +EILSEQ EINPROGRESS -ESTALE -EUCLEAN -ENOTNAM -ENAVAIL +EINTR +EINVAL +EIO +EIPSEC +EISCONN +EISDIR EISNAM -EREMOTEIO -EDQUOT -ECANCELED EKEYEXPIRED EKEYREJECTED EKEYREVOKED +EL2HLT +EL2NSYNC +EL3HLT +EL3RST +ELAST +ELIBACC +ELIBBAD +ELIBEXEC +ELIBMAX +ELIBSCN +ELNRNG +ELOOP EMEDIUMTYPE +EMFILE +EMLINK +EMSGSIZE +EMULTIHOP +ENAMETOOLONG +ENAVAIL +ENEEDAUTH +ENETDOWN +ENETRESET +ENETUNREACH +ENFILE +ENOANO +ENOATTR +ENOBUFS +ENOCSI +ENODATA +ENODEV +ENOENT +ENOEXEC ENOKEY +ENOLCK +ENOLINK ENOMEDIUM +ENOMEM +ENOMSG +ENONET +ENOPKG +ENOPOLICY +ENOPROTOOPT +ENOSPC +ENOSR +ENOSTR +ENOSYS +ENOTBLK +ENOTCAPABLE +ENOTCONN +ENOTDIR +ENOTEMPTY +ENOTNAM ENOTRECOVERABLE -EOWNERDEAD -ERFKILL -EAUTH -EBADRPC -EDOOFUS -EFTYPE -ENEEDAUTH -ENOATTR +ENOTSOCK ENOTSUP +ENOTTY +ENOTUNIQ +ENXIO +EOPNOTSUPP +EOVERFLOW +EOWNERDEAD +EPERM +EPFNOSUPPORT +EPIPE EPROCLIM EPROCUNAVAIL EPROGMISMATCH EPROGUNAVAIL +EPROTO +EPROTONOSUPPORT +EPROTOTYPE +EPWROFF +EQFULL +ERANGE +EREMCHG +EREMOTE +EREMOTEIO +ERESTART +ERFKILL +EROFS ERPCMISMATCH -EIPSEC -EHWPOISON -ECAPMODE -ENOTCAPABLE +ESHLIBVERS +ESHUTDOWN +ESOCKTNOSUPPORT +ESPIPE +ESRCH +ESRMNT +ESTALE +ESTRPIPE +ETIME +ETIMEDOUT +ETOOMANYREFS +ETXTBSY +EUCLEAN +EUNATCH +EUSERS +EWOULDBLOCK +EXDEV +EXFULL diff --git a/doc/maintainers.rdoc b/doc/maintainers.rdoc index 89f1d306178752..e7baf933c68686 100644 --- a/doc/maintainers.rdoc +++ b/doc/maintainers.rdoc @@ -178,6 +178,9 @@ Zachary Scott (zzak) === Libraries +[lib/bundler.rb, lib/bundler/*] + Hiroshi SHIBATA (hsbt) + https://github.com/bundler/bundler [lib/cmath.rb] _unmaintained_ https://github.com/ruby/cmath diff --git a/doc/syntax/literals.rdoc b/doc/syntax/literals.rdoc index b8ed7f7c54b0e7..62b80db04bd09e 100644 --- a/doc/syntax/literals.rdoc +++ b/doc/syntax/literals.rdoc @@ -71,6 +71,33 @@ Examples: All these numbers have the same decimal value, 170. Like integers and floats you may use an underscore for readability. +=== Rational numbers + +Numbers suffixed by +r+ are Rational numbers. + + 12r #=> (12/1) + 12.3r #=> (123/10) + +Rational numbers are exact, whereas Float numbers are inexact. + + 0.1r + 0.2r #=> (3/10) + 0.1 + 0.2 #=> 0.30000000000000004 + +=== Complex numbers + +Numbers suffixed by +i+ are Complex (or imaginary) numbers. + + 1i #=> (0+1i) + 1i * 1i #=> (-1+0i) + +Also Rational numbers may be imaginary numbers. + + 12.3ri #=> (0+(123/10)*i) + ++i+ must be placed after +r+, the opposite is not allowed. + + 12.3ir #=> syntax error + == Strings The most common way of writing strings is using ": diff --git a/enc/unicode.c b/enc/unicode.c index 0d692520e85d54..b08b1450ba9935 100644 --- a/enc/unicode.c +++ b/enc/unicode.c @@ -683,8 +683,10 @@ onigenc_unicode_case_map(OnigCaseFoldType* flagP, MODIFIED; if (flags & ONIGENC_CASE_FOLD_TURKISH_AZERI && code == 'i') code = I_WITH_DOT_ABOVE; - else - code += 'A' - 'a'; + else { + code -= 'a'; + code += 'A'; + } } } else if (code >= 'A' && code <= 'Z') { @@ -770,10 +772,15 @@ onigenc_unicode_case_map(OnigCaseFoldType* flagP, } } } - else if ((folded = onigenc_unicode_unfold1_lookup(code)) != 0 /* data about character found in CaseUnfold_11_Table */ - && flags & OnigCaseFoldFlags(folded->n)) { /* needs and data availability match */ - MODIFIED; - code = folded->code[(flags & OnigCaseFoldFlags(folded->n) & ONIGENC_CASE_TITLECASE) ? 1 : 0]; + else if ((folded = onigenc_unicode_unfold1_lookup(code)) != 0) { /* data about character found in CaseUnfold_11_Table */ + if ((flags & ONIGENC_CASE_TITLECASE) /* Titlecase needed, */ + && (OnigCaseFoldFlags(folded->n) & ONIGENC_CASE_IS_TITLECASE)) { /* but already Titlecase */ + /* already Titlecase, no changes needed */ + } + else if (flags & OnigCaseFoldFlags(folded->n)) { /* needs and data availability match */ + MODIFIED; + code = folded->code[(flags & OnigCaseFoldFlags(folded->n) & ONIGENC_CASE_TITLECASE) ? 1 : 0]; + } } } to += ONIGENC_CODE_TO_MBC(enc, code, to); @@ -786,8 +793,8 @@ onigenc_unicode_case_map(OnigCaseFoldType* flagP, return (int )(to - to_start); } -/* for extended grapheme cluster */ -/* TODO: generate from Unicode data */ +/* These GAZ/E_Base/Emoji tables are for extended grapheme cluster */ +/* TODO: generate these 3 tables from Unicode data */ const OnigCodePoint onigenc_unicode_GCB_ranges_GAZ[] = { 0, diff --git a/enc/unicode/10.0.0/name2ctype.h b/enc/unicode/10.0.0/name2ctype.h index 4a1422d17a974b..dc23d54ab8a712 100644 --- a/enc/unicode/10.0.0/name2ctype.h +++ b/enc/unicode/10.0.0/name2ctype.h @@ -34756,9 +34756,11 @@ struct uniname2ctype_struct { }; #define uniname2ctype_offset(str) offsetof(struct uniname2ctype_pool_t, uniname2ctype_pool_##str) -#if !1+0 -static const struct uniname2ctype_struct *uniname2ctype_p(const char *, unsigned int); +static const struct uniname2ctype_struct *uniname2ctype_p( +#if !(1+0) /* if ANSI, old style not to conflict with generated prototype */ + const char *, unsigned int #endif +); #ifndef USE_UNICODE_PROPERTIES #define TOTAL_KEYWORDS 15 @@ -38379,3 +38381,12 @@ uniname2ctype(const UChar *name, unsigned int len) #define ONIG_UNICODE_VERSION_MAJOR 10 #define ONIG_UNICODE_VERSION_MINOR 0 #define ONIG_UNICODE_VERSION_TEENY 0 +#if defined ONIG_UNICODE_EMOJI_VERSION_STRING && !( \ + ONIG_UNICODE_EMOJI_VERSION_MAJOR == 5 && \ + ONIG_UNICODE_EMOJI_VERSION_MINOR == 0 && \ + 1) +# error ONIG_UNICODE_EMOJI_VERSION_STRING mismatch +#endif +#define ONIG_UNICODE_EMOJI_VERSION_STRING "5.0" +#define ONIG_UNICODE_EMOJI_VERSION_MAJOR 5 +#define ONIG_UNICODE_EMOJI_VERSION_MINOR 0 diff --git a/enc/unicode/case-folding.rb b/enc/unicode/case-folding.rb index 829efefaf17f8b..362d6ebfd9bc23 100755 --- a/enc/unicode/case-folding.rb +++ b/enc/unicode/case-folding.rb @@ -264,7 +264,6 @@ def flags(from, type, to) from = Array(from).map {|i| "%04X" % i}.join(" ") to = Array(to).map {|i| "%04X" % i}.join(" ") item = map(from) - specials_index = nil specials = [] case type when 'CaseFold_11' @@ -309,7 +308,7 @@ def flags(from, type, to) end unless item.upper == item.title if item.code == item.title - raise "Unpredicted case 1 in enc/unicode/case_folding.rb. Please contact https://bugs.ruby-lang.org/." + flags += '|IT' # was unpredicted case 1 elsif item.title==to[1] flags += '|ST' else @@ -410,8 +409,8 @@ def debug!() end s = f.string end if dest - open(dest, "wb") do |f| - f.print(s) + open(dest, "wb") do |file| + file.print(s) end else STDOUT.print(s) diff --git a/enum.c b/enum.c index 9485b7eb604a54..3be6941c3a8880 100644 --- a/enum.c +++ b/enum.c @@ -14,6 +14,7 @@ #include "ruby/util.h" #include "id.h" #include "symbol.h" +#include "transient_heap.h" #include @@ -1171,9 +1172,10 @@ enum_sort_by(VALUE obj) rb_ary_concat(ary, buf); } if (RARRAY_LEN(ary) > 2) { - RARRAY_PTR_USE(ary, ptr, - ruby_qsort(ptr, RARRAY_LEN(ary)/2, 2*sizeof(VALUE), - sort_by_cmp, (void *)ary)); + rb_ary_transient_heap_evacuate(ary, TRUE); /* should be malloc heap */ + RARRAY_PTR_USE(ary, ptr, + ruby_qsort(ptr, RARRAY_LEN(ary)/2, 2*sizeof(VALUE), + sort_by_cmp, (void *)ary)); } if (RBASIC(ary)->klass) { rb_raise(rb_eRuntimeError, "sort_by reentered"); diff --git a/enumerator.c b/enumerator.c index 466e0645604ded..2eb0c24a2bc100 100644 --- a/enumerator.c +++ b/enumerator.c @@ -12,6 +12,7 @@ ************************************************/ +#include "ruby/ruby.h" #include "internal.h" #include "id.h" @@ -161,6 +162,13 @@ struct proc_entry { static VALUE generator_allocate(VALUE klass); static VALUE generator_init(VALUE obj, VALUE proc); +static VALUE rb_cEnumChain; + +struct enum_chain { + VALUE enums; + long pos; +}; + static VALUE rb_cArithSeq; /* @@ -2411,6 +2419,300 @@ stop_result(VALUE self) return rb_attr_get(self, id_result); } +/* + * Document-class: Enumerator::Chain + * + * Enumerator::Chain is a subclass of Enumerator, which represents a + * chain of enumerables that works as a single enumerator. + * + * This type of objects can be created by Enumerable#chain and + * Enumerator#+. + */ + +static void +enum_chain_mark(void *p) +{ + struct enum_chain *ptr = p; + rb_gc_mark(ptr->enums); +} + +#define enum_chain_free RUBY_TYPED_DEFAULT_FREE + +static size_t +enum_chain_memsize(const void *p) +{ + return sizeof(struct enum_chain); +} + +static const rb_data_type_t enum_chain_data_type = { + "chain", + { + enum_chain_mark, + enum_chain_free, + enum_chain_memsize, + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY +}; + +static struct enum_chain * +enum_chain_ptr(VALUE obj) +{ + struct enum_chain *ptr; + + TypedData_Get_Struct(obj, struct enum_chain, &enum_chain_data_type, ptr); + if (!ptr || ptr->enums == Qundef) { + rb_raise(rb_eArgError, "uninitialized chain"); + } + return ptr; +} + +/* :nodoc: */ +static VALUE +enum_chain_allocate(VALUE klass) +{ + struct enum_chain *ptr; + VALUE obj; + + obj = TypedData_Make_Struct(klass, struct enum_chain, &enum_chain_data_type, ptr); + ptr->enums = Qundef; + ptr->pos = -1; + + return obj; +} + +/* + * call-seq: + * Enumerator::Chain.new(*enums) -> enum + * + * Generates a new enumerator object that iterates over the elements + * of given enumerable objects in sequence. + * + * e = Enumerator::Chain.new(1..3, [4, 5]) + * e.to_a #=> [1, 2, 3, 4, 5] + * e.size #=> 5 + */ +static VALUE +enum_chain_initialize(VALUE obj, VALUE enums) +{ + struct enum_chain *ptr; + + rb_check_frozen(obj); + TypedData_Get_Struct(obj, struct enum_chain, &enum_chain_data_type, ptr); + + if (!ptr) rb_raise(rb_eArgError, "unallocated chain"); + + ptr->enums = rb_obj_freeze(enums); + ptr->pos = -1; + + return obj; +} + +/* :nodoc: */ +static VALUE +enum_chain_init_copy(VALUE obj, VALUE orig) +{ + struct enum_chain *ptr0, *ptr1; + + if (!OBJ_INIT_COPY(obj, orig)) return obj; + ptr0 = enum_chain_ptr(orig); + + TypedData_Get_Struct(obj, struct enum_chain, &enum_chain_data_type, ptr1); + + if (!ptr1) rb_raise(rb_eArgError, "unallocated chain"); + + ptr1->enums = ptr0->enums; + ptr1->pos = ptr0->pos; + + return obj; +} + +static VALUE +enum_chain_total_size(VALUE enums) +{ + VALUE total = INT2FIX(0); + + RARRAY_PTR_USE(enums, ptr, { + long i; + + for (i = 0; i < RARRAY_LEN(enums); i++) { + VALUE size = enum_size(ptr[i]); + + if (NIL_P(size) || (RB_TYPE_P(size, T_FLOAT) && isinf(NUM2DBL(size)))) { + return size; + } + if (!RB_INTEGER_TYPE_P(size)) { + return Qnil; + } + + total = rb_funcall(total, '+', 1, size); + } + }); + + return total; +} + +/* + * call-seq: + * obj.size -> int, Float::INFINITY or nil + * + * Returns the total size of the enumerator chain calculated by + * summing up the size of each enumerable in the chain. If any of the + * enumerables reports its size as nil or Float::INFINITY, that value + * is returned as the total size. + */ +static VALUE +enum_chain_size(VALUE obj) +{ + return enum_chain_total_size(enum_chain_ptr(obj)->enums); +} + +static VALUE +enum_chain_enum_size(VALUE obj, VALUE args, VALUE eobj) +{ + return enum_chain_size(obj); +} + +static VALUE +enum_chain_yield_block(VALUE arg, VALUE block, int argc, VALUE *argv) +{ + return rb_funcallv(block, id_call, argc, argv); +} + +static VALUE +enum_chain_enum_no_size(VALUE obj, VALUE args, VALUE eobj) +{ + return Qnil; +} + +/* + * call-seq: + * obj.each(*args) { |...| ... } -> obj + * obj.each(*args) -> enumerator + * + * Iterates over the elements of the first enumerable by calling the + * "each" method on it with the given arguments, then proceeds to the + * following enumerables in sequence until all of the enumerables are + * exhausted. + * + * If no block is given, returns an enumerator. + */ +static VALUE +enum_chain_each(int argc, VALUE *argv, VALUE obj) +{ + VALUE enums, block; + struct enum_chain *objptr; + + RETURN_SIZED_ENUMERATOR(obj, argc, argv, argc > 0 ? enum_chain_enum_no_size : enum_chain_enum_size); + + objptr = enum_chain_ptr(obj); + enums = objptr->enums; + block = rb_block_proc(); + + RARRAY_PTR_USE(enums, ptr, { + long i; + + for (i = 0; i < RARRAY_LEN(enums); i++) { + objptr->pos = i; + rb_block_call(ptr[i], id_each, argc, argv, enum_chain_yield_block, block); + } + }); + + return obj; +} + +/* + * call-seq: + * obj.rewind -> obj + * + * Rewinds the enumerator chain by calling the "rewind" method on each + * enumerable in reverse order. Each call is performed only if the + * enumerable responds to the method. + */ +static VALUE +enum_chain_rewind(VALUE obj) +{ + struct enum_chain *objptr = enum_chain_ptr(obj); + VALUE enums = objptr->enums; + + RARRAY_PTR_USE(enums, ptr, { + long i; + + for (i = objptr->pos; 0 <= i && i < RARRAY_LEN(enums); objptr->pos = --i) { + rb_check_funcall(ptr[i], id_rewind, 0, 0); + } + }); + + return obj; +} + +static VALUE +inspect_enum_chain(VALUE obj, VALUE dummy, int recur) +{ + VALUE klass = rb_obj_class(obj); + struct enum_chain *ptr; + + TypedData_Get_Struct(obj, struct enum_chain, &enum_chain_data_type, ptr); + + if (!ptr || ptr->enums == Qundef) { + return rb_sprintf("#<%"PRIsVALUE": uninitialized>", rb_class_path(klass)); + } + + if (recur) { + return rb_sprintf("#<%"PRIsVALUE": ...>", rb_class_path(klass)); + } + + return rb_sprintf("#<%"PRIsVALUE": %+"PRIsVALUE">", rb_class_path(klass), ptr->enums); +} + +/* + * call-seq: + * obj.inspect -> string + * + * Returns a printable version of the enumerator chain. + */ +static VALUE +enum_chain_inspect(VALUE obj) +{ + return rb_exec_recursive(inspect_enum_chain, obj, 0); +} + +/* + * call-seq: + * e.chain(*enums) -> enumerator + * + * Returns an enumerator object generated from this enumerator and + * given enumerables. + * + * e = (1..3).chain([4, 5]) + * e.to_a #=> [1, 2, 3, 4, 5] + */ +static VALUE +enum_chain(int argc, VALUE *argv, VALUE obj) +{ + VALUE enums = rb_ary_new_from_values(1, &obj); + rb_ary_cat(enums, argv, argc); + + return enum_chain_initialize(enum_chain_allocate(rb_cEnumChain), enums); +} + +/* + * call-seq: + * e + enum -> enumerator + * + * Returns an enumerator object generated from this enumerator and a + * given enumerable. + * + * e = (1..3).each + [4, 5] + * e.to_a #=> [1, 2, 3, 4, 5] + */ +static VALUE +enumerator_plus(VALUE obj, VALUE eobj) +{ + VALUE enums = rb_ary_new_from_args(2, obj, eobj); + + return enum_chain_initialize(enum_chain_allocate(rb_cEnumChain), enums); +} + /* * Document-class: Enumerator::ArithmeticSequence * @@ -2790,15 +3092,16 @@ static double arith_seq_float_step_size(double beg, double end, double step, int excl) { double const epsilon = DBL_EPSILON; - double n = (end - beg) / step; - double err = (fabs(beg) + fabs(end) + fabs(end - beg)) / fabs(step) * epsilon; + double n, err; - if (isinf(step)) { - return step > 0 ? beg <= end : beg >= end; - } if (step == 0) { return HUGE_VAL; } + n = (end - beg) / step; + err = (fabs(beg) + fabs(end) + fabs(end - beg)) / fabs(step) * epsilon; + if (isinf(step)) { + return step > 0 ? beg <= end : beg >= end; + } if (err > 0.5) err = 0.5; if (excl) { if (n <= 0) return 0; @@ -2906,6 +3209,8 @@ InitVM_Enumerator(void) rb_define_method(rb_cEnumerator, "rewind", enumerator_rewind, 0); rb_define_method(rb_cEnumerator, "inspect", enumerator_inspect, 0); rb_define_method(rb_cEnumerator, "size", enumerator_size, 0); + rb_define_method(rb_cEnumerator, "+", enumerator_plus, 1); + rb_define_method(rb_mEnumerable, "chain", enum_chain, -1); /* Lazy */ rb_cLazy = rb_define_class_under(rb_cEnumerator, "Lazy", rb_cEnumerator); @@ -2959,6 +3264,16 @@ InitVM_Enumerator(void) rb_define_method(rb_cYielder, "yield", yielder_yield, -2); rb_define_method(rb_cYielder, "<<", yielder_yield_push, 1); + /* Chain */ + rb_cEnumChain = rb_define_class_under(rb_cEnumerator, "Chain", rb_cEnumerator); + rb_define_alloc_func(rb_cEnumChain, enum_chain_allocate); + rb_define_method(rb_cEnumChain, "initialize", enum_chain_initialize, -2); + rb_define_method(rb_cEnumChain, "initialize_copy", enum_chain_init_copy, 1); + rb_define_method(rb_cEnumChain, "each", enum_chain_each, -1); + rb_define_method(rb_cEnumChain, "size", enum_chain_size, 0); + rb_define_method(rb_cEnumChain, "rewind", enum_chain_rewind, 0); + rb_define_method(rb_cEnumChain, "inspect", enum_chain_inspect, 0); + /* ArithmeticSequence */ rb_cArithSeq = rb_define_class_under(rb_cEnumerator, "ArithmeticSequence", rb_cEnumerator); rb_undef_alloc_func(rb_cArithSeq); diff --git a/error.c b/error.c index fc3022f8541712..8e97f4919df819 100644 --- a/error.c +++ b/error.c @@ -830,6 +830,13 @@ rb_typeddata_is_kind_of(VALUE obj, const rb_data_type_t *data_type) return 1; } +#undef rb_typeddata_is_instance_of +int +rb_typeddata_is_instance_of(VALUE obj, const rb_data_type_t *data_type) +{ + return rb_typeddata_is_instance_of_inline(obj, data_type); +} + void * rb_check_typeddata(VALUE obj, const rb_data_type_t *data_type) { diff --git a/eval.c b/eval.c index 35255e09753075..ecb79ccdcb6340 100644 --- a/eval.c +++ b/eval.c @@ -233,7 +233,7 @@ ruby_cleanup(volatile int ex) } } - mjit_finish(); /* We still need ISeqs here. */ + mjit_finish(TRUE); /* We still need ISeqs here. */ ruby_finalize_1(); @@ -531,7 +531,7 @@ setup_exception(rb_execution_context_t *ec, int tag, volatile VALUE mesg, VALUE mesg = rb_obj_dup(mesg); } } - if (cause != Qundef) { + if (cause != Qundef && !THROW_DATA_P(cause)) { exc_setup_cause(mesg, cause); } if (NIL_P(bt)) { diff --git a/eval_error.c b/eval_error.c index 82ada4dc40f0c1..8aab7ba05d0b72 100644 --- a/eval_error.c +++ b/eval_error.c @@ -195,7 +195,7 @@ print_backtrace(const VALUE eclass, const VALUE errat, const VALUE str, int reve long len = RARRAY_LEN(errat); int skip = eclass == rb_eSysStackError; const int threshold = 1000000000; - int width = ((int)log10((double)(len > threshold ? + int width = (len <= 1) ? INT_MIN : ((int)log10((double)(len > threshold ? ((len - 1) / threshold) : len - 1)) + (len < threshold ? 0 : 9) + 1); diff --git a/ext/date/date_core.c b/ext/date/date_core.c index ac81268193a540..5a7eb385c9ad2e 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -51,6 +51,9 @@ static double positive_inf, negative_inf; #define f_add3(x,y,z) f_add(f_add(x, y), z) #define f_sub3(x,y,z) f_sub(f_sub(x, y), z) +static VALUE date_initialize(int argc, VALUE *argv, VALUE self); +static VALUE datetime_initialize(int argc, VALUE *argv, VALUE self); + inline static int f_cmp(VALUE x, VALUE y) { @@ -94,7 +97,7 @@ f_ge_p(VALUE x, VALUE y) { if (FIXNUM_P(x) && FIXNUM_P(y)) return f_boolcast(FIX2LONG(x) >= FIX2LONG(y)); - return rb_funcall(x, rb_intern(">="), 1, y); + return rb_funcall(x, id_ge_p, 1, y); } inline static VALUE @@ -102,7 +105,7 @@ f_eqeq_p(VALUE x, VALUE y) { if (FIXNUM_P(x) && FIXNUM_P(y)) return f_boolcast(FIX2LONG(x) == FIX2LONG(y)); - return rb_funcall(x, rb_intern("=="), 1, y); + return rb_funcall(x, id_eqeq_p, 1, y); } inline static VALUE @@ -322,7 +325,7 @@ do {\ (x)->year = _year;\ (x)->mon = _mon;\ (x)->mday = _mday;\ - (x)->flags = _flags;\ + (x)->flags = (_flags) & ~COMPLEX_DAT;\ } while (0) #else #define set_to_simple(obj, x, _nth, _jd ,_sg, _year, _mon, _mday, _flags) \ @@ -332,7 +335,7 @@ do {\ (x)->sg = (date_sg_t)(_sg);\ (x)->year = _year;\ (x)->pc = PACK2(_mon, _mday);\ - (x)->flags = _flags;\ + (x)->flags = (_flags) & ~COMPLEX_DAT;\ } while (0) #endif @@ -352,7 +355,7 @@ do {\ (x)->hour = _hour;\ (x)->min = _min;\ (x)->sec = _sec;\ - (x)->flags = _flags;\ + (x)->flags = (_flags) | COMPLEX_DAT;\ } while (0) #else #define set_to_complex(obj, x, _nth, _jd ,_df, _sf, _of, _sg,\ @@ -366,7 +369,7 @@ do {\ (x)->sg = (date_sg_t)(_sg);\ (x)->year = _year;\ (x)->pc = PACK5(_mon, _mday, _hour, _min, _sec);\ - (x)->flags = _flags;\ + (x)->flags = (_flags) | COMPLEX_DAT;\ } while (0) #endif @@ -2966,7 +2969,7 @@ d_simple_new_internal(VALUE klass, obj = TypedData_Make_Struct(klass, struct SimpleDateData, &d_lite_type, dat); - set_to_simple(obj, dat, nth, jd, sg, y, m, d, flags & ~COMPLEX_DAT); + set_to_simple(obj, dat, nth, jd, sg, y, m, d, flags); assert(have_jd_p(dat) || have_civil_p(dat)); @@ -2988,7 +2991,7 @@ d_complex_new_internal(VALUE klass, obj = TypedData_Make_Struct(klass, struct ComplexDateData, &d_lite_type, dat); set_to_complex(obj, dat, nth, jd, df, sf, of, sg, - y, m, d, h, min, s, flags | COMPLEX_DAT); + y, m, d, h, min, s, flags); assert(have_jd_p(dat) || have_civil_p(dat)); assert(have_df_p(dat) || have_time_p(dat)); @@ -3382,10 +3385,21 @@ date_s_ordinal(int argc, VALUE *argv, VALUE klass) */ static VALUE date_s_civil(int argc, VALUE *argv, VALUE klass) +{ + return date_initialize(argc, argv, d_lite_s_alloc_simple(klass)); +} + +static VALUE +date_initialize(int argc, VALUE *argv, VALUE self) { VALUE vy, vm, vd, vsg, y, fr, fr2, ret; int m, d; double sg; + struct SimpleDateData *dat = rb_check_typeddata(self, &d_lite_type); + + if (!simple_dat_p(dat)) { + rb_raise(rb_eTypeError, "Date expected"); + } rb_scan_args(argc, argv, "04", &vy, &vm, &vd, &vsg); @@ -3415,11 +3429,7 @@ date_s_civil(int argc, VALUE *argv, VALUE klass) &rm, &rd)) rb_raise(rb_eArgError, "invalid date"); - ret = d_simple_new_internal(klass, - nth, 0, - sg, - ry, rm, rd, - HAVE_CIVIL); + set_to_simple(self, dat, nth, 0, sg, ry, rm, rd, HAVE_CIVIL); } else { VALUE nth; @@ -3431,12 +3441,9 @@ date_s_civil(int argc, VALUE *argv, VALUE klass) &ns)) rb_raise(rb_eArgError, "invalid date"); - ret = d_simple_new_internal(klass, - nth, rjd, - sg, - ry, rm, rd, - HAVE_JD | HAVE_CIVIL); + set_to_simple(self, dat, nth, rjd, sg, ry, rm, rd, HAVE_JD | HAVE_CIVIL); } + ret = self; add_frac(); return ret; } @@ -4696,7 +4703,7 @@ do {\ }\ } while (0) -#ifndef NDEBUG +#if 0 static VALUE d_lite_initialize(int argc, VALUE *argv, VALUE self) { @@ -4749,7 +4756,7 @@ d_lite_initialize(int argc, VALUE *argv, VALUE self) "cannot load complex into simple"); set_to_complex(self, &dat->c, nth, rjd, df, sf, of, sg, - 0, 0, 0, 0, 0, 0, HAVE_JD | HAVE_DF | COMPLEX_DAT); + 0, 0, 0, 0, 0, 0, HAVE_JD | HAVE_DF); } } return self; @@ -4768,8 +4775,28 @@ d_lite_initialize_copy(VALUE copy, VALUE date) { get_d2(copy, date); if (simple_dat_p(bdat)) { - adat->s = bdat->s; - adat->s.flags &= ~COMPLEX_DAT; + if (simple_dat_p(adat)) { + adat->s = bdat->s; + } + else { + adat->c.flags = bdat->s.flags | COMPLEX_DAT; + adat->c.nth = bdat->s.nth; + adat->c.jd = bdat->s.jd; + adat->c.df = 0; + adat->c.sf = INT2FIX(0); + adat->c.of = 0; + adat->c.sg = bdat->s.sg; + adat->c.year = bdat->s.year; +#ifndef USE_PACK + adat->c.mon = bdat->s.mon; + adat->c.mday = bdat->s.mday; + adat->c.hour = bdat->s.hour; + adat->c.min = bdat->s.min; + adat->c.sec = bdat->s.sec; +#else + adat->c.pc = bdat->s.pc; +#endif + } } else { if (!complex_dat_p(adat)) @@ -4777,7 +4804,6 @@ d_lite_initialize_copy(VALUE copy, VALUE date) "cannot load complex into simple"); adat->c = bdat->c; - adat->c.flags |= COMPLEX_DAT; } } return copy; @@ -6245,7 +6271,7 @@ cmp_gen(VALUE self, VALUE other) return INT2FIX(f_cmp(m_ajd(dat), other)); else if (k_date_p(other)) return INT2FIX(f_cmp(m_ajd(dat), f_ajd(other))); - return rb_num_coerce_cmp(self, other, rb_intern("<=>")); + return rb_num_coerce_cmp(self, other, id_cmp); } static VALUE @@ -6374,7 +6400,7 @@ equal_gen(VALUE self, VALUE other) return f_eqeq_p(m_real_local_jd(dat), other); else if (k_date_p(other)) return f_eqeq_p(m_real_local_jd(dat), f_jd(other)); - return rb_num_coerce_cmp(self, other, rb_intern("==")); + return rb_num_coerce_cmp(self, other, id_eqeq_p); } /* @@ -7102,6 +7128,10 @@ d_lite_marshal_dump(VALUE self) static VALUE d_lite_marshal_load(VALUE self, VALUE a) { + VALUE nth, sf; + int jd, df, of; + double sg; + get_d1(self); rb_check_frozen(self); @@ -7114,63 +7144,33 @@ d_lite_marshal_load(VALUE self, VALUE a) case 2: /* 1.6.x */ case 3: /* 1.8.x, 1.9.2 */ { - VALUE ajd, of, sg, nth, sf; - int jd, df, rof; - double rsg; - + VALUE ajd, vof, vsg; if (RARRAY_LEN(a) == 2) { ajd = f_sub(RARRAY_AREF(a, 0), half_days_in_day); - of = INT2FIX(0); - sg = RARRAY_AREF(a, 1); - if (!k_numeric_p(sg)) - sg = DBL2NUM(RTEST(sg) ? GREGORIAN : JULIAN); + vof = INT2FIX(0); + vsg = RARRAY_AREF(a, 1); + if (!k_numeric_p(vsg)) + vsg = DBL2NUM(RTEST(vsg) ? GREGORIAN : JULIAN); } else { ajd = RARRAY_AREF(a, 0); - of = RARRAY_AREF(a, 1); - sg = RARRAY_AREF(a, 2); + vof = RARRAY_AREF(a, 1); + vsg = RARRAY_AREF(a, 2); } - old_to_new(ajd, of, sg, - &nth, &jd, &df, &sf, &rof, &rsg); - - if (!df && f_zero_p(sf) && !rof) { - set_to_simple(self, &dat->s, nth, jd, rsg, 0, 0, 0, HAVE_JD); - } else { - if (!complex_dat_p(dat)) - rb_raise(rb_eArgError, - "cannot load complex into simple"); - - set_to_complex(self, &dat->c, nth, jd, df, sf, rof, rsg, - 0, 0, 0, 0, 0, 0, - HAVE_JD | HAVE_DF | COMPLEX_DAT); - } + old_to_new(ajd, vof, vsg, + &nth, &jd, &df, &sf, &of, &sg); } break; case 6: { - VALUE nth, sf; - int jd, df, of; - double sg; - nth = RARRAY_AREF(a, 0); jd = NUM2INT(RARRAY_AREF(a, 1)); df = NUM2INT(RARRAY_AREF(a, 2)); sf = RARRAY_AREF(a, 3); of = NUM2INT(RARRAY_AREF(a, 4)); sg = NUM2DBL(RARRAY_AREF(a, 5)); - if (!df && f_zero_p(sf) && !of) { - set_to_simple(self, &dat->s, nth, jd, sg, 0, 0, 0, HAVE_JD); - } else { - if (!complex_dat_p(dat)) - rb_raise(rb_eArgError, - "cannot load complex into simple"); - - set_to_complex(self, &dat->c, nth, jd, df, sf, of, sg, - 0, 0, 0, 0, 0, 0, - HAVE_JD | HAVE_DF | COMPLEX_DAT); - } } break; default: @@ -7178,6 +7178,18 @@ d_lite_marshal_load(VALUE self, VALUE a) break; } + if (simple_dat_p(dat)) { + if (df || !f_zero_p(sf) || of) { + rb_raise(rb_eArgError, + "cannot load complex into simple"); + } + set_to_simple(self, &dat->s, nth, jd, sg, 0, 0, 0, HAVE_JD); + } else { + set_to_complex(self, &dat->c, nth, jd, df, sf, of, sg, + 0, 0, 0, 0, 0, 0, + HAVE_JD | HAVE_DF); + } + if (FL_TEST(a, FL_EXIVAR)) { rb_copy_generic_ivar(self, a); FL_SET(self, FL_EXIVAR); @@ -7357,10 +7369,21 @@ datetime_s_ordinal(int argc, VALUE *argv, VALUE klass) */ static VALUE datetime_s_civil(int argc, VALUE *argv, VALUE klass) +{ + return datetime_initialize(argc, argv, d_lite_s_alloc_complex(klass)); +} + +static VALUE +datetime_initialize(int argc, VALUE *argv, VALUE self) { VALUE vy, vm, vd, vh, vmin, vs, vof, vsg, y, fr, fr2, ret; int m, d, h, min, s, rof; double sg; + struct ComplexDateData *dat = rb_check_typeddata(self, &d_lite_type); + + if (!complex_dat_p(dat)) { + rb_raise(rb_eTypeError, "DateTime expected"); + } rb_scan_args(argc, argv, "08", &vy, &vm, &vd, &vh, &vmin, &vs, &vof, &vsg); @@ -7404,13 +7427,13 @@ datetime_s_civil(int argc, VALUE *argv, VALUE klass) rb_raise(rb_eArgError, "invalid date"); canon24oc(); - ret = d_complex_new_internal(klass, - nth, 0, - 0, INT2FIX(0), - rof, sg, - ry, rm, rd, - rh, rmin, rs, - HAVE_CIVIL | HAVE_TIME); + set_to_complex(self, dat, + nth, 0, + 0, INT2FIX(0), + rof, sg, + ry, rm, rd, + rh, rmin, rs, + HAVE_CIVIL | HAVE_TIME); } else { VALUE nth; @@ -7429,14 +7452,15 @@ datetime_s_civil(int argc, VALUE *argv, VALUE klass) time_to_df(rh, rmin, rs), rof); - ret = d_complex_new_internal(klass, - nth, rjd2, - 0, INT2FIX(0), - rof, sg, - ry, rm, rd, - rh, rmin, rs, - HAVE_JD | HAVE_CIVIL | HAVE_TIME); + set_to_complex(self, dat, + nth, rjd2, + 0, INT2FIX(0), + rof, sg, + ry, rm, rd, + rh, rmin, rs, + HAVE_JD | HAVE_CIVIL | HAVE_TIME); } + ret = self; add_frac(); return ret; } @@ -9232,7 +9256,7 @@ Init_date_core(void) */ rb_define_const(cDate, "GREGORIAN", DBL2NUM(GREGORIAN)); - rb_define_alloc_func(cDate, d_lite_s_alloc); + rb_define_alloc_func(cDate, d_lite_s_alloc_simple); #ifndef NDEBUG #define de_define_private_method rb_define_private_method @@ -9285,7 +9309,6 @@ Init_date_core(void) rb_define_singleton_method(cDate, "jd", date_s_jd, -1); rb_define_singleton_method(cDate, "ordinal", date_s_ordinal, -1); rb_define_singleton_method(cDate, "civil", date_s_civil, -1); - rb_define_singleton_method(cDate, "new", date_s_civil, -1); rb_define_singleton_method(cDate, "commercial", date_s_commercial, -1); #ifndef NDEBUG @@ -9313,10 +9336,7 @@ Init_date_core(void) rb_define_singleton_method(cDate, "_jisx0301", date_s__jisx0301, 1); rb_define_singleton_method(cDate, "jisx0301", date_s_jisx0301, -1); -#ifndef NDEBUG -#define de_define_method rb_define_method - de_define_method(cDate, "initialize", d_lite_initialize, -1); -#endif + rb_define_method(cDate, "initialize", date_initialize, -1); rb_define_method(cDate, "initialize_copy", d_lite_initialize_copy, 1); #ifndef NDEBUG @@ -9574,6 +9594,7 @@ Init_date_core(void) */ cDateTime = rb_define_class("DateTime", cDate); + rb_define_alloc_func(cDateTime, d_lite_s_alloc_complex); rb_define_singleton_method(cDateTime, "jd", datetime_s_jd, -1); rb_define_singleton_method(cDateTime, "ordinal", datetime_s_ordinal, -1); diff --git a/ext/json/json.gemspec b/ext/json/json.gemspec index b8f3009a9c7b79..1c18efbedc712c 100644 Binary files a/ext/json/json.gemspec and b/ext/json/json.gemspec differ diff --git a/ext/objspace/objspace.c b/ext/objspace/objspace.c index f9157d6c0e1d81..1ac69af844da3a 100644 --- a/ext/objspace/objspace.c +++ b/ext/objspace/objspace.c @@ -824,7 +824,7 @@ static int collect_values_of_values(VALUE category, VALUE category_objects, VALUE categories) { VALUE ary = rb_ary_new(); - st_foreach(rb_hash_tbl(category_objects), collect_values, ary); + rb_hash_foreach(category_objects, collect_values, ary); rb_hash_aset(categories, category, ary); return ST_CONTINUE; } diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index 38e650e1a37d13..69758aed7a53fb 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -398,7 +398,7 @@ ossl_debug_set(VALUE self, VALUE val) } /* - * call-seq + * call-seq: * OpenSSL.fips_mode -> true | false */ static VALUE diff --git a/ext/openssl/ossl.h b/ext/openssl/ossl.h index 5a15839cb45bcc..39699bd5e6b758 100644 --- a/ext/openssl/ossl.h +++ b/ext/openssl/ossl.h @@ -13,8 +13,8 @@ #include RUBY_EXTCONF_H #include -#include #include +#include #include #include #include diff --git a/ext/pathname/lib/pathname.rb b/ext/pathname/lib/pathname.rb index 3baf818f083ee6..7fb923c6ae2509 100644 --- a/ext/pathname/lib/pathname.rb +++ b/ext/pathname/lib/pathname.rb @@ -503,6 +503,7 @@ def each_child(with_directory=true, &b) # ArgumentError is raised when it cannot find a relative path. # def relative_path_from(base_directory) + base_directory = Pathname.new(base_directory) unless Pathname === base_directory dest_directory = self.cleanpath.to_s base_directory = base_directory.cleanpath.to_s dest_prefix = dest_directory diff --git a/ext/psych/lib/psych.rb b/ext/psych/lib/psych.rb index 6ca5724d2dc115..5b24ebf0917b5d 100644 --- a/ext/psych/lib/psych.rb +++ b/ext/psych/lib/psych.rb @@ -294,10 +294,10 @@ def self.load yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: false, # * Hash # # Recursive data structures are not allowed by default. Arbitrary classes - # can be allowed by adding those classes to the +whitelist_classes+ keyword argument. They are + # can be allowed by adding those classes to the +permitted_classes+ keyword argument. They are # additive. For example, to allow Date deserialization: # - # Psych.safe_load(yaml, whitelist_classes: [Date]) + # Psych.safe_load(yaml, permitted_classes: [Date]) # # Now the Date class can be loaded in addition to the classes listed above. # @@ -311,7 +311,7 @@ def self.load yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: false, # Psych.safe_load yaml, aliases: true # => loads the aliases # # A Psych::DisallowedClass exception will be raised if the yaml contains a - # class that isn't in the whitelist. + # class that isn't in the +permitted_classes+ list. # # A Psych::BadAlias exception will be raised if the yaml contains aliases # but the +aliases+ keyword argument is set to false. @@ -325,15 +325,15 @@ def self.load yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: false, # Psych.safe_load("---\n foo: bar") # => {"foo"=>"bar"} # Psych.safe_load("---\n foo: bar", symbolize_names: true) # => {:foo=>"bar"} # - def self.safe_load yaml, legacy_whitelist_classes = NOT_GIVEN, legacy_whitelist_symbols = NOT_GIVEN, legacy_aliases = NOT_GIVEN, legacy_filename = NOT_GIVEN, whitelist_classes: [], whitelist_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false - if legacy_whitelist_classes != NOT_GIVEN - warn 'warning: Passing whitelist_classes with the 2nd argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, whitelist_classes: ...) instead.' - whitelist_classes = legacy_whitelist_classes + def self.safe_load yaml, legacy_permitted_classes = NOT_GIVEN, legacy_permitted_symbols = NOT_GIVEN, legacy_aliases = NOT_GIVEN, legacy_filename = NOT_GIVEN, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false + if legacy_permitted_classes != NOT_GIVEN + warn 'warning: Passing permitted_classes with the 2nd argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, permitted_classes: ...) instead.' + permitted_classes = legacy_permitted_classes end - if legacy_whitelist_symbols != NOT_GIVEN - warn 'warning: Passing whitelist_symbols with the 3rd argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, whitelist_symbols: ...) instead.' - whitelist_symbols = legacy_whitelist_symbols + if legacy_permitted_symbols != NOT_GIVEN + warn 'warning: Passing permitted_symbols with the 3rd argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, permitted_symbols: ...) instead.' + permitted_symbols = legacy_permitted_symbols end if legacy_aliases != NOT_GIVEN @@ -349,8 +349,8 @@ def self.safe_load yaml, legacy_whitelist_classes = NOT_GIVEN, legacy_whitelist_ result = parse(yaml, filename: filename) return fallback unless result - class_loader = ClassLoader::Restricted.new(whitelist_classes.map(&:to_s), - whitelist_symbols.map(&:to_s)) + class_loader = ClassLoader::Restricted.new(permitted_classes.map(&:to_s), + permitted_symbols.map(&:to_s)) scanner = ScalarScanner.new class_loader visitor = if aliases Visitors::ToRuby.new scanner, class_loader diff --git a/ext/pty/pty.c b/ext/pty/pty.c index fc2e529408002d..10eb0ac3121ff5 100644 --- a/ext/pty/pty.c +++ b/ext/pty/pty.c @@ -680,7 +680,7 @@ static VALUE cPTY; /* * Document-class: PTY * - * Creates and managed pseudo terminals (PTYs). See also + * Creates and manages pseudo terminals (PTYs). See also * http://en.wikipedia.org/wiki/Pseudo_terminal * * PTY allows you to allocate new terminals using ::open or ::spawn a new diff --git a/ext/socket/init.c b/ext/socket/init.c index c26ab135aba5ae..44d150697357eb 100644 --- a/ext/socket/init.c +++ b/ext/socket/init.c @@ -435,7 +435,7 @@ rsock_socket0(int domain, int type, int proto) static int cloexec_state = -1; /* <0: unknown, 0: ignored, >0: working */ if (cloexec_state > 0) { /* common path, if SOCK_CLOEXEC is defined */ - ret = socket(domain, type|SOCK_CLOEXEC, proto); + ret = socket(domain, type|SOCK_CLOEXEC|RSOCK_NONBLOCK_DEFAULT, proto); if (ret >= 0) { if (ret <= 2) goto fix_cloexec; @@ -443,7 +443,7 @@ rsock_socket0(int domain, int type, int proto) } } else if (cloexec_state < 0) { /* usually runs once only for detection */ - ret = socket(domain, type|SOCK_CLOEXEC, proto); + ret = socket(domain, type|SOCK_CLOEXEC|RSOCK_NONBLOCK_DEFAULT, proto); if (ret >= 0) { cloexec_state = rsock_detect_cloexec(ret); if (cloexec_state == 0 || ret <= 2) @@ -466,6 +466,9 @@ rsock_socket0(int domain, int type, int proto) return -1; fix_cloexec: rb_maygvl_fd_fix_cloexec(ret); + if (RSOCK_NONBLOCK_DEFAULT) { + rsock_make_fd_nonblock(ret); + } update_max_fd: rb_update_max_fd(ret); @@ -480,6 +483,9 @@ rsock_socket0(int domain, int type, int proto) if (ret == -1) return -1; rb_fd_fix_cloexec(ret); + if (RSOCK_NONBLOCK_DEFAULT) { + rsock_make_fd_nonblock(ret); + } return ret; } @@ -508,11 +514,30 @@ wait_connectable(int fd) int sockerr, revents; socklen_t sockerrlen; - /* only to clear pending error */ sockerrlen = (socklen_t)sizeof(sockerr); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (void *)&sockerr, &sockerrlen) < 0) return -1; + /* necessary for non-blocking sockets (at least ECONNREFUSED) */ + switch (sockerr) { + case 0: + break; +#ifdef EALREADY + case EALREADY: +#endif +#ifdef EISCONN + case EISCONN: +#endif +#ifdef ECONNREFUSED + case ECONNREFUSED: +#endif +#ifdef EHOSTUNREACH + case EHOSTUNREACH: +#endif + errno = sockerr; + return -1; + } + /* * Stevens book says, successful finish turn on RB_WAITFD_OUT and * failure finish turn on both RB_WAITFD_IN and RB_WAITFD_OUT. @@ -613,8 +638,8 @@ rsock_connect(int fd, const struct sockaddr *sockaddr, int len, int socks) return status; } -static void -make_fd_nonblock(int fd) +void +rsock_make_fd_nonblock(int fd) { int flags; #ifdef F_GETFL @@ -640,6 +665,9 @@ cloexec_accept(int socket, struct sockaddr *address, socklen_t *address_len, #ifdef HAVE_ACCEPT4 static int try_accept4 = 1; #endif + if (RSOCK_NONBLOCK_DEFAULT) { + nonblock = 1; + } if (address_len) len0 = *address_len; #ifdef HAVE_ACCEPT4 if (try_accept4) { @@ -659,7 +687,7 @@ cloexec_accept(int socket, struct sockaddr *address, socklen_t *address_len, rb_maygvl_fd_fix_cloexec(ret); #ifndef SOCK_NONBLOCK if (nonblock) { - make_fd_nonblock(ret); + rsock_make_fd_nonblock(ret); } #endif if (address_len && len0 < *address_len) *address_len = len0; @@ -676,7 +704,7 @@ cloexec_accept(int socket, struct sockaddr *address, socklen_t *address_len, if (address_len && len0 < *address_len) *address_len = len0; rb_maygvl_fd_fix_cloexec(ret); if (nonblock) { - make_fd_nonblock(ret); + rsock_make_fd_nonblock(ret); } return ret; } diff --git a/ext/socket/rubysocket.h b/ext/socket/rubysocket.h index 922df9b87b7712..0ce77a5f6ef3bb 100644 --- a/ext/socket/rubysocket.h +++ b/ext/socket/rubysocket.h @@ -26,7 +26,13 @@ # if defined(_MSC_VER) # undef HAVE_TYPE_STRUCT_SOCKADDR_DL # endif +/* + * FIXME: failures if we make nonblocking the default + * [ruby-core:89973] [ruby-core:89976] [ruby-core:89977] [Bug #14968] + */ +# define RSOCK_NONBLOCK_DEFAULT (0) #else +# define RSOCK_NONBLOCK_DEFAULT (0) # include # include # ifdef HAVE_NETINET_IN_SYSTM_H @@ -408,7 +414,7 @@ NORETURN(void rsock_sys_fail_raddrinfo_or_sockaddr(const char *, VALUE addr, VAL * all cases. For some syscalls (e.g. accept/accept4), blocking on the * syscall instead of relying on select/poll allows the kernel to use * "wake-one" behavior and avoid the thundering herd problem. - * This is likely safe on all other *nix-like systems, so this whitelist + * This is likely safe on all other *nix-like systems, so this safe list * can be expanded by interested parties. */ #if defined(__linux__) @@ -433,6 +439,8 @@ static inline void rsock_maybe_wait_fd(int fd) { } VALUE rsock_read_nonblock(VALUE sock, VALUE length, VALUE buf, VALUE ex); VALUE rsock_write_nonblock(VALUE sock, VALUE buf, VALUE ex); +void rsock_make_fd_nonblock(int fd); + #if !defined HAVE_INET_NTOP && ! defined _WIN32 const char *inet_ntop(int, const void *, char *, size_t); #elif defined __MINGW32__ diff --git a/ext/socket/socket.c b/ext/socket/socket.c index ead3592d8aa326..0059595e1b9bcd 100644 --- a/ext/socket/socket.c +++ b/ext/socket/socket.c @@ -175,16 +175,17 @@ rsock_socketpair0(int domain, int type, int protocol, int sv[2]) { int ret; static int cloexec_state = -1; /* <0: unknown, 0: ignored, >0: working */ + static const int default_flags = SOCK_CLOEXEC|RSOCK_NONBLOCK_DEFAULT; if (cloexec_state > 0) { /* common path, if SOCK_CLOEXEC is defined */ - ret = socketpair(domain, type|SOCK_CLOEXEC, protocol, sv); + ret = socketpair(domain, type|default_flags, protocol, sv); if (ret == 0 && (sv[0] <= 2 || sv[1] <= 2)) { goto fix_cloexec; /* highly unlikely */ } goto update_max_fd; } else if (cloexec_state < 0) { /* usually runs once only for detection */ - ret = socketpair(domain, type|SOCK_CLOEXEC, protocol, sv); + ret = socketpair(domain, type|default_flags, protocol, sv); if (ret == 0) { cloexec_state = rsock_detect_cloexec(sv[0]); if ((cloexec_state == 0) || (sv[0] <= 2 || sv[1] <= 2)) @@ -213,6 +214,10 @@ rsock_socketpair0(int domain, int type, int protocol, int sv[2]) fix_cloexec: rb_maygvl_fd_fix_cloexec(sv[0]); rb_maygvl_fd_fix_cloexec(sv[1]); + if (RSOCK_NONBLOCK_DEFAULT) { + rsock_make_fd_nonblock(sv[0]); + rsock_make_fd_nonblock(sv[1]); + } update_max_fd: rb_update_max_fd(sv[0]); @@ -231,6 +236,10 @@ rsock_socketpair0(int domain, int type, int protocol, int sv[2]) rb_fd_fix_cloexec(sv[0]); rb_fd_fix_cloexec(sv[1]); + if (RSOCK_NONBLOCK_DEFAULT) { + rsock_make_fd_nonblock(sv[0]); + rsock_make_fd_nonblock(sv[1]); + } return ret; } #endif /* !SOCK_CLOEXEC */ diff --git a/ext/stringio/stringio.gemspec b/ext/stringio/stringio.gemspec index 510f071aea2248..df1d468f93e840 100644 --- a/ext/stringio/stringio.gemspec +++ b/ext/stringio/stringio.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.name = "stringio".freeze - s.version = "0.0.1" + s.version = "0.0.2" s.required_rubygems_version = Gem::Requirement.new(">= 2.6".freeze) s.require_paths = ["lib".freeze] @@ -19,8 +19,9 @@ Gem::Specification.new do |s| s.required_ruby_version = Gem::Requirement.new(">= 2.2".freeze) s.rubygems_version = "2.6.11".freeze s.summary = "Pseudo IO on String".freeze - s.cert_chain = %w[certs/nobu.pem] - s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/ + + # s.cert_chain = %w[certs/nobu.pem] + # s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/ s.add_development_dependency 'rake-compiler' end diff --git a/ext/win32ole/win32ole_method.c b/ext/win32ole/win32ole_method.c index 456a45cfc42c44..ffa93246570471 100644 --- a/ext/win32ole/win32ole_method.c +++ b/ext/win32ole/win32ole_method.c @@ -283,7 +283,7 @@ folemethod_initialize(VALUE self, VALUE oletype, VALUE method) } /* - * call-seq + * call-seq: * WIN32OLE_METHOD#name * * Returns the name of the method. diff --git a/gc.c b/gc.c index dc8f5808f2d691..4de27892e8c323 100644 --- a/gc.c +++ b/gc.c @@ -35,6 +35,7 @@ #include #include "ruby_assert.h" #include "debug_counter.h" +#include "transient_heap.h" #include "mjit.h" #undef rb_data_object_wrap @@ -60,16 +61,6 @@ # endif #endif -#if /* is ASAN enabled? */ \ - __has_feature(address_sanitizer) /* Clang */ || \ - defined(__SANITIZE_ADDRESS__) /* GCC 4.8.x */ - #define ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS \ - __attribute__((no_address_safety_analysis)) \ - __attribute__((noinline)) -#else - #define ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS -#endif - #ifdef HAVE_SYS_TIME_H #include #endif @@ -658,10 +649,8 @@ typedef struct rb_objspace { } rb_objspace_t; -#ifndef HEAP_PAGE_ALIGN_LOG /* default tiny heap size: 16KB */ #define HEAP_PAGE_ALIGN_LOG 14 -#endif #define CEILDIV(i, mod) (((i) + (mod) - 1)/(mod)) enum { HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG), @@ -845,8 +834,6 @@ static void rb_objspace_call_finalizer(rb_objspace_t *objspace); static VALUE define_final0(VALUE obj, VALUE block); static void negative_size_allocation_error(const char *); -static void *aligned_malloc(size_t, size_t); -static void aligned_free(void *); static void init_mark_stack(mark_stack_t *stack); @@ -875,7 +862,7 @@ static void gc_sweep_continue(rb_objspace_t *objspace, rb_heap_t *heap); static inline void gc_mark(rb_objspace_t *objspace, VALUE ptr); static void gc_mark_ptr(rb_objspace_t *objspace, VALUE ptr); -static void gc_mark_maybe(rb_objspace_t *objspace, VALUE ptr); +NO_SANITIZE("memory", static void gc_mark_maybe(rb_objspace_t *objspace, VALUE ptr)); static void gc_mark_children(rb_objspace_t *objspace, VALUE ptr); static int gc_mark_stacked_objects_incremental(rb_objspace_t *, size_t count); @@ -883,7 +870,7 @@ static int gc_mark_stacked_objects_all(rb_objspace_t *); static void gc_grey(rb_objspace_t *objspace, VALUE ptr); static inline int gc_mark_set(rb_objspace_t *objspace, VALUE obj); -static inline int is_pointer_to_heap(rb_objspace_t *objspace, void *ptr); +NO_SANITIZE("memory", static inline int is_pointer_to_heap(rb_objspace_t *objspace, void *ptr)); static void push_mark_stack(mark_stack_t *, VALUE); static int pop_mark_stack(mark_stack_t *, VALUE *); @@ -1190,6 +1177,7 @@ RVALUE_PAGE_OLD_UNCOLLECTIBLE_SET(rb_objspace_t *objspace, struct heap_page *pag { MARK_IN_BITMAP(&page->uncollectible_bits[0], obj); objspace->rgengc.old_objects++; + rb_transient_heap_promote(obj); #if RGENGC_PROFILE >= 2 objspace->profile.total_promoted_count++; @@ -1444,6 +1432,7 @@ heap_page_add_freeobj(rb_objspace_t *objspace, struct heap_page *page, VALUE obj if (RGENGC_CHECK_MODE && !is_pointer_to_heap(objspace, p)) { rb_bug("heap_page_add_freeobj: %p is not rvalue.", (void *)p); } + poison_object(obj); gc_report(3, objspace, "heap_page_add_freeobj: add %p to freelist\n", (void *)obj); } @@ -1486,7 +1475,7 @@ heap_page_free(rb_objspace_t *objspace, struct heap_page *page) { heap_allocated_pages--; objspace->profile.total_freed_pages++; - aligned_free(GET_PAGE_BODY(page->start)); + rb_aligned_free(GET_PAGE_BODY(page->start)); free(page); } @@ -1524,7 +1513,7 @@ heap_page_allocate(rb_objspace_t *objspace) int limit = HEAP_PAGE_OBJ_LIMIT; /* assign heap_page body (contains heap_page_header and RVALUEs) */ - page_body = (struct heap_page_body *)aligned_malloc(HEAP_PAGE_ALIGN, HEAP_PAGE_SIZE); + page_body = (struct heap_page_body *)rb_aligned_malloc(HEAP_PAGE_ALIGN, HEAP_PAGE_SIZE); if (page_body == 0) { rb_memerror(); } @@ -1532,7 +1521,7 @@ heap_page_allocate(rb_objspace_t *objspace) /* assign heap_page entry */ page = (struct heap_page *)calloc(1, sizeof(struct heap_page)); if (page == 0) { - aligned_free(page_body); + rb_aligned_free(page_body); rb_memerror(); } @@ -1768,6 +1757,7 @@ heap_get_freeobj_from_next_freepage(rb_objspace_t *objspace, rb_heap_t *heap) p = page->freelist; page->freelist = NULL; page->free_slots = 0; + unpoison_object((VALUE)p, true); return p; } @@ -1778,6 +1768,7 @@ heap_get_freeobj_head(rb_objspace_t *objspace, rb_heap_t *heap) if (LIKELY(p != NULL)) { heap->freelist = p->as.free.next; } + unpoison_object((VALUE)p, true); return (VALUE)p; } @@ -1788,6 +1779,7 @@ heap_get_freeobj(rb_objspace_t *objspace, rb_heap_t *heap) while (1) { if (LIKELY(p != NULL)) { + unpoison_object((VALUE)p, true); heap->freelist = p->as.free.next; return (VALUE)p; } @@ -1954,10 +1946,10 @@ newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, int wb_protect #if GC_DEBUG_STRESS_TO_CLASS if (UNLIKELY(stress_to_class)) { - long i, cnt = RARRAY_LEN(stress_to_class); - for (i = 0; i < cnt; ++i) { + long i, cnt = RARRAY_LEN(stress_to_class); + for (i = 0; i < cnt; ++i) { if (klass == RARRAY_AREF(stress_to_class, i)) rb_memerror(); - } + } } #endif if (!(during_gc || @@ -2215,15 +2207,18 @@ obj_free(rb_objspace_t *objspace, VALUE obj) switch (BUILTIN_TYPE(obj)) { case T_OBJECT: - if (!(RANY(obj)->as.basic.flags & ROBJECT_EMBED) && - RANY(obj)->as.object.as.heap.ivptr) { - xfree(RANY(obj)->as.object.as.heap.ivptr); - RB_DEBUG_COUNTER_INC(obj_obj_ptr); - } - else { + if ((RANY(obj)->as.basic.flags & ROBJECT_EMBED) || + RANY(obj)->as.object.as.heap.ivptr == NULL) { RB_DEBUG_COUNTER_INC(obj_obj_embed); - } - break; + } + else if (ROBJ_TRANSIENT_P(obj)) { + RB_DEBUG_COUNTER_INC(obj_obj_transient); + } + else { + xfree(RANY(obj)->as.object.as.heap.ivptr); + RB_DEBUG_COUNTER_INC(obj_obj_ptr); + } + break; case T_MODULE: case T_CLASS: mjit_remove_class_serial(RCLASS_SERIAL(obj)); @@ -2259,26 +2254,46 @@ obj_free(rb_objspace_t *objspace, VALUE obj) rb_str_free(obj); break; case T_ARRAY: - rb_ary_free(obj); + rb_ary_free(obj); break; case T_HASH: - if (RANY(obj)->as.hash.ntbl) { #if USE_DEBUG_COUNTER - if (RHASH_SIZE(obj) >= 8) { - RB_DEBUG_COUNTER_INC(obj_hash_ge8); - } - if (RHASH_SIZE(obj) >= 4) { - RB_DEBUG_COUNTER_INC(obj_hash_ge4); - } - else { - RB_DEBUG_COUNTER_INC(obj_hash_under4); - } -#endif - st_free_table(RANY(obj)->as.hash.ntbl); - } + if (RHASH_SIZE(obj) >= 8) { + RB_DEBUG_COUNTER_INC(obj_hash_ge8); + } + else if (RHASH_SIZE(obj) >= 4) { + RB_DEBUG_COUNTER_INC(obj_hash_ge4); + } + else if (RHASH_SIZE(obj) >= 1) { + RB_DEBUG_COUNTER_INC(obj_hash_under4); + } else { RB_DEBUG_COUNTER_INC(obj_hash_empty); } + + if (RHASH_ARRAY_P(obj)) { + RB_DEBUG_COUNTER_INC(obj_hash_array); + } + else { + RB_DEBUG_COUNTER_INC(obj_hash_st); + } +#endif + if (/* RHASH_ARRAY_P(obj) */ !FL_TEST_RAW(obj, RHASH_ST_TABLE_FLAG)) { + li_table *tab = RHASH(obj)->as.li; + + if (tab) { + if (RHASH_TRANSIENT_P(obj)) { + RB_DEBUG_COUNTER_INC(obj_hash_transient); + } + else { + ruby_xfree(tab); + } + } + } + else { + GC_ASSERT(RHASH_TABLE_P(obj)); + st_free_table(RHASH(obj)->as.st); + } break; case T_REGEXP: if (RANY(obj)->as.regexp.ptr) { @@ -2380,14 +2395,17 @@ obj_free(rb_objspace_t *objspace, VALUE obj) break; case T_STRUCT: - if ((RBASIC(obj)->flags & RSTRUCT_EMBED_LEN_MASK) == 0 && - RANY(obj)->as.rstruct.as.heap.ptr) { - xfree((void *)RANY(obj)->as.rstruct.as.heap.ptr); - RB_DEBUG_COUNTER_INC(obj_struct_ptr); - } - else { + if ((RBASIC(obj)->flags & RSTRUCT_EMBED_LEN_MASK) || + RANY(obj)->as.rstruct.as.heap.ptr == NULL) { RB_DEBUG_COUNTER_INC(obj_struct_embed); } + else if (RSTRUCT_TRANSIENT_P(obj)) { + RB_DEBUG_COUNTER_INC(obj_struct_transient); + } + else { + xfree((void *)RANY(obj)->as.rstruct.as.heap.ptr); + RB_DEBUG_COUNTER_INC(obj_struct_ptr); + } break; case T_SYMBOL: @@ -2596,8 +2614,11 @@ static int internal_object_p(VALUE obj) { RVALUE *p = (RVALUE *)obj; + void *ptr = __asan_region_is_poisoned(p, SIZEOF_VALUE); + bool used_p = p->as.basic.flags; + unpoison_object(obj, false); - if (p->as.basic.flags) { + if (used_p) { switch (BUILTIN_TYPE(p)) { case T_NODE: UNEXPECTED_NODE(internal_object_p); @@ -2618,6 +2639,9 @@ internal_object_p(VALUE obj) return 0; } } + if (ptr || ! used_p) { + poison_object(obj); + } return 1; } @@ -2908,13 +2932,16 @@ static void finalize_list(rb_objspace_t *objspace, VALUE zombie) { while (zombie) { - VALUE next_zombie = RZOMBIE(zombie)->next; - struct heap_page *page = GET_HEAP_PAGE(zombie); + VALUE next_zombie; + struct heap_page *page; + unpoison_object(zombie, false); + next_zombie = RZOMBIE(zombie)->next; + page = GET_HEAP_PAGE(zombie); run_final(objspace, zombie); RZOMBIE(zombie)->basic.flags = 0; - heap_pages_final_slots--; + if (LIKELY(heap_pages_final_slots)) heap_pages_final_slots--; page->final_slots--; page->free_slots++; heap_page_add_freeobj(objspace, GET_HEAP_PAGE(zombie), zombie); @@ -3028,6 +3055,7 @@ rb_objspace_call_finalizer(rb_objspace_t *objspace) for (i = 0; i < heap_allocated_pages; i++) { p = heap_pages_sorted[i]->start; pend = p + heap_pages_sorted[i]->total_slots; while (p < pend) { + unpoison_object((VALUE)p, false); switch (BUILTIN_TYPE(p)) { case T_DATA: if (!DATA_PTR(p) || !RANY(p)->as.data.dfree) break; @@ -3051,6 +3079,7 @@ rb_objspace_call_finalizer(rb_objspace_t *objspace) } break; } + poison_object((VALUE)p); p++; } } @@ -3331,9 +3360,13 @@ obj_memsize_of(VALUE obj, int use_all_types) size += rb_ary_memsize(obj); break; case T_HASH: - if (RHASH(obj)->ntbl) { - size += st_memsize(RHASH(obj)->ntbl); + if (RHASH_ARRAY_P(obj)) { + size += sizeof(li_table); } + else { + VM_ASSERT(RHASH_ST_TABLE(obj) != NULL); + size += st_memsize(RHASH_ST_TABLE(obj)); + } break; case T_REGEXP: if (RREGEXP_PTR(obj)) { @@ -3485,7 +3518,7 @@ count_objects(int argc, VALUE *argv, VALUE os) hash = rb_hash_new(); } else if (!RHASH_EMPTY_P(hash)) { - st_foreach(RHASH_TBL_RAW(hash), set_zero, hash); + rb_hash_stlike_foreach(hash, set_zero, hash); } rb_hash_aset(hash, ID2SYM(rb_intern("TOTAL")), SIZET2NUM(total)); rb_hash_aset(hash, ID2SYM(rb_intern("FREE")), SIZET2NUM(freed)); @@ -3590,6 +3623,7 @@ gc_page_sweep(rb_objspace_t *objspace, rb_heap_t *heap, struct heap_page *sweep_ if (bitset) { p = offset + i * BITS_BITLENGTH; do { + unpoison_object((VALUE)p, false); if (bitset & 1) { switch (BUILTIN_TYPE(p)) { default: { /* majority case */ @@ -3608,6 +3642,7 @@ gc_page_sweep(rb_objspace_t *objspace, rb_heap_t *heap, struct heap_page *sweep_ heap_page_add_freeobj(objspace, sweep_page, (VALUE)p); gc_report(3, objspace, "page_sweep: %s is added to freelist\n", obj_info((VALUE)p)); freed_slots++; + poison_object((VALUE)p); } break; } @@ -4127,7 +4162,7 @@ ruby_stack_check(void) return stack_check(GET_EC(), STACKFRAME_FOR_CALL_CFUNC); } -ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS +ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS(static void mark_locations_array(rb_objspace_t *objspace, register const VALUE *x, register long n)); static void mark_locations_array(rb_objspace_t *objspace, register const VALUE *x, register long n) { @@ -4219,7 +4254,23 @@ mark_keyvalue(st_data_t key, st_data_t value, st_data_t data) } static void -mark_hash(rb_objspace_t *objspace, st_table *tbl) +mark_hash(rb_objspace_t *objspace, VALUE hash) +{ + rb_hash_stlike_foreach(hash, mark_keyvalue, (st_data_t)objspace); + + if (RHASH_ARRAY_P(hash)) { + if (objspace->mark_func_data == NULL && RHASH_TRANSIENT_P(hash)) { + rb_transient_heap_mark(hash, RHASH_ARRAY(hash)); + } + } + else { + VM_ASSERT(!RHASH_TRANSIENT_P(hash)); + } + gc_mark(objspace, RHASH(hash)->ifnone); +} + +static void +mark_st(rb_objspace_t *objspace, st_table *tbl) { if (!tbl) return; st_foreach(tbl, mark_keyvalue, (st_data_t)objspace); @@ -4228,7 +4279,7 @@ mark_hash(rb_objspace_t *objspace, st_table *tbl) void rb_mark_hash(st_table *tbl) { - mark_hash(&rb_objspace, tbl); + mark_st(&rb_objspace, tbl); } static void @@ -4250,7 +4301,8 @@ mark_method_entry(rb_objspace_t *objspace, const rb_method_entry_t *me) gc_mark(objspace, def->body.attr.location); break; case VM_METHOD_TYPE_BMETHOD: - gc_mark(objspace, def->body.proc); + gc_mark(objspace, def->body.bmethod.proc); + if (def->body.bmethod.hooks) rb_hook_list_mark(def->body.bmethod.hooks); break; case VM_METHOD_TYPE_ALIAS: gc_mark(objspace, (VALUE)def->body.alias.original_me); @@ -4381,10 +4433,17 @@ gc_mark_maybe(rb_objspace_t *objspace, VALUE obj) { (void)VALGRIND_MAKE_MEM_DEFINED(&obj, sizeof(obj)); if (is_pointer_to_heap(objspace, (void *)obj)) { - int type = BUILTIN_TYPE(obj); + int type; + void *ptr = __asan_region_is_poisoned((void *)obj, SIZEOF_VALUE); + + unpoison_object(obj, false); + type = BUILTIN_TYPE(obj); if (type != T_ZOMBIE && type != T_NONE) { gc_mark_ptr(objspace, obj); } + if (ptr) { + poison_object(obj); + } } } @@ -4515,6 +4574,7 @@ static void gc_mark_ptr(rb_objspace_t *objspace, VALUE obj) { if (LIKELY(objspace->mark_func_data == NULL)) { + if (RB_TYPE_P(obj, T_NONE)) rb_bug("..."); rgengc_check_relation(objspace, obj); if (!gc_mark_set(objspace, obj)) return; /* already marked */ gc_aging(objspace, obj); @@ -4671,21 +4731,28 @@ gc_mark_children(rb_objspace_t *objspace, VALUE obj) break; case T_ARRAY: - if (FL_TEST(obj, ELTS_SHARED)) { - gc_mark(objspace, any->as.array.as.heap.aux.shared); + if (FL_TEST(obj, ELTS_SHARED)) { + VALUE root = any->as.array.as.heap.aux.shared; + gc_mark(objspace, root); } else { long i, len = RARRAY_LEN(obj); - const VALUE *ptr = RARRAY_CONST_PTR(obj); + const VALUE *ptr = RARRAY_CONST_PTR_TRANSIENT(obj); for (i=0; i < len; i++) { - gc_mark(objspace, *ptr++); + gc_mark(objspace, ptr[i]); } - } + + if (objspace->mark_func_data == NULL) { + if (!FL_TEST_RAW(obj, RARRAY_EMBED_FLAG) && + RARRAY_TRANSIENT_P(obj)) { + rb_transient_heap_mark(obj, ptr); + } + } + } break; case T_HASH: - mark_hash(objspace, any->as.hash.ntbl); - gc_mark(objspace, any->as.hash.ifnone); + mark_hash(objspace, obj); break; case T_STRING: @@ -4708,10 +4775,18 @@ gc_mark_children(rb_objspace_t *objspace, VALUE obj) case T_OBJECT: { - uint32_t i, len = ROBJECT_NUMIV(obj); - VALUE *ptr = ROBJECT_IVPTR(obj); - for (i = 0; i < len; i++) { - gc_mark(objspace, *ptr++); + const VALUE * const ptr = ROBJECT_IVPTR(obj); + + if (ptr) { + uint32_t i, len = ROBJECT_NUMIV(obj); + for (i = 0; i < len; i++) { + gc_mark(objspace, ptr[i]); + } + + if (objspace->mark_func_data == NULL && + ROBJ_TRANSIENT_P(obj)) { + rb_transient_heap_mark(obj, ptr); + } } } break; @@ -4755,12 +4830,18 @@ gc_mark_children(rb_objspace_t *objspace, VALUE obj) case T_STRUCT: { - long len = RSTRUCT_LEN(obj); - const VALUE *ptr = RSTRUCT_CONST_PTR(obj); + long i; + const long len = RSTRUCT_LEN(obj); + const VALUE * const ptr = RSTRUCT_CONST_PTR(obj); - while (len--) { - gc_mark(objspace, *ptr++); - } + for (i=0; imark_func_data == NULL && + RSTRUCT_TRANSIENT_P(obj)) { + rb_transient_heap_mark(obj, ptr); + } } break; @@ -5455,6 +5536,13 @@ rb_gc_verify_internal_consistency(void) gc_verify_internal_consistency(Qnil); } +static VALUE +gc_verify_transient_heap_internal_consistency(VALUE dmy) +{ + rb_transient_heap_verify(); + return Qnil; +} + /* marks */ static void @@ -5671,6 +5759,8 @@ gc_marks_finish(rb_objspace_t *objspace) #endif } + rb_transient_heap_finish_marking(); + gc_event_hook(objspace, RUBY_INTERNAL_EVENT_GC_END_MARK, 0); return TRUE; @@ -6562,6 +6652,7 @@ gc_start(rb_objspace_t *objspace, int reason) objspace->profile.heap_used_at_gc_start = heap_allocated_pages; gc_prof_setup_new_record(objspace, reason); gc_reset_malloc_info(objspace); + rb_transient_heap_start_marking(do_full_mark); gc_event_hook(objspace, RUBY_INTERNAL_EVENT_GC_START, 0 /* TODO: pass minor/immediate flag? */); GC_ASSERT(during_gc); @@ -7812,8 +7903,8 @@ rb_memerror(void) EC_JUMP_TAG(ec, TAG_RAISE); } -static void * -aligned_malloc(size_t alignment, size_t size) +void * +rb_aligned_malloc(size_t alignment, size_t size) { void *res; @@ -7846,8 +7937,8 @@ aligned_malloc(size_t alignment, size_t size) return res; } -static void -aligned_free(void *ptr) +void +rb_aligned_free(void *ptr) { #if defined __MINGW32__ __mingw_aligned_free(ptr); @@ -9543,6 +9634,13 @@ rb_raw_obj_info(char *buff, const int buff_size, VALUE obj) { if (SPECIAL_CONST_P(obj)) { snprintf(buff, buff_size, "%s", obj_type_name(obj)); + + if (FIXNUM_P(obj)) { + snprintf(buff, buff_size, "%s %ld", buff, FIX2LONG(obj)); + } + else if (SYMBOL_P(obj)) { + snprintf(buff, buff_size, "%s %s", buff, rb_id2name(SYM2ID(obj))); + } } else { #define TF(c) ((c) != 0 ? "true" : "false") @@ -9551,13 +9649,21 @@ rb_raw_obj_info(char *buff, const int buff_size, VALUE obj) #if USE_RGENGC const int age = RVALUE_FLAGS_AGE(RBASIC(obj)->flags); - snprintf(buff, buff_size, "%p [%d%s%s%s%s] %s", - (void *)obj, age, - C(RVALUE_UNCOLLECTIBLE_BITMAP(obj), "L"), - C(RVALUE_MARK_BITMAP(obj), "M"), - C(RVALUE_MARKING_BITMAP(obj), "R"), - C(RVALUE_WB_UNPROTECTED_BITMAP(obj), "U"), - obj_type_name(obj)); + if (is_pointer_to_heap(&rb_objspace, (void *)obj)) { + snprintf(buff, buff_size, "%p [%d%s%s%s%s] %s", + (void *)obj, age, + C(RVALUE_UNCOLLECTIBLE_BITMAP(obj), "L"), + C(RVALUE_MARK_BITMAP(obj), "M"), + C(RVALUE_MARKING_BITMAP(obj), "R"), + C(RVALUE_WB_UNPROTECTED_BITMAP(obj), "U"), + obj_type_name(obj)); + } + else { + /* fake */ + snprintf(buff, buff_size, "%p [%dXXXX] %s", + (void *)obj, age, + obj_type_name(obj)); + } #else snprintf(buff, buff_size, "%p [%s] %s", (void *)obj, @@ -9587,15 +9693,37 @@ rb_raw_obj_info(char *buff, const int buff_size, VALUE obj) UNEXPECTED_NODE(rb_raw_obj_info); break; case T_ARRAY: - snprintf(buff, buff_size, "%s [%s%s] len: %d", buff, - C(ARY_EMBED_P(obj), "E"), - C(ARY_SHARED_P(obj), "S"), - (int)RARRAY_LEN(obj)); + if (FL_TEST(obj, ELTS_SHARED)) { + snprintf(buff, buff_size, "%s shared -> %s", buff, + rb_obj_info(RARRAY(obj)->as.heap.aux.shared)); + } + else if (FL_TEST(obj, RARRAY_EMBED_FLAG)) { + snprintf(buff, buff_size, "%s [%s%s] len: %d (embed)", buff, + C(ARY_EMBED_P(obj), "E"), + C(ARY_SHARED_P(obj), "S"), + (int)RARRAY_LEN(obj)); + } + else { + snprintf(buff, buff_size, "%s [%s%s%s] len: %d, capa:%d ptr:%p", buff, + C(ARY_EMBED_P(obj), "E"), + C(ARY_SHARED_P(obj), "S"), + C(RARRAY_TRANSIENT_P(obj), "T"), + (int)RARRAY_LEN(obj), + ARY_EMBED_P(obj) ? -1 : (int)RARRAY(obj)->as.heap.aux.capa, + (void *)RARRAY_CONST_PTR_TRANSIENT(obj)); + } break; case T_STRING: { snprintf(buff, buff_size, "%s %s", buff, RSTRING_PTR(obj)); break; } + case T_HASH: { + snprintf(buff, buff_size, "%s [%c%c] %d", buff, + RHASH_ARRAY_P(obj) ? 'A' : 'S', + RHASH_TRANSIENT_P(obj) ? 'T' : ' ', + (int)RHASH_SIZE(obj)); + break; + } case T_CLASS: { VALUE class_path = rb_class_path_cached(obj); if (!NIL_P(class_path)) { @@ -9603,6 +9731,19 @@ rb_raw_obj_info(char *buff, const int buff_size, VALUE obj) } break; } + case T_OBJECT: + { + uint32_t len = ROBJECT_NUMIV(obj); + + if (RANY(obj)->as.basic.flags & ROBJECT_EMBED) { + snprintf(buff, buff_size, "%s (embed) len:%d", buff, len); + } + else { + VALUE *ptr = ROBJECT_IVPTR(obj); + snprintf(buff, buff_size, "%s len:%d ptr:%p", buff, len, (void *)ptr); + } + } + break; case T_DATA: { const struct rb_block *block; const rb_iseq_t *iseq; @@ -9954,6 +10095,7 @@ Init_GC(void) /* internal methods */ rb_define_singleton_method(rb_mGC, "verify_internal_consistency", gc_verify_internal_consistency, 0); + rb_define_singleton_method(rb_mGC, "verify_transient_heap_internal_consistency", gc_verify_transient_heap_internal_consistency, 0); #if MALLOC_ALLOCATED_SIZE rb_define_singleton_method(rb_mGC, "malloc_allocated_size", gc_malloc_allocated_size, 0); rb_define_singleton_method(rb_mGC, "malloc_allocations", gc_malloc_allocations, 0); diff --git a/hash.c b/hash.c index 7a8f3f0faf3e08..ad9f49519263cf 100644 --- a/hash.c +++ b/hash.c @@ -20,7 +20,9 @@ #include "id.h" #include "symbol.h" #include "gc.h" - +#include "debug_counter.h" +#include "transient_heap.h" +#include "ruby_assert.h" #ifdef __APPLE__ # ifdef HAVE_CRT_EXTERNS_H # include @@ -29,6 +31,10 @@ # endif #endif +#ifndef HASH_DEBUG +#define HASH_DEBUG 0 +#endif + #define HAS_EXTRA_STATES(hash, klass) ( \ ((klass = has_extra_methods(rb_obj_class(hash))) != 0) || \ FL_TEST((hash), FL_EXIVAR|FL_TAINT|HASH_PROC_DEFAULT) || \ @@ -287,7 +293,9 @@ rb_ident_hash(st_data_t n) * many integers get interpreted as 2.0 or -2.0 [Bug #10761] */ if (FLONUM_P(n)) { - n ^= (st_data_t)rb_float_value(n); + union { double d; st_data_t i; } u; + u.d = rb_float_value(n); + n ^= u.i; } #endif @@ -299,6 +307,773 @@ static const struct st_hash_type identhash = { rb_ident_hash, }; +#define EQUAL(x,y) ((x) == (y) || (*objhash.compare)((x),(y)) == 0) +#define PTR_EQUAL(ptr, hash_val, key_) \ + ((ptr)->hash == (hash_val) && EQUAL((key_), (ptr)->key)) + +#define RESERVED_HASH_VAL ((st_hash_t) 0) +#define RESERVED_HASH_SUBSTITUTION_VAL (~(st_hash_t) 0) + +#define SET_KEY(entry, _key) (entry)->key = (_key) +#define SET_HASH(entry, _hash) (entry)->hash = (_hash) +#define SET_RECORD(entry, _value) (entry)->record = (_value) + +typedef st_data_t st_hash_t; + +static inline st_hash_t +do_hash(st_data_t key) +{ + st_hash_t hash = (st_hash_t)(*objhash.hash)(key); + return (RESERVED_HASH_VAL == hash) ? RESERVED_HASH_SUBSTITUTION_VAL : hash; +} + +static inline void +set_entry(li_table_entry *entry, st_data_t key, st_data_t val, st_hash_t hash) +{ + SET_HASH(entry, hash); + SET_KEY(entry, key); + SET_RECORD(entry, val); +} + +static inline void +clear_entry(li_table_entry* entry) +{ + SET_KEY(entry, Qundef); + SET_RECORD(entry, Qundef); + SET_HASH(entry, 0); +} + +static inline int +empty_entry(li_table_entry *entry) +{ + return entry->hash == 0; +} + +#define RHASH_ARRAY_SIZE(h) (HASH_ASSERT(RHASH_ARRAY_P(h)), \ + RHASH_ARRAY_SIZE_RAW(h)) + +#define RHASH_ARRAY_BOUND_RAW(h) \ + ((unsigned int)((RBASIC(h)->flags >> RHASH_ARRAY_BOUND_SHIFT) & \ + (RHASH_ARRAY_BOUND_MASK >> RHASH_ARRAY_BOUND_SHIFT))) + +#define RHASH_ARRAY_BOUND(h) (HASH_ASSERT(RHASH_ARRAY_P(h)), \ + RHASH_ARRAY_BOUND_RAW(h)) + +#define RHASH_ST_TABLE_SET(h, s) rb_hash_st_table_set(h, s) +#define RHASH_TYPE(hash) (RHASH_ARRAY_P(hash) ? &objhash : RHASH_ST_TABLE(hash)->type) +#define RHASH_ARRAY_REF(hash, n) (&RHASH_ARRAY(hash)->entries[n]) + +#if HASH_DEBUG +#define hash_verify(hash) hash_verify_(hash, __FILE__, __LINE__) +#define HASH_ASSERT(expr) RUBY_ASSERT_MESG_WHEN(1, expr, #expr) + +void +rb_hash_dump(VALUE hash) +{ + rb_obj_info_dump(hash); + + if (RHASH_ARRAY_P(hash)) { + unsigned i, n = 0, bound = RHASH_ARRAY_BOUND(hash); + + fprintf(stderr, " size:%u bound:%u\n", + RHASH_ARRAY_SIZE(hash), RHASH_ARRAY_BOUND(hash)); + + for (i=0; ihash; */ + k = cur_entry->key; + v = cur_entry->record; + fprintf(stderr, " %d key:%s val:%s\n", i, + rb_raw_obj_info(b1, 0x100, k), + rb_raw_obj_info(b2, 0x100, v)); + n++; + } + else { + fprintf(stderr, " %d empty\n", i); + } + } + } +} + +static VALUE +hash_verify_(VALUE hash, const char *file, int line) +{ + HASH_ASSERT(RB_TYPE_P(hash, T_HASH)); + + if (RHASH_ARRAY_P(hash)) { + unsigned i, n = 0, bound = RHASH_ARRAY_BOUND(hash); + + for (i=0; ihash; + k = cur_entry->key; + v = cur_entry->record; + HASH_ASSERT(h != 0); + HASH_ASSERT(k != Qundef); + HASH_ASSERT(v != Qundef); + n++; + } + } + if (n != RHASH_ARRAY_SIZE(hash)) { + rb_bug("n:%u, RHASH_ARRAY_SIZE:%u", n, RHASH_ARRAY_SIZE(hash)); + } + } + else { + HASH_ASSERT(RHASH_ST_TABLE(hash) != NULL); + HASH_ASSERT(RHASH_ARRAY_SIZE_RAW(hash) == 0); + HASH_ASSERT(RHASH_ARRAY_BOUND_RAW(hash) == 0); + } + + if (RHASH_TRANSIENT_P(hash)) { + volatile st_data_t MAYBE_UNUSED(key) = RHASH_ARRAY_REF(hash, 0)->key; /* read */ + HASH_ASSERT(RHASH_ARRAY(hash) != NULL); + HASH_ASSERT(rb_transient_heap_managed_ptr_p(RHASH_ARRAY(hash))); + } + return hash; +} + +#else +#define hash_verify(h) ((void)0) +#define HASH_ASSERT(e) ((void)0) +#endif + +static inline int +RHASH_TABLE_EMPTY(VALUE hash) +{ + if (RHASH(hash)->as.li == NULL) { + HASH_ASSERT(RHASH_ARRAY_P(hash)); + return TRUE; + } + else { + return FALSE; + } +} + +MJIT_FUNC_EXPORTED int +rb_hash_array_p(VALUE hash) +{ + if (FL_TEST_RAW((hash), RHASH_ST_TABLE_FLAG)) { + HASH_ASSERT(RHASH(hash)->as.st != NULL); + return FALSE; + } + else { + return TRUE; + } +} + +struct li_table * +rb_hash_array(VALUE hash) +{ + HASH_ASSERT(RHASH_ARRAY_P(hash)); + return RHASH(hash)->as.li; +} + +MJIT_FUNC_EXPORTED st_table * +rb_hash_st_table(VALUE hash) +{ + HASH_ASSERT(!RHASH_ARRAY_P(hash)); + return RHASH(hash)->as.st; +} + +void +rb_hash_st_table_set(VALUE hash, st_table *st) +{ + HASH_ASSERT(st != NULL); + FL_SET_RAW((hash), RHASH_ST_TABLE_FLAG); + RHASH(hash)->as.st = st; +} + +static void +hash_array_set(VALUE hash, struct li_table *li) +{ + HASH_ASSERT(RHASH_ARRAY_P(hash)); + HASH_ASSERT((RHASH_TRANSIENT_P(hash) && li == NULL) ? FALSE : TRUE); + RHASH(hash)->as.li = li; + hash_verify(hash); +} + +#define RHASH_ARRAY_SET(h, a) hash_array_set(h, a) + +#define RHASH_SET_ST_FLAG(h) FL_SET_RAW(h, RHASH_ST_TABLE_FLAG) +#define RHASH_UNSET_ST_FLAG(h) FL_UNSET_RAW(h, RHASH_ST_TABLE_FLAG) + +#define RHASH_ARRAY_BOUND_SET(h, n) do { \ + st_index_t tmp_n = (n); \ + HASH_ASSERT(RHASH_ARRAY_P(h)); \ + HASH_ASSERT(tmp_n <= RHASH_ARRAY_MAX_BOUND); \ + RBASIC(h)->flags &= ~RHASH_ARRAY_BOUND_MASK; \ + RBASIC(h)->flags |= (tmp_n) << RHASH_ARRAY_BOUND_SHIFT; \ +} while (0) + +#define RHASH_ARRAY_SIZE_SET(h, n) do { \ + st_index_t tmp_n = n; \ + HASH_ASSERT(RHASH_ARRAY_P(h)); \ + RBASIC(h)->flags &= ~RHASH_ARRAY_SIZE_MASK; \ + RBASIC(h)->flags |= (tmp_n) << RHASH_ARRAY_SIZE_SHIFT; \ +} while (0) + +#define HASH_ARRAY_SIZE_ADD(h, n) do { \ + HASH_ASSERT(RHASH_ARRAY_P(h)); \ + RHASH_ARRAY_SIZE_SET((h), RHASH_ARRAY_SIZE(h)+(n)); \ + hash_verify(h); \ +} while (0) + +#define RHASH_ARRAY_SIZE_INC(h) HASH_ARRAY_SIZE_ADD(h, 1) +#define RHASH_ARRAY_SIZE_DEC(h) do { \ + HASH_ASSERT(RHASH_ARRAY_P(h)); \ + RHASH_ARRAY_SIZE_SET((h), RHASH_ARRAY_SIZE(h) - 1); \ + hash_verify(h); \ +} while (0) + +#define RHASH_CLEAR_BITS(h) do { \ + RBASIC(h)->flags &= ~RHASH_ARRAY_SIZE_MASK; \ + RBASIC(h)->flags &= ~RHASH_ARRAY_BOUND_MASK; \ +} while (0) + + +static li_table* +linear_init_table(VALUE hash) +{ + li_table *tab; + + tab = (li_table*)rb_transient_heap_alloc(hash, sizeof(li_table)); + if (tab != NULL) { + RHASH_SET_TRANSIENT_FLAG(hash); + } + else { + RHASH_UNSET_TRANSIENT_FLAG(hash); + tab = (li_table*)ruby_xmalloc(sizeof(li_table)); + } + + RHASH_ARRAY_SIZE_SET(hash, 0); + RHASH_ARRAY_BOUND_SET(hash, 0); + RHASH_ARRAY_SET(hash, tab); + return tab; +} + +static unsigned +find_entry(VALUE hash, st_hash_t hash_value, st_data_t key) +{ + unsigned i, bound = RHASH_ARRAY_BOUND(hash); + + /* if table is NULL, then bound also should be 0 */ + + for (i = 0; i < bound; i++) { + if (PTR_EQUAL(RHASH_ARRAY_REF(hash, i), hash_value, key)) { + return i; + } + } + return RHASH_ARRAY_MAX_BOUND; +} + +static inline void +linear_free_and_clear_table(VALUE hash) +{ + li_table *tab = RHASH_ARRAY(hash); + + if (tab) { + if (RHASH_TRANSIENT_P(hash)) { + RHASH_UNSET_TRANSIENT_FLAG(hash); + } + else { + ruby_xfree(RHASH_ARRAY(hash)); + } + RHASH_CLEAR_BITS(hash); + RHASH_ARRAY_SET(hash, NULL); + } + HASH_ASSERT(RHASH_ARRAY_SIZE(hash) == 0); + HASH_ASSERT(RHASH_ARRAY_BOUND(hash) == 0); + HASH_ASSERT(RHASH_TRANSIENT_P(hash) == 0); +} + +void st_add_direct_with_hash(st_table *tab, st_data_t key, st_data_t value, st_hash_t hash); /* st.c */ + +static void +linear_try_convert_table(VALUE hash) +{ + st_table *new_tab; + li_table_entry *entry; + const unsigned size = RHASH_ARRAY_SIZE(hash); + st_index_t i; + + if (!RHASH_ARRAY_P(hash) || size < RHASH_ARRAY_MAX_SIZE) { + return; + } + + new_tab = st_init_table_with_size(&objhash, size * 2); + + for (i = 0; i < RHASH_ARRAY_MAX_BOUND; i++) { + entry = RHASH_ARRAY_REF(hash, i); + HASH_ASSERT(entry->hash != 0); + + st_add_direct_with_hash(new_tab, entry->key, entry->record, entry->hash); + } + linear_free_and_clear_table(hash); + RHASH_ST_TABLE_SET(hash, new_tab); + return; +} + +static st_table * +linear_force_convert_table(VALUE hash, const char *file, int line) +{ + st_table *new_tab; + + if (RHASH_TABLE_P(hash)) { + return RHASH_ST_TABLE(hash); + } + + if (RHASH_ARRAY(hash)) { + li_table_entry *entry; + unsigned i, bound = RHASH_ARRAY_BOUND(hash); + +#if RHASH_CONVERT_TABLE_DEBUG + rb_obj_info_dump(hash); + fprintf(stderr, "force_convert: %s:%d\n", file, line); + RB_DEBUG_COUNTER_INC(obj_hash_force_convert); +#endif + + new_tab = st_init_table_with_size(&objhash, RHASH_ARRAY_SIZE(hash)); + + for (i = 0; i < bound; i++) { + entry = RHASH_ARRAY_REF(hash, i); + if (empty_entry(entry)) continue; + + st_add_direct_with_hash(new_tab, entry->key, entry->record, entry->hash); + } + linear_free_and_clear_table(hash); + } + else { + new_tab = st_init_table(&objhash); + } + RHASH_ST_TABLE_SET(hash, new_tab); + + return new_tab; +} + +static li_table * +hash_ltbl(VALUE hash) +{ + if (RHASH_TABLE_EMPTY(hash)) { + linear_init_table(hash); + } + return RHASH_ARRAY(hash); +} + +static int +linear_compact_table(VALUE hash) +{ + const unsigned bound = RHASH_ARRAY_BOUND(hash); + const unsigned size = RHASH_ARRAY_SIZE(hash); + + if (size == bound) { + return size; + } + else { + unsigned i, j=0; + li_table_entry *entries = RHASH_ARRAY_REF(hash, 0); + + for (i=0; i= RHASH_ARRAY_MAX_SIZE) { + return 1; + } + else { + if (UNLIKELY(bin >= RHASH_ARRAY_MAX_BOUND)) { + bin = linear_compact_table(hash); + hash_ltbl(hash); + } + HASH_ASSERT(bin < RHASH_ARRAY_MAX_BOUND); + + entry = &tab->entries[bin]; + set_entry(entry, key, val, hash_value); + RHASH_ARRAY_BOUND_SET(hash, bin+1); + RHASH_ARRAY_SIZE_INC(hash); + return 0; + } +} + +static int +linear_foreach(VALUE hash, int (*func)(ANYARGS), st_data_t arg) +{ + if (RHASH_ARRAY_SIZE(hash) > 0) { + unsigned i, bound = RHASH_ARRAY_BOUND(hash); + + for (i = 0; i < bound; i++) { + enum st_retval retval; + li_table_entry *cur_entry = RHASH_ARRAY_REF(hash, i); + if (empty_entry(cur_entry)) continue; + retval = (*func)(cur_entry->key, cur_entry->record, arg, 0); + /* cur_entry is not valid after that */ + + switch (retval) { + case ST_CONTINUE: + break; + case ST_CHECK: + case ST_STOP: + return 0; + case ST_DELETE: + clear_entry(RHASH_ARRAY_REF(hash, i)); + RHASH_ARRAY_SIZE_DEC(hash); + break; + } + } + } + return 0; +} + +static int +linear_foreach_check(VALUE hash, int (*func)(ANYARGS), st_data_t arg, + st_data_t never) +{ + if (RHASH_ARRAY_SIZE(hash) > 0) { + unsigned i, ret = 0, bound = RHASH_ARRAY_BOUND(hash); + enum st_retval retval; + li_table_entry *cur_entry; + st_data_t key; + st_hash_t hash_value; + + for (i = 0; i < bound; i++) { + cur_entry = RHASH_ARRAY_REF(hash, i); + if (empty_entry(cur_entry)) + continue; + key = cur_entry->key; + hash_value = cur_entry->hash; + + retval = (*func)(key, cur_entry->record, arg, 0); + hash_verify(hash); + + cur_entry = RHASH_ARRAY_REF(hash, i); + + switch (retval) { + case ST_CHECK: { + if (cur_entry->key == never && cur_entry->hash == 0) break; + ret = find_entry(hash, hash_value, key); + if (ret == RHASH_ARRAY_MAX_BOUND) { + retval = (*func)(0, 0, arg, 1); + return 2; + } + } + case ST_CONTINUE: + break; + case ST_STOP: + return 0; + case ST_DELETE: { + if (!empty_entry(cur_entry)) { + clear_entry(cur_entry); + RHASH_ARRAY_SIZE_DEC(hash); + } + break; + } + } + } + } + return 0; +} + +static int +linear_update(VALUE hash, st_data_t key, + st_update_callback_func *func, st_data_t arg) +{ + int retval, existing; + unsigned bin; + st_data_t value = 0, old_key; + st_hash_t hash_value = do_hash(key); + + if (RHASH_ARRAY_SIZE(hash) > 0) { + bin = find_entry(hash, hash_value, key); + existing = (bin != RHASH_ARRAY_MAX_BOUND) ? TRUE : FALSE; + } + else { + hash_ltbl(hash); /* allocate ltbl if needed */ + existing = FALSE; + } + + if (existing) { + li_table_entry *entry = RHASH_ARRAY_REF(hash, bin); + key = entry->key; + value = entry->record; + } + old_key = key; + retval = (*func)(&key, &value, arg, existing); + + switch (retval) { + case ST_CONTINUE: + if (!existing) { + if (linear_add_direct_with_hash(hash, key, value, hash_value)) { + return -1; + } + } + else { + li_table_entry *entry = RHASH_ARRAY_REF(hash, bin); + if (old_key != key) { + entry->key = key; + } + entry->record = value; + } + break; + case ST_DELETE: + if (existing) { + clear_entry(RHASH_ARRAY_REF(hash, bin)); + RHASH_ARRAY_SIZE_DEC(hash); + } + break; + } + return existing; +} + +static int +linear_insert(VALUE hash, st_data_t key, st_data_t value) +{ + unsigned bin = RHASH_ARRAY_BOUND(hash); + st_hash_t hash_value = do_hash(key); + + hash_ltbl(hash); /* prepare ltbl */ + + bin = find_entry(hash, hash_value, key); + if (bin == RHASH_ARRAY_MAX_BOUND) { + if (RHASH_ARRAY_SIZE(hash) >= RHASH_ARRAY_MAX_SIZE) { + return -1; + } + else if (bin >= RHASH_ARRAY_MAX_BOUND) { + bin = linear_compact_table(hash); + hash_ltbl(hash); + } + HASH_ASSERT(bin < RHASH_ARRAY_MAX_BOUND); + + set_entry(RHASH_ARRAY_REF(hash, bin), key, value, hash_value); + RHASH_ARRAY_BOUND_SET(hash, bin+1); + RHASH_ARRAY_SIZE_INC(hash); + return 0; + } + else { + RHASH_ARRAY_REF(hash, bin)->record = value; + return 1; + } +} + +static int +linear_lookup(VALUE hash, st_data_t key, st_data_t *value) +{ + st_hash_t hash_value = do_hash(key); + unsigned bin = find_entry(hash, hash_value, key); + + if (bin == RHASH_ARRAY_MAX_BOUND) { + return 0; + } + else { + HASH_ASSERT(bin < RHASH_ARRAY_MAX_BOUND); + if (value != NULL) { + *value = RHASH_ARRAY_REF(hash, bin)->record; + } + return 1; + } +} + +static int +linear_delete(VALUE hash, st_data_t *key, st_data_t *value) +{ + unsigned bin; + st_hash_t hash_value = do_hash(*key); + + + bin = find_entry(hash, hash_value, *key); + + if (bin == RHASH_ARRAY_MAX_BOUND) { + if (value != 0) *value = 0; + return 0; + } + else { + li_table_entry *entry = RHASH_ARRAY_REF(hash, bin); + if (value != 0) *value = entry->record; + clear_entry(entry); + RHASH_ARRAY_SIZE_DEC(hash); + return 1; + } +} + +static int +linear_shift(VALUE hash, st_data_t *key, st_data_t *value) +{ + if (RHASH_ARRAY_SIZE(hash) > 0) { + unsigned i, bound = RHASH_ARRAY_BOUND(hash); + li_table_entry *entry, *entries = RHASH_ARRAY(hash)->entries; + + for (i = 0; i < bound; i++) { + entry = &entries[i]; + if (!empty_entry(entry)) { + if (value != 0) *value = entry->record; + *key = entry->key; + clear_entry(entry); + RHASH_ARRAY_SIZE_DEC(hash); + return 1; + } + } + } + if (value != 0) *value = 0; + return 0; +} + +static long +linear_keys(VALUE hash, st_data_t *keys, st_index_t size) +{ + unsigned i, bound = RHASH_ARRAY_BOUND(hash); + st_data_t *keys_start = keys, *keys_end = keys + size; + + for (i = 0; i < bound; i++) { + if (keys == keys_end) { + break; + } + else { + li_table_entry *cur_entry = RHASH_ARRAY_REF(hash, i); + if (!empty_entry(cur_entry)) + *keys++ = cur_entry->key; + } + } + + return keys - keys_start; +} + +static long +linear_values(VALUE hash, st_data_t *values, st_index_t size) +{ + unsigned i, bound = RHASH_ARRAY_BOUND(hash); + st_data_t *values_start = values, *values_end = values + size; + + for (i = 0; i < bound; i++) { + if (values == values_end) { + break; + } + else { + li_table_entry *cur_entry = RHASH_ARRAY_REF(hash, i); + if (!empty_entry(cur_entry)) + *values++ = cur_entry->record; + } + } + + return values - values_start; +} + +static li_table* +linear_copy(VALUE hash1, VALUE hash2) +{ + li_table *old_tab = RHASH_ARRAY(hash2); + + if (old_tab != NULL) { + li_table *new_tab = RHASH_ARRAY(hash1); + if (new_tab == NULL) { + new_tab = (li_table*) rb_transient_heap_alloc(hash1, sizeof(li_table)); + if (new_tab != NULL) { + RHASH_SET_TRANSIENT_FLAG(hash1); + } + else { + RHASH_UNSET_TRANSIENT_FLAG(hash1); + new_tab = (li_table*)ruby_xmalloc(sizeof(li_table)); + } + } + *new_tab = *old_tab; + RHASH_ARRAY_BOUND_SET(hash1, RHASH_ARRAY_BOUND(hash2)); + RHASH_ARRAY_SIZE_SET(hash1, RHASH_ARRAY_SIZE(hash2)); + RHASH_ARRAY_SET(hash1, new_tab); + + rb_gc_writebarrier_remember(hash1); + return new_tab; + } + else { + RHASH_ARRAY_BOUND_SET(hash1, RHASH_ARRAY_BOUND(hash2)); + RHASH_ARRAY_SIZE_SET(hash1, RHASH_ARRAY_SIZE(hash2)); + + if (RHASH_TRANSIENT_P(hash1)) { + RHASH_UNSET_TRANSIENT_FLAG(hash1); + } + else if (RHASH_ARRAY(hash1)) { + ruby_xfree(RHASH_ARRAY(hash1)); + } + + RHASH_ARRAY_SET(hash1, NULL); + + rb_gc_writebarrier_remember(hash1); + return old_tab; + } +} + +static void +linear_clear(VALUE hash) +{ + if (RHASH_ARRAY(hash) != NULL) { + RHASH_ARRAY_SIZE_SET(hash, 0); + RHASH_ARRAY_BOUND_SET(hash, 0); + } + else { + HASH_ASSERT(RHASH_ARRAY_SIZE(hash) == 0); + HASH_ASSERT(RHASH_ARRAY_BOUND(hash) == 0); + } +} + +#if USE_TRANSIENT_HEAP +void +rb_hash_transient_heap_evacuate(VALUE hash, int promote) +{ + if (RHASH_TRANSIENT_P(hash)) { + li_table *new_tab; + li_table *old_tab = RHASH_ARRAY(hash); + + if (UNLIKELY(old_tab == NULL)) { + rb_gc_force_recycle(hash); + return; + } + HASH_ASSERT(old_tab != NULL); + if (promote) { + promote: + new_tab = ruby_xmalloc(sizeof(li_table)); + RHASH_UNSET_TRANSIENT_FLAG(hash); + } + else { + new_tab = rb_transient_heap_alloc(hash, sizeof(li_table)); + if (new_tab == NULL) goto promote; + } + *new_tab = *old_tab; + RHASH_ARRAY_SET(hash, new_tab); + } + hash_verify(hash); +} +#endif + typedef int st_foreach_func(st_data_t, st_data_t, st_data_t); struct foreach_safe_arg { @@ -342,6 +1117,27 @@ struct hash_foreach_arg { VALUE arg; }; +static int +hash_linear_foreach_iter(st_data_t key, st_data_t value, st_data_t argp, int error) +{ + struct hash_foreach_arg *arg = (struct hash_foreach_arg *)argp; + int status; + + if (error) return ST_STOP; + status = (*arg->func)((VALUE)key, (VALUE)value, arg->arg); + /* TODO: rehash check? rb_raise(rb_eRuntimeError, "rehash occurred during iteration"); */ + + switch (status) { + case ST_DELETE: + return ST_DELETE; + case ST_CONTINUE: + break; + case ST_STOP: + return ST_STOP; + } + return ST_CHECK; +} + static int hash_foreach_iter(st_data_t key, st_data_t value, st_data_t argp, int error) { @@ -350,10 +1146,10 @@ hash_foreach_iter(st_data_t key, st_data_t value, st_data_t argp, int error) st_table *tbl; if (error) return ST_STOP; - tbl = RHASH(arg->hash)->ntbl; + tbl = RHASH_ST_TABLE(arg->hash); status = (*arg->func)((VALUE)key, (VALUE)value, arg->arg); - if (RHASH(arg->hash)->ntbl != tbl) { - rb_raise(rb_eRuntimeError, "rehash occurred during iteration"); + if (RHASH_ST_TABLE(arg->hash) != tbl) { + rb_raise(rb_eRuntimeError, "rehash occurred during iteration"); } switch (status) { case ST_DELETE: @@ -380,12 +1176,32 @@ hash_foreach_ensure(VALUE hash) return 0; } +int +rb_hash_stlike_foreach(VALUE hash, int (*func)(ANYARGS), st_data_t arg) +{ + if (RHASH_ARRAY_P(hash)) { + return linear_foreach(hash, func, arg); + } + else { + return st_foreach(RHASH_ST_TABLE(hash), func, arg); + } +} + static VALUE hash_foreach_call(VALUE arg) { VALUE hash = ((struct hash_foreach_arg *)arg)->hash; - if (st_foreach_check(RHASH(hash)->ntbl, hash_foreach_iter, (st_data_t)arg, (st_data_t)Qundef)) { - rb_raise(rb_eRuntimeError, "hash modified during iteration"); + int ret = 0; + if (RHASH_ARRAY_P(hash)) { + ret = linear_foreach_check(hash, hash_linear_foreach_iter, + (st_data_t)arg, (st_data_t)Qundef); + } + else if (RHASH_TABLE_P(hash)) { + ret = st_foreach_check(RHASH_ST_TABLE(hash), hash_foreach_iter, + (st_data_t)arg, (st_data_t)Qundef); + } + if (ret) { + rb_raise(rb_eRuntimeError, "ret: %d, hash modified during iteration", ret); } return Qnil; } @@ -395,13 +1211,14 @@ rb_hash_foreach(VALUE hash, int (*func)(ANYARGS), VALUE farg) { struct hash_foreach_arg arg; - if (!RHASH(hash)->ntbl) + if (RHASH_TABLE_EMPTY(hash)) return; RHASH_ITER_LEV(hash)++; arg.hash = hash; arg.func = (rb_foreach_func *)func; arg.arg = farg; rb_ensure(hash_foreach_call, (VALUE)&arg, hash_foreach_ensure, hash); + hash_verify(hash); } static VALUE @@ -439,7 +1256,7 @@ VALUE rb_hash_new_compare_by_id(void) { VALUE hash = rb_hash_new(); - RHASH(hash)->ntbl = rb_init_identtable(); + RHASH_ST_TABLE_SET(hash, rb_init_identtable()); return hash; } @@ -447,8 +1264,14 @@ MJIT_FUNC_EXPORTED VALUE rb_hash_new_with_size(st_index_t size) { VALUE ret = rb_hash_new(); - if (size) - RHASH(ret)->ntbl = st_init_table_with_size(&objhash, size); + if (size) { + if (size <= RHASH_ARRAY_MAX_SIZE) { + RHASH_ARRAY_SET(ret, linear_init_table(ret)); + } + else { + RHASH_ST_TABLE_SET(ret, st_init_table_with_size(&objhash, size)); + } + } return ret; } @@ -457,8 +1280,12 @@ hash_dup(VALUE hash, VALUE klass, VALUE flags) { VALUE ret = hash_alloc_flags(klass, flags, RHASH_IFNONE(hash)); - if (!RHASH_EMPTY_P(hash)) - RHASH(ret)->ntbl = st_copy(RHASH(hash)->ntbl); + if (!RHASH_EMPTY_P(hash)) { + if (RHASH_ARRAY_P(hash)) + linear_copy(ret, hash); + else if (RHASH_TABLE_P(hash)) + RHASH_ST_TABLE_SET(ret, st_copy(RHASH_ST_TABLE(hash))); + } return ret; } @@ -479,33 +1306,30 @@ rb_hash_modify_check(VALUE hash) rb_check_frozen(hash); } -static struct st_table * -hash_tbl(VALUE hash) +MJIT_FUNC_EXPORTED struct st_table * +#if RHASH_CONVERT_TABLE_DEBUG +rb_hash_tbl_raw(VALUE hash, const char *file, int line) { - if (!RHASH(hash)->ntbl) { - RHASH(hash)->ntbl = st_init_table(&objhash); - } - return RHASH(hash)->ntbl; + return linear_force_convert_table(hash, file, line); } - -struct st_table * -rb_hash_tbl(VALUE hash) +#else +rb_hash_tbl_raw(VALUE hash) { - OBJ_WB_UNPROTECT(hash); - return hash_tbl(hash); + return linear_force_convert_table(hash, NULL, 0); } +#endif -MJIT_FUNC_EXPORTED struct st_table * -rb_hash_tbl_raw(VALUE hash) +struct st_table * +rb_hash_tbl(VALUE hash, const char *file, int line) { - return hash_tbl(hash); + OBJ_WB_UNPROTECT(hash); + return RHASH_TBL_RAW(hash); } static void rb_hash_modify(VALUE hash) { rb_hash_modify_check(hash); - hash_tbl(hash); } NORETURN(static void no_new_key(void)); @@ -545,6 +1369,22 @@ struct update_arg { typedef int (*tbl_update_func)(st_data_t *, st_data_t *, st_data_t, int); +int +rb_hash_stlike_update(VALUE hash, st_data_t key, st_update_callback_func func, st_data_t arg) +{ + if (RHASH_ARRAY_P(hash)) { + int result = linear_update(hash, (st_data_t)key, func, arg); + if (result == -1) { + linear_try_convert_table(hash); + } + else { + return result; + } + } + + return st_update(RHASH_ST_TABLE(hash), (st_data_t)key, func, arg); +} + static int tbl_update(VALUE hash, VALUE key, tbl_update_func func, st_data_t optional_arg) { @@ -558,7 +1398,7 @@ tbl_update(VALUE hash, VALUE key, tbl_update_func func, st_data_t optional_arg) arg.new_value = 0; arg.old_value = Qundef; - result = st_update(RHASH(hash)->ntbl, (st_data_t)key, func, (st_data_t)&arg); + result = rb_hash_stlike_update(hash, key, func, (st_data_t)&arg); /* write barrier */ if (arg.new_key) RB_OBJ_WRITTEN(hash, arg.old_key, arg.new_key); @@ -673,12 +1513,15 @@ rb_hash_s_create(int argc, VALUE *argv, VALUE klass) VALUE hash, tmp; if (argc == 1) { - tmp = rb_hash_s_try_convert(Qnil, argv[0]); + tmp = rb_hash_s_try_convert(Qnil, argv[0]); if (!NIL_P(tmp)) { hash = hash_alloc(klass); - if (RHASH(tmp)->ntbl) { - RHASH(hash)->ntbl = st_copy(RHASH(tmp)->ntbl); + if (RHASH_ARRAY_P(tmp)) { + linear_copy(hash, tmp); } + else { + RHASH_ST_TABLE_SET(hash, st_copy(RHASH_ST_TABLE(tmp))); + } return hash; } @@ -725,7 +1568,7 @@ rb_hash_s_create(int argc, VALUE *argv, VALUE klass) hash = hash_alloc(klass); rb_hash_bulk_insert(argc, argv, hash); - + hash_verify(hash); return hash; } @@ -767,9 +1610,12 @@ struct rehash_arg { static int rb_hash_rehash_i(VALUE key, VALUE value, VALUE arg) { - st_table *tbl = (st_table *)arg; - - st_insert(tbl, (st_data_t)key, (st_data_t)value); + if (RHASH_ARRAY_P(arg)) { + linear_insert(arg, (st_data_t)key, (st_data_t)value); + } + else { + st_insert(RHASH_ST_TABLE(arg), (st_data_t)key, (st_data_t)value); + } return ST_CONTINUE; } @@ -803,17 +1649,25 @@ rb_hash_rehash(VALUE hash) rb_raise(rb_eRuntimeError, "rehash during iteration"); } rb_hash_modify_check(hash); - if (!RHASH(hash)->ntbl) - return hash; - tmp = hash_alloc(0); - tbl = st_init_table_with_size(RHASH(hash)->ntbl->type, RHASH(hash)->ntbl->num_entries); - RHASH(tmp)->ntbl = tbl; - - rb_hash_foreach(hash, rb_hash_rehash_i, (VALUE)tbl); - st_free_table(RHASH(hash)->ntbl); - RHASH(hash)->ntbl = tbl; - RHASH(tmp)->ntbl = 0; - + if (RHASH_ARRAY_P(hash)) { + tmp = hash_alloc(0); + linear_init_table(tmp); + rb_hash_foreach(hash, rb_hash_rehash_i, (VALUE)tmp); + linear_free_and_clear_table(hash); + linear_copy(hash, tmp); + linear_free_and_clear_table(tmp); + } + else if (RHASH_TABLE_P(hash)) { + st_table *old_tab = RHASH_ST_TABLE(hash); + tmp = hash_alloc(0); + tbl = st_init_table_with_size(old_tab->type, old_tab->num_entries); + RHASH_ST_TABLE_SET(tmp, tbl); + rb_hash_foreach(hash, rb_hash_rehash_i, (VALUE)tmp); + st_free_table(old_tab); + RHASH_ST_TABLE_SET(hash, tbl); + RHASH_CLEAR(tmp); + } + hash_verify(hash); return hash; } @@ -850,10 +1704,27 @@ rb_hash_aref(VALUE hash, VALUE key) { st_data_t val; - if (!RHASH(hash)->ntbl || !st_lookup(RHASH(hash)->ntbl, key, &val)) { - return rb_hash_default_value(hash, key); + if (RHASH_ARRAY_P(hash) && linear_lookup(hash, key, &val)) { + return (VALUE)val; + } + else if (RHASH_TABLE_P(hash) && st_lookup(RHASH_ST_TABLE(hash), key, &val)) { + return (VALUE)val; + } + hash_verify(hash); + return rb_hash_default_value(hash, key); +} + +MJIT_FUNC_EXPORTED int +rb_hash_stlike_lookup(VALUE hash, st_data_t key, st_data_t *pval) +{ + hash_verify(hash); + + if (RHASH_ARRAY_P(hash)) { + return linear_lookup(hash, key, pval); + } + else { + return st_lookup(RHASH_ST_TABLE(hash), key, pval); } - return (VALUE)val; } VALUE @@ -861,10 +1732,12 @@ rb_hash_lookup2(VALUE hash, VALUE key, VALUE def) { st_data_t val; - if (!RHASH(hash)->ntbl || !st_lookup(RHASH(hash)->ntbl, key, &val)) { - return def; /* without Hash#default */ + if (rb_hash_stlike_lookup(hash, key, &val)) { + return (VALUE)val; + } + else { + return def; /* without Hash#default */ } - return (VALUE)val; } VALUE @@ -916,19 +1789,23 @@ rb_hash_fetch_m(int argc, VALUE *argv, VALUE hash) if (block_given && argc == 2) { rb_warn("block supersedes default value argument"); } - if (!RHASH(hash)->ntbl || !st_lookup(RHASH(hash)->ntbl, key, &val)) { - if (block_given) return rb_yield(key); - if (argc == 1) { - VALUE desc = rb_protect(rb_inspect, key, 0); - if (NIL_P(desc)) { - desc = rb_any_to_s(key); - } - desc = rb_str_ellipsize(desc, 65); - rb_key_err_raise(rb_sprintf("key not found: %"PRIsVALUE, desc), hash, key); + if (RHASH_ARRAY_P(hash) && linear_lookup(hash, key, &val)) { + return (VALUE)val; + } + else if (RHASH_TABLE_P(hash) && st_lookup(RHASH_ST_TABLE(hash), key, &val)) { + return (VALUE)val; + } + if (block_given) return rb_yield(key); + if (argc == 1) { + VALUE desc = rb_protect(rb_inspect, key, 0); + if (NIL_P(desc)) { + desc = rb_any_to_s(key); } - return argv[1]; + desc = rb_str_ellipsize(desc, 65); + rb_key_err_raise(rb_sprintf("key not found: %"PRIsVALUE, desc), hash, key); } - return (VALUE)val; + hash_verify(hash); + return argv[1]; } VALUE @@ -1107,6 +1984,17 @@ rb_hash_index(VALUE hash, VALUE value) return rb_hash_key(hash, value); } +int +rb_hash_stlike_delete(VALUE hash, st_data_t *pkey, st_data_t *pval) +{ + if (RHASH_ARRAY_P(hash)) { + return linear_delete(hash, pkey, pval); + } + else { + return st_delete(RHASH_ST_TABLE(hash), pkey, pval); + } +} + /* * delete a specified entry a given key. * if there is the corresponding entry, return a value of the entry. @@ -1117,14 +2005,11 @@ rb_hash_delete_entry(VALUE hash, VALUE key) { st_data_t ktmp = (st_data_t)key, val; - if (!RHASH(hash)->ntbl) { - return Qundef; - } - else if (st_delete(RHASH(hash)->ntbl, &ktmp, &val)) { - return (VALUE)val; + if (rb_hash_stlike_delete(hash, &ktmp, &val)) { + return (VALUE)val; } else { - return Qundef; + return Qundef; } } @@ -1219,14 +2104,29 @@ rb_hash_shift(VALUE hash) struct shift_var var; rb_hash_modify_check(hash); - if (RHASH(hash)->ntbl) { + if (RHASH_ARRAY_P(hash)) { var.key = Qundef; if (RHASH_ITER_LEV(hash) == 0) { - if (st_shift(RHASH(hash)->ntbl, &var.key, &var.val)) { + if (linear_shift(hash, &var.key, &var.val)) { return rb_assoc_new(var.key, var.val); } } else { + rb_hash_foreach(hash, shift_i_safe, (VALUE)&var); + if (var.key != Qundef) { + rb_hash_delete_entry(hash, var.key); + return rb_assoc_new(var.key, var.val); + } + } + } + if (RHASH_TABLE_P(hash)) { + var.key = Qundef; + if (RHASH_ITER_LEV(hash) == 0) { + if (st_shift(RHASH_ST_TABLE(hash), &var.key, &var.val)) { + return rb_assoc_new(var.key, var.val); + } + } + else { rb_hash_foreach(hash, shift_i_safe, (VALUE)&var); if (var.key != Qundef) { rb_hash_delete_entry(hash, var.key); @@ -1272,8 +2172,9 @@ rb_hash_delete_if(VALUE hash) { RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); rb_hash_modify_check(hash); - if (RHASH(hash)->ntbl) - rb_hash_foreach(hash, delete_if_i, hash); + if (!RHASH_TABLE_EMPTY(hash)) { + rb_hash_foreach(hash, delete_if_i, hash); + } return hash; } @@ -1296,7 +2197,7 @@ rb_hash_reject_bang(VALUE hash) n = RHASH_SIZE(hash); if (!n) return Qnil; rb_hash_foreach(hash, delete_if_i, hash); - if (n == RHASH(hash)->ntbl->num_entries) return Qnil; + if (n == RHASH_SIZE(hash)) return Qnil; return hash; } @@ -1438,6 +2339,8 @@ select_i(VALUE key, VALUE value, VALUE result) * call-seq: * hsh.select {|key, value| block} -> a_hash * hsh.select -> an_enumerator + * hsh.filter {|key, value| block} -> a_hash + * hsh.filter -> an_enumerator * * Returns a new hash consisting of entries for which the block returns true. * @@ -1446,6 +2349,8 @@ select_i(VALUE key, VALUE value, VALUE result) * h = { "a" => 100, "b" => 200, "c" => 300 } * h.select {|k,v| k > "a"} #=> {"b" => 200, "c" => 300} * h.select {|k,v| v < 200} #=> {"a" => 100} + * + * Hash#filter is an alias for Hash#select. */ VALUE @@ -1474,9 +2379,13 @@ keep_if_i(VALUE key, VALUE value, VALUE hash) * call-seq: * hsh.select! {| key, value | block } -> hsh or nil * hsh.select! -> an_enumerator + * hsh.filter! {| key, value | block } -> hsh or nil + * hsh.filter! -> an_enumerator * - * Equivalent to Hash#keep_if, but returns - * nil if no changes were made. + * Equivalent to Hash#keep_if, but returns + * +nil+ if no changes were made. + * + * Hash#filter! is an alias for Hash#select!. */ VALUE @@ -1486,11 +2395,10 @@ rb_hash_select_bang(VALUE hash) RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); rb_hash_modify_check(hash); - if (!RHASH(hash)->ntbl) - return Qnil; - n = RHASH(hash)->ntbl->num_entries; + n = RHASH_SIZE(hash); + if (!n) return Qnil; rb_hash_foreach(hash, keep_if_i, hash); - if (n == RHASH(hash)->ntbl->num_entries) return Qnil; + if (n == RHASH_SIZE(hash)) return Qnil; return hash; } @@ -1500,10 +2408,11 @@ rb_hash_select_bang(VALUE hash) * hsh.keep_if -> an_enumerator * * Deletes every key-value pair from hsh for which block - * evaluates to false. + * evaluates to +false+. * * If no block is given, an enumerator is returned instead. * + * See also Hash#select!. */ VALUE @@ -1511,8 +2420,9 @@ rb_hash_keep_if(VALUE hash) { RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); rb_hash_modify_check(hash); - if (RHASH(hash)->ntbl) - rb_hash_foreach(hash, keep_if_i, hash); + if (!RHASH_TABLE_EMPTY(hash)) { + rb_hash_foreach(hash, keep_if_i, hash); + } return hash; } @@ -1537,13 +2447,15 @@ VALUE rb_hash_clear(VALUE hash) { rb_hash_modify_check(hash); - if (!RHASH(hash)->ntbl) - return hash; - if (RHASH(hash)->ntbl->num_entries > 0) { - if (RHASH_ITER_LEV(hash) > 0) - rb_hash_foreach(hash, clear_i, 0); - else - st_clear(RHASH(hash)->ntbl); + + if (RHASH_ITER_LEV(hash) > 0) { + rb_hash_foreach(hash, clear_i, 0); + } + else if (RHASH_ARRAY_P(hash)) { + linear_clear(hash); + } + else { + st_clear(RHASH_ST_TABLE(hash)); } return hash; @@ -1618,14 +2530,15 @@ VALUE rb_hash_aset(VALUE hash, VALUE key, VALUE val) { int iter_lev = RHASH_ITER_LEV(hash); - st_table *tbl = RHASH(hash)->ntbl; rb_hash_modify(hash); - if (!tbl) { + + if (RHASH_TABLE_EMPTY(hash)) { if (iter_lev > 0) no_new_key(); - tbl = hash_tbl(hash); + linear_init_table(hash); } - if (tbl->type == &identhash || rb_obj_class(key) != rb_cString) { + + if (RHASH_TYPE(hash) == &identhash || rb_obj_class(key) != rb_cString) { RHASH_UPDATE_ITER(hash, iter_lev, key, hash_aset, val); } else { @@ -1646,8 +2559,6 @@ replace_i(VALUE key, VALUE val, VALUE hash) static VALUE rb_hash_initialize_copy(VALUE hash, VALUE hash2) { - st_table *ntbl; - rb_hash_modify_check(hash); hash2 = to_hash(hash2); @@ -1655,15 +2566,23 @@ rb_hash_initialize_copy(VALUE hash, VALUE hash2) if (hash == hash2) return hash; - ntbl = RHASH(hash)->ntbl; - if (RHASH(hash2)->ntbl) { - if (ntbl) st_free_table(ntbl); - RHASH(hash)->ntbl = st_copy(RHASH(hash2)->ntbl); - if (RHASH(hash)->ntbl->num_entries) + if (RHASH_ARRAY_P(hash2)) { + if (RHASH_ARRAY_P(hash)) linear_free_and_clear_table(hash); + linear_copy(hash, hash2); + if (RHASH_ARRAY_SIZE(hash)) rb_hash_rehash(hash); } - else if (ntbl) { - st_clear(ntbl); + else if (RHASH_TABLE_P(hash2)) { + if (RHASH_TABLE_P(hash)) st_free_table(RHASH_ST_TABLE(hash)); + RHASH_ST_TABLE_SET(hash, st_copy(RHASH_ST_TABLE(hash2))); + if (RHASH_ST_TABLE(hash)->num_entries) + rb_hash_rehash(hash); + } + else if (RHASH_ARRAY_P(hash)) { + linear_clear(hash); + } + else if (RHASH_TABLE_P(hash)) { + st_clear(RHASH_ST_TABLE(hash)); } COPY_DEFAULT(hash, hash2); @@ -1686,19 +2605,28 @@ rb_hash_initialize_copy(VALUE hash, VALUE hash2) static VALUE rb_hash_replace(VALUE hash, VALUE hash2) { - st_table *table2; - rb_hash_modify_check(hash); if (hash == hash2) return hash; hash2 = to_hash(hash2); COPY_DEFAULT(hash, hash2); - table2 = RHASH(hash2)->ntbl; - rb_hash_clear(hash); - if (table2) hash_tbl(hash)->type = table2->type; - rb_hash_foreach(hash2, replace_i, hash); + + if (RHASH_ARRAY_P(hash)) { + if (RHASH_ARRAY_P(hash2)) { + linear_copy(hash, hash2); + } + else { + goto st_to_st; + } + } + else { + if (RHASH_ARRAY_P(hash2)) linear_force_convert_table(hash2, __FILE__, __LINE__); + st_to_st: + RHASH_TBL_RAW(hash)->type = RHASH_ST_TABLE(hash2)->type; + rb_hash_foreach(hash2, replace_i, hash); + } return hash; } @@ -1725,6 +2653,11 @@ rb_hash_size(VALUE hash) return INT2FIX(RHASH_SIZE(hash)); } +size_t +rb_hash_size_num(VALUE hash) +{ + return (long)RHASH_SIZE(hash); +} /* * call-seq: @@ -1922,7 +2855,7 @@ rb_hash_transform_keys_bang(VALUE hash) { RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); rb_hash_modify_check(hash); - if (RHASH(hash)->ntbl) { + if (!RHASH_TABLE_EMPTY(hash)) { long i; VALUE pairs = rb_hash_flatten(0, NULL, hash); rb_hash_clear(hash); @@ -1996,7 +2929,7 @@ rb_hash_transform_values_bang(VALUE hash) { RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); rb_hash_modify_check(hash); - if (RHASH(hash)->ntbl) + if (!RHASH_TABLE_EMPTY(hash)) rb_hash_foreach(hash, transform_values_i, hash); return hash; } @@ -2187,12 +3120,20 @@ rb_hash_keys(VALUE hash) if (size == 0) return keys; if (ST_DATA_COMPATIBLE_P(VALUE)) { - st_table *table = RHASH(hash)->ntbl; + if (RHASH_ARRAY_P(hash)) { + rb_gc_writebarrier_remember(keys); + RARRAY_PTR_USE(keys, ptr, { + size = linear_keys(hash, ptr, size); + }); + } + else if (RHASH_TABLE_P(hash)) { + st_table *table = RHASH_ST_TABLE(hash); - rb_gc_writebarrier_remember(keys); - RARRAY_PTR_USE(keys, ptr, { - size = st_keys(table, ptr, size); - }); + rb_gc_writebarrier_remember(keys); + RARRAY_PTR_USE(keys, ptr, { + size = st_keys(table, ptr, size); + }); + } rb_ary_set_len(keys, size); } else { @@ -2231,12 +3172,20 @@ rb_hash_values(VALUE hash) if (size == 0) return values; if (ST_DATA_COMPATIBLE_P(VALUE)) { - st_table *table = RHASH(hash)->ntbl; + if (RHASH_ARRAY_P(hash)) { + rb_gc_writebarrier_remember(values); + RARRAY_PTR_USE(values, ptr, { + size = linear_values(hash, ptr, size); + }); + } + else if (RHASH_TABLE_P(hash)) { + st_table *table = RHASH_ST_TABLE(hash); - rb_gc_writebarrier_remember(values); - RARRAY_PTR_USE(values, ptr, { - size = st_values(table, ptr, size); - }); + rb_gc_writebarrier_remember(values); + RARRAY_PTR_USE(values, ptr, { + size = st_values(table, ptr, size); + }); + } rb_ary_set_len(values, size); } else { @@ -2268,9 +3217,10 @@ rb_hash_values(VALUE hash) MJIT_FUNC_EXPORTED VALUE rb_hash_has_key(VALUE hash, VALUE key) { - if (!RHASH(hash)->ntbl) - return Qfalse; - if (st_lookup(RHASH(hash)->ntbl, key, 0)) { + if (RHASH_ARRAY_P(hash) && linear_lookup(hash, key, 0)) { + return Qtrue; + } + else if (RHASH_TABLE_P(hash) && st_lookup(RHASH_ST_TABLE(hash), key, 0)) { return Qtrue; } return Qfalse; @@ -2314,7 +3264,7 @@ rb_hash_has_value(VALUE hash, VALUE val) struct equal_data { VALUE result; - st_table *tbl; + VALUE hash; int eql; }; @@ -2324,10 +3274,15 @@ eql_i(VALUE key, VALUE val1, VALUE arg) struct equal_data *data = (struct equal_data *)arg; st_data_t val2; - if (!st_lookup(data->tbl, key, &val2)) { + if (RHASH_ARRAY_P(data->hash) && !linear_lookup(data->hash, key, &val2)) { data->result = Qfalse; return ST_STOP; } + else if (RHASH_TABLE_P(data->hash) && !st_lookup(RHASH_ST_TABLE(data->hash), key, &val2)) { + data->result = Qfalse; + return ST_STOP; + } + if (!(data->eql ? rb_eql(val1, (VALUE)val2) : (int)rb_equal(val1, (VALUE)val2))) { data->result = Qfalse; return ST_STOP; @@ -2372,19 +3327,21 @@ hash_equal(VALUE hash1, VALUE hash2, int eql) } if (RHASH_SIZE(hash1) != RHASH_SIZE(hash2)) return Qfalse; - if (!RHASH(hash1)->ntbl || !RHASH(hash2)->ntbl) - return Qtrue; - if (RHASH(hash1)->ntbl->type != RHASH(hash2)->ntbl->type) - return Qfalse; + if (!RHASH_TABLE_EMPTY(hash1) && !RHASH_TABLE_EMPTY(hash2)) { + if (RHASH_TYPE(hash1) != RHASH_TYPE(hash2)) + return Qfalse; + + data.hash = hash2; + data.eql = eql; + return rb_exec_recursive_paired(recursive_eql, hash1, hash2, (VALUE)&data); + } + #if 0 if (!(rb_equal(RHASH_IFNONE(hash1), RHASH_IFNONE(hash2)) && FL_TEST(hash1, HASH_PROC_DEFAULT) == FL_TEST(hash2, HASH_PROC_DEFAULT))) return Qfalse; #endif - - data.tbl = RHASH(hash2)->ntbl; - data.eql = eql; - return rb_exec_recursive_paired(recursive_eql, hash1, hash2, (VALUE)&data); + return Qtrue; } /* @@ -2739,7 +3696,8 @@ static VALUE reset_hash_type(VALUE arg) { struct reset_hash_type_arg *p = (struct reset_hash_type_arg *)arg; - RHASH(p->hash)->ntbl->type = p->orighash; + HASH_ASSERT(RHASH_TABLE_P(p->hash)); + RHASH_ST_TABLE(p->hash)->type = p->orighash; return Qundef; } @@ -2777,7 +3735,10 @@ rb_hash_assoc(VALUE hash, VALUE key) VALUE args[2]; if (RHASH_EMPTY_P(hash)) return Qnil; - table = RHASH(hash)->ntbl; + + linear_force_convert_table(hash, __FILE__, __LINE__); + HASH_ASSERT(RHASH_TABLE_P(hash)); + table = RHASH_ST_TABLE(hash); orighash = table->type; if (orighash != &identhash) { @@ -2787,7 +3748,7 @@ rb_hash_assoc(VALUE hash, VALUE key) assochash.compare = assoc_cmp; assochash.hash = orighash->hash; - table->type = &assochash; + table->type = &assochash; args[0] = hash; args[1] = key; ensure_arg.hash = hash; @@ -2954,11 +3915,12 @@ rb_hash_compact(VALUE hash) static VALUE rb_hash_compact_bang(VALUE hash) { + st_index_t n; rb_hash_modify_check(hash); - if (RHASH(hash)->ntbl) { - st_index_t n = RHASH(hash)->ntbl->num_entries; + n = RHASH_SIZE(hash); + if (n) { rb_hash_foreach(hash, delete_if_nil, hash); - if (n != RHASH(hash)->ntbl->num_entries) + if (n != RHASH_SIZE(hash)) return hash; } return Qnil; @@ -2983,15 +3945,23 @@ rb_hash_compact_bang(VALUE hash) static VALUE rb_hash_compare_by_id(VALUE hash) { + VALUE tmp; st_table *identtable; + if (rb_hash_compare_by_id_p(hash)) return hash; + rb_hash_modify_check(hash); + linear_force_convert_table(hash, __FILE__, __LINE__); + HASH_ASSERT(RHASH_TABLE_P(hash)); + tmp = hash_alloc(0); identtable = rb_init_identtable_with_size(RHASH_SIZE(hash)); - rb_hash_foreach(hash, rb_hash_rehash_i, (VALUE)identtable); - if (RHASH(hash)->ntbl) - st_free_table(RHASH(hash)->ntbl); - RHASH(hash)->ntbl = identtable; + RHASH_ST_TABLE_SET(tmp, identtable); + rb_hash_foreach(hash, rb_hash_rehash_i, (VALUE)tmp); + st_free_table(RHASH_ST_TABLE(hash)); + RHASH_ST_TABLE_SET(hash, identtable); + RHASH_CLEAR(tmp); + rb_gc_force_recycle(tmp); return hash; } @@ -3008,19 +3978,19 @@ rb_hash_compare_by_id(VALUE hash) MJIT_FUNC_EXPORTED VALUE rb_hash_compare_by_id_p(VALUE hash) { - if (!RHASH(hash)->ntbl) - return Qfalse; - if (RHASH(hash)->ntbl->type == &identhash) { + if (RHASH_TABLE_P(hash) && RHASH_ST_TABLE(hash)->type == &identhash) { return Qtrue; } - return Qfalse; + else { + return Qfalse; + } } VALUE rb_ident_hash_new(void) { VALUE hash = rb_hash_new(); - RHASH(hash)->ntbl = st_init_table(&identhash); + RHASH_ST_TABLE_SET(hash, st_init_table(&identhash)); return hash; } @@ -3282,11 +4252,68 @@ add_new_i(st_data_t *key, st_data_t *val, st_data_t arg, int existing) int rb_hash_add_new_element(VALUE hash, VALUE key, VALUE val) { - st_table *tbl = rb_hash_tbl_raw(hash); + st_table *tbl; + int ret = 0; VALUE args[2]; args[0] = hash; args[1] = val; + + if (RHASH_ARRAY_P(hash)) { + hash_ltbl(hash); + + ret = linear_update(hash, (st_data_t)key, add_new_i, (st_data_t)args); + if (ret != -1) { + return ret; + } + linear_try_convert_table(hash); + } + tbl = RHASH_TBL_RAW(hash); return st_update(tbl, (st_data_t)key, add_new_i, (st_data_t)args); + +} + +static st_data_t +key_stringify(VALUE key) +{ + return (rb_obj_class(key) == rb_cString && !RB_OBJ_FROZEN(key)) ? + rb_hash_key_str(key) : key; +} + +static void +linear_bulk_insert(VALUE hash, long argc, const VALUE *argv) +{ + long i; + for (i = 0; i < argc; ) { + st_data_t k = key_stringify(argv[i++]); + st_data_t v = argv[i++]; + linear_insert(hash, k, v); + RB_OBJ_WRITTEN(hash, Qundef, k); + RB_OBJ_WRITTEN(hash, Qundef, v); + } +} + +MJIT_FUNC_EXPORTED void +rb_hash_bulk_insert(long argc, const VALUE *argv, VALUE hash) +{ + st_index_t size; + + HASH_ASSERT(argc % 2 == 0); + if (! argc) + return; + size = argc / 2; + if (RHASH_TABLE_EMPTY(hash)) { + if (size <= RHASH_ARRAY_MAX_SIZE) + hash_ltbl(hash); + else + RHASH_TBL_RAW(hash); + } + if (RHASH_ARRAY_P(hash) && + (RHASH_ARRAY_SIZE(hash) + size <= RHASH_ARRAY_MAX_SIZE)) { + linear_bulk_insert(hash, argc, argv); + return; + } + + rb_hash_bulk_insert_into_st_table(argc, argv, hash); } static int path_tainted = -1; @@ -3468,8 +4495,8 @@ env_delete(VALUE obj, VALUE name) /* * call-seq: - * ENV.delete(name) -> value - * ENV.delete(name) { |name| } -> value + * ENV.delete(name) -> value + * ENV.delete(name) { |name| block } -> value * * Deletes the environment variable with +name+ and returns the value of the * variable. If a block is given it will be called when the named environment @@ -3508,9 +4535,9 @@ rb_f_getenv(VALUE obj, VALUE name) /* * :yield: missing_name * call-seq: - * ENV.fetch(name) -> value - * ENV.fetch(name, default) -> value - * ENV.fetch(name) { |missing_name| ... } -> value + * ENV.fetch(name) -> value + * ENV.fetch(name, default) -> value + * ENV.fetch(name) { |missing_name| block } -> value * * Retrieves the environment variable +name+. * @@ -3887,8 +4914,8 @@ rb_env_size(VALUE ehash, VALUE args, VALUE eobj) /* * call-seq: - * ENV.each_key { |name| } -> Hash - * ENV.each_key -> Enumerator + * ENV.each_key { |name| block } -> Hash + * ENV.each_key -> Enumerator * * Yields each environment variable name. * @@ -3935,8 +4962,8 @@ env_values(void) /* * call-seq: - * ENV.each_value { |value| } -> Hash - * ENV.each_value -> Enumerator + * ENV.each_value { |value| block } -> Hash + * ENV.each_value -> Enumerator * * Yields each environment variable +value+. * @@ -3958,10 +4985,10 @@ env_each_value(VALUE ehash) /* * call-seq: - * ENV.each { |name, value| } -> Hash - * ENV.each -> Enumerator - * ENV.each_pair { |name, value| } -> Hash - * ENV.each_pair -> Enumerator + * ENV.each { |name, value| block } -> Hash + * ENV.each -> Enumerator + * ENV.each_pair { |name, value| block } -> Hash + * ENV.each_pair -> Enumerator * * Yields each environment variable +name+ and +value+. * @@ -4003,10 +5030,10 @@ env_each_pair(VALUE ehash) /* * call-seq: - * ENV.reject! { |name, value| } -> ENV or nil - * ENV.reject! -> Enumerator + * ENV.reject! { |name, value| block } -> ENV or nil + * ENV.reject! -> Enumerator * - * Equivalent to ENV#delete_if but returns +nil+ if no changes were made. + * Equivalent to ENV.delete_if but returns +nil+ if no changes were made. * * Returns an Enumerator if no block was given. */ @@ -4037,8 +5064,8 @@ env_reject_bang(VALUE ehash) /* * call-seq: - * ENV.delete_if { |name, value| } -> Hash - * ENV.delete_if -> Enumerator + * ENV.delete_if { |name, value| block } -> Hash + * ENV.delete_if -> Enumerator * * Deletes every environment variable for which the block evaluates to +true+. * @@ -4074,12 +5101,16 @@ env_values_at(int argc, VALUE *argv) /* * call-seq: - * ENV.select { |name, value| } -> Hash - * ENV.select -> Enumerator + * ENV.select { |name, value| block } -> Hash + * ENV.select -> Enumerator + * ENV.filter { |name, value| block } -> Hash + * ENV.filter -> Enumerator * * Returns a copy of the environment for entries where the block returns true. * * Returns an Enumerator if no block was given. + * + * ENV.filter is an alias for ENV.select. */ static VALUE env_select(VALUE ehash) @@ -4107,10 +5138,14 @@ env_select(VALUE ehash) /* * call-seq: - * ENV.select! { |name, value| } -> ENV or nil - * ENV.select! -> Enumerator + * ENV.select! { |name, value| block } -> ENV or nil + * ENV.select! -> Enumerator + * ENV.filter! { |name, value| block } -> ENV or nil + * ENV.filter! -> Enumerator + * + * Equivalent to ENV.keep_if but returns +nil+ if no changes were made. * - * Equivalent to ENV#keep_if but returns +nil+ if no changes were made. + * ENV.filter! is an alias for ENV.select!. */ static VALUE env_select_bang(VALUE ehash) @@ -4139,8 +5174,8 @@ env_select_bang(VALUE ehash) /* * call-seq: - * ENV.keep_if { |name, value| } -> Hash - * ENV.keep_if -> Enumerator + * ENV.keep_if { |name, value| block } -> Hash + * ENV.keep_if -> Enumerator * * Deletes every environment variable where the block evaluates to +false+. * @@ -4527,10 +5562,10 @@ env_to_h(void) /* * call-seq: - * ENV.reject { |name, value| } -> Hash - * ENV.reject -> Enumerator + * ENV.reject { |name, value| block } -> Hash + * ENV.reject -> Enumerator * - * Same as ENV#delete_if, but works on (and returns) a copy of the + * Same as ENV.delete_if, but works on (and returns) a copy of the * environment. */ static VALUE @@ -4626,8 +5661,8 @@ env_update_i(VALUE key, VALUE val) /* * call-seq: - * ENV.update(hash) -> Hash - * ENV.update(hash) { |name, old_value, new_value| } -> Hash + * ENV.update(hash) -> Hash + * ENV.update(hash) { |name, old_value, new_value| block } -> Hash * * Adds the contents of +hash+ to the environment variables. If no block is * specified entries with duplicate keys are overwritten, otherwise the value diff --git a/ia64.s b/ia64.S similarity index 100% rename from ia64.s rename to ia64.S diff --git a/include/ruby/intern.h b/include/ruby/intern.h index 11a97cb6a21993..d60f370a6da95e 100644 --- a/include/ruby/intern.h +++ b/include/ruby/intern.h @@ -520,11 +520,12 @@ VALUE rb_hash_delete(VALUE,VALUE); VALUE rb_hash_set_ifnone(VALUE hash, VALUE ifnone); typedef VALUE rb_hash_update_func(VALUE newkey, VALUE oldkey, VALUE value); VALUE rb_hash_update_by(VALUE hash1, VALUE hash2, rb_hash_update_func *func); -struct st_table *rb_hash_tbl(VALUE); +struct st_table *rb_hash_tbl(VALUE, const char *file, int line); int rb_path_check(const char*); int rb_env_path_tainted(void); VALUE rb_env_clear(void); VALUE rb_hash_size(VALUE); +void rb_hash_free(VALUE); /* io.c */ #define rb_defout rb_stdout RUBY_EXTERN VALUE rb_fs; diff --git a/include/ruby/io.h b/include/ruby/io.h index 6cacd8a7102575..7caca17e3b8578 100644 --- a/include/ruby/io.h +++ b/include/ruby/io.h @@ -24,7 +24,6 @@ extern "C" { #endif #include -#include #include "ruby/encoding.h" #if defined(HAVE_STDIO_EXT_H) @@ -32,6 +31,7 @@ extern "C" { #endif #include "ruby/config.h" +#include #if defined(HAVE_POLL) # ifdef _AIX # define reqevents events diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h index a05651ac3bad05..58e28d2c9f80a4 100644 --- a/include/ruby/ruby.h +++ b/include/ruby/ruby.h @@ -1013,6 +1013,10 @@ struct RString { ((ptrvar) = RSTRING(str)->as.ary, (lenvar) = RSTRING_EMBED_LEN(str)) : \ ((ptrvar) = RSTRING(str)->as.heap.ptr, (lenvar) = RSTRING(str)->as.heap.len)) +#ifndef USE_TRANSIENT_HEAP +#define USE_TRANSIENT_HEAP 1 +#endif + enum ruby_rarray_flags { RARRAY_EMBED_LEN_MAX = 3, RARRAY_EMBED_FLAG = RUBY_FL_USER1, @@ -1020,12 +1024,20 @@ enum ruby_rarray_flags { RARRAY_EMBED_LEN_MASK = (RUBY_FL_USER4|RUBY_FL_USER3), RARRAY_EMBED_LEN_SHIFT = (RUBY_FL_USHIFT+3), +#if USE_TRANSIENT_HEAP + RARRAY_TRANSIENT_FLAG = RUBY_FL_USER13, +#define RARRAY_TRANSIENT_FLAG RARRAY_TRANSIENT_FLAG +#else +#define RARRAY_TRANSIENT_FLAG 0 +#endif + RARRAY_ENUM_END }; #define RARRAY_EMBED_FLAG (VALUE)RARRAY_EMBED_FLAG #define RARRAY_EMBED_LEN_MASK (VALUE)RARRAY_EMBED_LEN_MASK #define RARRAY_EMBED_LEN_MAX RARRAY_EMBED_LEN_MAX #define RARRAY_EMBED_LEN_SHIFT RARRAY_EMBED_LEN_SHIFT + struct RArray { struct RBasic basic; union { @@ -1046,9 +1058,19 @@ struct RArray { #define RARRAY_LEN(a) rb_array_len(a) #define RARRAY_LENINT(ary) rb_long2int(RARRAY_LEN(ary)) #define RARRAY_CONST_PTR(a) rb_array_const_ptr(a) +#define RARRAY_CONST_PTR_TRANSIENT(a) rb_array_const_ptr_transient(a) -#define RARRAY_PTR_USE_START(a) ((VALUE *)RARRAY_CONST_PTR(a)) -#define RARRAY_PTR_USE_END(a) /* */ +#if USE_TRANSIENT_HEAP +#define RARRAY_TRANSIENT_P(ary) FL_TEST_RAW((ary), RARRAY_TRANSIENT_FLAG) +#else +#define RARRAY_TRANSIENT_P(ary) 0 +#endif + +VALUE *rb_ary_ptr_use_start(VALUE ary); +void rb_ary_ptr_use_end(VALUE ary); + +#define RARRAY_PTR_USE_START(a) rb_ary_ptr_use_start(a) +#define RARRAY_PTR_USE_END(a) rb_ary_ptr_use_end(a) #define RARRAY_PTR_USE(ary, ptr_name, expr) do { \ const VALUE _ary = (ary); \ @@ -1057,11 +1079,12 @@ struct RArray { RARRAY_PTR_USE_END(_ary); \ } while (0) -#define RARRAY_AREF(a, i) (RARRAY_CONST_PTR(a)[i]) +#define RARRAY_AREF(a, i) (RARRAY_CONST_PTR_TRANSIENT(a)[i]) #define RARRAY_ASET(a, i, v) do { \ const VALUE _ary = (a); \ + const VALUE _v = (v); \ VALUE *ptr = (VALUE *)RARRAY_PTR_USE_START(_ary); \ - RB_OBJ_WRITE(_ary, &ptr[i], (v)); \ + RB_OBJ_WRITE(_ary, &ptr[i], _v); \ RARRAY_PTR_USE_END(_ary); \ } while (0) @@ -1079,11 +1102,13 @@ struct RRegexp { #define RREGEXP_SRC_LEN(r) RSTRING_LEN(RREGEXP(r)->src) #define RREGEXP_SRC_END(r) RSTRING_END(RREGEXP(r)->src) -/* RHASH_TBL allocates st_table if not available. */ -#define RHASH_TBL(h) rb_hash_tbl(h) +/* RHash is defined at internal.h */ +size_t rb_hash_size_num(VALUE hash); + +#define RHASH_TBL(h) rb_hash_tbl(h, __FILE__, __LINE__) #define RHASH_ITER_LEV(h) rb_hash_iter_lev(h) #define RHASH_IFNONE(h) rb_hash_ifnone(h) -#define RHASH_SIZE(h) NUM2SIZET(rb_hash_size(h)) +#define RHASH_SIZE(h) rb_hash_size_num(h) #define RHASH_EMPTY_P(h) (RHASH_SIZE(h) == 0) #define RHASH_SET_IFNONE(h, ifnone) rb_hash_set_ifnone((VALUE)h, ifnone) @@ -1689,11 +1714,11 @@ rb_alloc_tmp_buffer2(volatile VALUE *store, long count, size_t elsize) #else # define RUBY_ALLOCV_LIMIT 1024 # define RB_ALLOCV(v, n) ((n) < RUBY_ALLOCV_LIMIT ? \ - (RB_GC_GUARD(v) = 0, alloca(n)) : \ + ((v) = 0, alloca(n)) : \ rb_alloc_tmp_buffer(&(v), (n))) # define RB_ALLOCV_N(type, v, n) \ ((type*)(((size_t)(n) < RUBY_ALLOCV_LIMIT / sizeof(type)) ? \ - (RB_GC_GUARD(v) = 0, alloca((size_t)(n) * sizeof(type))) : \ + ((v) = 0, alloca((size_t)(n) * sizeof(type))) : \ rb_alloc_tmp_buffer2(&(v), (long)(n), sizeof(type)))) #endif #define RB_ALLOCV_END(v) rb_free_tmp_buffer(&(v)) @@ -2110,12 +2135,25 @@ rb_array_len(VALUE a) #endif static inline const VALUE * -rb_array_const_ptr(VALUE a) +rb_array_const_ptr_transient(VALUE a) { return FIX_CONST_VALUE_PTR((RBASIC(a)->flags & RARRAY_EMBED_FLAG) ? RARRAY(a)->as.ary : RARRAY(a)->as.heap.ptr); } +static inline const VALUE * +rb_array_const_ptr(VALUE a) +{ +#if USE_TRANSIENT_HEAP + void rb_ary_detransient(VALUE a); + + if (RARRAY_TRANSIENT_P(a)) { + rb_ary_detransient(a); + } +#endif + return rb_array_const_ptr_transient(a); +} + #if defined(EXTLIB) && defined(USE_DLN_A_OUT) /* hook for external modules */ static char *dln_libs_to_be_linked[] = { EXTLIB, 0 }; @@ -2278,10 +2316,10 @@ ERRORFUNC(("variable argument length doesn't match"), int rb_scan_args_length_mi rb_scan_args_length_mismatch(fmt, varc))) # if defined(__has_attribute) && __has_attribute(diagnose_if) -# define rb_scan_args_verify(fmt, varc) 0 +# define rb_scan_args_verify(fmt, varc) (void)0 # elif defined(__GNUC__) # define rb_scan_args_verify(fmt, varc) \ - __extension__ ({ \ + (void)__extension__ ({ \ int verify; \ _Pragma("GCC diagnostic push"); \ _Pragma("GCC diagnostic ignored \"-Warray-bounds\""); \ @@ -2291,7 +2329,7 @@ ERRORFUNC(("variable argument length doesn't match"), int rb_scan_args_length_mi }) # else # define rb_scan_args_verify(fmt, varc) \ - rb_scan_args_verify_count(fmt, varc) + (void)rb_scan_args_verify_count(fmt, varc) # endif ALWAYS_INLINE(static int rb_scan_args_lead_p(const char *fmt)); diff --git a/include/ruby/st.h b/include/ruby/st.h index ede3ff44567fc0..149e0ebaef3945 100644 --- a/include/ruby/st.h +++ b/include/ruby/st.h @@ -143,7 +143,7 @@ CONSTFUNC(st_index_t st_hash_end(st_index_t h)); CONSTFUNC(st_index_t st_hash_start(st_index_t h)); #define st_hash_start(h) ((st_index_t)(h)) -void rb_hash_bulk_insert(long, const VALUE *, VALUE); +void rb_hash_bulk_insert_into_st_table(long, const VALUE *, VALUE); RUBY_SYMBOL_EXPORT_END diff --git a/inits.c b/inits.c index c9687de516bd52..f730903b5ede75 100644 --- a/inits.c +++ b/inits.c @@ -16,6 +16,10 @@ void rb_call_inits(void) { +#if USE_TRANSIENT_HEAP + CALL(TransientHeap); +#endif + CALL(vm_postponed_job); CALL(Method); CALL(RandomSeedCore); CALL(sym); diff --git a/insns.def b/insns.def index 132ce2f179601b..efd5bb74a268f3 100644 --- a/insns.def +++ b/insns.def @@ -380,7 +380,7 @@ concatstrings (rb_num_t num) (...) (VALUE val) -// attr rb_snum_t sp_inc = 1 - num; +// attr rb_snum_t sp_inc = 1 - (rb_snum_t)num; { val = rb_str_concat_literals(num, STACK_ADDR_FROM_TOP(num)); } @@ -416,7 +416,7 @@ toregexp /* This instruction has StringValue(), which is a method call. But it * seems that path is never covered. */ // attr bool leaf = true; /* yes it is */ -// attr rb_snum_t sp_inc = 1 - cnt; +// attr rb_snum_t sp_inc = 1 - (rb_snum_t)cnt; { const VALUE ary = rb_ary_tmp_new_from_values(0, cnt, STACK_ADDR_FROM_TOP(cnt)); val = rb_reg_new_ary(ary, (int)opt); @@ -439,7 +439,7 @@ newarray (rb_num_t num) (...) (VALUE val) -// attr rb_snum_t sp_inc = 1 - num; +// attr rb_snum_t sp_inc = 1 - (rb_snum_t)num; { val = rb_ary_new4(num, STACK_ADDR_FROM_TOP(num)); } @@ -469,7 +469,7 @@ expandarray (..., VALUE ary) (...) // attr bool leaf = false; /* has rb_check_array_type() */ -// attr rb_snum_t sp_inc = num - 1 + (flag & 1 ? 1 : 0); +// attr rb_snum_t sp_inc = (rb_snum_t)num - 1 + (flag & 1 ? 1 : 0); { vm_expandarray(GET_SP(), ary, num, (int)flag); } @@ -503,7 +503,7 @@ newhash (...) (VALUE val) // attr bool leaf = false; /* has rb_hash_key_str() */ -// attr rb_snum_t sp_inc = 1 - num; +// attr rb_snum_t sp_inc = 1 - (rb_snum_t)num; { RUBY_DTRACE_CREATE_HOOK(HASH, num); @@ -524,7 +524,7 @@ newhashfromarray { VM_ASSERT(num * 2 == (rb_num_t)RARRAY_LEN(ary)); hash = rb_hash_new_with_size(num); - rb_hash_bulk_insert(num * 2, RARRAY_CONST_PTR(ary), hash); + rb_hash_bulk_insert(num * 2, RARRAY_CONST_PTR_TRANSIENT(ary), hash); } /* put new Range object.(Range.new(low, high, flag)) */ @@ -804,7 +804,7 @@ opt_newarray_max * necessary. No way to detect such method calls beforehand. We * cannot but mark it being not leaf. */ // attr bool leaf = false; /* has rb_funcall() */ -// attr rb_snum_t sp_inc = 1 - num; +// attr rb_snum_t sp_inc = 1 - (rb_snum_t)num; { val = vm_opt_newarray_max(num, STACK_ADDR_FROM_TOP(num)); } @@ -816,7 +816,7 @@ opt_newarray_min (VALUE val) /* Same discussion as opt_newarray_max. */ // attr bool leaf = false; /* has rb_funcall() */ -// attr rb_snum_t sp_inc = 1 - num; +// attr rb_snum_t sp_inc = 1 - (rb_snum_t)num; { val = vm_opt_newarray_min(num, STACK_ADDR_FROM_TOP(num)); } @@ -985,7 +985,7 @@ branchnil /* push inline-cached value and go to dst if it is valid */ DEFINE_INSN -getinlinecache +opt_getinlinecache (OFFSET dst, IC ic) () (VALUE val) @@ -1001,7 +1001,7 @@ getinlinecache /* set inline cache */ DEFINE_INSN -setinlinecache +opt_setinlinecache (IC ic) (VALUE val) (VALUE val) diff --git a/internal.h b/internal.h index 697a1196fa4e65..8f69c6f1dab245 100644 --- a/internal.h +++ b/internal.h @@ -54,6 +54,34 @@ extern "C" { # define WARN_UNUSED_RESULT(x) x #endif +#if 0 +#elif defined(NO_SANITIZE) +# define ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS(x) \ + NO_SANITIZE("address", NOINLINE(x)) +#elif defined(NO_SANITIZE_ADDRESS) +# define ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS(x) \ + NO_SANITIZE_ADDRESS(NOINLINE(x)) +#elif defined(NO_ADDRESS_SAFETY_ANALYSIS) +# define ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS(x) \ + NO_ADDRESS_SAFETY_ANALYSIS(NOINLINE(x)) +#else +# define ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS(x) x +#endif + +#if defined(NO_SANITIZE) && defined(__GNUC__) &&! defined(__clang__) +/* GCC warns about unknown sanitizer, which is annoying. */ +#undef NO_SANITIZE +#define NO_SANITIZE(x, y) \ + COMPILER_WARNING_PUSH; \ + COMPILER_WARNING_IGNORED(-Wattributes); \ + __attribute__((__no_sanitize__(x))) y; \ + COMPILER_WARNING_POP +#endif + +#ifndef NO_SANITIZE +# define NO_SANITIZE(x, y) y +#endif + #ifdef HAVE_VALGRIND_MEMCHECK_H # include # ifndef VALGRIND_MAKE_MEM_DEFINED @@ -77,6 +105,64 @@ extern "C" { # define __has_extension __has_feature #endif +#ifndef MJIT_HEADER + +#ifdef HAVE_SANITIZER_ASAN_INTERFACE_H +# include +#endif + +#if !__has_feature(address_sanitizer) +# define __asan_poison_memory_region(x, y) +# define __asan_unpoison_memory_region(x, y) +# define __asan_region_is_poisoned(x, y) 0 +#endif + +#ifdef HAVE_SANITIZER_MSAN_INTERFACE_H +# include +#endif + +#if !__has_feature(memory_sanitizer) +# define __msan_allocated_memory(x, y) +# define __msan_poison(x, y) +# define __msan_unpoison(x, y) +# define __msan_unpoison_string(x) +#endif + +static inline void +poison_memory_region(const volatile void *ptr, size_t size) +{ + __msan_poison(ptr, size); + __asan_poison_memory_region(ptr, size); +} + +static inline void +poison_object(VALUE obj) +{ + struct RVALUE *ptr = (void *)obj; + poison_memory_region(ptr, SIZEOF_VALUE); +} + +static inline void +unpoison_memory_region(const volatile void *ptr, size_t size, bool malloc_p) +{ + __asan_unpoison_memory_region(ptr, size); + if (malloc_p) { + __msan_allocated_memory(ptr, size); + } + else { + __msan_unpoison(ptr, size); + } +} + +static inline void +unpoison_object(VALUE obj, bool newobj_p) +{ + struct RVALUE *ptr = (void *)obj; + unpoison_memory_region(ptr, SIZEOF_VALUE, newobj_p); +} + +#endif + /* Prevent compiler from reordering access */ #define ACCESS_ONCE(type,x) (*((volatile type *)&(x))) @@ -672,23 +758,99 @@ struct RComplex { #define RCOMPLEX_SET_REAL(cmp, r) RB_OBJ_WRITE((cmp), &((struct RComplex *)(cmp))->real,(r)) #define RCOMPLEX_SET_IMAG(cmp, i) RB_OBJ_WRITE((cmp), &((struct RComplex *)(cmp))->imag,(i)) +enum ruby_rhash_flags { + RHASH_ST_TABLE_FLAG = FL_USER3, + RHASH_ARRAY_MAX_SIZE = 8, + RHASH_ARRAY_SIZE_MASK = (FL_USER4|FL_USER5|FL_USER6|FL_USER7), + RHASH_ARRAY_SIZE_SHIFT = (FL_USHIFT+4), + RHASH_ARRAY_BOUND_MASK = (FL_USER8|FL_USER9|FL_USER10|FL_USER11), + RHASH_ARRAY_BOUND_SHIFT = (FL_USHIFT+8), + + RHASH_ENUM_END +}; + +#define HASH_PROC_DEFAULT FL_USER2 + +#define RHASH_ARRAY_SIZE_RAW(h) \ + ((unsigned int)((RBASIC(h)->flags & RHASH_ARRAY_SIZE_MASK) >> RHASH_ARRAY_SIZE_SHIFT)) + +int rb_hash_array_p(VALUE hash); +struct li_table *rb_hash_array(VALUE hash); +st_table *rb_hash_st_table(VALUE hash); +void rb_hash_st_table_set(VALUE hash, st_table *st); + +#if 0 /* for debug */ +#define RHASH_ARRAY_P(hash) rb_hash_array_p(hash) +#define RHASH_ARRAY(h) rb_hash_array(h) +#define RHASH_ST_TABLE(h) rb_hash_st_table(h) +#else +#define RHASH_ARRAY_P(hash) (!FL_TEST_RAW((hash), RHASH_ST_TABLE_FLAG)) +#define RHASH_ARRAY(hash) (RHASH(hash)->as.li) +#define RHASH_ST_TABLE(hash) (RHASH(hash)->as.st) +#endif + +#define RHASH(obj) (R_CAST(RHash)(obj)) +#define RHASH_ST_SIZE(h) (RHASH_ST_TABLE(h)->num_entries) +#define RHASH_TABLE_P(h) (!RHASH_ARRAY_P(h)) +#define RHASH_CLEAR(h) (FL_UNSET_RAW(h, RHASH_ST_TABLE_FLAG), RHASH(h)->as.li = NULL) + +#define RHASH_ARRAY_SIZE_MASK (VALUE)RHASH_ARRAY_SIZE_MASK +#define RHASH_ARRAY_SIZE_SHIFT RHASH_ARRAY_SIZE_SHIFT +#define RHASH_ARRAY_BOUND_MASK (VALUE)RHASH_ARRAY_BOUND_MASK +#define RHASH_ARRAY_BOUND_SHIFT RHASH_ARRAY_BOUND_SHIFT + +#if USE_TRANSIENT_HEAP +#define RHASH_TRANSIENT_FLAG FL_USER14 +#define RHASH_TRANSIENT_P(hash) FL_TEST_RAW((hash), RHASH_TRANSIENT_FLAG) +#define RHASH_SET_TRANSIENT_FLAG(h) FL_SET_RAW(h, RHASH_TRANSIENT_FLAG) +#define RHASH_UNSET_TRANSIENT_FLAG(h) FL_UNSET_RAW(h, RHASH_TRANSIENT_FLAG) +#else +#define RHASH_TRANSIENT_P(hash) 0 +#define RHASH_SET_TRANSIENT_FLAG(h) ((void)0) +#define RHASH_UNSET_TRANSIENT_FLAG(h) ((void)0) +#endif + +#define RHASH_ARRAY_MAX_SIZE 8 +#define RHASH_ARRAY_MAX_BOUND RHASH_ARRAY_MAX_SIZE + +typedef struct li_table_entry { + VALUE hash; + VALUE key; + VALUE record; +} li_table_entry; + +typedef struct li_table { + li_table_entry entries[RHASH_ARRAY_MAX_SIZE]; +} li_table; + +/* + * RHASH_ARRAY_P(h): + * * as.li == NULL or + * as.li points li_table. + * * as.li is allocated by transient heap or xmalloc. + * + * !RHASH_ARRAY_P(h): + * * as.st points st_table. + */ struct RHash { struct RBasic basic; - struct st_table *ntbl; /* possibly 0 */ + union { + struct st_table *st; + struct li_table *li; /* possibly 0 */ + } as; int iter_lev; const VALUE ifnone; }; -#define RHASH(obj) (R_CAST(RHash)(obj)) - #ifdef RHASH_ITER_LEV -#undef RHASH_ITER_LEV -#undef RHASH_IFNONE -#undef RHASH_SIZE -#define RHASH_ITER_LEV(h) (RHASH(h)->iter_lev) -#define RHASH_IFNONE(h) (RHASH(h)->ifnone) -#define RHASH_SIZE(h) (RHASH(h)->ntbl ? RHASH(h)->ntbl->num_entries : (st_index_t)0) -#endif +# undef RHASH_ITER_LEV +# undef RHASH_IFNONE +# undef RHASH_SIZE + +# define RHASH_ITER_LEV(h) (RHASH(h)->iter_lev) +# define RHASH_IFNONE(h) (RHASH(h)->ifnone) +# define RHASH_SIZE(h) (RHASH_ARRAY_P(h) ? RHASH_ARRAY_SIZE_RAW(h) : RHASH_ST_SIZE(h)) +#endif /* #ifdef RHASH_ITER_LEV */ /* missing/setproctitle.c */ #ifndef HAVE_SETPROCTITLE @@ -698,14 +860,26 @@ extern void ruby_init_setproctitle(int argc, char *argv[]); #define RSTRUCT_EMBED_LEN_MAX RSTRUCT_EMBED_LEN_MAX #define RSTRUCT_EMBED_LEN_MASK RSTRUCT_EMBED_LEN_MASK #define RSTRUCT_EMBED_LEN_SHIFT RSTRUCT_EMBED_LEN_SHIFT + enum { RSTRUCT_EMBED_LEN_MAX = 3, RSTRUCT_EMBED_LEN_MASK = (RUBY_FL_USER2|RUBY_FL_USER1), RSTRUCT_EMBED_LEN_SHIFT = (RUBY_FL_USHIFT+1), + RSTRUCT_TRANSIENT_FLAG = FL_USER3, RSTRUCT_ENUM_END }; +#if USE_TRANSIENT_HEAP +#define RSTRUCT_TRANSIENT_P(st) FL_TEST_RAW((obj), RSTRUCT_TRANSIENT_FLAG) +#define RSTRUCT_TRANSIENT_SET(st) FL_SET_RAW((st), RSTRUCT_TRANSIENT_FLAG) +#define RSTRUCT_TRANSIENT_UNSET(st) FL_UNSET_RAW((st), RSTRUCT_TRANSIENT_FLAG) +#else +#define RSTRUCT_TRANSIENT_P(st) 0 +#define RSTRUCT_TRANSIENT_SET(st) ((void)0) +#define RSTRUCT_TRANSIENT_UNSET(st) ((void)0) +#endif + struct RStruct { struct RBasic basic; union { @@ -746,6 +920,13 @@ rb_struct_const_ptr(VALUE st) RSTRUCT(st)->as.ary : RSTRUCT(st)->as.heap.ptr); } +static inline const VALUE * +rb_struct_const_heap_ptr(VALUE st) +{ + /* TODO: check embed on debug mode */ + return RSTRUCT(st)->as.heap.ptr; +} + /* class.c */ struct rb_deprecated_classext_struct { @@ -1073,6 +1254,37 @@ VALUE rb_gvar_set(struct rb_global_entry *, VALUE); VALUE rb_gvar_defined(struct rb_global_entry *); /* array.c */ + +#ifndef ARRAY_DEBUG +#define ARRAY_DEBUG 0 +#endif + +#ifdef ARRAY_DEBUG +#define RARRAY_PTR_IN_USE_FLAG FL_USER14 +#define ARY_PTR_USING_P(ary) FL_TEST_RAW((ary), RARRAY_PTR_IN_USE_FLAG) + +#else + +/* disable debug function */ +#undef RARRAY_PTR_USE_START +#undef RARRAY_PTR_USE_END +#define RARRAY_PTR_USE_START(a) ((VALUE *)RARRAY_CONST_PTR_TRANSIENT(a)) +#define RARRAY_PTR_USE_END(a) +#define ARY_PTR_USING_P(ary) 0 + +#endif + +#if USE_TRANSIENT_HEAP +#define RARY_TRANSIENT_SET(ary) FL_SET_RAW((ary), RARRAY_TRANSIENT_FLAG); +#define RARY_TRANSIENT_UNSET(ary) FL_UNSET_RAW((ary), RARRAY_TRANSIENT_FLAG); +#else +#undef RARRAY_TRANSIENT_P +#define RARRAY_TRANSIENT_P(a) 0 +#define RARY_TRANSIENT_SET(ary) ((void)0) +#define RARY_TRANSIENT_UNSET(ary) ((void)0) +#endif + + VALUE rb_ary_last(int, const VALUE *, VALUE); void rb_ary_set_len(VALUE, long); void rb_ary_delete_same(VALUE, VALUE); @@ -1100,7 +1312,7 @@ static inline VALUE rb_ary_entry_internal(VALUE ary, long offset) { long len = RARRAY_LEN(ary); - const VALUE *ptr = RARRAY_CONST_PTR(ary); + const VALUE *ptr = RARRAY_CONST_PTR_TRANSIENT(ary); if (len == 0) return Qnil; if (offset < 0) { offset += len; @@ -1173,12 +1385,13 @@ VALUE rb_complex_plus(VALUE, VALUE); VALUE rb_complex_mul(VALUE, VALUE); VALUE rb_complex_abs(VALUE x); VALUE rb_complex_sqrt(VALUE x); -VALUE rb_dbl_complex_polar(double abs, double ang); +VALUE rb_dbl_complex_polar_pi(double abs, double ang); VALUE rb_complex_pow(VALUE self, VALUE other); +struct rb_thread_struct; /* cont.c */ VALUE rb_obj_is_fiber(VALUE); -void rb_fiber_reset_root_local_storage(VALUE); +void rb_fiber_reset_root_local_storage(struct rb_thread_struct *); void ruby_register_rollback_func_for_ensure(VALUE (*ensure_func)(ANYARGS), VALUE (*rollback_func)(ANYARGS)); /* debug.c */ @@ -1337,8 +1550,18 @@ RUBY_SYMBOL_EXPORT_END rb_wb_unprotected_newobj_of(klass, flags)) #define NEWOBJ_OF(obj,type,klass,flags) RB_NEWOBJ_OF(obj,type,klass,flags) +void *rb_aligned_malloc(size_t, size_t); +void rb_aligned_free(void *); + /* hash.c */ +#if RHASH_CONVERT_TABLE_DEBUG +struct st_table *rb_hash_tbl_raw(VALUE hash, const char *file, int line); +#define RHASH_TBL_RAW(h) rb_hash_tbl_raw(h, __FILE__, __LINE__) +#else struct st_table *rb_hash_tbl_raw(VALUE hash); +#define RHASH_TBL_RAW(h) rb_hash_tbl_raw(h) +#endif + VALUE rb_hash_new_with_size(st_index_t size); RUBY_SYMBOL_EXPORT_BEGIN VALUE rb_hash_new_compare_by_id(void); @@ -1353,14 +1576,17 @@ st_table *rb_init_identtable_with_size(st_index_t size); VALUE rb_hash_compare_by_id_p(VALUE hash); VALUE rb_to_hash_type(VALUE obj); VALUE rb_hash_key_str(VALUE); - -#define RHASH_TBL_RAW(h) rb_hash_tbl_raw(h) VALUE rb_hash_keys(VALUE hash); VALUE rb_hash_values(VALUE hash); VALUE rb_hash_rehash(VALUE hash); int rb_hash_add_new_element(VALUE hash, VALUE key, VALUE val); -#define HASH_PROC_DEFAULT FL_USER2 VALUE rb_hash_set_pair(VALUE hash, VALUE pair); +void rb_hash_bulk_insert(long, const VALUE *, VALUE); + +int rb_hash_stlike_lookup(VALUE hash, st_data_t key, st_data_t *pval); +int rb_hash_stlike_delete(VALUE hash, st_data_t *pkey, st_data_t *pval); +int rb_hash_stlike_foreach(VALUE hash, int (*func)(ANYARGS), st_data_t arg); +int rb_hash_stlike_update(VALUE hash, st_data_t key, st_update_callback_func func, st_data_t arg); /* inits.c */ void rb_call_inits(void); @@ -1408,10 +1634,12 @@ VALUE rb_math_sqrt(VALUE); extern int mjit_enabled; VALUE mjit_pause(int wait_p); VALUE mjit_resume(void); +void mjit_finish(int close_handle_p); #else #define mjit_enabled 0 static inline VALUE mjit_pause(int wait_p){ return Qnil; } /* unreachable */ static inline VALUE mjit_resume(void){ return Qnil; } /* unreachable */ +static inline void mjit_finish(int close_handle_p){} #endif /* newline.c */ @@ -1529,6 +1757,7 @@ rb_num_negative_int_p(VALUE num) VALUE rb_float_abs(VALUE flt); VALUE rb_float_equal(VALUE x, VALUE y); VALUE rb_float_eql(VALUE x, VALUE y); +VALUE rb_flo_div_flo(VALUE x, VALUE y); #if USE_FLONUM #define RUBY_BIT_ROTL(v, n) (((v) << (n)) | ((v) >> ((sizeof(v) * 8) - n))) @@ -1899,6 +2128,16 @@ extern rb_encoding OnigEncodingUTF_8; #endif /* variable.c */ +#if USE_TRANSIENT_HEAP +#define ROBJECT_TRANSIENT_FLAG FL_USER13 +#define ROBJ_TRANSIENT_P(obj) FL_TEST_RAW((obj), ROBJECT_TRANSIENT_FLAG) +#define ROBJ_TRANSIENT_SET(obj) FL_SET_RAW((obj), ROBJECT_TRANSIENT_FLAG) +#define ROBJ_TRANSIENT_UNSET(obj) FL_UNSET_RAW((obj), ROBJECT_TRANSIENT_FLAG) +#else +#define ROBJ_TRANSIENT_P(obj) 0 +#define ROBJ_TRANSIENT_SET(obj) ((void)0) +#define ROBJ_TRANSIENT_UNSET(obj) ((void)0) +#endif void rb_gc_mark_global_tbl(void); size_t rb_generic_ivar_memsize(VALUE); VALUE rb_search_class_path(VALUE); @@ -1983,6 +2222,9 @@ const char *rb_objspace_data_type_name(VALUE obj); /* Temporary. This API will be removed (renamed). */ VALUE rb_thread_io_blocking_region(rb_blocking_function_t *func, void *data1, int fd); +/* array.c (export) */ +void rb_ary_detransient(VALUE a); + /* bignum.c (export) */ VALUE rb_big_mul_normal(VALUE x, VALUE y); VALUE rb_big_mul_balance(VALUE x, VALUE y); @@ -2024,6 +2266,13 @@ NORETURN(void rb_unexpected_type(VALUE,int)); ((t) == RUBY_T_DATA && RTYPEDDATA_P(v)) ? \ rb_unexpected_type((VALUE)(v), (t)) : (void)0) +static inline int +rb_typeddata_is_instance_of_inline(VALUE obj, const rb_data_type_t *data_type) +{ + return RB_TYPE_P(obj, T_DATA) && RTYPEDDATA_P(obj) && (RTYPEDDATA_TYPE(obj) == data_type); +} +#define rb_typeddata_is_instance_of rb_typeddata_is_instance_of_inline + /* file.c (export) */ #if defined HAVE_READLINK && defined RUBY_ENCODING_H VALUE rb_readlink(VALUE path, rb_encoding *enc); diff --git a/io.c b/io.c index 32629165cf0d8a..ac35e1ee4fa9f8 100644 --- a/io.c +++ b/io.c @@ -135,6 +135,15 @@ off_t __syscall(quad_t number, ...); #define rename(f, t) rb_w32_urename((f), (t)) #endif +#if defined(_WIN32) +# define RUBY_PIPE_NONBLOCK_DEFAULT (0) +#elif defined(O_NONBLOCK) + /* disabled for [Bug #15356] (Rack::Deflater + rails) failure: */ +# define RUBY_PIPE_NONBLOCK_DEFAULT (0) +#else /* any platforms where O_NONBLOCK does not exist? */ +# define RUBY_PIPE_NONBLOCK_DEFAULT (0) +#endif + VALUE rb_cIO; VALUE rb_eEOFError; VALUE rb_eIOError; @@ -185,14 +194,22 @@ static rb_atomic_t max_file_descriptor = NOFILE; void rb_update_max_fd(int fd) { - struct stat buf; rb_atomic_t afd = (rb_atomic_t)fd; rb_atomic_t max_fd = max_file_descriptor; + int err; if (afd <= max_fd) return; - if (fstat(fd, &buf) != 0 && errno == EBADF) { +#if defined(HAVE_FCNTL) && defined(F_GETFL) + err = fcntl(fd, F_GETFL) == -1; +#else + { + struct stat buf; + err = fstat(fd, &buf) != 0; + } +#endif + if (err && errno == EBADF) { rb_bug("rb_update_max_fd: invalid fd (%d) given.", fd); } @@ -217,7 +234,7 @@ rb_maygvl_fd_fix_cloexec(int fd) flags2 = flags | FD_CLOEXEC; /* Set CLOEXEC for non-standard file descriptors: 3, 4, 5, ... */ if (flags != flags2) { ret = fcntl(fd, F_SETFD, flags2); - if (ret == -1) { + if (ret != 0) { rb_bug("rb_maygvl_fd_fix_cloexec: fcntl(%d, F_SETFD, %d) failed: %s", fd, flags2, strerror(errno)); } } @@ -261,7 +278,7 @@ rb_cloexec_open(const char *pathname, int flags, mode_t mode) flags |= O_NOINHERIT; #endif ret = open(pathname, flags, mode); - if (ret == -1) return -1; + if (ret < 0) return ret; if (ret <= 2 || o_cloexec_state == 0) { rb_maygvl_fd_fix_cloexec(ret); } @@ -310,12 +327,30 @@ rb_cloexec_dup2(int oldfd, int newfd) #else ret = dup2(oldfd, newfd); #endif - if (ret == -1) return -1; + if (ret < 0) return ret; } rb_maygvl_fd_fix_cloexec(ret); return ret; } +static int +rb_fd_set_nonblock(int fd) +{ +#ifdef _WIN32 + return rb_w32_set_nonblock(fd); +#elif defined(F_GETFL) + int oflags = fcntl(fd, F_GETFL); + + if (oflags == -1) + return -1; + if (oflags & O_NONBLOCK) + return 0; + oflags |= O_NONBLOCK; + return fcntl(fd, F_SETFL, oflags); +#endif + return 0; +} + int rb_cloexec_pipe(int fildes[2]) { @@ -324,7 +359,7 @@ rb_cloexec_pipe(int fildes[2]) #if defined(HAVE_PIPE2) static int try_pipe2 = 1; if (try_pipe2) { - ret = pipe2(fildes, O_CLOEXEC); + ret = pipe2(fildes, O_CLOEXEC | RUBY_PIPE_NONBLOCK_DEFAULT); if (ret != -1) return ret; /* pipe2 is available since Linux 2.6.27, glibc 2.9. */ @@ -339,7 +374,7 @@ rb_cloexec_pipe(int fildes[2]) #else ret = pipe(fildes); #endif - if (ret == -1) return -1; + if (ret < 0) return ret; #ifdef __CYGWIN__ if (ret == 0 && fildes[1] == -1) { close(fildes[0]); @@ -350,6 +385,10 @@ rb_cloexec_pipe(int fildes[2]) #endif rb_maygvl_fd_fix_cloexec(fildes[0]); rb_maygvl_fd_fix_cloexec(fildes[1]); + if (RUBY_PIPE_NONBLOCK_DEFAULT) { + rb_fd_set_nonblock(fildes[0]); + rb_fd_set_nonblock(fildes[1]); + } return ret; } @@ -382,7 +421,7 @@ rb_cloexec_fcntl_dupfd(int fd, int minfd) ret = fcntl(fd, F_DUPFD, minfd); #elif defined(HAVE_DUP) ret = dup(fd); - if (ret != -1 && ret < minfd) { + if (ret >= 0 && ret < minfd) { const int prev_fd = ret; ret = rb_cloexec_fcntl_dupfd(fd, minfd); close(prev_fd); @@ -391,7 +430,7 @@ rb_cloexec_fcntl_dupfd(int fd, int minfd) #else # error "dup() or fcntl(F_DUPFD) must be supported." #endif - if (ret == -1) return -1; + if (ret < 0) return ret; rb_maygvl_fd_fix_cloexec(ret); return ret; } @@ -615,7 +654,8 @@ static void io_fd_check_closed(int fd) { if (fd < 0) { - rb_raise(rb_eIOError, closed_stream); + rb_thread_check_ints(); /* check for ruby_error_stream_closed */ + rb_raise(rb_eIOError, closed_stream); } } @@ -920,6 +960,7 @@ io_alloc(VALUE klass) struct io_internal_read_struct { int fd; + int nonblock; void *buf; size_t capa; }; @@ -938,11 +979,24 @@ struct io_internal_writev_struct { }; #endif +static int nogvl_wait_for_single_fd(int fd, short events); static VALUE internal_read_func(void *ptr) { struct io_internal_read_struct *iis = ptr; - return read(iis->fd, iis->buf, iis->capa); + ssize_t r; +retry: + r = read(iis->fd, iis->buf, iis->capa); + if (r < 0 && !iis->nonblock) { + int e = errno; + if (e == EAGAIN || e == EWOULDBLOCK) { + if (nogvl_wait_for_single_fd(iis->fd, RB_WAITFD_IN) != -1) { + goto retry; + } + errno = e; + } + } + return r; } #if defined __APPLE__ @@ -980,7 +1034,9 @@ static ssize_t rb_read_internal(int fd, void *buf, size_t count) { struct io_internal_read_struct iis; + iis.fd = fd; + iis.nonblock = 0; iis.buf = buf; iis.capa = count; @@ -1255,8 +1311,8 @@ io_binwrite_string(VALUE arg) r = rb_writev_internal(fptr->fd, iov, 2); - if (r == -1) - return -1; + if (r < 0) + return r; if (fptr->wbuf.len <= r) { r -= fptr->wbuf.len; @@ -1487,7 +1543,7 @@ io_write(VALUE io, VALUE str, int nosync) rb_io_check_writable(fptr); n = io_fwrite(str, fptr, nosync); - if (n == -1L) rb_sys_fail_path(fptr->pathv); + if (n < 0L) rb_sys_fail_path(fptr->pathv); return LONG2FIX(n); } @@ -1674,7 +1730,7 @@ io_writev(int argc, VALUE *argv, VALUE io) /* sync at last item */ n = io_fwrite(rb_obj_as_string(argv[i]), fptr, (i < argc-1)); } - if (n == -1L) rb_sys_fail_path(fptr->pathv); + if (n < 0L) rb_sys_fail_path(fptr->pathv); total = rb_fix_plus(LONG2FIX(n), total); } @@ -1973,6 +2029,16 @@ rb_io_rewind(VALUE io) return INT2FIX(0); } +static int +fptr_wait_readable(rb_io_t *fptr) +{ + int ret = rb_io_wait_readable(fptr->fd); + + if (ret) + rb_io_check_closed(fptr); + return ret; +} + static int io_fillbuf(rb_io_t *fptr) { @@ -1993,7 +2059,7 @@ io_fillbuf(rb_io_t *fptr) r = rb_read_internal(fptr->fd, fptr->rbuf.ptr, fptr->rbuf.capa); } if (r < 0) { - if (rb_io_wait_readable(fptr->fd)) + if (fptr_wait_readable(fptr)) goto retry; { int e = errno; @@ -2340,7 +2406,7 @@ io_bufread(char *ptr, long len, rb_io_t *fptr) c = rb_read_internal(fptr->fd, ptr+offset, n); if (c == 0) break; if (c < 0) { - if (rb_io_wait_readable(fptr->fd)) + if (fptr_wait_readable(fptr)) goto again; return -1; } @@ -2526,7 +2592,7 @@ fill_cbuf(rb_io_t *fptr, int ec_flags) if (res == econv_source_buffer_empty) { if (fptr->rbuf.len == 0) { READ_CHECK(fptr); - if (io_fillbuf(fptr) == -1) { + if (io_fillbuf(fptr) < 0) { if (!fptr->readconv) { return MORE_CHAR_FINISHED; } @@ -2696,41 +2762,23 @@ read_all(rb_io_t *fptr, long siz, VALUE str) void rb_io_set_nonblock(rb_io_t *fptr) { -#ifdef _WIN32 - if (rb_w32_set_nonblock(fptr->fd) != 0) { + if (rb_fd_set_nonblock(fptr->fd) != 0) { rb_sys_fail_path(fptr->pathv); } -#else - int oflags; -#ifdef F_GETFL - oflags = fcntl(fptr->fd, F_GETFL); - if (oflags == -1) { - rb_sys_fail_path(fptr->pathv); - } -#else - oflags = 0; -#endif - if ((oflags & O_NONBLOCK) == 0) { - oflags |= O_NONBLOCK; - if (fcntl(fptr->fd, F_SETFL, oflags) == -1) { - rb_sys_fail_path(fptr->pathv); - } - } -#endif } -struct read_internal_arg { - int fd; - char *str_ptr; - long len; -}; - static VALUE read_internal_call(VALUE arg) { - struct read_internal_arg *p = (struct read_internal_arg *)arg; - p->len = rb_read_internal(p->fd, p->str_ptr, p->len); - return Qundef; + struct io_internal_read_struct *iis = (struct io_internal_read_struct *)arg; + + return rb_thread_io_blocking_region(internal_read_func, iis, iis->fd); +} + +static long +read_internal_locktmp(VALUE str, struct io_internal_read_struct *iis) +{ + return (long)rb_str_locktmp_ensure(str, read_internal_call, (VALUE)iis); } static int @@ -2749,7 +2797,7 @@ io_getpartial(int argc, VALUE *argv, VALUE io, VALUE opts, int nonblock) rb_io_t *fptr; VALUE length, str; long n, len; - struct read_internal_arg arg; + struct io_internal_read_struct iis; int shrinkable; rb_scan_args(argc, argv, "11", &length, &str); @@ -2776,14 +2824,14 @@ io_getpartial(int argc, VALUE *argv, VALUE io, VALUE opts, int nonblock) rb_io_set_nonblock(fptr); } io_setstrbuf(&str, len); - arg.fd = fptr->fd; - arg.str_ptr = RSTRING_PTR(str); - arg.len = len; - rb_str_locktmp_ensure(str, read_internal_call, (VALUE)&arg); - n = arg.len; + iis.fd = fptr->fd; + iis.nonblock = nonblock; + iis.buf = RSTRING_PTR(str); + iis.capa = len; + n = read_internal_locktmp(str, &iis); if (n < 0) { int e = errno; - if (!nonblock && rb_io_wait_readable(fptr->fd)) + if (!nonblock && fptr_wait_readable(fptr)) goto again; if (nonblock && (e == EWOULDBLOCK || e == EAGAIN)) { if (no_exception_p(opts)) @@ -2890,7 +2938,7 @@ io_read_nonblock(VALUE io, VALUE length, VALUE str, VALUE ex) { rb_io_t *fptr; long n, len; - struct read_internal_arg arg; + struct io_internal_read_struct iis; int shrinkable; if ((len = NUM2LONG(length)) < 0) { @@ -2909,11 +2957,11 @@ io_read_nonblock(VALUE io, VALUE length, VALUE str, VALUE ex) if (n <= 0) { rb_io_set_nonblock(fptr); shrinkable |= io_setstrbuf(&str, len); - arg.fd = fptr->fd; - arg.str_ptr = RSTRING_PTR(str); - arg.len = len; - rb_str_locktmp_ensure(str, read_internal_call, (VALUE)&arg); - n = arg.len; + iis.fd = fptr->fd; + iis.nonblock = 1; + iis.buf = RSTRING_PTR(str); + iis.capa = len; + n = read_internal_locktmp(str, &iis); if (n < 0) { int e = errno; if ((e == EWOULDBLOCK || e == EAGAIN)) { @@ -2954,7 +3002,7 @@ io_write_nonblock(VALUE io, VALUE str, VALUE ex) rb_io_set_nonblock(fptr); n = write(fptr->fd, RSTRING_PTR(str), RSTRING_LEN(str)); - if (n == -1) { + if (n < 0) { int e = errno; if (e == EWOULDBLOCK || e == EAGAIN) { if (ex == Qfalse) { @@ -4212,8 +4260,18 @@ rb_io_ungetbyte(VALUE io, VALUE b) rb_io_check_byte_readable(fptr); if (NIL_P(b)) return Qnil; if (FIXNUM_P(b)) { - char cc = FIX2INT(b); - b = rb_str_new(&cc, 1); + int i = FIX2INT(b); + if (0 <= i && i <= UCHAR_MAX) { + unsigned char cc = i & 0xFF; + b = rb_str_new((const char *)&cc, 1); + } + else { + rb_raise(rb_eRangeError, + "integer %d too big to convert into `unsigned char'", i); + } + } + else if (RB_TYPE_P(b, T_BIGNUM)) { + rb_raise(rb_eRangeError, "bignum too big to convert into `unsigned char'"); } else { SafeStringValue(b); @@ -4385,7 +4443,7 @@ rb_io_set_close_on_exec(VALUE io, VALUE arg) if ((ret & FD_CLOEXEC) != flag) { ret = (ret & ~FD_CLOEXEC) | flag; ret = fcntl(fd, F_SETFD, ret); - if (ret == -1) rb_sys_fail_path(fptr->pathv); + if (ret != 0) rb_sys_fail_path(fptr->pathv); } } @@ -4397,7 +4455,7 @@ rb_io_set_close_on_exec(VALUE io, VALUE arg) if ((ret & FD_CLOEXEC) != flag) { ret = (ret & ~FD_CLOEXEC) | flag; ret = fcntl(fd, F_SETFD, ret); - if (ret == -1) rb_sys_fail_path(fptr->pathv); + if (ret != 0) rb_sys_fail_path(fptr->pathv); } } return Qnil; @@ -4527,7 +4585,8 @@ static void free_io_buffer(rb_io_buffer_t *buf); static void clear_codeconv(rb_io_t *fptr); static void -fptr_finalize_flush(rb_io_t *fptr, int noraise, int keepgvl) +fptr_finalize_flush(rb_io_t *fptr, int noraise, int keepgvl, + struct list_head *busy) { VALUE err = Qnil; int fd = fptr->fd; @@ -4559,6 +4618,14 @@ fptr_finalize_flush(rb_io_t *fptr, int noraise, int keepgvl) fptr->stdio_file = 0; fptr->mode &= ~(FMODE_READABLE|FMODE_WRITABLE); + /* + * ensure waiting_fd users do not hit EBADF, wait for them + * to exit before we call close(). + */ + if (busy) { + do rb_thread_schedule(); while (!list_empty(busy)); + } + if (IS_PREP_STDIO(fptr) || fd <= 2) { /* need to keep FILE objects of stdin, stdout and stderr */ } @@ -4591,7 +4658,7 @@ fptr_finalize_flush(rb_io_t *fptr, int noraise, int keepgvl) static void fptr_finalize(rb_io_t *fptr, int noraise) { - fptr_finalize_flush(fptr, noraise, FALSE); + fptr_finalize_flush(fptr, noraise, FALSE, 0); free_io_buffer(&fptr->rbuf); free_io_buffer(&fptr->wbuf); clear_codeconv(fptr); @@ -4716,8 +4783,8 @@ io_close_fptr(VALUE io) if (fptr->fd < 0) return 0; if (rb_notify_fd_close(fptr->fd, &busy)) { - fptr_finalize_flush(fptr, FALSE, KEEPGVL); /* calls close(fptr->fd) */ - do rb_thread_schedule(); while (!list_empty(&busy)); + /* calls close(fptr->fd): */ + fptr_finalize_flush(fptr, FALSE, KEEPGVL, &busy); } rb_io_fptr_cleanup(fptr, FALSE); return fptr; @@ -4987,7 +5054,7 @@ rb_io_sysseek(int argc, VALUE *argv, VALUE io) } errno = 0; pos = lseek(fptr->fd, pos, whence); - if (pos == -1 && errno) rb_sys_fail_path(fptr->pathv); + if (pos < 0 && errno) rb_sys_fail_path(fptr->pathv); return OFFT2NUM(pos); } @@ -5027,7 +5094,7 @@ rb_io_syswrite(VALUE io, VALUE str) tmp = rb_str_tmp_frozen_acquire(str); RSTRING_GETMEM(tmp, ptr, len); n = rb_write_internal(fptr->fd, ptr, len); - if (n == -1) rb_sys_fail_path(fptr->pathv); + if (n < 0) rb_sys_fail_path(fptr->pathv); rb_str_tmp_frozen_release(str, tmp); return LONG2FIX(n); @@ -5059,7 +5126,7 @@ rb_io_sysread(int argc, VALUE *argv, VALUE io) VALUE len, str; rb_io_t *fptr; long n, ilen; - struct read_internal_arg arg; + struct io_internal_read_struct iis; int shrinkable; rb_scan_args(argc, argv, "11", &len, &str); @@ -5087,14 +5154,13 @@ rb_io_sysread(int argc, VALUE *argv, VALUE io) rb_io_check_closed(fptr); io_setstrbuf(&str, ilen); - rb_str_locktmp(str); - arg.fd = fptr->fd; - arg.str_ptr = RSTRING_PTR(str); - arg.len = ilen; - rb_ensure(read_internal_call, (VALUE)&arg, rb_str_unlocktmp, str); - n = arg.len; + iis.fd = fptr->fd; + iis.nonblock = 1; /* for historical reasons, maybe (see above) */ + iis.buf = RSTRING_PTR(str); + iis.capa = ilen; + n = read_internal_locktmp(str, &iis); - if (n == -1) { + if (n < 0) { rb_sys_fail_path(fptr->pathv); } io_set_read_length(str, n, shrinkable); @@ -5179,7 +5245,7 @@ rb_io_pread(int argc, VALUE *argv, VALUE io) rb_str_locktmp(str); n = (ssize_t)rb_ensure(pread_internal_call, (VALUE)&arg, rb_str_unlocktmp, str); - if (n == -1) { + if (n < 0) { rb_sys_fail_path(fptr->pathv); } io_set_read_length(str, n, shrinkable); @@ -5245,7 +5311,7 @@ rb_io_pwrite(VALUE io, VALUE str, VALUE offset) arg.count = (size_t)RSTRING_LEN(tmp); n = (ssize_t)rb_thread_io_blocking_region(internal_pwrite_func, &arg, fptr->fd); - if (n == -1) rb_sys_fail_path(fptr->pathv); + if (n < 0) rb_sys_fail_path(fptr->pathv); rb_str_tmp_frozen_release(str, tmp); return SSIZET2NUM(n); @@ -6292,7 +6358,7 @@ rb_pipe(int *pipes) { int ret; ret = rb_cloexec_pipe(pipes); - if (ret == -1) { + if (ret < 0) { if (rb_gc_for_fd(errno)) { ret = rb_cloexec_pipe(pipes); } @@ -6369,9 +6435,9 @@ linux_get_maxfd(void) char buf[4096], *p, *np, *e; ssize_t ss; fd = rb_cloexec_open("/proc/self/status", O_RDONLY|O_NOCTTY, 0); - if (fd == -1) return -1; + if (fd < 0) return fd; ss = read(fd, buf, sizeof(buf)); - if (ss == -1) goto err; + if (ss < 0) goto err; p = buf; e = buf + ss; while ((int)sizeof("FDSize:\t0\n")-1 <= e-p && @@ -6390,7 +6456,7 @@ linux_get_maxfd(void) err: close(fd); - return -1; + return (int)ss; } #endif @@ -6560,7 +6626,7 @@ pipe_open(VALUE execarg_obj, const char *modestr, int fmode, # if defined(HAVE_SPAWNVE) if (eargp->envp_str) envp = (char **)RSTRING_PTR(eargp->envp_str); # endif - while ((pid = DO_SPAWN(cmd, args, envp)) == -1) { + while ((pid = DO_SPAWN(cmd, args, envp)) < 0) { /* exec failed */ switch (e = errno) { case EAGAIN: @@ -6593,7 +6659,7 @@ pipe_open(VALUE execarg_obj, const char *modestr, int fmode, } /* parent */ - if (pid == -1) { + if (pid < 0) { # if defined(HAVE_WORKING_FORK) e = errno; # endif @@ -8193,7 +8259,7 @@ rb_io_initialize(int argc, VALUE *argv, VALUE io) oflags = fcntl(fd, F_GETFL); if (oflags == -1) rb_sys_fail(0); #else - if (fstat(fd, &st) == -1) rb_sys_fail(0); + if (fstat(fd, &st) < 0) rb_sys_fail(0); #endif rb_update_max_fd(fd); #if defined(HAVE_FCNTL) && defined(F_GETFL) @@ -10163,7 +10229,7 @@ rb_io_s_pipe(int argc, VALUE *argv, VALUE klass) VALUE ret; argc = rb_scan_args(argc, argv, "02:", &v1, &v2, &opt); - if (rb_pipe(pipes) == -1) + if (rb_pipe(pipes) < 0) rb_sys_fail(0); args[0] = klass; @@ -10637,11 +10703,11 @@ struct copy_stream_struct { int src_fd; int dst_fd; - int close_src; - int close_dst; + unsigned close_src : 1; + unsigned close_dst : 1; + int error_no; off_t total; const char *syserr; - int error_no; const char *notimp; VALUE th; }; @@ -10707,6 +10773,7 @@ nogvl_wait_for_single_fd(int fd, short events) return poll(&fds, 1, -1); } #else /* !USE_POLL */ +# include "vm_core.h" # define IOWAIT_SYSCALL "select" static int nogvl_wait_for_single_fd(int fd, short events) @@ -10725,7 +10792,7 @@ nogvl_wait_for_single_fd(int fd, short events) ret = rb_fd_select(fd + 1, 0, &fds, 0, 0); break; default: - assert(0 && "not supported yet, should never get here"); + VM_UNREACHABLE(nogvl_wait_for_single_fd); } rb_fd_term(&fds); @@ -10745,12 +10812,12 @@ maygvl_copy_stream_wait_read(int has_gvl, struct copy_stream_struct *stp) else { ret = nogvl_wait_for_single_fd(stp->src_fd, RB_WAITFD_IN); } - } while (ret == -1 && maygvl_copy_stream_continue_p(has_gvl, stp)); + } while (ret < 0 && maygvl_copy_stream_continue_p(has_gvl, stp)); - if (ret == -1) { + if (ret < 0) { stp->syserr = IOWAIT_SYSCALL; stp->error_no = errno; - return -1; + return ret; } return 0; } @@ -10762,12 +10829,12 @@ nogvl_copy_stream_wait_write(struct copy_stream_struct *stp) do { ret = nogvl_wait_for_single_fd(stp->dst_fd, RB_WAITFD_OUT); - } while (ret == -1 && maygvl_copy_stream_continue_p(0, stp)); + } while (ret < 0 && maygvl_copy_stream_continue_p(0, stp)); - if (ret == -1) { + if (ret < 0) { stp->syserr = IOWAIT_SYSCALL; stp->error_no = errno; - return -1; + return ret; } return 0; } @@ -10787,30 +10854,31 @@ simple_copy_file_range(int in_fd, off_t *in_offset, int out_fd, off_t *out_offse static int nogvl_copy_file_range(struct copy_stream_struct *stp) { - struct stat src_stat, dst_stat; + struct stat sb; ssize_t ss; + off_t src_size; int ret; - off_t copy_length, src_offset, *src_offset_ptr; - ret = fstat(stp->src_fd, &src_stat); - if (ret == -1) { + ret = fstat(stp->src_fd, &sb); + if (ret < 0) { stp->syserr = "fstat"; stp->error_no = errno; - return -1; + return ret; } - if (!S_ISREG(src_stat.st_mode)) + if (!S_ISREG(sb.st_mode)) return 0; - ret = fstat(stp->dst_fd, &dst_stat); - if (ret == -1) { + src_size = sb.st_size; + ret = fstat(stp->dst_fd, &sb); + if (ret < 0) { stp->syserr = "fstat"; stp->error_no = errno; - return -1; + return ret; } src_offset = stp->src_offset; - if (src_offset != (off_t)-1) { + if (src_offset >= (off_t)0) { src_offset_ptr = &src_offset; } else { @@ -10818,20 +10886,20 @@ nogvl_copy_file_range(struct copy_stream_struct *stp) } copy_length = stp->copy_length; - if (copy_length == (off_t)-1) { - if (src_offset == (off_t)-1) { + if (copy_length < (off_t)0) { + if (src_offset < (off_t)0) { off_t current_offset; errno = 0; current_offset = lseek(stp->src_fd, 0, SEEK_CUR); - if (current_offset == (off_t)-1 && errno) { + if (current_offset < (off_t)0 && errno) { stp->syserr = "lseek"; stp->error_no = errno; - return -1; + return (int)current_offset; } - copy_length = src_stat.st_size - current_offset; + copy_length = src_size - current_offset; } else { - copy_length = src_stat.st_size - src_offset; + copy_length = src_size - src_offset; } } @@ -10850,7 +10918,7 @@ nogvl_copy_file_range(struct copy_stream_struct *stp) goto retry_copy_file_range; } } - if (ss == -1) { + if (ss < 0) { if (maygvl_copy_stream_continue_p(0, stp)) { goto retry_copy_file_range; } @@ -10869,8 +10937,10 @@ nogvl_copy_file_range(struct copy_stream_struct *stp) #if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN case EWOULDBLOCK: #endif - if (nogvl_copy_stream_wait_write(stp) == -1) - return -1; + { + int ret = nogvl_copy_stream_wait_write(stp); + if (ret < 0) return ret; + } goto retry_copy_file_range; case EBADF: { @@ -10885,7 +10955,7 @@ nogvl_copy_file_range(struct copy_stream_struct *stp) } stp->syserr = "copy_file_range"; stp->error_no = errno; - return -1; + return (int)ss; } return 1; } @@ -10924,7 +10994,7 @@ simple_sendfile(int out_fd, int in_fd, off_t *offset, off_t count) # else r = sendfile(in_fd, out_fd, pos, (size_t)count, NULL, &sbytes, 0); # endif - if (r != 0 && sbytes == 0) return -1; + if (r != 0 && sbytes == 0) return r; if (offset) { *offset += sbytes; } @@ -10942,51 +11012,52 @@ simple_sendfile(int out_fd, int in_fd, off_t *offset, off_t count) static int nogvl_copy_stream_sendfile(struct copy_stream_struct *stp) { - struct stat src_stat, dst_stat; + struct stat sb; ssize_t ss; int ret; - + off_t src_size; off_t copy_length; off_t src_offset; int use_pread; - ret = fstat(stp->src_fd, &src_stat); - if (ret == -1) { + ret = fstat(stp->src_fd, &sb); + if (ret < 0) { stp->syserr = "fstat"; stp->error_no = errno; - return -1; + return ret; } - if (!S_ISREG(src_stat.st_mode)) + if (!S_ISREG(sb.st_mode)) return 0; - ret = fstat(stp->dst_fd, &dst_stat); - if (ret == -1) { + src_size = sb.st_size; + ret = fstat(stp->dst_fd, &sb); + if (ret < 0) { stp->syserr = "fstat"; stp->error_no = errno; - return -1; + return ret; } #ifndef __linux__ - if ((dst_stat.st_mode & S_IFMT) != S_IFSOCK) + if ((sb.st_mode & S_IFMT) != S_IFSOCK) return 0; #endif src_offset = stp->src_offset; - use_pread = src_offset != (off_t)-1; + use_pread = src_offset >= (off_t)0; copy_length = stp->copy_length; - if (copy_length == (off_t)-1) { + if (copy_length < (off_t)0) { if (use_pread) - copy_length = src_stat.st_size - src_offset; + copy_length = src_size - src_offset; else { off_t cur; errno = 0; cur = lseek(stp->src_fd, 0, SEEK_CUR); - if (cur == (off_t)-1 && errno) { + if (cur < (off_t)0 && errno) { stp->syserr = "lseek"; stp->error_no = errno; - return -1; + return (int)cur; } - copy_length = src_stat.st_size - cur; + copy_length = src_size - cur; } } @@ -11010,7 +11081,7 @@ nogvl_copy_stream_sendfile(struct copy_stream_struct *stp) goto retry_sendfile; } } - if (ss == -1) { + if (ss < 0) { if (maygvl_copy_stream_continue_p(0, stp)) goto retry_sendfile; switch (errno) { @@ -11023,24 +11094,27 @@ nogvl_copy_stream_sendfile(struct copy_stream_struct *stp) #if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN case EWOULDBLOCK: #endif + { + int ret; #ifndef __linux__ - /* - * Linux requires stp->src_fd to be a mmap-able (regular) file, - * select() reports regular files to always be "ready", so - * there is no need to select() on it. - * Other OSes may have the same limitation for sendfile() which - * allow us to bypass maygvl_copy_stream_wait_read()... - */ - if (maygvl_copy_stream_wait_read(0, stp) == -1) - return -1; -#endif - if (nogvl_copy_stream_wait_write(stp) == -1) - return -1; + /* + * Linux requires stp->src_fd to be a mmap-able (regular) file, + * select() reports regular files to always be "ready", so + * there is no need to select() on it. + * Other OSes may have the same limitation for sendfile() which + * allow us to bypass maygvl_copy_stream_wait_read()... + */ + ret = maygvl_copy_stream_wait_read(0, stp); + if (ret < 0) return ret; +#endif + ret = nogvl_copy_stream_wait_write(stp); + if (ret < 0) return ret; + } goto retry_sendfile; } stp->syserr = "sendfile"; stp->error_no = errno; - return -1; + return (int)ss; } return 1; } @@ -11060,7 +11134,7 @@ maygvl_copy_stream_read(int has_gvl, struct copy_stream_struct *stp, char *buf, { ssize_t ss; retry_read: - if (offset == (off_t)-1) { + if (offset < (off_t)0) { ss = maygvl_read(has_gvl, stp->src_fd, buf, len); } else { @@ -11074,7 +11148,7 @@ maygvl_copy_stream_read(int has_gvl, struct copy_stream_struct *stp, char *buf, if (ss == 0) { return 0; } - if (ss == -1) { + if (ss < 0) { if (maygvl_copy_stream_continue_p(has_gvl, stp)) goto retry_read; switch (errno) { @@ -11082,18 +11156,19 @@ maygvl_copy_stream_read(int has_gvl, struct copy_stream_struct *stp, char *buf, #if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN case EWOULDBLOCK: #endif - if (maygvl_copy_stream_wait_read(has_gvl, stp) == -1) - return -1; + { + int ret = maygvl_copy_stream_wait_read(has_gvl, stp); + if (ret < 0) return ret; + } goto retry_read; #ifdef ENOSYS case ENOSYS: stp->notimp = "pread"; - return -1; + return ss; #endif } - stp->syserr = offset == (off_t)-1 ? "read" : "pread"; + stp->syserr = offset < (off_t)0 ? "read" : "pread"; stp->error_no = errno; - return -1; } return ss; } @@ -11105,17 +11180,17 @@ nogvl_copy_stream_write(struct copy_stream_struct *stp, char *buf, size_t len) int off = 0; while (len) { ss = write(stp->dst_fd, buf+off, len); - if (ss == -1) { - if (maygvl_copy_stream_continue_p(0, stp)) - continue; + if (ss < 0) { + if (maygvl_copy_stream_continue_p(0, stp)) + continue; if (errno == EAGAIN || errno == EWOULDBLOCK) { - if (nogvl_copy_stream_wait_write(stp) == -1) - return -1; + int ret = nogvl_copy_stream_wait_write(stp); + if (ret < 0) return ret; continue; } stp->syserr = "write"; stp->error_no = errno; - return -1; + return (int)ss; } off += (int)ss; len -= (int)ss; @@ -11137,15 +11212,15 @@ nogvl_copy_stream_read_write(struct copy_stream_struct *stp) int use_pread; copy_length = stp->copy_length; - use_eof = copy_length == (off_t)-1; + use_eof = copy_length < (off_t)0; src_offset = stp->src_offset; - use_pread = src_offset != (off_t)-1; + use_pread = src_offset >= (off_t)0; if (use_pread && stp->close_src) { off_t r; errno = 0; r = lseek(stp->src_fd, src_offset, SEEK_SET); - if (r == (off_t)-1 && errno) { + if (r < (off_t)0 && errno) { stp->syserr = "lseek"; stp->error_no = errno; return; @@ -11220,7 +11295,7 @@ copy_stream_fallback_body(VALUE arg) off_t off = stp->src_offset; ID read_method = id_readpartial; - if (stp->src_fd == -1) { + if (stp->src_fd < 0) { if (!rb_respond_to(stp->src, read_method)) { read_method = id_read; } @@ -11229,7 +11304,7 @@ copy_stream_fallback_body(VALUE arg) while (1) { long numwrote; long l; - if (stp->copy_length == (off_t)-1) { + if (stp->copy_length < (off_t)0) { l = buflen; } else { @@ -11239,7 +11314,7 @@ copy_stream_fallback_body(VALUE arg) } l = buflen < rest ? buflen : (long)rest; } - if (stp->src_fd == -1) { + if (stp->src_fd < 0) { VALUE rc = rb_funcall(stp->src, read_method, 2, INT2FIX(l), buf); if (read_method == id_read && NIL_P(rc)) @@ -11250,11 +11325,11 @@ copy_stream_fallback_body(VALUE arg) rb_str_resize(buf, buflen); ss = maygvl_copy_stream_read(1, stp, RSTRING_PTR(buf), l, off); rb_str_resize(buf, ss > 0 ? ss : 0); - if (ss == -1) + if (ss < 0) return Qnil; if (ss == 0) rb_eof_error(); - if (off != (off_t)-1) + if (off >= (off_t)0) off += ss; } n = rb_io_write(stp->dst, buf); @@ -11272,7 +11347,7 @@ copy_stream_fallback_body(VALUE arg) static VALUE copy_stream_fallback(struct copy_stream_struct *stp) { - if (stp->src_fd == -1 && stp->src_offset != (off_t)-1) { + if (stp->src_fd < 0 && stp->src_offset >= (off_t)0) { rb_raise(rb_eArgError, "cannot specify src_offset for non-IO"); } rb_rescue2(copy_stream_fallback_body, (VALUE)stp, @@ -11362,10 +11437,10 @@ copy_stream_body(VALUE arg) if (dst_fptr) io_ascii8bit_binmode(dst_fptr); - if (stp->src_offset == (off_t)-1 && src_fptr && src_fptr->rbuf.len) { + if (stp->src_offset < (off_t)0 && src_fptr && src_fptr->rbuf.len) { size_t len = src_fptr->rbuf.len; VALUE str; - if (stp->copy_length != (off_t)-1 && stp->copy_length < (off_t)len) { + if (stp->copy_length >= (off_t)0 && stp->copy_length < (off_t)len) { len = (size_t)stp->copy_length; } str = rb_str_buf_new(len); @@ -11379,7 +11454,7 @@ copy_stream_body(VALUE arg) rb_io_write(dst_io, str); rb_str_resize(str, 0); stp->total += len; - if (stp->copy_length != (off_t)-1) + if (stp->copy_length >= (off_t)0) stp->copy_length -= len; } @@ -11390,7 +11465,7 @@ copy_stream_body(VALUE arg) if (stp->copy_length == 0) return Qnil; - if (src_fd == -1 || dst_fd == -1) { + if (src_fd < 0 || dst_fd < 0) { return copy_stream_fallback(stp); } diff --git a/iseq.c b/iseq.c index 164a1f477cfdb1..0b5f7c14e47b21 100644 --- a/iseq.c +++ b/iseq.c @@ -111,6 +111,11 @@ rb_iseq_free(const rb_iseq_t *iseq) compile_data_free(ISEQ_COMPILE_DATA(iseq)); ruby_xfree(body); } + + if (iseq->local_hooks) { + rb_hook_list_free(iseq->local_hooks); + } + RUBY_FREE_LEAVE("iseq"); } @@ -247,6 +252,9 @@ rb_iseq_mark(const rb_iseq_t *iseq) } } + if (iseq->local_hooks) { + rb_hook_list_mark(iseq->local_hooks); + } if (FL_TEST(iseq, ISEQ_NOT_LOADED_YET)) { rb_gc_mark(iseq->aux.loader.obj); @@ -371,7 +379,7 @@ rb_iseq_pathobj_set(const rb_iseq_t *iseq, VALUE path, VALUE realpath) } static rb_iseq_location_t * -iseq_location_setup(rb_iseq_t *iseq, VALUE name, VALUE path, VALUE realpath, VALUE first_lineno, const rb_code_location_t *code_location) +iseq_location_setup(rb_iseq_t *iseq, VALUE name, VALUE path, VALUE realpath, VALUE first_lineno, const rb_code_location_t *code_location, const int node_id) { rb_iseq_location_t *loc = &iseq->body->location; @@ -380,6 +388,7 @@ iseq_location_setup(rb_iseq_t *iseq, VALUE name, VALUE path, VALUE realpath, VAL RB_OBJ_WRITE(iseq, &loc->base_label, name); loc->first_lineno = first_lineno; if (code_location) { + loc->node_id = node_id; loc->code_location = *code_location; } else { @@ -420,7 +429,7 @@ set_relation(rb_iseq_t *iseq, const rb_iseq_t *piseq) static VALUE prepare_iseq_build(rb_iseq_t *iseq, - VALUE name, VALUE path, VALUE realpath, VALUE first_lineno, const rb_code_location_t *code_location, + VALUE name, VALUE path, VALUE realpath, VALUE first_lineno, const rb_code_location_t *code_location, const int node_id, const rb_iseq_t *parent, enum iseq_type type, const rb_compile_option_t *option) { @@ -435,7 +444,7 @@ prepare_iseq_build(rb_iseq_t *iseq, set_relation(iseq, parent); name = rb_fstring(name); - iseq_location_setup(iseq, name, path, realpath, first_lineno, code_location); + iseq_location_setup(iseq, name, path, realpath, first_lineno, code_location, node_id); if (iseq != body->local_iseq) { RB_OBJ_WRITE(iseq, &body->location.base_label, body->local_iseq->body->location.label); } @@ -510,9 +519,9 @@ rb_iseq_insns_info_decode_positions(const struct rb_iseq_constant_body *body) void rb_iseq_init_trace(rb_iseq_t *iseq) { - iseq->aux.trace_events = 0; - if (ruby_vm_event_enabled_flags & ISEQ_TRACE_EVENTS) { - rb_iseq_trace_set(iseq, ruby_vm_event_enabled_flags & ISEQ_TRACE_EVENTS); + iseq->aux.global_trace_events = 0; + if (ruby_vm_event_enabled_global_flags & ISEQ_TRACE_EVENTS) { + rb_iseq_trace_set(iseq, ruby_vm_event_enabled_global_flags & ISEQ_TRACE_EVENTS); } } @@ -704,7 +713,7 @@ rb_iseq_new_with_opt(const rb_ast_body_t *ast, VALUE name, VALUE path, VALUE rea new_opt = option ? *option : COMPILE_OPTION_DEFAULT; if (ast && ast->compile_option) rb_iseq_make_compile_option(&new_opt, ast->compile_option); - prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, parent, type, &new_opt); + prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, node ? nd_node_id(node) : -1, parent, type, &new_opt); rb_iseq_compile_node(iseq, node); finish_iseq_build(iseq); @@ -721,7 +730,7 @@ rb_iseq_new_ifunc(const struct vm_ifunc *ifunc, VALUE name, VALUE path, VALUE re rb_iseq_t *iseq = iseq_alloc(); if (!option) option = &COMPILE_OPTION_DEFAULT; - prepare_iseq_build(iseq, name, path, realpath, first_lineno, NULL, parent, type, option); + prepare_iseq_build(iseq, name, path, realpath, first_lineno, NULL, -1, parent, type, option); rb_iseq_compile_ifunc(iseq, ifunc); finish_iseq_build(iseq); @@ -780,7 +789,7 @@ iseq_load(VALUE data, const rb_iseq_t *parent, VALUE opt) rb_iseq_t *iseq = iseq_alloc(); VALUE magic, version1, version2, format_type, misc; - VALUE name, path, realpath, first_lineno, code_location; + VALUE name, path, realpath, first_lineno, code_location, node_id; VALUE type, body, locals, params, exception; st_data_t iseq_type; @@ -821,6 +830,8 @@ iseq_load(VALUE data, const rb_iseq_t *parent, VALUE opt) rb_raise(rb_eTypeError, "unsupport type: :%"PRIsVALUE, rb_sym2str(type)); } + node_id = rb_hash_aref(misc, ID2SYM(rb_intern("node_id"))); + code_location = rb_hash_aref(misc, ID2SYM(rb_intern("code_location"))); if (RB_TYPE_P(code_location, T_ARRAY) && RARRAY_LEN(code_location) == 4) { tmp_loc.beg_pos.lineno = NUM2INT(rb_ary_entry(code_location, 0)); @@ -831,7 +842,7 @@ iseq_load(VALUE data, const rb_iseq_t *parent, VALUE opt) make_compile_option(&option, opt); option.peephole_optimization = FALSE; /* because peephole optimization can modify original iseq */ - prepare_iseq_build(iseq, name, path, realpath, first_lineno, &tmp_loc, + prepare_iseq_build(iseq, name, path, realpath, first_lineno, &tmp_loc, NUM2INT(node_id), parent, (enum iseq_type)iseq_type, &option); rb_iseq_build_from_ary(iseq, misc, locals, params, exception, body); @@ -1665,7 +1676,7 @@ rb_iseq_clear_event_flags(const rb_iseq_t *iseq, size_t pos, rb_event_flag_t res struct iseq_insn_info_entry *entry = (struct iseq_insn_info_entry *)get_insn_info(iseq, pos); if (entry) { entry->events &= ~reset; - if (!(entry->events & iseq->aux.trace_events)) { + if (!(entry->events & iseq->aux.global_trace_events)) { void rb_iseq_trace_flag_cleared(const rb_iseq_t *iseq, size_t pos); rb_iseq_trace_flag_cleared(iseq, pos); } @@ -1936,8 +1947,7 @@ rb_iseq_disasm_insn(VALUE ret, const VALUE *code, size_t pos, events & RUBY_EVENT_B_CALL ? "Bc" : "", events & RUBY_EVENT_B_RETURN ? "Br" : "", events & RUBY_EVENT_COVERAGE_LINE ? "Cli" : "", - events & RUBY_EVENT_COVERAGE_BRANCH ? "Cbr" : "" - ); + events & RUBY_EVENT_COVERAGE_BRANCH ? "Cbr" : ""); } } @@ -2134,47 +2144,6 @@ rb_iseq_disasm(const rb_iseq_t *iseq) return rb_iseq_disasm_recursive(iseq, rb_str_new(0, 0)); } -static VALUE -rb_iseq_all_children(const rb_iseq_t *iseq) -{ - unsigned int i; - VALUE *code = rb_iseq_original_iseq(iseq); - VALUE all_children = rb_obj_hide(rb_ident_hash_new()); - VALUE child; - const struct rb_iseq_constant_body *const body = iseq->body; - - if (body->catch_table) { - for (i = 0; i < body->catch_table->size; i++) { - const struct iseq_catch_table_entry *entry = &body->catch_table->entries[i]; - child = (VALUE)entry->iseq; - if (child) { - rb_hash_aset(all_children, child, Qtrue); - } - } - } - for (i=0; iiseq_size;) { - VALUE insn = code[i]; - int len = insn_len(insn); - const char *types = insn_op_types(insn); - int j; - - for (j=0; types[j]; j++) { - switch (types[j]) { - case TS_ISEQ: - child = code[i+j+1]; - if (child) { - rb_hash_aset(all_children, child, Qtrue); - } - break; - default: - break; - } - } - i += len; - } - return all_children; -} - /* * call-seq: * iseq.disasm -> str @@ -2200,10 +2169,58 @@ iseqw_disasm(VALUE self) } static int -iseqw_each_child_i(VALUE key, VALUE value, VALUE dummy) +iseq_iterate_children(const rb_iseq_t *iseq, void (*iter_func)(const rb_iseq_t *child_iseq, void *data), void *data) { - rb_yield(iseqw_new((const rb_iseq_t *)key)); - return ST_CONTINUE; + unsigned int i; + VALUE *code = rb_iseq_original_iseq(iseq); + const struct rb_iseq_constant_body *const body = iseq->body; + const rb_iseq_t *child; + VALUE all_children = rb_obj_hide(rb_ident_hash_new()); + + if (body->catch_table) { + for (i = 0; i < body->catch_table->size; i++) { + const struct iseq_catch_table_entry *entry = &body->catch_table->entries[i]; + child = entry->iseq; + if (child) { + if (rb_hash_aref(all_children, (VALUE)child) == Qnil) { + rb_hash_aset(all_children, (VALUE)child, Qtrue); + (*iter_func)(child, data); + } + } + } + } + + for (i=0; iiseq_size;) { + VALUE insn = code[i]; + int len = insn_len(insn); + const char *types = insn_op_types(insn); + int j; + + for (j=0; types[j]; j++) { + switch (types[j]) { + case TS_ISEQ: + child = (const rb_iseq_t *)code[i+j+1]; + if (child) { + if (rb_hash_aref(all_children, (VALUE)child) == Qnil) { + rb_hash_aset(all_children, (VALUE)child, Qtrue); + (*iter_func)(child, data); + } + } + break; + default: + break; + } + } + i += len; + } + + return (int)RHASH_SIZE(all_children); +} + +static void +yield_each_children(const rb_iseq_t *child_iseq, void *data) +{ + rb_yield(iseqw_new(child_iseq)); } /* @@ -2218,8 +2235,7 @@ static VALUE iseqw_each_child(VALUE self) { const rb_iseq_t *iseq = iseqw_check(self); - VALUE all_children = rb_iseq_all_children(iseq); - rb_hash_foreach(all_children, iseqw_each_child_i, Qnil); + iseq_iterate_children(iseq, yield_each_children, NULL); return self; } @@ -2304,14 +2320,17 @@ iseqw_s_of(VALUE klass, VALUE body) rb_secure(1); if (rb_obj_is_proc(body)) { - iseq = vm_proc_iseq(body); + iseq = vm_proc_iseq(body); - if (!rb_obj_is_iseq((VALUE)iseq)) { - iseq = NULL; - } + if (!rb_obj_is_iseq((VALUE)iseq)) { + iseq = NULL; + } } - else { - iseq = rb_method_iseq(body); + else if (rb_obj_is_method(body)) { + iseq = rb_method_iseq(body); + } + else if (rb_typeddata_is_instance_of(body, &iseqw_data_type)) { + return body; } return iseq ? iseqw_new(iseq) : Qnil; @@ -2730,6 +2749,7 @@ iseq_data_to_ary(const rb_iseq_t *iseq) rb_hash_aset(misc, ID2SYM(rb_intern("arg_size")), INT2FIX(iseq_body->param.size)); rb_hash_aset(misc, ID2SYM(rb_intern("local_size")), INT2FIX(iseq_body->local_table_size)); rb_hash_aset(misc, ID2SYM(rb_intern("stack_max")), INT2FIX(iseq_body->stack_max)); + rb_hash_aset(misc, ID2SYM(rb_intern("node_id")), INT2FIX(iseq_body->location.node_id)); rb_hash_aset(misc, ID2SYM(rb_intern("code_location")), rb_ary_new_from_args(4, INT2FIX(iseq_body->location.code_location.beg_pos.lineno), @@ -2962,12 +2982,131 @@ rb_iseq_trace_flag_cleared(const rb_iseq_t *iseq, size_t pos) encoded_iseq_trace_instrument(&iseq_encoded[pos], 0); } +static int +iseq_add_local_tracepoint(const rb_iseq_t *iseq, rb_event_flag_t turnon_events, VALUE tpval, unsigned int target_line) +{ + unsigned int pc; + int n = 0; + const struct rb_iseq_constant_body *const body = iseq->body; + VALUE *iseq_encoded = (VALUE *)body->iseq_encoded; + + VM_ASSERT((iseq->flags & ISEQ_USE_COMPILE_DATA) == 0); + + for (pc=0; pciseq_size;) { + const struct iseq_insn_info_entry *entry = get_insn_info(iseq, pc); + rb_event_flag_t pc_events = entry->events; + rb_event_flag_t target_events = turnon_events; + unsigned int line = (int)entry->line_no; + + if (target_line == 0 || target_line == line) { + /* ok */ + } + else { + target_events &= ~RUBY_EVENT_LINE; + } + + if (pc_events & target_events) { + n++; + } + pc += encoded_iseq_trace_instrument(&iseq_encoded[pc], pc_events & (target_events | iseq->aux.global_trace_events)); + } + + if (n > 0) { + if (iseq->local_hooks == NULL) { + ((rb_iseq_t *)iseq)->local_hooks = RB_ZALLOC(rb_hook_list_t); + } + rb_hook_list_connect_tracepoint((VALUE)iseq, iseq->local_hooks, tpval, target_line); + } + + return n; +} + +struct trace_set_local_events_struct { + rb_event_flag_t turnon_events; + VALUE tpval; + unsigned int target_line; + int n; +}; + +static void +iseq_add_local_tracepoint_i(const rb_iseq_t *iseq, void *p) +{ + struct trace_set_local_events_struct *data = (struct trace_set_local_events_struct *)p; + data->n += iseq_add_local_tracepoint(iseq, data->turnon_events, data->tpval, data->target_line); + iseq_iterate_children(iseq, iseq_add_local_tracepoint_i, p); +} + +int +rb_iseq_add_local_tracepoint_recursively(const rb_iseq_t *iseq, rb_event_flag_t turnon_events, VALUE tpval, unsigned int target_line) +{ + struct trace_set_local_events_struct data; + data.turnon_events = turnon_events; + data.tpval = tpval; + data.target_line = target_line; + data.n = 0; + + iseq_add_local_tracepoint_i(iseq, (void *)&data); + if (0) rb_funcall(Qnil, rb_intern("puts"), 1, rb_iseq_disasm(iseq)); /* for debug */ + return data.n; +} + +static int +iseq_remove_local_tracepoint(const rb_iseq_t *iseq, VALUE tpval) +{ + int n = 0; + + if (iseq->local_hooks) { + unsigned int pc; + const struct rb_iseq_constant_body *const body = iseq->body; + VALUE *iseq_encoded = (VALUE *)body->iseq_encoded; + rb_event_flag_t local_events = 0; + + rb_hook_list_remove_tracepoint(iseq->local_hooks, tpval); + local_events = iseq->local_hooks->events; + + if (local_events == 0) { + if (iseq->local_hooks->running == 0) { + rb_hook_list_free(iseq->local_hooks); + } + ((rb_iseq_t *)iseq)->local_hooks = NULL; + } + + for (pc = 0; pciseq_size;) { + rb_event_flag_t pc_events = rb_iseq_event_flags(iseq, pc); + pc += encoded_iseq_trace_instrument(&iseq_encoded[pc], pc_events & (local_events | iseq->aux.global_trace_events)); + } + } + return n; +} + +struct trace_clear_local_events_struct { + VALUE tpval; + int n; +}; + +static void +iseq_remove_local_tracepoint_i(const rb_iseq_t *iseq, void *p) +{ + struct trace_clear_local_events_struct *data = (struct trace_clear_local_events_struct *)p; + data->n += iseq_remove_local_tracepoint(iseq, data->tpval); + iseq_iterate_children(iseq, iseq_remove_local_tracepoint_i, p); +} + +int +rb_iseq_remove_local_tracepoint_recursively(const rb_iseq_t *iseq, VALUE tpval) +{ + struct trace_clear_local_events_struct data; + data.tpval = tpval; + data.n = 0; + + iseq_remove_local_tracepoint_i(iseq, (void *)&data); + return data.n; +} + void rb_iseq_trace_set(const rb_iseq_t *iseq, rb_event_flag_t turnon_events) { - VM_ASSERT((turnon_events & ~ISEQ_TRACE_EVENTS) == 0); - - if (iseq->aux.trace_events == turnon_events) { + if (iseq->aux.global_trace_events == turnon_events) { return; } if (iseq->flags & ISEQ_USE_COMPILE_DATA) { @@ -2975,16 +3114,18 @@ rb_iseq_trace_set(const rb_iseq_t *iseq, rb_event_flag_t turnon_events) return; } else { - unsigned int i; + unsigned int pc; const struct rb_iseq_constant_body *const body = iseq->body; VALUE *iseq_encoded = (VALUE *)body->iseq_encoded; - ((rb_iseq_t *)iseq)->aux.trace_events = turnon_events; - - for (i=0; iiseq_size;) { - rb_event_flag_t events = rb_iseq_event_flags(iseq, i); - i += encoded_iseq_trace_instrument(&iseq_encoded[i], events & turnon_events); + rb_event_flag_t enabled_events; + rb_event_flag_t local_events = iseq->local_hooks ? iseq->local_hooks->events : 0; + ((rb_iseq_t *)iseq)->aux.global_trace_events = turnon_events; + enabled_events = turnon_events | local_events; + + for (pc=0; pciseq_size;) { + rb_event_flag_t pc_events = rb_iseq_event_flags(iseq, pc); + pc += encoded_iseq_trace_instrument(&iseq_encoded[pc], pc_events & enabled_events); } - /* clear for debugging: ISEQ_ORIGINAL_ISEQ_CLEAR(iseq); */ } } @@ -3009,7 +3150,7 @@ rb_iseq_trace_set_all(rb_event_flag_t turnon_events) } /* This is exported since Ruby 2.5 but not internally used for now. If you're going to use this, please - update `ruby_vm_event_enabled_flags` and set `mjit_call_p = FALSE` as well to cancel MJIT code. */ + update `ruby_vm_event_enabled_global_flags` and set `mjit_call_p = FALSE` as well to cancel MJIT code. */ void rb_iseq_trace_on_all(void) { diff --git a/iseq.h b/iseq.h index 370e16cf442794..abf93ff594f9b4 100644 --- a/iseq.h +++ b/iseq.h @@ -148,6 +148,8 @@ void rb_ibf_load_iseq_complete(rb_iseq_t *iseq); const rb_iseq_t *rb_iseq_ibf_load(VALUE str); VALUE rb_iseq_ibf_load_extra_data(VALUE str); void rb_iseq_init_trace(rb_iseq_t *iseq); +int rb_iseq_add_local_tracepoint_recursively(const rb_iseq_t *iseq, rb_event_flag_t turnon_events, VALUE tpval, unsigned int target_line); +int rb_iseq_remove_local_tracepoint_recursively(const rb_iseq_t *iseq, VALUE tpval); #if VM_INSN_INFO_TABLE_IMPL == 2 unsigned int *rb_iseq_insns_info_decode_positions(const struct rb_iseq_constant_body *body); diff --git a/lib/bundler.gemspec b/lib/bundler.gemspec new file mode 100644 index 00000000000000..2b2bb412d501ea --- /dev/null +++ b/lib/bundler.gemspec @@ -0,0 +1,64 @@ +# coding: utf-8 +# frozen_string_literal: true + +begin + require File.expand_path("../lib/bundler/version", __FILE__) +rescue LoadError + # for Ruby core repository + require File.expand_path("../bundler/version", __FILE__) +end + +require "shellwords" + +Gem::Specification.new do |s| + s.name = "bundler" + s.version = Bundler::VERSION + s.license = "MIT" + s.authors = [ + "André Arko", "Samuel Giddins", "Colby Swandale", "Hiroshi Shibata", + "David Rodríguez", "Grey Baker", "Stephanie Morillo", "Chris Morris", "James Wen", "Tim Moore", + "André Medeiros", "Jessica Lynn Suttles", "Terence Lee", "Carl Lerche", + "Yehuda Katz" + ] + s.email = ["team@bundler.io"] + s.homepage = "http://bundler.io" + s.summary = "The best way to manage your application's dependencies" + s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably" + + if s.respond_to?(:metadata=) + s.metadata = { + "bug_tracker_uri" => "http://github.com/bundler/bundler/issues", + "changelog_uri" => "https://github.com/bundler/bundler/blob/master/CHANGELOG.md", + "homepage_uri" => "https://bundler.io/", + "source_code_uri" => "http://github.com/bundler/bundler/", + } + end + + if s.version >= Gem::Version.new("2.a".dup) + s.required_ruby_version = ">= 2.3.0" + s.required_rubygems_version = ">= 2.5.0" + else + s.required_ruby_version = ">= 1.8.7" + s.required_rubygems_version = ">= 1.3.6" + end + + s.add_development_dependency "automatiek", "~> 0.1.0" + s.add_development_dependency "mustache", "0.99.6" + s.add_development_dependency "rake", "~> 10.0" + s.add_development_dependency "rdiscount", "~> 2.2" + s.add_development_dependency "ronn", "~> 0.7.3" + s.add_development_dependency "rspec", "~> 3.6" + + # s.files = `git ls-files -z`.split("\x0").select {|f| f.match(%r{^(lib|exe)/}) } + # we don't check in man pages, but we need to ship them because + # we use them to generate the long-form help for each command. + # s.files += Dir.glob("man/**/*") + # Include the CHANGELOG.md, LICENSE.md, README.md manually + # s.files += %w[CHANGELOG.md LICENSE.md README.md] + # include the gemspec itself because warbler breaks w/o it + s.files += %w[bundler.gemspec] + + s.bindir = "exe" + s.executables = %w[bundle bundler] + s.require_paths = ["lib"] +end diff --git a/lib/bundler.rb b/lib/bundler.rb new file mode 100644 index 00000000000000..cf3a289df2b40c --- /dev/null +++ b/lib/bundler.rb @@ -0,0 +1,567 @@ +# frozen_string_literal: true + +require "bundler/compatibility_guard" + +require "bundler/vendored_fileutils" +require "pathname" +require "rbconfig" +require "thread" + +require "bundler/errors" +require "bundler/environment_preserver" +require "bundler/plugin" +require "bundler/rubygems_ext" +require "bundler/rubygems_integration" +require "bundler/version" +require "bundler/constants" +require "bundler/current_ruby" +require "bundler/build_metadata" + +module Bundler + environment_preserver = EnvironmentPreserver.new(ENV, EnvironmentPreserver::BUNDLER_KEYS) + ORIGINAL_ENV = environment_preserver.restore + ENV.replace(environment_preserver.backup) + SUDO_MUTEX = Mutex.new + + autoload :Definition, "bundler/definition" + autoload :Dependency, "bundler/dependency" + autoload :DepProxy, "bundler/dep_proxy" + autoload :Deprecate, "bundler/deprecate" + autoload :Dsl, "bundler/dsl" + autoload :EndpointSpecification, "bundler/endpoint_specification" + autoload :Env, "bundler/env" + autoload :Fetcher, "bundler/fetcher" + autoload :FeatureFlag, "bundler/feature_flag" + autoload :GemHelper, "bundler/gem_helper" + autoload :GemHelpers, "bundler/gem_helpers" + autoload :GemRemoteFetcher, "bundler/gem_remote_fetcher" + autoload :GemVersionPromoter, "bundler/gem_version_promoter" + autoload :Graph, "bundler/graph" + autoload :Index, "bundler/index" + autoload :Injector, "bundler/injector" + autoload :Installer, "bundler/installer" + autoload :LazySpecification, "bundler/lazy_specification" + autoload :LockfileParser, "bundler/lockfile_parser" + autoload :MatchPlatform, "bundler/match_platform" + autoload :ProcessLock, "bundler/process_lock" + autoload :RemoteSpecification, "bundler/remote_specification" + autoload :Resolver, "bundler/resolver" + autoload :Retry, "bundler/retry" + autoload :RubyDsl, "bundler/ruby_dsl" + autoload :RubyGemsGemInstaller, "bundler/rubygems_gem_installer" + autoload :RubyVersion, "bundler/ruby_version" + autoload :Runtime, "bundler/runtime" + autoload :Settings, "bundler/settings" + autoload :SharedHelpers, "bundler/shared_helpers" + autoload :Source, "bundler/source" + autoload :SourceList, "bundler/source_list" + autoload :SpecSet, "bundler/spec_set" + autoload :StubSpecification, "bundler/stub_specification" + autoload :UI, "bundler/ui" + autoload :URICredentialsFilter, "bundler/uri_credentials_filter" + autoload :VersionRanges, "bundler/version_ranges" + + class << self + def configure + @configured ||= configure_gem_home_and_path + end + + def ui + (defined?(@ui) && @ui) || (self.ui = UI::Silent.new) + end + + def ui=(ui) + Bundler.rubygems.ui = ui ? UI::RGProxy.new(ui) : nil + @ui = ui + end + + # Returns absolute path of where gems are installed on the filesystem. + def bundle_path + @bundle_path ||= Pathname.new(configured_bundle_path.path).expand_path(root) + end + + def configured_bundle_path + @configured_bundle_path ||= settings.path.tap(&:validate!) + end + + # Returns absolute location of where binstubs are installed to. + def bin_path + @bin_path ||= begin + path = settings[:bin] || "bin" + path = Pathname.new(path).expand_path(root).expand_path + SharedHelpers.filesystem_access(path) {|p| FileUtils.mkdir_p(p) } + path + end + end + + def setup(*groups) + # Return if all groups are already loaded + return @setup if defined?(@setup) && @setup + + definition.validate_runtime! + + SharedHelpers.print_major_deprecations! + + if groups.empty? + # Load all groups, but only once + @setup = load.setup + else + load.setup(*groups) + end + end + + def require(*groups) + setup(*groups).require(*groups) + end + + def load + @load ||= Runtime.new(root, definition) + end + + def environment + SharedHelpers.major_deprecation 3, "Bundler.environment has been removed in favor of Bundler.load" + load + end + + # Returns an instance of Bundler::Definition for given Gemfile and lockfile + # + # @param unlock [Hash, Boolean, nil] Gems that have been requested + # to be updated or true if all gems should be updated + # @return [Bundler::Definition] + def definition(unlock = nil) + @definition = nil if unlock + @definition ||= begin + configure + Definition.build(default_gemfile, default_lockfile, unlock) + end + end + + def frozen_bundle? + frozen = settings[:deployment] + frozen ||= settings[:frozen] unless feature_flag.deployment_means_frozen? + frozen + end + + def locked_gems + @locked_gems ||= + if defined?(@definition) && @definition + definition.locked_gems + elsif Bundler.default_lockfile.file? + lock = Bundler.read_file(Bundler.default_lockfile) + LockfileParser.new(lock) + end + end + + def ruby_scope + "#{Bundler.rubygems.ruby_engine}/#{Bundler.rubygems.config_map[:ruby_version]}" + end + + def user_home + @user_home ||= begin + home = Bundler.rubygems.user_home + bundle_home = home ? File.join(home, ".bundle") : nil + + warning = if home.nil? + "Your home directory is not set." + elsif !File.directory?(home) + "`#{home}` is not a directory." + elsif !File.writable?(home) && (!File.directory?(bundle_home) || !File.writable?(bundle_home)) + "`#{home}` is not writable." + end + + if warning + Kernel.send(:require, "etc") + user_home = tmp_home_path(Etc.getlogin, warning) + Bundler.ui.warn "#{warning}\nBundler will use `#{user_home}' as your home directory temporarily.\n" + user_home + else + Pathname.new(home) + end + end + end + + def tmp_home_path(login, warning) + login ||= "unknown" + Kernel.send(:require, "tmpdir") + path = Pathname.new(Dir.tmpdir).join("bundler", "home") + SharedHelpers.filesystem_access(path) do |tmp_home_path| + unless tmp_home_path.exist? + tmp_home_path.mkpath + tmp_home_path.chmod(0o777) + end + tmp_home_path.join(login).tap(&:mkpath) + end + rescue RuntimeError => e + raise e.exception("#{warning}\nBundler also failed to create a temporary home directory at `#{path}':\n#{e}") + end + + def user_bundle_path(dir = "home") + env_var, fallback = case dir + when "home" + ["BUNDLE_USER_HOME", Pathname.new(user_home).join(".bundle")] + when "cache" + ["BUNDLE_USER_CACHE", user_bundle_path.join("cache")] + when "config" + ["BUNDLE_USER_CONFIG", user_bundle_path.join("config")] + when "plugin" + ["BUNDLE_USER_PLUGIN", user_bundle_path.join("plugin")] + else + raise BundlerError, "Unknown user path requested: #{dir}" + end + # `fallback` will already be a Pathname, but Pathname.new() is + # idempotent so it's OK + Pathname.new(ENV.fetch(env_var, fallback)) + end + + def user_cache + user_bundle_path("cache") + end + + def home + bundle_path.join("bundler") + end + + def install_path + home.join("gems") + end + + def specs_path + bundle_path.join("specifications") + end + + def root + @root ||= begin + SharedHelpers.root + rescue GemfileNotFound + bundle_dir = default_bundle_dir + raise GemfileNotFound, "Could not locate Gemfile or .bundle/ directory" unless bundle_dir + Pathname.new(File.expand_path("..", bundle_dir)) + end + end + + def app_config_path + if app_config = ENV["BUNDLE_APP_CONFIG"] + Pathname.new(app_config).expand_path(root) + else + root.join(".bundle") + end + end + + def app_cache(custom_path = nil) + path = custom_path || root + Pathname.new(path).join(settings.app_cache_path) + end + + def tmp(name = Process.pid.to_s) + Kernel.send(:require, "tmpdir") + Pathname.new(Dir.mktmpdir(["bundler", name])) + end + + def rm_rf(path) + FileUtils.remove_entry_secure(path) if path && File.exist?(path) + rescue ArgumentError + message = < e + raise MarshalError, "#{e.class}: #{e.message}" + end + + def load_gemspec(file, validate = false) + @gemspec_cache ||= {} + key = File.expand_path(file) + @gemspec_cache[key] ||= load_gemspec_uncached(file, validate) + # Protect against caching side-effected gemspecs by returning a + # new instance each time. + @gemspec_cache[key].dup if @gemspec_cache[key] + end + + def load_gemspec_uncached(file, validate = false) + path = Pathname.new(file) + contents = read_file(file) + spec = if contents.start_with?("---") # YAML header + eval_yaml_gemspec(path, contents) + else + # Eval the gemspec from its parent directory, because some gemspecs + # depend on "./" relative paths. + SharedHelpers.chdir(path.dirname.to_s) do + eval_gemspec(path, contents) + end + end + return unless spec + spec.loaded_from = path.expand_path.to_s + Bundler.rubygems.validate(spec) if validate + spec + end + + def clear_gemspec_cache + @gemspec_cache = {} + end + + def git_present? + return @git_present if defined?(@git_present) + @git_present = Bundler.which("git") || Bundler.which("git.exe") + end + + def feature_flag + @feature_flag ||= FeatureFlag.new(VERSION) + end + + def reset! + reset_paths! + Plugin.reset! + reset_rubygems! + end + + def reset_paths! + @bin_path = nil + @bundler_major_version = nil + @bundle_path = nil + @configured = nil + @configured_bundle_path = nil + @definition = nil + @load = nil + @locked_gems = nil + @root = nil + @settings = nil + @setup = nil + @user_home = nil + end + + def reset_rubygems! + return unless defined?(@rubygems) && @rubygems + rubygems.undo_replacements + rubygems.reset + @rubygems = nil + end + + private + + def eval_yaml_gemspec(path, contents) + Kernel.send(:require, "bundler/psyched_yaml") + + # If the YAML is invalid, Syck raises an ArgumentError, and Psych + # raises a Psych::SyntaxError. See psyched_yaml.rb for more info. + Gem::Specification.from_yaml(contents) + rescue YamlLibrarySyntaxError, ArgumentError, Gem::EndOfYAMLException, Gem::Exception + eval_gemspec(path, contents) + end + + def eval_gemspec(path, contents) + eval(contents, TOPLEVEL_BINDING.dup, path.expand_path.to_s) + rescue ScriptError, StandardError => e + msg = "There was an error while loading `#{path.basename}`: #{e.message}" + + if e.is_a?(LoadError) && RUBY_VERSION >= "1.9" + msg += "\nDoes it try to require a relative path? That's been removed in Ruby 1.9" + end + + raise GemspecError, Dsl::DSLError.new(msg, path, e.backtrace, contents) + end + + def configure_gem_home_and_path + configure_gem_path + configure_gem_home + bundle_path + end + + def configure_gem_path(env = ENV) + blank_home = env["GEM_HOME"].nil? || env["GEM_HOME"].empty? + if !use_system_gems? + # this needs to be empty string to cause + # PathSupport.split_gem_path to only load up the + # Bundler --path setting as the GEM_PATH. + env["GEM_PATH"] = "" + elsif blank_home + possibles = [Bundler.rubygems.gem_dir, Bundler.rubygems.gem_path] + paths = possibles.flatten.compact.uniq.reject(&:empty?) + env["GEM_PATH"] = paths.join(File::PATH_SEPARATOR) + end + end + + def configure_gem_home + Bundler::SharedHelpers.set_env "GEM_HOME", File.expand_path(bundle_path, root) + Bundler.rubygems.clear_paths + end + + # @param env [Hash] + def with_env(env) + backup = ENV.to_hash + ENV.replace(env) + yield + ensure + ENV.replace(backup) + end + end +end diff --git a/lib/bundler/build_metadata.rb b/lib/bundler/build_metadata.rb new file mode 100644 index 00000000000000..33f91e91622a57 --- /dev/null +++ b/lib/bundler/build_metadata.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Bundler + # Represents metadata from when the Bundler gem was built. + module BuildMetadata + # begin ivars + @release = false + # end ivars + + # A hash representation of the build metadata. + def self.to_h + { + "Built At" => built_at, + "Git SHA" => git_commit_sha, + "Released Version" => release?, + } + end + + # A string representing the date the bundler gem was built. + def self.built_at + @built_at ||= Time.now.utc.strftime("%Y-%m-%d").freeze + end + + # The SHA for the git commit the bundler gem was built from. + def self.git_commit_sha + return @git_commit_sha if @git_commit_sha + + # If Bundler has been installed without its .git directory and without a + # commit instance variable then we can't determine its commits SHA. + git_dir = File.join(File.expand_path("../../..", __FILE__), ".git") + if File.directory?(git_dir) + return @git_commit_sha = Dir.chdir(git_dir) { `git rev-parse --short HEAD`.strip.freeze } + end + + # If Bundler is a submodule in RubyGems, get the submodule commit + git_sub_dir = File.join(File.expand_path("../../../..", __FILE__), ".git") + if File.directory?(git_sub_dir) + return @git_commit_sha = Dir.chdir(git_sub_dir) do + `git ls-tree --abbrev=8 HEAD bundler`.split(/\s/).fetch(2, "").strip.freeze + end + end + + @git_commit_sha ||= "unknown" + end + + # Whether this is an official release build of Bundler. + def self.release? + @release + end + end +end diff --git a/lib/bundler/capistrano.rb b/lib/bundler/capistrano.rb new file mode 100644 index 00000000000000..40e2e5dbe8b9e8 --- /dev/null +++ b/lib/bundler/capistrano.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "bundler/shared_helpers" +Bundler::SharedHelpers.major_deprecation 3, + "The Bundler task for Capistrano. Please use http://github.com/capistrano/bundler" + +# Capistrano task for Bundler. +# +# Add "require 'bundler/capistrano'" in your Capistrano deploy.rb, and +# Bundler will be activated after each new deployment. +require "bundler/deployment" +require "capistrano/version" + +if defined?(Capistrano::Version) && Gem::Version.new(Capistrano::Version).release >= Gem::Version.new("3.0") + raise "For Capistrano 3.x integration, please use http://github.com/capistrano/bundler" +end + +Capistrano::Configuration.instance(:must_exist).load do + before "deploy:finalize_update", "bundle:install" + Bundler::Deployment.define_task(self, :task, :except => { :no_release => true }) + set :rake, lambda { "#{fetch(:bundle_cmd, "bundle")} exec rake" } +end diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb new file mode 100644 index 00000000000000..3efe1936130c81 --- /dev/null +++ b/lib/bundler/cli.rb @@ -0,0 +1,790 @@ +# frozen_string_literal: true + +require "bundler" +require "bundler/vendored_thor" + +module Bundler + class CLI < Thor + require "bundler/cli/common" + + package_name "Bundler" + + AUTO_INSTALL_CMDS = %w[show binstubs outdated exec open console licenses clean].freeze + PARSEABLE_COMMANDS = %w[ + check config help exec platform show version + ].freeze + + def self.start(*) + super + rescue Exception => e + Bundler.ui = UI::Shell.new + raise e + ensure + Bundler::SharedHelpers.print_major_deprecations! + end + + def self.dispatch(*) + super do |i| + i.send(:print_command) + i.send(:warn_on_outdated_bundler) + end + end + + def initialize(*args) + super + + custom_gemfile = options[:gemfile] || Bundler.settings[:gemfile] + if custom_gemfile && !custom_gemfile.empty? + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", File.expand_path(custom_gemfile) + Bundler.reset_paths! + end + + Bundler.settings.set_command_option_if_given :retry, options[:retry] + + current_cmd = args.last[:current_command].name + auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) + rescue UnknownArgumentError => e + raise InvalidOption, e.message + ensure + self.options ||= {} + unprinted_warnings = Bundler.ui.unprinted_warnings + Bundler.ui = UI::Shell.new(options) + Bundler.ui.level = "debug" if options["verbose"] + unprinted_warnings.each {|w| Bundler.ui.warn(w) } + + if ENV["RUBYGEMS_GEMDEPS"] && !ENV["RUBYGEMS_GEMDEPS"].empty? + Bundler.ui.warn( + "The RUBYGEMS_GEMDEPS environment variable is set. This enables RubyGems' " \ + "experimental Gemfile mode, which may conflict with Bundler and cause unexpected errors. " \ + "To remove this warning, unset RUBYGEMS_GEMDEPS.", :wrap => true + ) + end + end + + def self.deprecated_option(*args, &blk) + return if Bundler.feature_flag.forget_cli_options? + method_option(*args, &blk) + end + + check_unknown_options!(:except => [:config, :exec]) + stop_on_unknown_option! :exec + + desc "cli_help", "Prints a summary of bundler commands", :hide => true + def cli_help + version + Bundler.ui.info "\n" + + primary_commands = ["install", "update", + Bundler.feature_flag.cache_command_is_package? ? "cache" : "package", + "exec", "config", "help"] + + list = self.class.printable_commands(true) + by_name = list.group_by {|name, _message| name.match(/^bundle (\w+)/)[1] } + utilities = by_name.keys.sort - primary_commands + primary_commands.map! {|name| (by_name[name] || raise("no primary command #{name}")).first } + utilities.map! {|name| by_name[name].first } + + shell.say "Bundler commands:\n\n" + + shell.say " Primary commands:\n" + shell.print_table(primary_commands, :indent => 4, :truncate => true) + shell.say + shell.say " Utilities:\n" + shell.print_table(utilities, :indent => 4, :truncate => true) + shell.say + self.class.send(:class_options_help, shell) + end + default_task(Bundler.feature_flag.default_cli_command) + + class_option "no-color", :type => :boolean, :desc => "Disable colorization in output" + class_option "retry", :type => :numeric, :aliases => "-r", :banner => "NUM", + :desc => "Specify the number of times you wish to attempt network commands" + class_option "verbose", :type => :boolean, :desc => "Enable verbose output mode", :aliases => "-V" + + def help(cli = nil) + case cli + when "gemfile" then command = "gemfile" + when nil then command = "bundle" + else command = "bundle-#{cli}" + end + + man_path = File.expand_path("../../../man", __FILE__) + man_pages = Hash[Dir.glob(File.join(man_path, "*")).grep(/.*\.\d*\Z/).collect do |f| + [File.basename(f, ".*"), f] + end] + + if man_pages.include?(command) + if Bundler.which("man") && man_path !~ %r{^file:/.+!/META-INF/jruby.home/.+} + Kernel.exec "man #{man_pages[command]}" + else + puts File.read("#{man_path}/#{File.basename(man_pages[command])}.txt") + end + elsif command_path = Bundler.which("bundler-#{cli}") + Kernel.exec(command_path, "--help") + else + super + end + end + + def self.handle_no_command_error(command, has_namespace = $thor_runner) + if Bundler.feature_flag.plugins? && Bundler::Plugin.command?(command) + return Bundler::Plugin.exec_command(command, ARGV[1..-1]) + end + + return super unless command_path = Bundler.which("bundler-#{command}") + + Kernel.exec(command_path, *ARGV[1..-1]) + end + + desc "init [OPTIONS]", "Generates a Gemfile into the current working directory" + long_desc <<-D + Init generates a default Gemfile in the current working directory. When adding a + Gemfile to a gem with a gemspec, the --gemspec option will automatically add each + dependency listed in the gemspec file to the newly created Gemfile. + D + deprecated_option "gemspec", :type => :string, :banner => "Use the specified .gemspec to create the Gemfile" + def init + require "bundler/cli/init" + Init.new(options.dup).run + end + + desc "check [OPTIONS]", "Checks if the dependencies listed in Gemfile are satisfied by currently installed gems" + long_desc <<-D + Check searches the local machine for each of the gems requested in the Gemfile. If + all gems are found, Bundler prints a success message and exits with a status of 0. + If not, the first missing gem is listed and Bundler exits status 1. + D + method_option "dry-run", :type => :boolean, :default => false, :banner => + "Lock the Gemfile" + method_option "gemfile", :type => :string, :banner => + "Use the specified gemfile instead of Gemfile" + method_option "path", :type => :string, :banner => + "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME).#{" Bundler will remember this value for future installs on this machine" unless Bundler.feature_flag.forget_cli_options?}" + map "c" => "check" + def check + require "bundler/cli/check" + Check.new(options).run + end + + desc "remove [GEM [GEM ...]]", "Removes gems from the Gemfile" + long_desc <<-D + Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid. If the gem is not found, Bundler prints a error message and if gem could not be removed due to any reason Bundler will display a warning. + D + method_option "install", :type => :boolean, :banner => + "Runs 'bundle install' after removing the gems from the Gemfile" + def remove(*gems) + require "bundler/cli/remove" + Remove.new(gems, options).run + end + + desc "install [OPTIONS]", "Install the current environment to the system" + long_desc <<-D + Install will install all of the gems in the current bundle, making them available + for use. In a freshly checked out repository, this command will give you the same + gem versions as the last person who updated the Gemfile and ran `bundle update`. + + Passing [DIR] to install (e.g. vendor) will cause the unpacked gems to be installed + into the [DIR] directory rather than into system gems. + + If the bundle has already been installed, bundler will tell you so and then exit. + D + deprecated_option "binstubs", :type => :string, :lazy_default => "bin", :banner => + "Generate bin stubs for bundled gems to ./bin" + deprecated_option "clean", :type => :boolean, :banner => + "Run bundle clean automatically after install" + deprecated_option "deployment", :type => :boolean, :banner => + "Install using defaults tuned for deployment environments" + deprecated_option "frozen", :type => :boolean, :banner => + "Do not allow the Gemfile.lock to be updated after this install" + method_option "full-index", :type => :boolean, :banner => + "Fall back to using the single-file index of all gems" + method_option "gemfile", :type => :string, :banner => + "Use the specified gemfile instead of Gemfile" + method_option "jobs", :aliases => "-j", :type => :numeric, :banner => + "Specify the number of jobs to run in parallel" + method_option "local", :type => :boolean, :banner => + "Do not attempt to fetch gems remotely and use the gem cache instead" + deprecated_option "no-cache", :type => :boolean, :banner => + "Don't update the existing gem cache." + method_option "redownload", :type => :boolean, :aliases => "--force", :banner => + "Force downloading every gem." + deprecated_option "no-prune", :type => :boolean, :banner => + "Don't remove stale gems from the cache." + deprecated_option "path", :type => :string, :banner => + "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). Bundler will remember this value for future installs on this machine" + method_option "quiet", :type => :boolean, :banner => + "Only output warnings and errors." + deprecated_option "shebang", :type => :string, :banner => + "Specify a different shebang executable name than the default (usually 'ruby')" + method_option "standalone", :type => :array, :lazy_default => [], :banner => + "Make a bundle that can work without the Bundler runtime" + deprecated_option "system", :type => :boolean, :banner => + "Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application" + method_option "trust-policy", :alias => "P", :type => :string, :banner => + "Gem trust policy (like gem install -P). Must be one of " + + Bundler.rubygems.security_policy_keys.join("|") + deprecated_option "without", :type => :array, :banner => + "Exclude gems that are part of the specified named group." + deprecated_option "with", :type => :array, :banner => + "Include gems that are part of the specified named group." + map "i" => "install" + def install + SharedHelpers.major_deprecation(3, "The `--force` option has been renamed to `--redownload`") if ARGV.include?("--force") + require "bundler/cli/install" + Bundler.settings.temporary(:no_install => false) do + Install.new(options.dup).run + end + end + + desc "update [OPTIONS]", "Update the current environment" + long_desc <<-D + Update will install the newest versions of the gems listed in the Gemfile. Use + update when you have changed the Gemfile, or if you want to get the newest + possible versions of the gems in the bundle. + D + method_option "full-index", :type => :boolean, :banner => + "Fall back to using the single-file index of all gems" + method_option "gemfile", :type => :string, :banner => + "Use the specified gemfile instead of Gemfile" + method_option "group", :aliases => "-g", :type => :array, :banner => + "Update a specific group" + method_option "jobs", :aliases => "-j", :type => :numeric, :banner => + "Specify the number of jobs to run in parallel" + method_option "local", :type => :boolean, :banner => + "Do not attempt to fetch gems remotely and use the gem cache instead" + method_option "quiet", :type => :boolean, :banner => + "Only output warnings and errors." + method_option "source", :type => :array, :banner => + "Update a specific source (and all gems associated with it)" + method_option "redownload", :type => :boolean, :aliases => "--force", :banner => + "Force downloading every gem." + method_option "ruby", :type => :boolean, :banner => + "Update ruby specified in Gemfile.lock" + method_option "bundler", :type => :string, :lazy_default => "> 0.a", :banner => + "Update the locked version of bundler" + method_option "patch", :type => :boolean, :banner => + "Prefer updating only to next patch version" + method_option "minor", :type => :boolean, :banner => + "Prefer updating only to next minor version" + method_option "major", :type => :boolean, :banner => + "Prefer updating to next major version (default)" + method_option "strict", :type => :boolean, :banner => + "Do not allow any gem to be updated past latest --patch | --minor | --major" + method_option "conservative", :type => :boolean, :banner => + "Use bundle install conservative update behavior and do not allow shared dependencies to be updated." + method_option "all", :type => :boolean, :banner => + "Update everything." + def update(*gems) + SharedHelpers.major_deprecation(3, "The `--force` option has been renamed to `--redownload`") if ARGV.include?("--force") + require "bundler/cli/update" + Update.new(options, gems).run + end + + desc "show GEM [OPTIONS]", "Shows all gems that are part of the bundle, or the path to a given gem" + long_desc <<-D + Show lists the names and versions of all gems that are required by your Gemfile. + Calling show with [GEM] will list the exact location of that gem on your machine. + D + method_option "paths", :type => :boolean, + :banner => "List the paths of all gems that are required by your Gemfile." + method_option "outdated", :type => :boolean, + :banner => "Show verbose output including whether gems are outdated." + def show(gem_name = nil) + if ARGV[0] == "show" + rest = ARGV[1..-1] + + new_command = rest.find {|arg| !arg.start_with?("--") } ? "info" : "list" + + new_arguments = rest.map do |arg| + next arg if arg != "--paths" + next "--path" if new_command == "info" + end + + old_argv = ARGV.join(" ") + new_argv = [new_command, *new_arguments.compact].join(" ") + + Bundler::SharedHelpers.major_deprecation(3, "use `bundle #{new_argv}` instead of `bundle #{old_argv}`") + end + require "bundler/cli/show" + Show.new(options, gem_name).run + end + # TODO: 2.0 remove `bundle show` + + if Bundler.feature_flag.list_command? + desc "list", "List all gems in the bundle" + method_option "name-only", :type => :boolean, :banner => "print only the gem names" + method_option "only-group", :type => :string, :banner => "print gems from a particular group" + method_option "without-group", :type => :string, :banner => "print all gems expect from a group" + method_option "paths", :type => :boolean, :banner => "print the path to each gem in the bundle" + def list + require "bundler/cli/list" + List.new(options).run + end + + map %w[ls] => "list" + else + map %w[list] => "show" + end + + desc "info GEM [OPTIONS]", "Show information for the given gem" + method_option "path", :type => :boolean, :banner => "Print full path to gem" + def info(gem_name) + require "bundler/cli/info" + Info.new(options, gem_name).run + end + + desc "binstubs GEM [OPTIONS]", "Install the binstubs of the listed gem" + long_desc <<-D + Generate binstubs for executables in [GEM]. Binstubs are put into bin, + or the --binstubs directory if one has been set. Calling binstubs with [GEM [GEM]] + will create binstubs for all given gems. + D + method_option "force", :type => :boolean, :default => false, :banner => + "Overwrite existing binstubs if they exist" + method_option "path", :type => :string, :lazy_default => "bin", :banner => + "Binstub destination directory (default bin)" + method_option "shebang", :type => :string, :banner => + "Specify a different shebang executable name than the default (usually 'ruby')" + method_option "standalone", :type => :boolean, :banner => + "Make binstubs that can work without the Bundler runtime" + method_option "all", :type => :boolean, :banner => + "Install binstubs for all gems" + def binstubs(*gems) + require "bundler/cli/binstubs" + Binstubs.new(options, gems).run + end + + desc "add GEM VERSION", "Add gem to Gemfile and run bundle install" + long_desc <<-D + Adds the specified gem to Gemfile (if valid) and run 'bundle install' in one step. + D + method_option "version", :aliases => "-v", :type => :string + method_option "group", :aliases => "-g", :type => :string + method_option "source", :aliases => "-s", :type => :string + method_option "skip-install", :type => :boolean, :banner => + "Adds gem to the Gemfile but does not install it" + method_option "optimistic", :type => :boolean, :banner => "Adds optimistic declaration of version to gem" + method_option "strict", :type => :boolean, :banner => "Adds strict declaration of version to gem" + def add(*gems) + require "bundler/cli/add" + Add.new(options.dup, gems).run + end + + desc "outdated GEM [OPTIONS]", "List installed gems with newer versions available" + long_desc <<-D + Outdated lists the names and versions of gems that have a newer version available + in the given source. Calling outdated with [GEM [GEM]] will only check for newer + versions of the given gems. Prerelease gems are ignored by default. If your gems + are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. + + For more information on patch level options (--major, --minor, --patch, + --update-strict) see documentation on the same options on the update command. + D + method_option "group", :type => :string, :banner => "List gems from a specific group" + method_option "groups", :type => :boolean, :banner => "List gems organized by groups" + method_option "local", :type => :boolean, :banner => + "Do not attempt to fetch gems remotely and use the gem cache instead" + method_option "pre", :type => :boolean, :banner => "Check for newer pre-release gems" + method_option "source", :type => :array, :banner => "Check against a specific source" + method_option "strict", :type => :boolean, :banner => + "Only list newer versions allowed by your Gemfile requirements" + method_option "update-strict", :type => :boolean, :banner => + "Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor | --major" + method_option "minor", :type => :boolean, :banner => "Prefer updating only to next minor version" + method_option "major", :type => :boolean, :banner => "Prefer updating to next major version (default)" + method_option "patch", :type => :boolean, :banner => "Prefer updating only to next patch version" + method_option "filter-major", :type => :boolean, :banner => "Only list major newer versions" + method_option "filter-minor", :type => :boolean, :banner => "Only list minor newer versions" + method_option "filter-patch", :type => :boolean, :banner => "Only list patch newer versions" + method_option "parseable", :aliases => "--porcelain", :type => :boolean, :banner => + "Use minimal formatting for more parseable output" + method_option "only-explicit", :type => :boolean, :banner => + "Only list gems specified in your Gemfile, not their dependencies" + def outdated(*gems) + require "bundler/cli/outdated" + Outdated.new(options, gems).run + end + + if Bundler.feature_flag.cache_command_is_package? + map %w[cache] => :package + else + desc "cache [OPTIONS]", "Cache all the gems to vendor/cache", :hide => true + unless Bundler.feature_flag.cache_command_is_package? + method_option "all", :type => :boolean, + :banner => "Include all sources (including path and git)." + end + method_option "all-platforms", :type => :boolean, :banner => "Include gems for all platforms present in the lockfile, not only the current one" + method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." + def cache + require "bundler/cli/cache" + Cache.new(options).run + end + end + + desc "#{Bundler.feature_flag.cache_command_is_package? ? :cache : :package} [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" + unless Bundler.feature_flag.cache_command_is_package? + method_option "all", :type => :boolean, + :banner => "Include all sources (including path and git)." + end + method_option "all-platforms", :type => :boolean, :banner => "Include gems for all platforms present in the lockfile, not only the current one" + method_option "cache-path", :type => :string, :banner => + "Specify a different cache path than the default (vendor/cache)." + method_option "gemfile", :type => :string, :banner => "Use the specified gemfile instead of Gemfile" + method_option "no-install", :type => :boolean, :banner => "Don't install the gems, only the package." + method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." + method_option "path", :type => :string, :banner => + "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). Bundler will remember this value for future installs on this machine" + method_option "quiet", :type => :boolean, :banner => "Only output warnings and errors." + method_option "frozen", :type => :boolean, :banner => + "Do not allow the Gemfile.lock to be updated after this package operation's install" + long_desc <<-D + The package command will copy the .gem files for every gem in the bundle into the + directory ./vendor/cache. If you then check that directory into your source + control repository, others who check out your source will be able to install the + bundle without having to download any additional gems. + D + def package + require "bundler/cli/package" + Package.new(options).run + end + map %w[pack] => :package + + desc "exec [OPTIONS]", "Run the command in context of the bundle" + method_option :keep_file_descriptors, :type => :boolean, :default => false + method_option :gemfile, :type => :string, :required => false + long_desc <<-D + Exec runs a command, providing it access to the gems in the bundle. While using + bundle exec you can require and call the bundled gems as if they were installed + into the system wide RubyGems repository. + D + map "e" => "exec" + def exec(*args) + require "bundler/cli/exec" + Exec.new(options, args).run + end + + desc "config NAME [VALUE]", "Retrieve or set a configuration value" + long_desc <<-D + Retrieves or sets a configuration value. If only one parameter is provided, retrieve the value. If two parameters are provided, replace the + existing value with the newly provided one. + + By default, setting a configuration value sets it for all projects + on the machine. + + If a global setting is superceded by local configuration, this command + will show the current value, as well as any superceded values and + where they were specified. + D + method_option "parseable", :type => :boolean, :banner => "Use minimal formatting for more parseable output" + def config(*args) + require "bundler/cli/config" + Config.new(options, args, self).run + end + + desc "open GEM", "Opens the source directory of the given bundled gem" + def open(name) + require "bundler/cli/open" + Open.new(options, name).run + end + + if Bundler.feature_flag.console_command? + desc "console [GROUP]", "Opens an IRB session with the bundle pre-loaded" + def console(group = nil) + require "bundler/cli/console" + Console.new(options, group).run + end + end + + desc "version", "Prints the bundler's version information" + def version + cli_help = current_command.name == "cli_help" + if cli_help || ARGV.include?("version") + build_info = " (#{BuildMetadata.built_at} commit #{BuildMetadata.git_commit_sha})" + end + + if !cli_help && Bundler.feature_flag.print_only_version_number? + Bundler.ui.info "#{Bundler::VERSION}#{build_info}" + else + Bundler.ui.info "Bundler version #{Bundler::VERSION}#{build_info}" + end + end + map %w[-v --version] => :version + + desc "licenses", "Prints the license of all gems in the bundle" + def licenses + Bundler.load.specs.sort_by {|s| s.license.to_s }.reverse_each do |s| + gem_name = s.name + license = s.license || s.licenses + + if license.empty? + Bundler.ui.warn "#{gem_name}: Unknown" + else + Bundler.ui.info "#{gem_name}: #{license}" + end + end + end + + if Bundler.feature_flag.viz_command? + desc "viz [OPTIONS]", "Generates a visual dependency graph", :hide => true + long_desc <<-D + Viz generates a PNG file of the current Gemfile as a dependency graph. + Viz requires the ruby-graphviz gem (and its dependencies). + The associated gems must also be installed via 'bundle install'. + D + method_option :file, :type => :string, :default => "gem_graph", :aliases => "-f", :desc => "The name to use for the generated file. see format option" + method_option :format, :type => :string, :default => "png", :aliases => "-F", :desc => "This is output format option. Supported format is png, jpg, svg, dot ..." + method_option :requirements, :type => :boolean, :default => false, :aliases => "-R", :desc => "Set to show the version of each required dependency." + method_option :version, :type => :boolean, :default => false, :aliases => "-v", :desc => "Set to show each gem version." + method_option :without, :type => :array, :default => [], :aliases => "-W", :banner => "GROUP[ GROUP...]", :desc => "Exclude gems that are part of the specified named group." + def viz + SharedHelpers.major_deprecation 3, "The `viz` command has been moved to the `bundle-viz` gem, see https://github.com/bundler/bundler-viz" + require "bundler/cli/viz" + Viz.new(options.dup).run + end + end + + old_gem = instance_method(:gem) + + desc "gem NAME [OPTIONS]", "Creates a skeleton for creating a rubygem" + method_option :exe, :type => :boolean, :default => false, :aliases => ["--bin", "-b"], :desc => "Generate a binary executable for your library." + method_option :coc, :type => :boolean, :desc => "Generate a code of conduct file. Set a default with `bundle config gem.coc true`." + method_option :edit, :type => :string, :aliases => "-e", :required => false, :banner => "EDITOR", + :lazy_default => [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }, + :desc => "Open generated gemspec in the specified editor (defaults to $EDITOR or $BUNDLER_EDITOR)" + method_option :ext, :type => :boolean, :default => false, :desc => "Generate the boilerplate for C extension code" + method_option :mit, :type => :boolean, :desc => "Generate an MIT license file. Set a default with `bundle config gem.mit true`." + method_option :test, :type => :string, :lazy_default => "rspec", :aliases => "-t", :banner => "rspec", + :desc => "Generate a test directory for your library, either rspec or minitest. Set a default with `bundle config gem.test rspec`." + def gem(name) + end + + commands["gem"].tap do |gem_command| + def gem_command.run(instance, args = []) + arity = 1 # name + + require "bundler/cli/gem" + cmd_args = args + [instance] + cmd_args.unshift(instance.options) + + cmd = begin + Gem.new(*cmd_args) + rescue ArgumentError => e + instance.class.handle_argument_error(self, e, args, arity) + end + + cmd.run + end + end + + undef_method(:gem) + define_method(:gem, old_gem) + private :gem + + def self.source_root + File.expand_path(File.join(File.dirname(__FILE__), "templates")) + end + + desc "clean [OPTIONS]", "Cleans up unused gems in your bundler directory", :hide => true + method_option "dry-run", :type => :boolean, :default => false, :banner => + "Only print out changes, do not clean gems" + method_option "force", :type => :boolean, :default => false, :banner => + "Forces clean even if --path is not set" + def clean + require "bundler/cli/clean" + Clean.new(options.dup).run + end + + desc "platform [OPTIONS]", "Displays platform compatibility information" + method_option "ruby", :type => :boolean, :default => false, :banner => + "only display ruby related platform information" + def platform + require "bundler/cli/platform" + Platform.new(options).run + end + + desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile", :hide => true + method_option "source", :type => :string, :banner => + "Install gem from the given source" + method_option "group", :type => :string, :banner => + "Install gem into a bundler group" + def inject(name, version) + SharedHelpers.major_deprecation 3, "The `inject` command has been replaced by the `add` command" + require "bundler/cli/inject" + Inject.new(options.dup, name, version).run + end + + desc "lock", "Creates a lockfile without installing" + method_option "update", :type => :array, :lazy_default => true, :banner => + "ignore the existing lockfile, update all gems by default, or update list of given gems" + method_option "local", :type => :boolean, :default => false, :banner => + "do not attempt to fetch remote gemspecs and use the local gem cache only" + method_option "print", :type => :boolean, :default => false, :banner => + "print the lockfile to STDOUT instead of writing to the file system" + method_option "lockfile", :type => :string, :default => nil, :banner => + "the path the lockfile should be written to" + method_option "full-index", :type => :boolean, :default => false, :banner => + "Fall back to using the single-file index of all gems" + method_option "add-platform", :type => :array, :default => [], :banner => + "Add a new platform to the lockfile" + method_option "remove-platform", :type => :array, :default => [], :banner => + "Remove a platform from the lockfile" + method_option "patch", :type => :boolean, :banner => + "If updating, prefer updating only to next patch version" + method_option "minor", :type => :boolean, :banner => + "If updating, prefer updating only to next minor version" + method_option "major", :type => :boolean, :banner => + "If updating, prefer updating to next major version (default)" + method_option "strict", :type => :boolean, :banner => + "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" + method_option "conservative", :type => :boolean, :banner => + "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" + def lock + require "bundler/cli/lock" + Lock.new(options).run + end + + desc "env", "Print information about the environment Bundler is running under" + def env + Env.write($stdout) + end + + desc "doctor [OPTIONS]", "Checks the bundle for common problems" + long_desc <<-D + Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If + missing dependencies are detected, Bundler prints them and exits status 1. + Otherwise, Bundler prints a success message and exits with a status of 0. + D + method_option "gemfile", :type => :string, :banner => + "Use the specified gemfile instead of Gemfile" + method_option "quiet", :type => :boolean, :banner => + "Only output warnings and errors." + def doctor + require "bundler/cli/doctor" + Doctor.new(options).run + end + + desc "issue", "Learn how to report an issue in Bundler" + def issue + require "bundler/cli/issue" + Issue.new.run + end + + desc "pristine [GEMS...]", "Restores installed gems to pristine condition" + long_desc <<-D + Restores installed gems to pristine condition from files located in the + gem cache. Gems installed from a git repository will be issued `git + checkout --force`. + D + def pristine(*gems) + require "bundler/cli/pristine" + Pristine.new(gems).run + end + + if Bundler.feature_flag.plugins? + require "bundler/cli/plugin" + desc "plugin", "Manage the bundler plugins" + subcommand "plugin", Plugin + end + + # Reformat the arguments passed to bundle that include a --help flag + # into the corresponding `bundle help #{command}` call + def self.reformatted_help_args(args) + bundler_commands = all_commands.keys + help_flags = %w[--help -h] + exec_commands = %w[e ex exe exec] + help_used = args.index {|a| help_flags.include? a } + exec_used = args.index {|a| exec_commands.include? a } + command = args.find {|a| bundler_commands.include? a } + if exec_used && help_used + if exec_used + help_used == 1 + %w[help exec] + else + args + end + elsif help_used + args = args.dup + args.delete_at(help_used) + ["help", command || args].flatten.compact + else + args + end + end + + private + + # Automatically invoke `bundle install` and resume if + # Bundler.settings[:auto_install] exists. This is set through config cmd + # `bundle config auto_install 1`. + # + # Note that this method `nil`s out the global Definition object, so it + # should be called first, before you instantiate anything like an + # `Installer` that'll keep a reference to the old one instead. + def auto_install + return unless Bundler.settings[:auto_install] + + begin + Bundler.definition.specs + rescue GemNotFound + Bundler.ui.info "Automatically installing missing gems." + Bundler.reset! + invoke :install, [] + Bundler.reset! + end + end + + def current_command + _, _, config = @_initializer + config[:current_command] + end + + def print_command + return unless Bundler.ui.debug? + cmd = current_command + command_name = cmd.name + return if PARSEABLE_COMMANDS.include?(command_name) + command = ["bundle", command_name] + args + options_to_print = options.dup + options_to_print.delete_if do |k, v| + next unless o = cmd.options[k] + o.default == v + end + command << Thor::Options.to_switches(options_to_print.sort_by(&:first)).strip + command.reject!(&:empty?) + Bundler.ui.info "Running `#{command * " "}` with bundler #{Bundler::VERSION}" + end + + def warn_on_outdated_bundler + return if Bundler.settings[:disable_version_check] + + command_name = current_command.name + return if PARSEABLE_COMMANDS.include?(command_name) + + return unless SharedHelpers.md5_available? + + latest = Fetcher::CompactIndex. + new(nil, Source::Rubygems::Remote.new(URI("https://rubygems.org")), nil). + send(:compact_index_client). + instance_variable_get(:@cache). + dependencies("bundler"). + map {|d| Gem::Version.new(d.first) }. + max + return unless latest + + current = Gem::Version.new(VERSION) + return if current >= latest + latest_installed = Bundler.rubygems.find_name("bundler").map(&:version).max + + installation = "To install the latest version, run `gem install bundler#{" --pre" if latest.prerelease?}`" + if latest_installed && latest_installed > current + suggestion = "To update to the most recent installed version (#{latest_installed}), run `bundle update --bundler`" + suggestion = "#{installation}\n#{suggestion}" if latest_installed < latest + else + suggestion = installation + end + + Bundler.ui.warn "The latest bundler is #{latest}, but you are currently running #{current}.\n#{suggestion}" + rescue RuntimeError + nil + end + end +end diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb new file mode 100644 index 00000000000000..9709e71be0456a --- /dev/null +++ b/lib/bundler/cli/add.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Add + def initialize(options, gems) + @gems = gems + @options = options + @options[:group] = @options[:group].split(",").map(&:strip) if !@options[:group].nil? && !@options[:group].empty? + end + + def run + raise InvalidOption, "You can not specify `--strict` and `--optimistic` at the same time." if @options[:strict] && @options[:optimistic] + + # raise error when no gems are specified + raise InvalidOption, "Please specify gems to add." if @gems.empty? + + version = @options[:version].nil? ? nil : @options[:version].split(",").map(&:strip) + + unless version.nil? + version.each do |v| + raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN =~ v.to_s + end + end + + dependencies = @gems.map {|g| Bundler::Dependency.new(g, version, @options) } + + Injector.inject(dependencies, + :conservative_versioning => @options[:version].nil?, # Perform conservative versioning only when version is not specified + :optimistic => @options[:optimistic], + :strict => @options[:strict]) + + Installer.install(Bundler.root, Bundler.definition) unless @options["skip-install"] + end + end +end diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb new file mode 100644 index 00000000000000..266396eedc67e0 --- /dev/null +++ b/lib/bundler/cli/binstubs.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Binstubs + attr_reader :options, :gems + def initialize(options, gems) + @options = options + @gems = gems + end + + def run + Bundler.definition.validate_runtime! + path_option = options["path"] + path_option = nil if path_option && path_option.empty? + Bundler.settings.set_command_option :bin, path_option if options["path"] + Bundler.settings.set_command_option_if_given :shebang, options["shebang"] + installer = Installer.new(Bundler.root, Bundler.definition) + + installer_opts = { :force => options[:force], :binstubs_cmd => true } + + if options[:all] + raise InvalidOption, "Cannot specify --all with specific gems" unless gems.empty? + @gems = Bundler.definition.specs.map(&:name) + installer_opts.delete(:binstubs_cmd) + elsif gems.empty? + Bundler.ui.error "`bundle binstubs` needs at least one gem to run." + exit 1 + end + + gems.each do |gem_name| + spec = Bundler.definition.specs.find {|s| s.name == gem_name } + unless spec + raise GemNotFound, Bundler::CLI::Common.gem_not_found_message( + gem_name, Bundler.definition.specs + ) + end + + if options[:standalone] + next Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") if gem_name == "bundler" + Bundler.settings.temporary(:path => (Bundler.settings[:path] || Bundler.root)) do + installer.generate_standalone_bundler_executable_stubs(spec) + end + else + installer.generate_bundler_executable_stubs(spec, installer_opts) + end + end + end + end +end diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb new file mode 100644 index 00000000000000..9d2ba87d3408d6 --- /dev/null +++ b/lib/bundler/cli/cache.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Cache + attr_reader :options + def initialize(options) + @options = options + end + + def run + Bundler.definition.validate_runtime! + Bundler.definition.resolve_with_cache! + setup_cache_all + Bundler.settings.set_command_option_if_given :cache_all_platforms, options["all-platforms"] + Bundler.load.cache + Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] + Bundler.load.lock + rescue GemNotFound => e + Bundler.ui.error(e.message) + Bundler.ui.warn "Run `bundle install` to install missing gems." + exit 1 + end + + private + + def setup_cache_all + Bundler.settings.set_command_option_if_given :cache_all, options[:all] + + if Bundler.definition.has_local_dependencies? && !Bundler.feature_flag.cache_all? + Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ + "to package them as well, please pass the --all flag. This will be the default " \ + "on Bundler 2.0." + end + end + end +end diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb new file mode 100644 index 00000000000000..19c0aaea06ebbc --- /dev/null +++ b/lib/bundler/cli/check.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Check + attr_reader :options + + def initialize(options) + @options = options + end + + def run + Bundler.settings.set_command_option_if_given :path, options[:path] + + begin + definition = Bundler.definition + definition.validate_runtime! + not_installed = definition.missing_specs + rescue GemNotFound, VersionConflict + Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies." + Bundler.ui.warn "Install missing gems with `bundle install`." + exit 1 + end + + if not_installed.any? + Bundler.ui.error "The following gems are missing" + not_installed.each {|s| Bundler.ui.error " * #{s.name} (#{s.version})" } + Bundler.ui.warn "Install missing gems with `bundle install`" + exit 1 + elsif !Bundler.default_lockfile.file? && Bundler.frozen_bundle? + Bundler.ui.error "This bundle has been frozen, but there is no #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} present" + exit 1 + else + Bundler.load.lock(:preserve_unknown_sections => true) unless options[:"dry-run"] + Bundler.ui.info "The Gemfile's dependencies are satisfied" + end + end + end +end diff --git a/lib/bundler/cli/clean.rb b/lib/bundler/cli/clean.rb new file mode 100644 index 00000000000000..4a407fbae7ec98 --- /dev/null +++ b/lib/bundler/cli/clean.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Clean + attr_reader :options + + def initialize(options) + @options = options + end + + def run + require_path_or_force unless options[:"dry-run"] + Bundler.load.clean(options[:"dry-run"]) + end + + protected + + def require_path_or_force + return unless Bundler.use_system_gems? && !options[:force] + raise InvalidOption, "Cleaning all the gems on your system is dangerous! " \ + "If you're sure you want to remove every system gem not in this " \ + "bundle, run `bundle clean --force`." + end + end +end diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb new file mode 100644 index 00000000000000..9d40ee9dfd1b81 --- /dev/null +++ b/lib/bundler/cli/common.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Bundler + module CLI::Common + def self.output_post_install_messages(messages) + return if Bundler.settings["ignore_messages"] + messages.to_a.each do |name, msg| + print_post_install_message(name, msg) unless Bundler.settings["ignore_messages.#{name}"] + end + end + + def self.print_post_install_message(name, msg) + Bundler.ui.confirm "Post-install message from #{name}:" + Bundler.ui.info msg + end + + def self.output_without_groups_message + return if Bundler.settings[:without].empty? + Bundler.ui.confirm without_groups_message + end + + def self.without_groups_message + groups = Bundler.settings[:without] + group_list = [groups[0...-1].join(", "), groups[-1..-1]]. + reject {|s| s.to_s.empty? }.join(" and ") + group_str = (groups.size == 1) ? "group" : "groups" + "Gems in the #{group_str} #{group_list} were not installed." + end + + def self.select_spec(name, regex_match = nil) + specs = [] + regexp = Regexp.new(name) if regex_match + + Bundler.definition.specs.each do |spec| + return spec if spec.name == name + specs << spec if regexp && spec.name =~ regexp + end + + case specs.count + when 0 + raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + when 1 + specs.first + else + ask_for_spec_from(specs) + end + rescue RegexpError + raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + end + + def self.ask_for_spec_from(specs) + if !$stdout.tty? && ENV["BUNDLE_SPEC_RUN"].nil? + raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + end + + specs.each_with_index do |spec, index| + Bundler.ui.info "#{index.succ} : #{spec.name}", true + end + Bundler.ui.info "0 : - exit -", true + + num = Bundler.ui.ask("> ").to_i + num > 0 ? specs[num - 1] : nil + end + + def self.gem_not_found_message(missing_gem_name, alternatives) + require "bundler/similarity_detector" + message = "Could not find gem '#{missing_gem_name}'." + alternate_names = alternatives.map {|a| a.respond_to?(:name) ? a.name : a } + suggestions = SimilarityDetector.new(alternate_names).similar_word_list(missing_gem_name) + message += "\nDid you mean #{suggestions}?" if suggestions + message + end + + def self.ensure_all_gems_in_lockfile!(names, locked_gems = Bundler.locked_gems) + locked_names = locked_gems.specs.map(&:name) + names.-(locked_names).each do |g| + raise GemNotFound, gem_not_found_message(g, locked_names) + end + end + + def self.configure_gem_version_promoter(definition, options) + patch_level = patch_level_options(options) + raise InvalidOption, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1 + definition.gem_version_promoter.tap do |gvp| + gvp.level = patch_level.first || :major + gvp.strict = options[:strict] || options["update-strict"] + end + end + + def self.patch_level_options(options) + [:major, :minor, :patch].select {|v| options.keys.include?(v.to_s) } + end + + def self.clean_after_install? + clean = Bundler.settings[:clean] + return clean unless clean.nil? + clean ||= Bundler.feature_flag.auto_clean_without_path? && Bundler.settings[:path].nil? + clean &&= !Bundler.use_system_gems? + clean + end + end +end diff --git a/lib/bundler/cli/config.rb b/lib/bundler/cli/config.rb new file mode 100644 index 00000000000000..12f71ea8fea984 --- /dev/null +++ b/lib/bundler/cli/config.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Config + attr_reader :name, :options, :scope, :thor + attr_accessor :args + + def initialize(options, args, thor) + @options = options + @args = args + @thor = thor + @name = peek = args.shift + @scope = "global" + return unless peek && peek.start_with?("--") + @name = args.shift + @scope = peek[2..-1] + end + + def run + unless name + confirm_all + return + end + + unless valid_scope?(scope) + Bundler.ui.error "Invalid scope --#{scope} given. Please use --local or --global." + exit 1 + end + + if scope == "delete" + Bundler.settings.set_local(name, nil) + Bundler.settings.set_global(name, nil) + return + end + + if args.empty? + if options[:parseable] + if value = Bundler.settings[name] + Bundler.ui.info("#{name}=#{value}") + end + return + end + + confirm(name) + return + end + + Bundler.ui.info(message) if message + Bundler.settings.send("set_#{scope}", name, new_value) + end + + private + + def confirm_all + if @options[:parseable] + thor.with_padding do + Bundler.settings.all.each do |setting| + val = Bundler.settings[setting] + Bundler.ui.info "#{setting}=#{val}" + end + end + else + Bundler.ui.confirm "Settings are listed in order of priority. The top value will be used.\n" + Bundler.settings.all.each do |setting| + Bundler.ui.confirm "#{setting}" + show_pretty_values_for(setting) + Bundler.ui.confirm "" + end + end + end + + def confirm(name) + Bundler.ui.confirm "Settings for `#{name}` in order of priority. The top value will be used" + show_pretty_values_for(name) + end + + def new_value + pathname = Pathname.new(args.join(" ")) + if name.start_with?("local.") && pathname.directory? + pathname.expand_path.to_s + else + args.join(" ") + end + end + + def message + locations = Bundler.settings.locations(name) + if @options[:parseable] + "#{name}=#{new_value}" if new_value + elsif scope == "global" + if locations[:local] + "Your application has set #{name} to #{locations[:local].inspect}. " \ + "This will override the global value you are currently setting" + elsif locations[:env] + "You have a bundler environment variable for #{name} set to " \ + "#{locations[:env].inspect}. This will take precedence over the global value you are setting" + elsif locations[:global] && locations[:global] != args.join(" ") + "You are replacing the current global value of #{name}, which is currently " \ + "#{locations[:global].inspect}" + end + elsif scope == "local" && locations[:local] != args.join(" ") + "You are replacing the current local value of #{name}, which is currently " \ + "#{locations[:local].inspect}" + end + end + + def show_pretty_values_for(setting) + thor.with_padding do + Bundler.settings.pretty_values_for(setting).each do |line| + Bundler.ui.info line + end + end + end + + def valid_scope?(scope) + %w[delete local global].include?(scope) + end + end +end diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb new file mode 100644 index 00000000000000..d45f30cdcf2ac1 --- /dev/null +++ b/lib/bundler/cli/console.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Console + attr_reader :options, :group + def initialize(options, group) + @options = options + @group = group + end + + def run + Bundler::SharedHelpers.major_deprecation 3, "bundle console will be replaced " \ + "by `bin/console` generated by `bundle gem `" + + group ? Bundler.require(:default, *(group.split.map!(&:to_sym))) : Bundler.require + ARGV.clear + + console = get_console(Bundler.settings[:console] || "irb") + console.start + end + + def get_console(name) + require name + get_constant(name) + rescue LoadError + Bundler.ui.error "Couldn't load console #{name}, falling back to irb" + require "irb" + get_constant("irb") + end + + def get_constant(name) + const_name = { + "pry" => :Pry, + "ripl" => :Ripl, + "irb" => :IRB, + }[name] + Object.const_get(const_name) + rescue NameError + Bundler.ui.error "Could not find constant #{const_name}" + exit 1 + end + end +end diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb new file mode 100644 index 00000000000000..3e0898ff8a8fb9 --- /dev/null +++ b/lib/bundler/cli/doctor.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Bundler + class CLI::Doctor + DARWIN_REGEX = /\s+(.+) \(compatibility / + LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/ + + attr_reader :options + + def initialize(options) + @options = options + end + + def otool_available? + Bundler.which("otool") + end + + def ldd_available? + Bundler.which("ldd") + end + + def dylibs_darwin(path) + output = `/usr/bin/otool -L "#{path}"`.chomp + dylibs = output.split("\n")[1..-1].map {|l| l.match(DARWIN_REGEX).captures[0] }.uniq + # ignore @rpath and friends + dylibs.reject {|dylib| dylib.start_with? "@" } + end + + def dylibs_ldd(path) + output = `/usr/bin/ldd "#{path}"`.chomp + output.split("\n").map do |l| + match = l.match(LDD_REGEX) + next if match.nil? + match.captures[0] + end.compact + end + + def dylibs(path) + case RbConfig::CONFIG["host_os"] + when /darwin/ + return [] unless otool_available? + dylibs_darwin(path) + when /(linux|solaris|bsd)/ + return [] unless ldd_available? + dylibs_ldd(path) + else # Windows, etc. + Bundler.ui.warn("Dynamic library check not supported on this platform.") + [] + end + end + + def bundles_for_gem(spec) + Dir.glob("#{spec.full_gem_path}/**/*.bundle") + end + + def check! + require "bundler/cli/check" + Bundler::CLI::Check.new({}).run + end + + def run + Bundler.ui.level = "error" if options[:quiet] + Bundler.settings.validate! + check! + + definition = Bundler.definition + broken_links = {} + + definition.specs.each do |spec| + bundles_for_gem(spec).each do |bundle| + bad_paths = dylibs(bundle).select {|f| !File.exist?(f) } + if bad_paths.any? + broken_links[spec] ||= [] + broken_links[spec].concat(bad_paths) + end + end + end + + permissions_valid = check_home_permissions + + if broken_links.any? + message = "The following gems are missing OS dependencies:" + broken_links.map do |spec, paths| + paths.uniq.map do |path| + "\n * #{spec.name}: #{path}" + end + end.flatten.sort.each {|m| message += m } + raise ProductionError, message + elsif !permissions_valid + Bundler.ui.info "No issues found with the installed bundle" + end + end + + private + + def check_home_permissions + require "find" + files_not_readable_or_writable = [] + files_not_rw_and_owned_by_different_user = [] + files_not_owned_by_current_user_but_still_rw = [] + Find.find(Bundler.home.to_s).each do |f| + if !File.writable?(f) || !File.readable?(f) + if File.stat(f).uid != Process.uid + files_not_rw_and_owned_by_different_user << f + else + files_not_readable_or_writable << f + end + elsif File.stat(f).uid != Process.uid + files_not_owned_by_current_user_but_still_rw << f + end + end + + ok = true + if files_not_owned_by_current_user_but_still_rw.any? + Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \ + "user, but are still readable/writable. These files are:\n - #{files_not_owned_by_current_user_but_still_rw.join("\n - ")}" + + ok = false + end + + if files_not_rw_and_owned_by_different_user.any? + Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \ + "user, and are not readable/writable. These files are:\n - #{files_not_rw_and_owned_by_different_user.join("\n - ")}" + + ok = false + end + + if files_not_readable_or_writable.any? + Bundler.ui.warn "Files exist in the Bundler home that are not " \ + "readable/writable by the current user. These files are:\n - #{files_not_readable_or_writable.join("\n - ")}" + + ok = false + end + + ok + end + end +end diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb new file mode 100644 index 00000000000000..c29d6323070cc7 --- /dev/null +++ b/lib/bundler/cli/exec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require "bundler/current_ruby" + +module Bundler + class CLI::Exec + attr_reader :options, :args, :cmd + + TRAPPED_SIGNALS = %w[INT].freeze + + def initialize(options, args) + @options = options + @cmd = args.shift + @args = args + + if Bundler.current_ruby.ruby_2? && !Bundler.current_ruby.jruby? + @args << { :close_others => !options.keep_file_descriptors? } + elsif options.keep_file_descriptors? + Bundler.ui.warn "Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec." + end + end + + def run + validate_cmd! + SharedHelpers.set_bundle_environment + if bin_path = Bundler.which(cmd) + if !Bundler.settings[:disable_exec_load] && ruby_shebang?(bin_path) + return kernel_load(bin_path, *args) + end + # First, try to exec directly to something in PATH + if Bundler.current_ruby.jruby_18? + kernel_exec(bin_path, *args) + else + kernel_exec([bin_path, cmd], *args) + end + else + # exec using the given command + kernel_exec(cmd, *args) + end + end + + private + + def validate_cmd! + return unless cmd.nil? + Bundler.ui.error "bundler: exec needs a command to run" + exit 128 + end + + def kernel_exec(*args) + ui = Bundler.ui + Bundler.ui = nil + Kernel.exec(*args) + rescue Errno::EACCES, Errno::ENOEXEC + Bundler.ui = ui + Bundler.ui.error "bundler: not executable: #{cmd}" + exit 126 + rescue Errno::ENOENT + Bundler.ui = ui + Bundler.ui.error "bundler: command not found: #{cmd}" + Bundler.ui.warn "Install missing gem executables with `bundle install`" + exit 127 + end + + def kernel_load(file, *args) + args.pop if args.last.is_a?(Hash) + ARGV.replace(args) + $0 = file + Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle) + ui = Bundler.ui + Bundler.ui = nil + require "bundler/setup" + TRAPPED_SIGNALS.each {|s| trap(s, "DEFAULT") } + Kernel.load(file) + rescue SystemExit, SignalException + raise + rescue Exception => e # rubocop:disable Lint/RescueException + Bundler.ui = ui + Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})" + backtrace = e.backtrace ? e.backtrace.take_while {|bt| !bt.start_with?(__FILE__) } : [] + abort "#{e.class}: #{e.message}\n #{backtrace.join("\n ")}" + end + + def process_title(file, args) + "#{file} #{args.join(" ")}".strip + end + + def ruby_shebang?(file) + possibilities = [ + "#!/usr/bin/env ruby\n", + "#!/usr/bin/env jruby\n", + "#!/usr/bin/env truffleruby\n", + "#!#{Gem.ruby}\n", + ] + + if File.zero?(file) + Bundler.ui.warn "#{file} is empty" + return false + end + + first_line = File.open(file, "rb") {|f| f.read(possibilities.map(&:size).max) } + possibilities.any? {|shebang| first_line.start_with?(shebang) } + end + end +end diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb new file mode 100644 index 00000000000000..58e2f8a3fdbfdb --- /dev/null +++ b/lib/bundler/cli/gem.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +require "pathname" + +module Bundler + class CLI + Bundler.require_thor_actions + include Thor::Actions + end + + class CLI::Gem + TEST_FRAMEWORK_VERSIONS = { + "rspec" => "3.0", + "minitest" => "5.0" + }.freeze + + attr_reader :options, :gem_name, :thor, :name, :target + + def initialize(options, gem_name, thor) + @options = options + @gem_name = resolve_name(gem_name) + + @thor = thor + thor.behavior = :invoke + thor.destination_root = nil + + @name = @gem_name + @target = SharedHelpers.pwd.join(gem_name) + + validate_ext_name if options[:ext] + end + + def run + Bundler.ui.confirm "Creating gem '#{name}'..." + + underscored_name = name.tr("-", "_") + namespaced_path = name.tr("-", "/") + constant_name = name.gsub(/-[_-]*(?![_-]|$)/) { "::" }.gsub(/([_-]+|(::)|^)(.|$)/) { $2.to_s + $3.upcase } + constant_array = constant_name.split("::") + + git_installed = Bundler.git_present? + + git_author_name = git_installed ? `git config user.name`.chomp : "" + github_username = git_installed ? `git config github.user`.chomp : "" + git_user_email = git_installed ? `git config user.email`.chomp : "" + + config = { + :name => name, + :underscored_name => underscored_name, + :namespaced_path => namespaced_path, + :makefile_path => "#{underscored_name}/#{underscored_name}", + :constant_name => constant_name, + :constant_array => constant_array, + :author => git_author_name.empty? ? "TODO: Write your name" : git_author_name, + :email => git_user_email.empty? ? "TODO: Write your email address" : git_user_email, + :test => options[:test], + :ext => options[:ext], + :exe => options[:exe], + :bundler_version => bundler_dependency_version, + :github_username => github_username.empty? ? "[USERNAME]" : github_username + } + ensure_safe_gem_name(name, constant_array) + + templates = { + "Gemfile.tt" => "Gemfile", + "lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb", + "lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb", + "newgem.gemspec.tt" => "#{name}.gemspec", + "Rakefile.tt" => "Rakefile", + "README.md.tt" => "README.md", + "bin/console.tt" => "bin/console", + "bin/setup.tt" => "bin/setup" + } + + executables = %w[ + bin/console + bin/setup + ] + + templates.merge!("gitignore.tt" => ".gitignore") if Bundler.git_present? + + if test_framework = ask_and_set_test_framework + config[:test] = test_framework + config[:test_framework_version] = TEST_FRAMEWORK_VERSIONS[test_framework] + + templates.merge!("travis.yml.tt" => ".travis.yml") + + case test_framework + when "rspec" + templates.merge!( + "rspec.tt" => ".rspec", + "spec/spec_helper.rb.tt" => "spec/spec_helper.rb", + "spec/newgem_spec.rb.tt" => "spec/#{namespaced_path}_spec.rb" + ) + when "minitest" + templates.merge!( + "test/test_helper.rb.tt" => "test/test_helper.rb", + "test/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb" + ) + end + end + + config[:test_task] = config[:test] == "minitest" ? "test" : "spec" + + if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?", + "This means that any other developer or company will be legally allowed to use your code " \ + "for free as long as they admit you created it. You can read more about the MIT license " \ + "at https://choosealicense.com/licenses/mit.") + config[:mit] = true + Bundler.ui.info "MIT License enabled in config" + templates.merge!("LICENSE.txt.tt" => "LICENSE.txt") + end + + if ask_and_set(:coc, "Do you want to include a code of conduct in gems you generate?", + "Codes of conduct can increase contributions to your project by contributors who " \ + "prefer collaborative, safe spaces. You can read more about the code of conduct at " \ + "contributor-covenant.org. Having a code of conduct means agreeing to the responsibility " \ + "of enforcing it, so be sure that you are prepared to do that. Be sure that your email " \ + "address is specified as a contact in the generated code of conduct so that people know " \ + "who to contact in case of a violation. For suggestions about " \ + "how to enforce codes of conduct, see https://bit.ly/coc-enforcement.") + config[:coc] = true + Bundler.ui.info "Code of conduct enabled in config" + templates.merge!("CODE_OF_CONDUCT.md.tt" => "CODE_OF_CONDUCT.md") + end + + templates.merge!("exe/newgem.tt" => "exe/#{name}") if config[:exe] + + if options[:ext] + templates.merge!( + "ext/newgem/extconf.rb.tt" => "ext/#{name}/extconf.rb", + "ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h", + "ext/newgem/newgem.c.tt" => "ext/#{name}/#{underscored_name}.c" + ) + end + + templates.each do |src, dst| + destination = target.join(dst) + SharedHelpers.filesystem_access(destination) do + thor.template("newgem/#{src}", destination, config) + end + end + + executables.each do |file| + SharedHelpers.filesystem_access(target.join(file)) do |path| + executable = (path.stat.mode | 0o111) + path.chmod(executable) + end + end + + if Bundler.git_present? + Bundler.ui.info "Initializing git repo in #{target}" + Dir.chdir(target) do + `git init` + `git add .` + end + end + + # Open gemspec in editor + open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit] + + Bundler.ui.info "Gem '#{name}' was successfully created. " \ + "For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html" + rescue Errno::EEXIST => e + raise GenericSystemCallError.new(e, "There was a conflict while creating the new gem.") + end + + private + + def resolve_name(name) + SharedHelpers.pwd.join(name).basename.to_s + end + + def ask_and_set(key, header, message) + choice = options[key] + choice = Bundler.settings["gem.#{key}"] if choice.nil? + + if choice.nil? + Bundler.ui.confirm header + choice = Bundler.ui.yes? "#{message} y/(n):" + Bundler.settings.set_global("gem.#{key}", choice) + end + + choice + end + + def validate_ext_name + return unless gem_name.index("-") + + Bundler.ui.error "You have specified a gem name which does not conform to the \n" \ + "naming guidelines for C extensions. For more information, \n" \ + "see the 'Extension Naming' section at the following URL:\n" \ + "http://guides.rubygems.org/gems-with-extensions/\n" + exit 1 + end + + def ask_and_set_test_framework + test_framework = options[:test] || Bundler.settings["gem.test"] + + if test_framework.nil? + Bundler.ui.confirm "Do you want to generate tests with your gem?" + result = Bundler.ui.ask "Type 'rspec' or 'minitest' to generate those test files now and " \ + "in the future. rspec/minitest/(none):" + if result =~ /rspec|minitest/ + test_framework = result + else + test_framework = false + end + end + + if Bundler.settings["gem.test"].nil? + Bundler.settings.set_global("gem.test", test_framework) + end + + test_framework + end + + def bundler_dependency_version + v = Gem::Version.new(Bundler::VERSION) + req = v.segments[0..1] + req << "a" if v.prerelease? + req.join(".") + end + + def ensure_safe_gem_name(name, constant_array) + if name =~ /^\d/ + Bundler.ui.error "Invalid gem name #{name} Please give a name which does not start with numbers." + exit 1 + end + + constant_name = constant_array.join("::") + + existing_constant = constant_array.inject(Object) do |c, s| + defined = begin + c.const_defined?(s) + rescue NameError + Bundler.ui.error "Invalid gem name #{name} -- `#{constant_name}` is an invalid constant name" + exit 1 + end + (defined && c.const_get(s)) || break + end + + return unless existing_constant + Bundler.ui.error "Invalid gem name #{name} constant #{constant_name} is already in use. Please choose another gem name." + exit 1 + end + + def open_editor(editor, file) + thor.run(%(#{editor} "#{file}")) + end + end +end diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb new file mode 100644 index 00000000000000..958b5250677575 --- /dev/null +++ b/lib/bundler/cli/info.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Info + attr_reader :gem_name, :options + def initialize(options, gem_name) + @options = options + @gem_name = gem_name + end + + def run + spec = spec_for_gem(gem_name) + + spec_not_found(gem_name) unless spec + return print_gem_path(spec) if @options[:path] + print_gem_info(spec) + end + + private + + def spec_for_gem(gem_name) + spec = Bundler.definition.specs.find {|s| s.name == gem_name } + spec || default_gem_spec(gem_name) + end + + def default_gem_spec(gem_name) + return unless Gem::Specification.respond_to?(:find_all_by_name) + gem_spec = Gem::Specification.find_all_by_name(gem_name).last + return gem_spec if gem_spec && gem_spec.respond_to?(:default_gem?) && gem_spec.default_gem? + end + + def spec_not_found(gem_name) + raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(gem_name, Bundler.definition.dependencies) + end + + def print_gem_path(spec) + Bundler.ui.info spec.full_gem_path + end + + def print_gem_info(spec) + gem_info = String.new + gem_info << " * #{spec.name} (#{spec.version}#{spec.git_version})\n" + gem_info << "\tSummary: #{spec.summary}\n" if spec.summary + gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage + gem_info << "\tPath: #{spec.full_gem_path}\n" + gem_info << "\tDefault Gem: yes" if spec.respond_to?(:default_gem?) && spec.default_gem? + Bundler.ui.info gem_info + end + end +end diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb new file mode 100644 index 00000000000000..40df7972693654 --- /dev/null +++ b/lib/bundler/cli/init.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Init + attr_reader :options + def initialize(options) + @options = options + end + + def run + if File.exist?(gemfile) + Bundler.ui.error "#{gemfile} already exists at #{File.expand_path(gemfile)}" + exit 1 + end + + unless File.writable?(Dir.pwd) + Bundler.ui.error "Can not create #{gemfile} as the current directory is not writable." + exit 1 + end + + if options[:gemspec] + gemspec = File.expand_path(options[:gemspec]) + unless File.exist?(gemspec) + Bundler.ui.error "Gem specification #{gemspec} doesn't exist" + exit 1 + end + + spec = Bundler.load_gemspec_uncached(gemspec) + + File.open(gemfile, "wb") do |file| + file << "# Generated from #{gemspec}\n" + file << spec.to_gemfile + end + else + FileUtils.cp(File.expand_path("../../templates/#{gemfile}", __FILE__), gemfile) + end + + puts "Writing new #{gemfile} to #{SharedHelpers.pwd}/#{gemfile}" + end + + private + + def gemfile + @gemfile ||= Bundler.feature_flag.init_gems_rb? ? "gems.rb" : "Gemfile" + end + end +end diff --git a/lib/bundler/cli/inject.rb b/lib/bundler/cli/inject.rb new file mode 100644 index 00000000000000..b00675d34883d4 --- /dev/null +++ b/lib/bundler/cli/inject.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Inject + attr_reader :options, :name, :version, :group, :source, :gems + def initialize(options, name, version) + @options = options + @name = name + @version = version || last_version_number + @group = options[:group].split(",") unless options[:group].nil? + @source = options[:source] + @gems = [] + end + + def run + # The required arguments allow Thor to give useful feedback when the arguments + # are incorrect. This adds those first two arguments onto the list as a whole. + gems.unshift(source).unshift(group).unshift(version).unshift(name) + + # Build an array of Dependency objects out of the arguments + deps = [] + # when `inject` support addition of more than one gem, then this loop will + # help. Currently this loop is running once. + gems.each_slice(4) do |gem_name, gem_version, gem_group, gem_source| + ops = Gem::Requirement::OPS.map {|key, _val| key } + has_op = ops.any? {|op| gem_version.start_with? op } + gem_version = "~> #{gem_version}" unless has_op + deps << Bundler::Dependency.new(gem_name, gem_version, "group" => gem_group, "source" => gem_source) + end + + added = Injector.inject(deps, options) + + if added.any? + Bundler.ui.confirm "Added to Gemfile:" + Bundler.ui.confirm(added.map do |d| + name = "'#{d.name}'" + requirement = ", '#{d.requirement}'" + group = ", :group => #{d.groups.inspect}" if d.groups != Array(:default) + source = ", :source => '#{d.source}'" unless d.source.nil? + %(gem #{name}#{requirement}#{group}#{source}) + end.join("\n")) + else + Bundler.ui.confirm "All gems were already present in the Gemfile" + end + end + + private + + def last_version_number + definition = Bundler.definition(true) + definition.resolve_remotely! + specs = definition.index[name].sort_by(&:version) + unless options[:pre] + specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } + end + spec = specs.last + spec.version.to_s + end + end +end diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb new file mode 100644 index 00000000000000..55e90ead0e1677 --- /dev/null +++ b/lib/bundler/cli/install.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Install + attr_reader :options + def initialize(options) + @options = options + end + + def run + Bundler.ui.level = "error" if options[:quiet] + + warn_if_root + + normalize_groups + + Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Bundler::FREEBSD + + # Disable color in deployment mode + Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment] + + check_for_options_conflicts + + check_trust_policy + + if options[:deployment] || options[:frozen] || Bundler.frozen_bundle? + unless Bundler.default_lockfile.exist? + flag = "--deployment flag" if options[:deployment] + flag ||= "--frozen flag" if options[:frozen] + flag ||= "deployment setting" + raise ProductionError, "The #{flag} requires a #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}. Please make " \ + "sure you have checked your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} into version control " \ + "before deploying." + end + + options[:local] = true if Bundler.app_cache.exist? + + if Bundler.feature_flag.deployment_means_frozen? + Bundler.settings.set_command_option :deployment, true + else + Bundler.settings.set_command_option :frozen, true + end + end + + # When install is called with --no-deployment, disable deployment mode + if options[:deployment] == false + Bundler.settings.set_command_option :frozen, nil + options[:system] = true + end + + normalize_settings + + Bundler::Fetcher.disable_endpoint = options["full-index"] + + if options["binstubs"] + Bundler::SharedHelpers.major_deprecation 3, + "The --binstubs option will be removed in favor of `bundle binstubs`" + end + + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? + + definition = Bundler.definition + definition.validate_runtime! + + installer = Installer.install(Bundler.root, definition, options) + Bundler.load.cache if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.frozen_bundle? + + Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}." + Bundler::CLI::Common.output_without_groups_message + + if Bundler.use_system_gems? + Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed." + else + relative_path = Bundler.configured_bundle_path.base_path_relative_to_pwd + Bundler.ui.confirm "Bundled gems are installed into `#{relative_path}`" + end + + Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + + warn_ambiguous_gems + + if CLI::Common.clean_after_install? + require "bundler/cli/clean" + Bundler::CLI::Clean.new(options).run + end + rescue GemNotFound, VersionConflict => e + if options[:local] && Bundler.app_cache.exist? + Bundler.ui.warn "Some gems seem to be missing from your #{Bundler.settings.app_cache_path} directory." + end + + unless Bundler.definition.has_rubygems_remotes? + Bundler.ui.warn <<-WARN, :wrap => true + Your Gemfile has no gem server sources. If you need gems that are \ + not already on your machine, add a line like this to your Gemfile: + source 'https://rubygems.org' + WARN + end + raise e + rescue Gem::InvalidSpecificationException => e + Bundler.ui.warn "You have one or more invalid gemspecs that need to be fixed." + raise e + end + + private + + def warn_if_root + return if Bundler.settings[:silence_root_warning] || Bundler::WINDOWS || !Process.uid.zero? + Bundler.ui.warn "Don't run Bundler as root. Bundler can ask for sudo " \ + "if it is needed, and installing your bundle as root will break this " \ + "application for all non-root users on this machine.", :wrap => true + end + + def dependencies_count_for(definition) + count = definition.dependencies.count + "#{count} Gemfile #{count == 1 ? "dependency" : "dependencies"}" + end + + def gems_installed_for(definition) + count = definition.specs.count + "#{count} #{count == 1 ? "gem" : "gems"} now installed" + end + + def check_for_group_conflicts_in_cli_options + conflicting_groups = Array(options[:without]) & Array(options[:with]) + return if conflicting_groups.empty? + raise InvalidOption, "You can't list a group in both with and without." \ + " The offending groups are: #{conflicting_groups.join(", ")}." + end + + def check_for_options_conflicts + if (options[:path] || options[:deployment]) && options[:system] + error_message = String.new + error_message << "You have specified both --path as well as --system. Please choose only one option.\n" if options[:path] + error_message << "You have specified both --deployment as well as --system. Please choose only one option.\n" if options[:deployment] + raise InvalidOption.new(error_message) + end + end + + def check_trust_policy + trust_policy = options["trust-policy"] + unless Bundler.rubygems.security_policies.keys.unshift(nil).include?(trust_policy) + raise InvalidOption, "RubyGems doesn't know about trust policy '#{trust_policy}'. " \ + "The known policies are: #{Bundler.rubygems.security_policies.keys.join(", ")}." + end + Bundler.settings.set_command_option_if_given :"trust-policy", trust_policy + end + + def normalize_groups + options[:with] &&= options[:with].join(":").tr(" ", ":").split(":") + options[:without] &&= options[:without].join(":").tr(" ", ":").split(":") + + check_for_group_conflicts_in_cli_options + + Bundler.settings.set_command_option :with, nil if options[:with] == [] + Bundler.settings.set_command_option :without, nil if options[:without] == [] + + with = options.fetch(:with, []) + with |= Bundler.settings[:with].map(&:to_s) + with -= options[:without] if options[:without] + + without = options.fetch(:without, []) + without |= Bundler.settings[:without].map(&:to_s) + without -= options[:with] if options[:with] + + options[:with] = with + options[:without] = without + end + + def normalize_settings + Bundler.settings.set_command_option :path, nil if options[:system] + Bundler.settings.temporary(:path_relative_to_cwd => false) do + Bundler.settings.set_command_option :path, "vendor/bundle" if options[:deployment] + end + Bundler.settings.set_command_option_if_given :path, options[:path] + Bundler.settings.temporary(:path_relative_to_cwd => false) do + Bundler.settings.set_command_option :path, "bundle" if options["standalone"] && Bundler.settings[:path].nil? + end + + bin_option = options["binstubs"] + bin_option = nil if bin_option && bin_option.empty? + Bundler.settings.set_command_option :bin, bin_option if options["binstubs"] + + Bundler.settings.set_command_option_if_given :shebang, options["shebang"] + + Bundler.settings.set_command_option_if_given :jobs, options["jobs"] + + Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] + + Bundler.settings.set_command_option_if_given :no_install, options["no-install"] + + Bundler.settings.set_command_option_if_given :clean, options["clean"] + + unless Bundler.settings[:without] == options[:without] && Bundler.settings[:with] == options[:with] + # need to nil them out first to get around validation for backwards compatibility + Bundler.settings.set_command_option :without, nil + Bundler.settings.set_command_option :with, nil + Bundler.settings.set_command_option :without, options[:without] - options[:with] + Bundler.settings.set_command_option :with, options[:with] + end + + options[:force] = options[:redownload] + end + + def warn_ambiguous_gems + Installer.ambiguous_gems.to_a.each do |name, installed_from_uri, *also_found_in_uris| + Bundler.ui.error "Warning: the gem '#{name}' was found in multiple sources." + Bundler.ui.error "Installed from: #{installed_from_uri}" + Bundler.ui.error "Also found in:" + also_found_in_uris.each {|uri| Bundler.ui.error " * #{uri}" } + Bundler.ui.error "You should add a source requirement to restrict this gem to your preferred source." + Bundler.ui.error "For example:" + Bundler.ui.error " gem '#{name}', :source => '#{installed_from_uri}'" + Bundler.ui.error "Then uninstall the gem '#{name}' (or delete all bundled gems) and then install again." + end + end + end +end diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb new file mode 100644 index 00000000000000..91f827ea99cc7e --- /dev/null +++ b/lib/bundler/cli/issue.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Bundler + class CLI::Issue + def run + Bundler.ui.info <<-EOS.gsub(/^ {8}/, "") + Did you find an issue with Bundler? Before filing a new issue, + be sure to check out these resources: + + 1. Check out our troubleshooting guide for quick fixes to common issues: + https://github.com/bundler/bundler/blob/master/doc/TROUBLESHOOTING.md + + 2. Instructions for common Bundler uses can be found on the documentation + site: http://bundler.io/ + + 3. Information about each Bundler command can be found in the Bundler + man pages: http://bundler.io/man/bundle.1.html + + Hopefully the troubleshooting steps above resolved your problem! If things + still aren't working the way you expect them to, please let us know so + that we can diagnose and help fix the problem you're having. Please + view the Filing Issues guide for more information: + https://github.com/bundler/bundler/blob/master/doc/contributing/ISSUES.md + + EOS + + Bundler.ui.info Bundler::Env.report + + Bundler.ui.info "\n## Bundle Doctor" + doctor + end + + def doctor + require "bundler/cli/doctor" + Bundler::CLI::Doctor.new({}).run + end + end +end diff --git a/lib/bundler/cli/list.rb b/lib/bundler/cli/list.rb new file mode 100644 index 00000000000000..d1799196e783f6 --- /dev/null +++ b/lib/bundler/cli/list.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Bundler + class CLI::List + def initialize(options) + @options = options + end + + def run + raise InvalidOption, "The `--only-group` and `--without-group` options cannot be used together" if @options["only-group"] && @options["without-group"] + + raise InvalidOption, "The `--name-only` and `--paths` options cannot be used together" if @options["name-only"] && @options[:paths] + + specs = if @options["only-group"] || @options["without-group"] + filtered_specs_by_groups + else + Bundler.load.specs + end.reject {|s| s.name == "bundler" }.sort_by(&:name) + + return Bundler.ui.info "No gems in the Gemfile" if specs.empty? + + return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"] + return specs.each {|s| Bundler.ui.info s.full_gem_path } if @options["paths"] + + Bundler.ui.info "Gems included by the bundle:" + + specs.each {|s| Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" } + + Bundler.ui.info "Use `bundle info` to print more detailed information about a gem" + end + + private + + def verify_group_exists(groups) + raise InvalidOption, "`#{@options["without-group"]}` group could not be found." if @options["without-group"] && !groups.include?(@options["without-group"].to_sym) + + raise InvalidOption, "`#{@options["only-group"]}` group could not be found." if @options["only-group"] && !groups.include?(@options["only-group"].to_sym) + end + + def filtered_specs_by_groups + definition = Bundler.definition + groups = definition.groups + + verify_group_exists(groups) + + show_groups = + if @options["without-group"] + groups.reject {|g| g == @options["without-group"].to_sym } + elsif @options["only-group"] + groups.select {|g| g == @options["only-group"].to_sym } + else + groups + end.map(&:to_sym) + + definition.specs_for(show_groups) + end + end +end diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb new file mode 100644 index 00000000000000..7dd078b1ef9544 --- /dev/null +++ b/lib/bundler/cli/lock.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Lock + attr_reader :options + + def initialize(options) + @options = options + end + + def run + unless Bundler.default_gemfile + Bundler.ui.error "Unable to find a Gemfile to lock" + exit 1 + end + + print = options[:print] + ui = Bundler.ui + Bundler.ui = UI::Silent.new if print + + Bundler::Fetcher.disable_endpoint = options["full-index"] + + update = options[:update] + if update.is_a?(Array) # unlocking specific gems + Bundler::CLI::Common.ensure_all_gems_in_lockfile!(update) + update = { :gems => update, :lock_shared_dependencies => options[:conservative] } + end + definition = Bundler.definition(update) + + Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) if options[:update] + + options["remove-platform"].each do |platform| + definition.remove_platform(platform) + end + + options["add-platform"].each do |platform_string| + platform = Gem::Platform.new(platform_string) + if platform.to_s == "unknown" + Bundler.ui.warn "The platform `#{platform_string}` is unknown to RubyGems " \ + "and adding it will likely lead to resolution errors" + end + definition.add_platform(platform) + end + + if definition.platforms.empty? + raise InvalidOption, "Removing all platforms from the bundle is not allowed" + end + + definition.resolve_remotely! unless options[:local] + + if print + puts definition.to_lock + else + file = options[:lockfile] + file = file ? File.expand_path(file) : Bundler.default_lockfile + puts "Writing lockfile to #{file}" + definition.lock(file) + end + + Bundler.ui = ui + end + end +end diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb new file mode 100644 index 00000000000000..552fe6f128ba4c --- /dev/null +++ b/lib/bundler/cli/open.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "shellwords" + +module Bundler + class CLI::Open + attr_reader :options, :name + def initialize(options, name) + @options = options + @name = name + end + + def run + editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? } + return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor + return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match) + path = spec.full_gem_path + Dir.chdir(path) do + command = Shellwords.split(editor) + [path] + Bundler.with_original_env do + system(*command) + end || Bundler.ui.info("Could not run '#{command.join(" ")}'") + end + end + end +end diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb new file mode 100644 index 00000000000000..2ca90293db0b74 --- /dev/null +++ b/lib/bundler/cli/outdated.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Outdated + attr_reader :options, :gems + + def initialize(options, gems) + @options = options + @gems = gems + end + + def run + check_for_deployment_mode + + sources = Array(options[:source]) + + gems.each do |gem_name| + Bundler::CLI::Common.select_spec(gem_name) + end + + Bundler.definition.validate_runtime! + current_specs = Bundler.ui.silence { Bundler.definition.resolve } + current_dependencies = {} + Bundler.ui.silence do + Bundler.load.dependencies.each do |dep| + current_dependencies[dep.name] = dep + end + end + + definition = if gems.empty? && sources.empty? + # We're doing a full update + Bundler.definition(true) + else + Bundler.definition(:gems => gems, :sources => sources) + end + + Bundler::CLI::Common.configure_gem_version_promoter( + Bundler.definition, + options + ) + + # the patch level options imply strict is also true. It wouldn't make + # sense otherwise. + strict = options[:strict] || + Bundler::CLI::Common.patch_level_options(options).any? + + filter_options_patch = options.keys & + %w[filter-major filter-minor filter-patch] + + definition_resolution = proc do + options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely! + end + + if options[:parseable] + Bundler.ui.silence(&definition_resolution) + else + definition_resolution.call + end + + Bundler.ui.info "" + outdated_gems_by_groups = {} + outdated_gems_list = [] + + # Loop through the current specs + gemfile_specs, dependency_specs = current_specs.partition do |spec| + current_dependencies.key? spec.name + end + + specs = if options["only-explicit"] + gemfile_specs + else + gemfile_specs + dependency_specs + end + + specs.sort_by(&:name).each do |current_spec| + next if !gems.empty? && !gems.include?(current_spec.name) + + dependency = current_dependencies[current_spec.name] + active_spec = retrieve_active_spec(strict, definition, current_spec) + + next if active_spec.nil? + if filter_options_patch.any? + update_present = update_present_via_semver_portions(current_spec, active_spec, options) + next unless update_present + end + + gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version) + next unless gem_outdated || (current_spec.git_version != active_spec.git_version) + groups = nil + if dependency && !options[:parseable] + groups = dependency.groups.join(", ") + end + + outdated_gems_list << { :active_spec => active_spec, + :current_spec => current_spec, + :dependency => dependency, + :groups => groups } + + outdated_gems_by_groups[groups] ||= [] + outdated_gems_by_groups[groups] << { :active_spec => active_spec, + :current_spec => current_spec, + :dependency => dependency, + :groups => groups } + end + + if outdated_gems_list.empty? + display_nothing_outdated_message(filter_options_patch) + else + unless options[:parseable] + if options[:pre] + Bundler.ui.info "Outdated gems included in the bundle (including " \ + "pre-releases):" + else + Bundler.ui.info "Outdated gems included in the bundle:" + end + end + + options_include_groups = [:group, :groups].select do |v| + options.keys.include?(v.to_s) + end + + if options_include_groups.any? + ordered_groups = outdated_gems_by_groups.keys.compact.sort + [nil, ordered_groups].flatten.each do |groups| + gems = outdated_gems_by_groups[groups] + contains_group = if groups + groups.split(",").include?(options[:group]) + else + options[:group] == "group" + end + + next if (!options[:groups] && !contains_group) || gems.nil? + + unless options[:parseable] + if groups + Bundler.ui.info "===== Group #{groups} =====" + else + Bundler.ui.info "===== Without group =====" + end + end + + gems.each do |gem| + print_gem( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + groups, + options_include_groups.any? + ) + end + end + else + outdated_gems_list.each do |gem| + print_gem( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + gem[:groups], + options_include_groups.any? + ) + end + end + + exit 1 + end + end + + private + + def retrieve_active_spec(strict, definition, current_spec) + if strict + active_spec = definition.find_resolved_spec(current_spec) + else + active_specs = definition.find_indexed_specs(current_spec) + 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_spec = active_specs.last + end + + active_spec + end + + def display_nothing_outdated_message(filter_options_patch) + unless options[:parseable] + if filter_options_patch.any? + display = filter_options_patch.map do |o| + o.sub("filter-", "") + end.join(" or ") + + Bundler.ui.info "No #{display} updates to display.\n" + else + Bundler.ui.info "Bundle up to date!\n" + end + end + end + + def print_gem(current_spec, active_spec, dependency, groups, options_include_groups) + spec_version = "#{active_spec.version}#{active_spec.git_version}" + spec_version += " (from #{active_spec.loaded_from})" if Bundler.ui.debug? && active_spec.loaded_from + current_version = "#{current_spec.version}#{current_spec.git_version}" + + if dependency && dependency.specific? + dependency_version = %(, requested #{dependency.requirement}) + end + + spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \ + "installed #{current_version}#{dependency_version})" + + output_message = if options[:parseable] + spec_outdated_info.to_s + elsif options_include_groups || !groups + " * #{spec_outdated_info}" + else + " * #{spec_outdated_info} in groups \"#{groups}\"" + end + + Bundler.ui.info output_message.rstrip + end + + def check_for_deployment_mode + return unless Bundler.frozen_bundle? + suggested_command = if Bundler.settings.locations("frozen")[:global] + "bundle config --delete frozen" + elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? + "bundle config --delete deployment" + else + "bundle install --no-deployment" + end + raise ProductionError, "You are trying to check outdated gems in " \ + "deployment mode. Run `bundle outdated` elsewhere.\n" \ + "\nIf this is a development machine, remove the " \ + "#{Bundler.default_gemfile} freeze" \ + "\nby running `#{suggested_command}`." + end + + def update_present_via_semver_portions(current_spec, active_spec, options) + current_major = current_spec.version.segments.first + active_major = active_spec.version.segments.first + + update_present = false + update_present = active_major > current_major if options["filter-major"] + + if !update_present && (options["filter-minor"] || options["filter-patch"]) && current_major == active_major + current_minor = get_version_semver_portion_value(current_spec, 1) + active_minor = get_version_semver_portion_value(active_spec, 1) + + update_present = active_minor > current_minor if options["filter-minor"] + + if !update_present && options["filter-patch"] && current_minor == active_minor + current_patch = get_version_semver_portion_value(current_spec, 2) + active_patch = get_version_semver_portion_value(active_spec, 2) + + update_present = active_patch > current_patch + end + end + + update_present + end + + def get_version_semver_portion_value(spec, version_portion_index) + version_section = spec.version.segments[version_portion_index, 1] + version_section.nil? ? 0 : (version_section.first || 0) + end + end +end diff --git a/lib/bundler/cli/package.rb b/lib/bundler/cli/package.rb new file mode 100644 index 00000000000000..2dcd0e1e29c31e --- /dev/null +++ b/lib/bundler/cli/package.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Package + attr_reader :options + + def initialize(options) + @options = options + end + + def run + Bundler.ui.level = "error" if options[:quiet] + Bundler.settings.set_command_option_if_given :path, options[:path] + Bundler.settings.set_command_option_if_given :cache_all_platforms, options["all-platforms"] + Bundler.settings.set_command_option_if_given :cache_path, options["cache-path"] + + setup_cache_all + install + + # TODO: move cache contents here now that all bundles are locked + custom_path = Bundler.settings[:path] if options[:path] + Bundler.load.cache(custom_path) + end + + private + + def install + require "bundler/cli/install" + options = self.options.dup + if Bundler.settings[:cache_all_platforms] + options["local"] = false + options["update"] = true + end + Bundler::CLI::Install.new(options).run + end + + def setup_cache_all + all = options.fetch(:all, Bundler.feature_flag.cache_command_is_package? || nil) + + Bundler.settings.set_command_option_if_given :cache_all, all + + if Bundler.definition.has_local_dependencies? && !Bundler.feature_flag.cache_all? + Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ + "to package them as well, please pass the --all flag. This will be the default " \ + "on Bundler 2.0." + end + end + end +end diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb new file mode 100644 index 00000000000000..e97cad49a4c052 --- /dev/null +++ b/lib/bundler/cli/platform.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Platform + attr_reader :options + def initialize(options) + @options = options + end + + def run + platforms, ruby_version = Bundler.ui.silence do + locked_ruby_version = Bundler.locked_gems && Bundler.locked_gems.ruby_version + gemfile_ruby_version = Bundler.definition.ruby_version && Bundler.definition.ruby_version.single_version_string + [Bundler.definition.platforms.map {|p| "* #{p}" }, + locked_ruby_version || gemfile_ruby_version] + end + output = [] + + if options[:ruby] + if ruby_version + output << ruby_version + else + output << "No ruby version specified" + end + else + output << "Your platform is: #{RUBY_PLATFORM}" + output << "Your app has gems that work on these platforms:\n#{platforms.join("\n")}" + + if ruby_version + output << "Your Gemfile specifies a Ruby version requirement:\n* #{ruby_version}" + + begin + Bundler.definition.validate_runtime! + output << "Your current platform satisfies the Ruby version requirement." + rescue RubyVersionMismatch => e + output << e.message + end + else + output << "Your Gemfile does not specify a Ruby version requirement." + end + end + + Bundler.ui.info output.join("\n\n") + end + end +end diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb new file mode 100644 index 00000000000000..5488a9f28db95f --- /dev/null +++ b/lib/bundler/cli/plugin.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "bundler/vendored_thor" +module Bundler + class CLI::Plugin < Thor + desc "install PLUGINS", "Install the plugin from the source" + long_desc <<-D + Install plugins either from the rubygems source provided (with --source option) or from a git source provided with (--git option). If no sources are provided, it uses Gem.sources + D + method_option "source", :type => :string, :default => nil, :banner => + "URL of the RubyGems source to fetch the plugin from" + method_option "version", :type => :string, :default => nil, :banner => + "The version of the plugin to fetch" + method_option "git", :type => :string, :default => nil, :banner => + "URL of the git repo to fetch from" + method_option "branch", :type => :string, :default => nil, :banner => + "The git branch to checkout" + method_option "ref", :type => :string, :default => nil, :banner => + "The git revision to check out" + def install(*plugins) + Bundler::Plugin.install(plugins, options) + end + end +end diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb new file mode 100644 index 00000000000000..4a411a83fc701a --- /dev/null +++ b/lib/bundler/cli/pristine.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Pristine + def initialize(gems) + @gems = gems + end + + def run + CLI::Common.ensure_all_gems_in_lockfile!(@gems) + definition = Bundler.definition + definition.validate_runtime! + installer = Bundler::Installer.new(Bundler.root, definition) + + Bundler.load.specs.each do |spec| + next if spec.name == "bundler" # Source::Rubygems doesn't install bundler + next if !@gems.empty? && !@gems.include?(spec.name) + + gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})" + gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY + + case source = spec.source + when Source::Rubygems + cached_gem = spec.cache_file + unless File.exist?(cached_gem) + Bundler.ui.error("Failed to pristine #{gem_name}. Cached gem #{cached_gem} does not exist.") + next + end + + FileUtils.rm_rf spec.full_gem_path + when Source::Git + source.remote! + if extension_cache_path = source.extension_cache_path(spec) + FileUtils.rm_rf extension_cache_path + end + FileUtils.rm_rf spec.extension_dir if spec.respond_to?(:extension_dir) + FileUtils.rm_rf spec.full_gem_path + else + Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.") + next + end + + Bundler::GemInstaller.new(spec, installer, false, 0, true).install_from_spec + end + end + end +end diff --git a/lib/bundler/cli/remove.rb b/lib/bundler/cli/remove.rb new file mode 100644 index 00000000000000..cd6a2cec281e52 --- /dev/null +++ b/lib/bundler/cli/remove.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Remove + def initialize(gems, options) + @gems = gems + @options = options + end + + def run + raise InvalidOption, "Please specify gems to remove." if @gems.empty? + + Injector.remove(@gems, {}) + + Installer.install(Bundler.root, Bundler.definition) if @options["install"] + end + end +end diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb new file mode 100644 index 00000000000000..61756801b2f121 --- /dev/null +++ b/lib/bundler/cli/show.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Show + attr_reader :options, :gem_name, :latest_specs + def initialize(options, gem_name) + @options = options + @gem_name = gem_name + @verbose = options[:verbose] || options[:outdated] + @latest_specs = fetch_latest_specs if @verbose + end + + def run + Bundler.ui.silence do + Bundler.definition.validate_runtime! + Bundler.load.lock + end + + if gem_name + if gem_name == "bundler" + path = File.expand_path("../../../..", __FILE__) + else + spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match) + return unless spec + path = spec.full_gem_path + unless File.directory?(path) + Bundler.ui.warn "The gem #{gem_name} has been deleted. It was installed at:" + end + end + return Bundler.ui.info(path) + end + + if options[:paths] + Bundler.load.specs.sort_by(&:name).map do |s| + Bundler.ui.info s.full_gem_path + end + else + Bundler.ui.info "Gems included by the bundle:" + Bundler.load.specs.sort_by(&:name).each do |s| + desc = " * #{s.name} (#{s.version}#{s.git_version})" + if @verbose + latest = latest_specs.find {|l| l.name == s.name } + Bundler.ui.info <<-END.gsub(/^ +/, "") + #{desc} + \tSummary: #{s.summary || "No description available."} + \tHomepage: #{s.homepage || "No website available."} + \tStatus: #{outdated?(s, latest) ? "Outdated - #{s.version} < #{latest.version}" : "Up to date"} + END + else + Bundler.ui.info desc + end + end + end + end + + private + + def fetch_latest_specs + definition = Bundler.definition(true) + if options[:outdated] + Bundler.ui.info "Fetching remote specs for outdated check...\n\n" + Bundler.ui.silence { definition.resolve_remotely! } + else + definition.resolve_with_cache! + end + Bundler.reset! + definition.specs + end + + def outdated?(current, latest) + return false unless latest + Gem::Version.new(current.version) < Gem::Version.new(latest.version) + end + end +end diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb new file mode 100644 index 00000000000000..bf300a843717a8 --- /dev/null +++ b/lib/bundler/cli/update.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Update + attr_reader :options, :gems + def initialize(options, gems) + @options = options + @gems = gems + end + + def run + Bundler.ui.level = "error" if options[:quiet] + + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? + + sources = Array(options[:source]) + groups = Array(options[:group]).map(&:to_sym) + + full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !options[:bundler] + + if full_update && !options[:all] + if Bundler.feature_flag.update_requires_all_flag? + raise InvalidOption, "To update everything, pass the `--all` flag." + end + SharedHelpers.major_deprecation 3, "Pass --all to `bundle update` to update everything" + elsif !full_update && options[:all] + raise InvalidOption, "Cannot specify --all along with specific options." + end + + if full_update + # We're doing a full update + Bundler.definition(true) + else + unless Bundler.default_lockfile.exist? + raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \ + "Run `bundle install` to update and install the bundled gems." + end + Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems) + + if groups.any? + deps = Bundler.definition.dependencies.select {|d| (d.groups & groups).any? } + gems.concat(deps.map(&:name)) + end + + Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby], + :lock_shared_dependencies => options[:conservative], + :bundler => options[:bundler]) + end + + Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) + + Bundler::Fetcher.disable_endpoint = options["full-index"] + + opts = options.dup + opts["update"] = true + opts["local"] = options[:local] + + Bundler.settings.set_command_option_if_given :jobs, opts["jobs"] + + Bundler.definition.validate_runtime! + installer = Installer.install Bundler.root, Bundler.definition, opts + Bundler.load.cache if Bundler.app_cache.exist? + + if CLI::Common.clean_after_install? + require "bundler/cli/clean" + Bundler::CLI::Clean.new(options).run + end + + if locked_gems = Bundler.definition.locked_gems + gems.each do |name| + locked_version = locked_gems.specs.find {|s| s.name == name } + locked_version &&= locked_version.version + next unless locked_version + new_version = Bundler.definition.specs[name].first + new_version &&= new_version.version + if !new_version + Bundler.ui.warn "Bundler attempted to update #{name} but it was removed from the bundle" + elsif new_version < locked_version + Bundler.ui.warn "Note: #{name} version regressed from #{locked_version} to #{new_version}" + elsif new_version == locked_version + Bundler.ui.warn "Bundler attempted to update #{name} but its version stayed the same" + end + end + end + + Bundler.ui.confirm "Bundle updated!" + Bundler::CLI::Common.output_without_groups_message + Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + end + end +end diff --git a/lib/bundler/cli/viz.rb b/lib/bundler/cli/viz.rb new file mode 100644 index 00000000000000..644f9b25cf704b --- /dev/null +++ b/lib/bundler/cli/viz.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Viz + attr_reader :options, :gem_name + def initialize(options) + @options = options + end + + def run + # make sure we get the right `graphviz`. There is also a `graphviz` + # gem we're not built to support + gem "ruby-graphviz" + require "graphviz" + + options[:without] = options[:without].join(":").tr(" ", ":").split(":") + output_file = File.expand_path(options[:file]) + + graph = Graph.new(Bundler.load, output_file, options[:version], options[:requirements], options[:format], options[:without]) + graph.viz + rescue LoadError => e + Bundler.ui.error e.inspect + Bundler.ui.warn "Make sure you have the graphviz ruby gem. You can install it with:" + Bundler.ui.warn "`gem install ruby-graphviz`" + rescue StandardError => e + raise unless e.message =~ /GraphViz not installed or dot not in PATH/ + Bundler.ui.error e.message + Bundler.ui.warn "Please install GraphViz. On a Mac with Homebrew, you can run `brew install graphviz`." + end + end +end diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb new file mode 100644 index 00000000000000..6c241ca07a0cbf --- /dev/null +++ b/lib/bundler/compact_index_client.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "pathname" +require "set" + +module Bundler + class CompactIndexClient + DEBUG_MUTEX = Mutex.new + def self.debug + return unless ENV["DEBUG_COMPACT_INDEX"] + DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") } + end + + class Error < StandardError; end + + require "bundler/compact_index_client/cache" + require "bundler/compact_index_client/updater" + + attr_reader :directory + + # @return [Lambda] A lambda that takes an array of inputs and a block, and + # maps the inputs with the block in parallel. + # + attr_accessor :in_parallel + + def initialize(directory, fetcher) + @directory = Pathname.new(directory) + @updater = Updater.new(fetcher) + @cache = Cache.new(@directory) + @endpoints = Set.new + @info_checksums_by_name = {} + @parsed_checksums = false + @mutex = Mutex.new + @in_parallel = lambda do |inputs, &blk| + inputs.map(&blk) + end + end + + def names + Bundler::CompactIndexClient.debug { "/names" } + update(@cache.names_path, "names") + @cache.names + end + + def versions + Bundler::CompactIndexClient.debug { "/versions" } + update(@cache.versions_path, "versions") + versions, @info_checksums_by_name = @cache.versions + versions + end + + def dependencies(names) + Bundler::CompactIndexClient.debug { "dependencies(#{names})" } + in_parallel.call(names) do |name| + update_info(name) + @cache.dependencies(name).map {|d| d.unshift(name) } + end.flatten(1) + end + + def spec(name, version, platform = nil) + Bundler::CompactIndexClient.debug { "spec(name = #{name}, version = #{version}, platform = #{platform})" } + update_info(name) + @cache.specific_dependency(name, version, platform) + end + + def update_and_parse_checksums! + Bundler::CompactIndexClient.debug { "update_and_parse_checksums!" } + return @info_checksums_by_name if @parsed_checksums + update(@cache.versions_path, "versions") + @info_checksums_by_name = @cache.checksums + @parsed_checksums = true + end + + private + + def update(local_path, remote_path) + Bundler::CompactIndexClient.debug { "update(#{local_path}, #{remote_path})" } + unless synchronize { @endpoints.add?(remote_path) } + Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" } + return + end + @updater.update(local_path, url(remote_path)) + end + + def update_info(name) + Bundler::CompactIndexClient.debug { "update_info(#{name})" } + path = @cache.info_path(name) + checksum = @updater.checksum_for_file(path) + unless existing = @info_checksums_by_name[name] + Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since it is missing from versions" } + return + end + if checksum == existing + Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since the versions checksum matches the local checksum" } + return + end + Bundler::CompactIndexClient.debug { "updating info for #{name} since the versions checksum #{existing} != the local checksum #{checksum}" } + update(path, "info/#{name}") + end + + def url(path) + path + end + + def synchronize + @mutex.synchronize { yield } + end + end +end diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb new file mode 100644 index 00000000000000..f6105d3bb3ad7b --- /dev/null +++ b/lib/bundler/compact_index_client/cache.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module Bundler + class CompactIndexClient + class Cache + attr_reader :directory + + def initialize(directory) + @directory = Pathname.new(directory).expand_path + info_roots.each do |dir| + SharedHelpers.filesystem_access(dir) do + FileUtils.mkdir_p(dir) + end + end + end + + def names + lines(names_path) + end + + def names_path + directory.join("names") + end + + def versions + versions_by_name = Hash.new {|hash, key| hash[key] = [] } + info_checksums_by_name = {} + + lines(versions_path).each do |line| + name, versions_string, info_checksum = line.split(" ", 3) + info_checksums_by_name[name] = info_checksum || "" + versions_string.split(",").each do |version| + if version.start_with?("-") + version = version[1..-1].split("-", 2).unshift(name) + versions_by_name[name].delete(version) + else + version = version.split("-", 2).unshift(name) + versions_by_name[name] << version + end + end + end + + [versions_by_name, info_checksums_by_name] + end + + def versions_path + directory.join("versions") + end + + def checksums + checksums = {} + + lines(versions_path).each do |line| + name, _, checksum = line.split(" ", 3) + checksums[name] = checksum + end + + checksums + end + + def dependencies(name) + lines(info_path(name)).map do |line| + parse_gem(line) + end + end + + def info_path(name) + name = name.to_s + if name =~ /[^a-z0-9_-]/ + name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}" + info_roots.last.join(name) + else + info_roots.first.join(name) + end + end + + def specific_dependency(name, version, platform) + pattern = [version, platform].compact.join("-") + return nil if pattern.empty? + + gem_lines = info_path(name).read + gem_line = gem_lines[/^#{Regexp.escape(pattern)}\b.*/, 0] + gem_line ? parse_gem(gem_line) : nil + end + + private + + def lines(path) + return [] unless path.file? + lines = SharedHelpers.filesystem_access(path, :read, &:read).split("\n") + header = lines.index("---") + header ? lines[header + 1..-1] : lines + end + + def parse_gem(string) + version_and_platform, rest = string.split(" ", 2) + version, platform = version_and_platform.split("-", 2) + dependencies, requirements = rest.split("|", 2).map {|s| s.split(",") } if rest + dependencies = dependencies ? dependencies.map {|d| parse_dependency(d) } : [] + requirements = requirements ? requirements.map {|r| parse_dependency(r) } : [] + [version, platform, dependencies, requirements] + end + + def parse_dependency(string) + dependency = string.split(":") + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency + end + + def info_roots + [ + directory.join("info"), + directory.join("info-special-characters"), + ] + end + end + end +end diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb new file mode 100644 index 00000000000000..4d6eb80044650c --- /dev/null +++ b/lib/bundler/compact_index_client/updater.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require "bundler/vendored_fileutils" +require "stringio" +require "zlib" + +module Bundler + class CompactIndexClient + class Updater + class MisMatchedChecksumError < Error + def initialize(path, server_checksum, local_checksum) + @path = path + @server_checksum = server_checksum + @local_checksum = local_checksum + end + + def message + "The checksum of /#{@path} does not match the checksum provided by the server! Something is wrong " \ + "(local checksum is #{@local_checksum.inspect}, was expecting #{@server_checksum.inspect})." + end + end + + def initialize(fetcher) + @fetcher = fetcher + require "tmpdir" + end + + def update(local_path, remote_path, retrying = nil) + headers = {} + + Dir.mktmpdir("bundler-compact-index-") do |local_temp_dir| + local_temp_path = Pathname.new(local_temp_dir).join(local_path.basename) + + # first try to fetch any new bytes on the existing file + if retrying.nil? && local_path.file? + SharedHelpers.filesystem_access(local_temp_path) do + FileUtils.cp local_path, local_temp_path + end + headers["If-None-Match"] = etag_for(local_temp_path) + headers["Range"] = + if local_temp_path.size.nonzero? + # Subtract a byte to ensure the range won't be empty. + # Avoids 416 (Range Not Satisfiable) responses. + "bytes=#{local_temp_path.size - 1}-" + else + "bytes=#{local_temp_path.size}-" + end + else + # Fastly ignores Range when Accept-Encoding: gzip is set + headers["Accept-Encoding"] = "gzip" + end + + response = @fetcher.call(remote_path, headers) + return nil if response.is_a?(Net::HTTPNotModified) + + content = response.body + if response["Content-Encoding"] == "gzip" + content = Zlib::GzipReader.new(StringIO.new(content)).read + end + + SharedHelpers.filesystem_access(local_temp_path) do + if response.is_a?(Net::HTTPPartialContent) && local_temp_path.size.nonzero? + local_temp_path.open("a") {|f| f << slice_body(content, 1..-1) } + else + local_temp_path.open("w") {|f| f << content } + end + end + + response_etag = (response["ETag"] || "").gsub(%r{\AW/}, "") + if etag_for(local_temp_path) == response_etag + SharedHelpers.filesystem_access(local_path) do + FileUtils.mv(local_temp_path, local_path) + end + return nil + end + + if retrying + raise MisMatchedChecksumError.new(remote_path, response_etag, etag_for(local_temp_path)) + end + + update(local_path, remote_path, :retrying) + end + rescue Errno::EACCES + raise Bundler::PermissionError, + "Bundler does not have write access to create a temp directory " \ + "within #{Dir.tmpdir}. Bundler must have write access to your " \ + "systems temp directory to function properly. " + rescue Zlib::GzipFile::Error + raise Bundler::HTTPError + end + + def etag_for(path) + sum = checksum_for_file(path) + sum ? %("#{sum}") : nil + end + + def slice_body(body, range) + if body.respond_to?(:byteslice) + body.byteslice(range) + else # pre-1.9.3 + body.unpack("@#{range.first}a#{range.end + 1}").first + end + end + + def checksum_for_file(path) + return nil unless path.file? + # This must use IO.read instead of Digest.file().hexdigest + # because we need to preserve \n line endings on windows when calculating + # the checksum + SharedHelpers.filesystem_access(path, :read) do + SharedHelpers.digest(:MD5).hexdigest(IO.read(path)) + end + end + end + end +end diff --git a/lib/bundler/compatibility_guard.rb b/lib/bundler/compatibility_guard.rb new file mode 100644 index 00000000000000..750a1db04fbb37 --- /dev/null +++ b/lib/bundler/compatibility_guard.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: false + +require "rubygems" +require "bundler/version" + +if Bundler::VERSION.split(".").first.to_i >= 2 + if Gem::Version.new(Object::RUBY_VERSION.dup) < Gem::Version.new("2.3") + abort "Bundler 2 requires Ruby 2.3 or later. Either install bundler 1 or update to a supported Ruby version." + end + + if Gem::Version.new(Gem::VERSION.dup) < Gem::Version.new("2.5") + abort "Bundler 2 requires RubyGems 2.5 or later. Either install bundler 1 or update to a supported RubyGems version." + end +end diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb new file mode 100644 index 00000000000000..2e4ebb37ee606d --- /dev/null +++ b/lib/bundler/constants.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bundler + WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/ + FREEBSD = RbConfig::CONFIG["host_os"] =~ /bsd/ + NULL = WINDOWS ? "NUL" : "/dev/null" +end diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb new file mode 100644 index 00000000000000..d5efaad6c52baf --- /dev/null +++ b/lib/bundler/current_ruby.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Bundler + # Returns current version of Ruby + # + # @return [CurrentRuby] Current version of Ruby + def self.current_ruby + @current_ruby ||= CurrentRuby.new + end + + class CurrentRuby + KNOWN_MINOR_VERSIONS = %w[ + 1.8 + 1.9 + 2.0 + 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + ].freeze + + KNOWN_MAJOR_VERSIONS = KNOWN_MINOR_VERSIONS.map {|v| v.split(".", 2).first }.uniq.freeze + + KNOWN_PLATFORMS = %w[ + jruby + maglev + mingw + mri + mswin + mswin64 + rbx + ruby + truffleruby + x64_mingw + ].freeze + + def ruby? + !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || + RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev" || RUBY_ENGINE == "truffleruby") + end + + def mri? + !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") + end + + def rbx? + ruby? && defined?(RUBY_ENGINE) && RUBY_ENGINE == "rbx" + end + + def jruby? + defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" + end + + def maglev? + defined?(RUBY_ENGINE) && RUBY_ENGINE == "maglev" + end + + def truffleruby? + defined?(RUBY_ENGINE) && RUBY_ENGINE == "truffleruby" + end + + def mswin? + Bundler::WINDOWS + end + + def mswin64? + Bundler::WINDOWS && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mswin64" && Bundler.local_platform.cpu == "x64" + end + + def mingw? + Bundler::WINDOWS && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mingw32" && Bundler.local_platform.cpu != "x64" + end + + def x64_mingw? + Bundler::WINDOWS && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mingw32" && Bundler.local_platform.cpu == "x64" + end + + (KNOWN_MINOR_VERSIONS + KNOWN_MAJOR_VERSIONS).each do |version| + trimmed_version = version.tr(".", "") + define_method(:"on_#{trimmed_version}?") do + RUBY_VERSION.start_with?("#{version}.") + end + + KNOWN_PLATFORMS.each do |platform| + define_method(:"#{platform}_#{trimmed_version}?") do + send(:"#{platform}?") && send(:"on_#{trimmed_version}?") + end + end + end + end +end diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb new file mode 100644 index 00000000000000..c5e94c712301c7 --- /dev/null +++ b/lib/bundler/definition.rb @@ -0,0 +1,993 @@ +# frozen_string_literal: true + +require "bundler/lockfile_parser" +require "set" + +module Bundler + class Definition + include GemHelpers + + attr_reader( + :dependencies, + :locked_deps, + :locked_gems, + :platforms, + :requires, + :ruby_version, + :lockfile, + :gemfiles + ) + + # Given a gemfile and lockfile creates a Bundler definition + # + # @param gemfile [Pathname] Path to Gemfile + # @param lockfile [Pathname,nil] Path to Gemfile.lock + # @param unlock [Hash, Boolean, nil] Gems that have been requested + # to be updated or true if all gems should be updated + # @return [Bundler::Definition] + def self.build(gemfile, lockfile, unlock) + unlock ||= {} + gemfile = Pathname.new(gemfile).expand_path + + raise GemfileNotFound, "#{gemfile} not found" unless gemfile.file? + + Dsl.evaluate(gemfile, lockfile, unlock) + end + + # + # How does the new system work? + # + # * Load information from Gemfile and Lockfile + # * Invalidate stale locked specs + # * All specs from stale source are stale + # * All specs that are reachable only through a stale + # dependency are stale. + # * If all fresh dependencies are satisfied by the locked + # specs, then we can try to resolve locally. + # + # @param lockfile [Pathname] Path to Gemfile.lock + # @param dependencies [Array(Bundler::Dependency)] array of dependencies from Gemfile + # @param sources [Bundler::SourceList] + # @param unlock [Hash, Boolean, nil] Gems that have been requested + # to be updated or true if all gems should be updated + # @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version + # @param optional_groups [Array(String)] A list of optional groups + def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = []) + if [true, false].include?(unlock) + @unlocking_bundler = false + @unlocking = unlock + else + unlock = unlock.dup + @unlocking_bundler = unlock.delete(:bundler) + unlock.delete_if {|_k, v| Array(v).empty? } + @unlocking = !unlock.empty? + end + + @dependencies = dependencies + @sources = sources + @unlock = unlock + @optional_groups = optional_groups + @remote = false + @specs = nil + @ruby_version = ruby_version + @gemfiles = gemfiles + + @lockfile = lockfile + @lockfile_contents = String.new + @locked_bundler_version = nil + @locked_ruby_version = nil + @locked_specs_incomplete_for_platform = false + + if lockfile && File.exist?(lockfile) + @lockfile_contents = Bundler.read_file(lockfile) + @locked_gems = LockfileParser.new(@lockfile_contents) + @locked_platforms = @locked_gems.platforms + @platforms = @locked_platforms.dup + @locked_bundler_version = @locked_gems.bundler_version + @locked_ruby_version = @locked_gems.ruby_version + + if unlock != true + @locked_deps = @locked_gems.dependencies + @locked_specs = SpecSet.new(@locked_gems.specs) + @locked_sources = @locked_gems.sources + else + @unlock = {} + @locked_deps = {} + @locked_specs = SpecSet.new([]) + @locked_sources = [] + end + else + @unlock = {} + @platforms = [] + @locked_gems = nil + @locked_deps = {} + @locked_specs = SpecSet.new([]) + @locked_sources = [] + @locked_platforms = [] + end + + @unlock[:gems] ||= [] + @unlock[:sources] ||= [] + @unlock[:ruby] ||= if @ruby_version && locked_ruby_version_object + @ruby_version.diff(locked_ruby_version_object) + end + @unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version) + + add_current_platform unless Bundler.frozen_bundle? + + converge_path_sources_to_gemspec_sources + @path_changes = converge_paths + @source_changes = converge_sources + + unless @unlock[:lock_shared_dependencies] + eager_unlock = expand_dependencies(@unlock[:gems], true) + @unlock[:gems] = @locked_specs.for(eager_unlock, [], false, false, false).map(&:name) + end + + @dependency_changes = converge_dependencies + @local_changes = converge_locals + + @requires = compute_requires + end + + def gem_version_promoter + @gem_version_promoter ||= begin + locked_specs = + if unlocking? && @locked_specs.empty? && !@lockfile_contents.empty? + # Definition uses an empty set of locked_specs to indicate all gems + # are unlocked, but GemVersionPromoter needs the locked_specs + # for conservative comparison. + Bundler::SpecSet.new(@locked_gems.specs) + else + @locked_specs + end + GemVersionPromoter.new(locked_specs, @unlock[:gems]) + end + end + + def resolve_with_cache! + raise "Specs already loaded" if @specs + sources.cached! + specs + end + + def resolve_remotely! + raise "Specs already loaded" if @specs + @remote = true + sources.remote! + specs + end + + # For given dependency list returns a SpecSet with Gemspec of all the required + # dependencies. + # 1. The method first resolves the dependencies specified in Gemfile + # 2. After that it tries and fetches gemspec of resolved dependencies + # + # @return [Bundler::SpecSet] + def specs + @specs ||= begin + begin + specs = resolve.materialize(Bundler.settings[:cache_all_platforms] ? dependencies : requested_dependencies) + rescue GemNotFound => e # Handle yanked gem + gem_name, gem_version = extract_gem_info(e) + locked_gem = @locked_specs[gem_name].last + raise if locked_gem.nil? || locked_gem.version.to_s != gem_version || !@remote + raise GemNotFound, "Your bundle is locked to #{locked_gem}, but that version could not " \ + "be found in any of the sources listed in your Gemfile. If you haven't changed sources, " \ + "that means the author of #{locked_gem} has removed it. You'll need to update your bundle " \ + "to a version other than #{locked_gem} that hasn't been removed in order to install." + end + unless specs["bundler"].any? + bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last + specs["bundler"] = bundler + end + + specs + end + end + + def new_specs + specs - @locked_specs + end + + def removed_specs + @locked_specs - specs + end + + def new_platform? + @new_platform + end + + def missing_specs + missing = [] + resolve.materialize(requested_dependencies, missing) + missing + end + + def missing_specs? + missing = missing_specs + return false if missing.empty? + Bundler.ui.debug "The definition is missing #{missing.map(&:full_name)}" + true + rescue BundlerError => e + @index = nil + @resolve = nil + @specs = nil + @gem_version_promoter = nil + + Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})" + true + end + + def requested_specs + @requested_specs ||= begin + groups = requested_groups + groups.map!(&:to_sym) + specs_for(groups) + end + end + + def current_dependencies + dependencies.select(&:should_include?) + end + + def specs_for(groups) + deps = dependencies.select {|d| (d.groups & groups).any? } + deps.delete_if {|d| !d.should_include? } + specs.for(expand_dependencies(deps)) + end + + # Resolve all the dependencies specified in Gemfile. It ensures that + # dependencies that have been already resolved via locked file and are fresh + # are reused when resolving dependencies + # + # @return [SpecSet] resolved dependencies + def resolve + @resolve ||= begin + last_resolve = converge_locked_specs + resolve = + if Bundler.frozen_bundle? + Bundler.ui.debug "Frozen, using resolution from the lockfile" + last_resolve + elsif !unlocking? && nothing_changed? + Bundler.ui.debug("Found no changes, using resolution from the lockfile") + last_resolve + else + # Run a resolve against the locally available gems + Bundler.ui.debug("Found changes from the lockfile, re-resolving dependencies because #{change_reason}") + last_resolve.merge Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve, platforms) + end + + # filter out gems that _can_ be installed on multiple platforms, but don't need + # to be + resolve.for(expand_dependencies(dependencies, true), [], false, false, false) + end + end + + def index + @index ||= Index.build do |idx| + dependency_names = @dependencies.map(&:name) + + sources.all_sources.each do |source| + source.dependency_names = dependency_names - pinned_spec_names(source) + idx.add_source source.specs + dependency_names.concat(source.unmet_deps).uniq! + end + + double_check_for_index(idx, dependency_names) + end + end + + # Suppose the gem Foo depends on the gem Bar. Foo exists in Source A. Bar has some versions that exist in both + # sources A and B. At this point, the API request will have found all the versions of Bar in source A, + # but will not have found any versions of Bar from source B, which is a problem if the requested version + # of Foo specifically depends on a version of Bar that is only found in source B. This ensures that for + # each spec we found, we add all possible versions from all sources to the index. + def double_check_for_index(idx, dependency_names) + pinned_names = pinned_spec_names + loop do + idxcount = idx.size + + names = :names # do this so we only have to traverse to get dependency_names from the index once + unmet_dependency_names = lambda do + return names unless names == :names + new_names = sources.all_sources.map(&:dependency_names_to_double_check) + return names = nil if new_names.compact! + names = new_names.flatten(1).concat(dependency_names) + names.uniq! + names -= pinned_names + names + end + + sources.all_sources.each do |source| + source.double_check_for(unmet_dependency_names) + end + + break if idxcount == idx.size + end + end + private :double_check_for_index + + def has_rubygems_remotes? + sources.rubygems_sources.any? {|s| s.remotes.any? } + end + + def has_local_dependencies? + !sources.path_sources.empty? || !sources.git_sources.empty? + end + + def spec_git_paths + sources.git_sources.map {|s| s.path.to_s } + end + + def groups + dependencies.map(&:groups).flatten.uniq + end + + def lock(file, preserve_unknown_sections = false) + contents = to_lock + + # Convert to \r\n if the existing lock has them + # i.e., Windows with `git config core.autocrlf=true` + contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match("\r\n") + + if @locked_bundler_version && Bundler.feature_flag.lockfile_upgrade_warning? + locked_major = @locked_bundler_version.segments.first + current_major = Gem::Version.create(Bundler::VERSION).segments.first + + if updating_major = locked_major < current_major + Bundler.ui.warn "Warning: the lockfile is being updated to Bundler #{current_major}, " \ + "after which you will be unable to return to Bundler #{@locked_bundler_version.segments.first}." + end + end + + preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) + + return if file && File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) + + if Bundler.frozen_bundle? + Bundler.ui.error "Cannot write a changed lockfile while frozen." + return + end + + SharedHelpers.filesystem_access(file) do |p| + File.open(p, "wb") {|f| f.puts(contents) } + end + end + + def locked_bundler_version + if @locked_bundler_version && @locked_bundler_version < Gem::Version.new(Bundler::VERSION) + new_version = Bundler::VERSION + end + + new_version || @locked_bundler_version || Bundler::VERSION + end + + def locked_ruby_version + return unless ruby_version + if @unlock[:ruby] || !@locked_ruby_version + Bundler::RubyVersion.system + else + @locked_ruby_version + end + end + + def locked_ruby_version_object + return unless @locked_ruby_version + @locked_ruby_version_object ||= begin + unless version = RubyVersion.from_string(@locked_ruby_version) + raise LockfileError, "The Ruby version #{@locked_ruby_version} from " \ + "#{@lockfile} could not be parsed. " \ + "Try running bundle update --ruby to resolve this." + end + version + end + end + + def to_lock + require "bundler/lockfile_generator" + LockfileGenerator.generate(self) + end + + def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) + msg = String.new + msg << "You are trying to install in deployment mode after changing\n" \ + "your Gemfile. Run `bundle install` elsewhere and add the\n" \ + "updated #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} to version control." + + unless explicit_flag + suggested_command = if Bundler.settings.locations("frozen")[:global] + "bundle config --delete frozen" + elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? + "bundle config --delete deployment" + else + "bundle install --no-deployment" + end + msg << "\n\nIf this is a development machine, remove the #{Bundler.default_gemfile} " \ + "freeze \nby running `#{suggested_command}`." + end + + added = [] + deleted = [] + changed = [] + + new_platforms = @platforms - @locked_platforms + deleted_platforms = @locked_platforms - @platforms + added.concat new_platforms.map {|p| "* platform: #{p}" } + deleted.concat deleted_platforms.map {|p| "* platform: #{p}" } + + gemfile_sources = sources.lock_sources + + new_sources = gemfile_sources - @locked_sources + deleted_sources = @locked_sources - gemfile_sources + + new_deps = @dependencies - @locked_deps.values + deleted_deps = @locked_deps.values - @dependencies + + # Check if it is possible that the source is only changed thing + if (new_deps.empty? && deleted_deps.empty?) && (!new_sources.empty? && !deleted_sources.empty?) + new_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } + deleted_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } + end + + if @locked_sources != gemfile_sources + if new_sources.any? + added.concat new_sources.map {|source| "* source: #{source}" } + end + + if deleted_sources.any? + deleted.concat deleted_sources.map {|source| "* source: #{source}" } + end + end + + added.concat new_deps.map {|d| "* #{pretty_dep(d)}" } if new_deps.any? + if deleted_deps.any? + deleted.concat deleted_deps.map {|d| "* #{pretty_dep(d)}" } + end + + both_sources = Hash.new {|h, k| h[k] = [] } + @dependencies.each {|d| both_sources[d.name][0] = d } + @locked_deps.each {|name, d| both_sources[name][1] = d.source } + + both_sources.each do |name, (dep, lock_source)| + next unless (dep.nil? && !lock_source.nil?) || (!dep.nil? && !lock_source.nil? && !lock_source.can_lock?(dep)) + gemfile_source_name = (dep && dep.source) || "no specified source" + lockfile_source_name = lock_source || "no specified source" + changed << "* #{name} from `#{gemfile_source_name}` to `#{lockfile_source_name}`" + end + + reason = change_reason + msg << "\n\n#{reason.split(", ").map(&:capitalize).join("\n")}" unless reason.strip.empty? + msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any? + msg << "\n\nYou have deleted from the Gemfile:\n" << deleted.join("\n") if deleted.any? + msg << "\n\nYou have changed in the Gemfile:\n" << changed.join("\n") if changed.any? + msg << "\n" + + raise ProductionError, msg if added.any? || deleted.any? || changed.any? || !nothing_changed? + end + + def validate_runtime! + validate_ruby! + validate_platforms! + end + + def validate_ruby! + return unless ruby_version + + if diff = ruby_version.diff(Bundler::RubyVersion.system) + problem, expected, actual = diff + + msg = case problem + when :engine + "Your Ruby engine is #{actual}, but your Gemfile specified #{expected}" + when :version + "Your Ruby version is #{actual}, but your Gemfile specified #{expected}" + when :engine_version + "Your #{Bundler::RubyVersion.system.engine} version is #{actual}, but your Gemfile specified #{ruby_version.engine} #{expected}" + when :patchlevel + if !expected.is_a?(String) + "The Ruby patchlevel in your Gemfile must be a string" + else + "Your Ruby patchlevel is #{actual}, but your Gemfile specified #{expected}" + end + end + + raise RubyVersionMismatch, msg + end + end + + def validate_platforms! + return if @platforms.any? do |bundle_platform| + Bundler.rubygems.platforms.any? do |local_platform| + MatchPlatform.platforms_match?(bundle_platform, local_platform) + end + end + + raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \ + "but your local platforms are #{Bundler.rubygems.platforms.map(&:to_s)}, and " \ + "there's no compatible match between those two lists." + end + + def add_platform(platform) + @new_platform ||= !@platforms.include?(platform) + @platforms |= [platform] + end + + def remove_platform(platform) + return if @platforms.delete(Gem::Platform.new(platform)) + raise InvalidOption, "Unable to remove the platform `#{platform}` since the only platforms are #{@platforms.join ", "}" + end + + def add_current_platform + current_platform = Bundler.local_platform + add_platform(current_platform) if Bundler.feature_flag.specific_platform? + add_platform(generic(current_platform)) + end + + def find_resolved_spec(current_spec) + specs.find_by_name_and_platform(current_spec.name, current_spec.platform) + end + + def find_indexed_specs(current_spec) + index[current_spec.name].select {|spec| spec.match_platform(current_spec.platform) }.sort_by(&:version) + end + + attr_reader :sources + private :sources + + def nothing_changed? + !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes && !@locked_specs_incomplete_for_platform + end + + def unlocking? + @unlocking + end + + private + + def change_reason + if unlocking? + unlock_reason = @unlock.reject {|_k, v| Array(v).empty? }.map do |k, v| + if v == true + k.to_s + else + v = Array(v) + "#{k}: (#{v.join(", ")})" + end + end.join(", ") + return "bundler is unlocking #{unlock_reason}" + end + [ + [@source_changes, "the list of sources changed"], + [@dependency_changes, "the dependencies in your gemfile changed"], + [@new_platform, "you added a new platform to your gemfile"], + [@path_changes, "the gemspecs for path gems changed"], + [@local_changes, "the gemspecs for git local gems changed"], + [@locked_specs_incomplete_for_platform, "the lockfile does not have all gems needed for the current platform"], + ].select(&:first).map(&:last).join(", ") + end + + def pretty_dep(dep, source = false) + SharedHelpers.pretty_dependency(dep, source) + end + + # Check if the specs of the given source changed + # according to the locked source. + def specs_changed?(source) + locked = @locked_sources.find {|s| s == source } + + !locked || dependencies_for_source_changed?(source, locked) || specs_for_source_changed?(source) + end + + def dependencies_for_source_changed?(source, locked_source = source) + deps_for_source = @dependencies.select {|s| s.source == source } + locked_deps_for_source = @locked_deps.values.select {|dep| dep.source == locked_source } + + Set.new(deps_for_source) != Set.new(locked_deps_for_source) + end + + def specs_for_source_changed?(source) + locked_index = Index.new + locked_index.use(@locked_specs.select {|s| source.can_lock?(s) }) + + # order here matters, since Index#== is checking source.specs.include?(locked_index) + locked_index != source.specs + rescue PathError, GitError => e + Bundler.ui.debug "Assuming that #{source} has not changed since fetching its specs errored (#{e})" + false + end + + # Get all locals and override their matching sources. + # Return true if any of the locals changed (for example, + # they point to a new revision) or depend on new specs. + def converge_locals + locals = [] + + Bundler.settings.local_overrides.map do |k, v| + spec = @dependencies.find {|s| s.name == k } + source = spec && spec.source + if source && source.respond_to?(:local_override!) + source.unlock! if @unlock[:gems].include?(spec.name) + locals << [source, source.local_override!(v)] + end + end + + sources_with_changes = locals.select do |source, changed| + changed || specs_changed?(source) + end.map(&:first) + !sources_with_changes.each {|source| @unlock[:sources] << source.name }.empty? + end + + def converge_paths + sources.path_sources.any? do |source| + specs_changed?(source) + end + end + + def converge_path_source_to_gemspec_source(source) + return source unless source.instance_of?(Source::Path) + gemspec_source = sources.path_sources.find {|s| s.is_a?(Source::Gemspec) && s.as_path_source == source } + gemspec_source || source + end + + def converge_path_sources_to_gemspec_sources + @locked_sources.map! do |source| + converge_path_source_to_gemspec_source(source) + end + @locked_specs.each do |spec| + spec.source &&= converge_path_source_to_gemspec_source(spec.source) + end + @locked_deps.each do |_, dep| + dep.source &&= converge_path_source_to_gemspec_source(dep.source) + end + end + + def converge_rubygems_sources + return false if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + + changes = false + + # Get the RubyGems sources from the Gemfile.lock + locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) } + # Get the RubyGems remotes from the Gemfile + actual_remotes = sources.rubygems_remotes + + # If there is a RubyGems source in both + if !locked_gem_sources.empty? && !actual_remotes.empty? + locked_gem_sources.each do |locked_gem| + # Merge the remotes from the Gemfile into the Gemfile.lock + changes |= locked_gem.replace_remotes(actual_remotes, Bundler.settings[:allow_deployment_source_credential_changes]) + end + end + + changes + end + + def converge_sources + changes = false + + changes |= converge_rubygems_sources + + # Replace the sources from the Gemfile with the sources from the Gemfile.lock, + # if they exist in the Gemfile.lock and are `==`. If you can't find an equivalent + # source in the Gemfile.lock, use the one from the Gemfile. + changes |= sources.replace_sources!(@locked_sources) + + sources.all_sources.each do |source| + # If the source is unlockable and the current command allows an unlock of + # the source (for example, you are doing a `bundle update ` of a git-pinned + # gem), unlock it. For git sources, this means to unlock the revision, which + # will cause the `ref` used to be the most recent for the branch (or master) if + # an explicit `ref` is not used. + if source.respond_to?(:unlock!) && @unlock[:sources].include?(source.name) + source.unlock! + changes = true + end + end + + changes + end + + def converge_dependencies + frozen = Bundler.frozen_bundle? + (@dependencies + @locked_deps.values).each do |dep| + locked_source = @locked_deps[dep.name] + # This is to make sure that if bundler is installing in deployment mode and + # after locked_source and sources don't match, we still use locked_source. + if frozen && !locked_source.nil? && + locked_source.respond_to?(:source) && locked_source.source.instance_of?(Source::Path) && locked_source.source.path.exist? + dep.source = locked_source.source + elsif dep.source + dep.source = sources.get(dep.source) + end + if dep.source.is_a?(Source::Gemspec) + dep.platforms.concat(@platforms.map {|p| Dependency::REVERSE_PLATFORM_MAP[p] }.flatten(1)).uniq! + end + end + + changes = false + # We want to know if all match, but don't want to check all entries + # This means we need to return false if any dependency doesn't match + # the lock or doesn't exist in the lock. + @dependencies.each do |dependency| + unless locked_dep = @locked_deps[dependency.name] + changes = true + next + end + + # Gem::Dependency#== matches Gem::Dependency#type. As the lockfile + # doesn't carry a notion of the dependency type, if you use + # add_development_dependency in a gemspec that's loaded with the gemspec + # directive, the lockfile dependencies and resolved dependencies end up + # with a mismatch on #type. Work around that by setting the type on the + # dep from the lockfile. + locked_dep.instance_variable_set(:@type, dependency.type) + + # We already know the name matches from the hash lookup + # so we only need to check the requirement now + changes ||= dependency.requirement != locked_dep.requirement + end + + changes + end + + # Remove elements from the locked specs that are expired. This will most + # commonly happen if the Gemfile has changed since the lockfile was last + # generated + def converge_locked_specs + deps = [] + + # Build a list of dependencies that are the same in the Gemfile + # and Gemfile.lock. If the Gemfile modified a dependency, but + # the gem in the Gemfile.lock still satisfies it, this is fine + # too. + @dependencies.each do |dep| + locked_dep = @locked_deps[dep.name] + + # If the locked_dep doesn't match the dependency we're looking for then we ignore the locked_dep + locked_dep = nil unless locked_dep == dep + + if in_locked_deps?(dep, locked_dep) || satisfies_locked_spec?(dep) + deps << dep + elsif dep.source.is_a?(Source::Path) && dep.current_platform? && (!locked_dep || dep.source != locked_dep.source) + @locked_specs.each do |s| + @unlock[:gems] << s.name if s.source == dep.source + end + + dep.source.unlock! if dep.source.respond_to?(:unlock!) + dep.source.specs.each {|s| @unlock[:gems] << s.name } + end + end + + unlock_source_unlocks_spec = Bundler.feature_flag.unlock_source_unlocks_spec? + + converged = [] + @locked_specs.each do |s| + # Replace the locked dependency's source with the equivalent source from the Gemfile + dep = @dependencies.find {|d| s.satisfies?(d) } + s.source = (dep && dep.source) || sources.get(s.source) + + # Don't add a spec to the list if its source is expired. For example, + # if you change a Git gem to RubyGems. + next if s.source.nil? + next if @unlock[:sources].include?(s.source.name) + + # XXX This is a backwards-compatibility fix to preserve the ability to + # unlock a single gem by passing its name via `--source`. See issue #3759 + # TODO: delete in Bundler 2 + next if unlock_source_unlocks_spec && @unlock[:sources].include?(s.name) + + # If the spec is from a path source and it doesn't exist anymore + # then we unlock it. + + # Path sources have special logic + if s.source.instance_of?(Source::Path) || s.source.instance_of?(Source::Gemspec) + other_sources_specs = begin + s.source.specs + rescue PathError, GitError + # if we won't need the source (according to the lockfile), + # don't error if the path/git source isn't available + next if @locked_specs. + for(requested_dependencies, [], false, true, false). + none? {|locked_spec| locked_spec.source == s.source } + + raise + end + + other = other_sources_specs[s].first + + # If the spec is no longer in the path source, unlock it. This + # commonly happens if the version changed in the gemspec + next unless other + + deps2 = other.dependencies.select {|d| d.type != :development } + runtime_dependencies = s.dependencies.select {|d| d.type != :development } + # If the dependencies of the path source have changed, unlock it + next unless runtime_dependencies.sort == deps2.sort + end + + converged << s + end + + resolve = SpecSet.new(converged) + expanded_deps = expand_dependencies(deps, true) + @locked_specs_incomplete_for_platform = !resolve.for(expanded_deps, @unlock[:gems], true, true) + resolve = resolve.for(expanded_deps, @unlock[:gems], false, false, false) + diff = nil + + # Now, we unlock any sources that do not have anymore gems pinned to it + sources.all_sources.each do |source| + next unless source.respond_to?(:unlock!) + + unless resolve.any? {|s| s.source == source } + diff ||= @locked_specs.to_a - resolve.to_a + source.unlock! if diff.any? {|s| s.source == source } + end + end + + resolve + end + + def in_locked_deps?(dep, locked_dep) + # Because the lockfile can't link a dep to a specific remote, we need to + # treat sources as equivalent anytime the locked dep has all the remotes + # that the Gemfile dep does. + locked_dep && locked_dep.source && dep.source && locked_dep.source.include?(dep.source) + end + + def satisfies_locked_spec?(dep) + @locked_specs[dep].any? {|s| s.satisfies?(dep) && (!dep.source || s.source.include?(dep.source)) } + end + + # This list of dependencies is only used in #resolve, so it's OK to add + # the metadata dependencies here + def expanded_dependencies + @expanded_dependencies ||= begin + expand_dependencies(dependencies + metadata_dependencies, @remote) + end + end + + def metadata_dependencies + @metadata_dependencies ||= begin + ruby_versions = concat_ruby_version_requirements(@ruby_version) + if ruby_versions.empty? || !@ruby_version.exact? + concat_ruby_version_requirements(RubyVersion.system) + concat_ruby_version_requirements(locked_ruby_version_object) unless @unlock[:ruby] + end + [ + Dependency.new("ruby\0", ruby_versions), + Dependency.new("rubygems\0", Gem::VERSION), + ] + end + end + + def concat_ruby_version_requirements(ruby_version, ruby_versions = []) + return ruby_versions unless ruby_version + if ruby_version.patchlevel + ruby_versions << ruby_version.to_gem_version_with_patchlevel + else + ruby_versions.concat(ruby_version.versions.map do |version| + requirement = Gem::Requirement.new(version) + if requirement.exact? + "~> #{version}.0" + else + requirement + end + end) + end + end + + def expand_dependencies(dependencies, remote = false) + sorted_platforms = Resolver.sort_platforms(@platforms) + deps = [] + dependencies.each do |dep| + dep = Dependency.new(dep, ">= 0") unless dep.respond_to?(:name) + next if !remote && !dep.current_platform? + platforms = dep.gem_platforms(sorted_platforms) + if platforms.empty? && !Bundler.settings[:disable_platform_warnings] + mapped_platforms = dep.platforms.map {|p| Dependency::PLATFORM_MAP[p] } + Bundler.ui.warn \ + "The dependency #{dep} will be unused by any of the platforms Bundler is installing for. " \ + "Bundler is installing for #{@platforms.join ", "} but the dependency " \ + "is only for #{mapped_platforms.join ", "}. " \ + "To add those platforms to the bundle, " \ + "run `bundle lock --add-platform #{mapped_platforms.join " "}`." + end + platforms.each do |p| + deps << DepProxy.new(dep, p) if remote || p == generic_local_platform + end + end + deps + end + + def requested_dependencies + groups = requested_groups + groups.map!(&:to_sym) + dependencies.reject {|d| !d.should_include? || (d.groups & groups).empty? } + end + + def source_requirements + # Load all specs from remote sources + index + + # Record the specs available in each gem's source, so that those + # specs will be available later when the resolver knows where to + # look for that gemspec (or its dependencies) + default = sources.default_source + source_requirements = { :default => default } + default = nil unless Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + dependencies.each do |dep| + next unless source = dep.source || default + source_requirements[dep.name] = source + end + metadata_dependencies.each do |dep| + source_requirements[dep.name] = sources.metadata_source + end + source_requirements["bundler"] = sources.metadata_source # needs to come last to override + source_requirements + end + + def pinned_spec_names(skip = nil) + pinned_names = [] + default = Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? && sources.default_source + @dependencies.each do |dep| + next unless dep_source = dep.source || default + next if dep_source == skip + pinned_names << dep.name + end + pinned_names + end + + def requested_groups + groups - Bundler.settings[:without] - @optional_groups + Bundler.settings[:with] + end + + def lockfiles_equal?(current, proposed, preserve_unknown_sections) + if preserve_unknown_sections + sections_to_ignore = LockfileParser.sections_to_ignore(@locked_bundler_version) + sections_to_ignore += LockfileParser.unknown_sections_in_lockfile(current) + sections_to_ignore += LockfileParser::ENVIRONMENT_VERSION_SECTIONS + pattern = /#{Regexp.union(sections_to_ignore)}\n(\s{2,}.*\n)+/ + whitespace_cleanup = /\n{2,}/ + current = current.gsub(pattern, "\n").gsub(whitespace_cleanup, "\n\n").strip + proposed = proposed.gsub(pattern, "\n").gsub(whitespace_cleanup, "\n\n").strip + end + current == proposed + end + + def extract_gem_info(error) + # This method will extract the error message like "Could not find foo-1.2.3 in any of the sources" + # to an array. The first element will be the gem name (e.g. foo), the second will be the version number. + error.message.scan(/Could not find (\w+)-(\d+(?:\.\d+)+)/).flatten + end + + def compute_requires + dependencies.reduce({}) do |requires, dep| + next requires unless dep.should_include? + requires[dep.name] = Array(dep.autorequire || dep.name).map do |file| + # Allow `require: true` as an alias for `require: ` + file == true ? dep.name : file + end + requires + end + end + + def additional_base_requirements_for_resolve + return [] unless @locked_gems && Bundler.feature_flag.only_update_to_newer_versions? + dependencies_by_name = dependencies.inject({}) {|memo, dep| memo.update(dep.name => dep) } + @locked_gems.specs.reduce({}) do |requirements, locked_spec| + name = locked_spec.name + next requirements if @locked_gems.dependencies[name] != dependencies_by_name[name] + dep = Gem::Dependency.new(name, ">= #{locked_spec.version}") + requirements[name] = DepProxy.new(dep, locked_spec.platform) + requirements + end.values + end + + def equivalent_rubygems_remotes?(source) + return false unless source.is_a?(Source::Rubygems) + + Bundler.settings[:allow_deployment_source_credential_changes] && source.equivalent_remotes?(sources.rubygems_remotes) + end + end +end diff --git a/lib/bundler/dep_proxy.rb b/lib/bundler/dep_proxy.rb new file mode 100644 index 00000000000000..6c32179ac188af --- /dev/null +++ b/lib/bundler/dep_proxy.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Bundler + class DepProxy + attr_reader :__platform, :dep + + def initialize(dep, platform) + @dep = dep + @__platform = platform + end + + def hash + @hash ||= [dep, __platform].hash + end + + def ==(other) + return false if other.class != self.class + dep == other.dep && __platform == other.__platform + end + + alias_method :eql?, :== + + def type + @dep.type + end + + def name + @dep.name + end + + def requirement + @dep.requirement + end + + def to_s + s = name.dup + s << " (#{requirement})" unless requirement == Gem::Requirement.default + s << " #{__platform}" unless __platform == Gem::Platform::RUBY + s + end + + private + + def method_missing(*args, &blk) + @dep.send(*args, &blk) + end + end +end diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb new file mode 100644 index 00000000000000..8840ad6a9ca691 --- /dev/null +++ b/lib/bundler/dependency.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require "rubygems/dependency" +require "bundler/shared_helpers" +require "bundler/rubygems_ext" + +module Bundler + class Dependency < Gem::Dependency + attr_reader :autorequire + attr_reader :groups, :platforms, :gemfile + + PLATFORM_MAP = { + :ruby => Gem::Platform::RUBY, + :ruby_18 => Gem::Platform::RUBY, + :ruby_19 => Gem::Platform::RUBY, + :ruby_20 => Gem::Platform::RUBY, + :ruby_21 => Gem::Platform::RUBY, + :ruby_22 => Gem::Platform::RUBY, + :ruby_23 => Gem::Platform::RUBY, + :ruby_24 => Gem::Platform::RUBY, + :ruby_25 => Gem::Platform::RUBY, + :mri => Gem::Platform::RUBY, + :mri_18 => Gem::Platform::RUBY, + :mri_19 => Gem::Platform::RUBY, + :mri_20 => Gem::Platform::RUBY, + :mri_21 => Gem::Platform::RUBY, + :mri_22 => Gem::Platform::RUBY, + :mri_23 => Gem::Platform::RUBY, + :mri_24 => Gem::Platform::RUBY, + :mri_25 => Gem::Platform::RUBY, + :rbx => Gem::Platform::RUBY, + :truffleruby => Gem::Platform::RUBY, + :jruby => Gem::Platform::JAVA, + :jruby_18 => Gem::Platform::JAVA, + :jruby_19 => Gem::Platform::JAVA, + :mswin => Gem::Platform::MSWIN, + :mswin_18 => Gem::Platform::MSWIN, + :mswin_19 => Gem::Platform::MSWIN, + :mswin_20 => Gem::Platform::MSWIN, + :mswin_21 => Gem::Platform::MSWIN, + :mswin_22 => Gem::Platform::MSWIN, + :mswin_23 => Gem::Platform::MSWIN, + :mswin_24 => Gem::Platform::MSWIN, + :mswin_25 => Gem::Platform::MSWIN, + :mswin64 => Gem::Platform::MSWIN64, + :mswin64_19 => Gem::Platform::MSWIN64, + :mswin64_20 => Gem::Platform::MSWIN64, + :mswin64_21 => Gem::Platform::MSWIN64, + :mswin64_22 => Gem::Platform::MSWIN64, + :mswin64_23 => Gem::Platform::MSWIN64, + :mswin64_24 => Gem::Platform::MSWIN64, + :mswin64_25 => Gem::Platform::MSWIN64, + :mingw => Gem::Platform::MINGW, + :mingw_18 => Gem::Platform::MINGW, + :mingw_19 => Gem::Platform::MINGW, + :mingw_20 => Gem::Platform::MINGW, + :mingw_21 => Gem::Platform::MINGW, + :mingw_22 => Gem::Platform::MINGW, + :mingw_23 => Gem::Platform::MINGW, + :mingw_24 => Gem::Platform::MINGW, + :mingw_25 => Gem::Platform::MINGW, + :x64_mingw => Gem::Platform::X64_MINGW, + :x64_mingw_20 => Gem::Platform::X64_MINGW, + :x64_mingw_21 => Gem::Platform::X64_MINGW, + :x64_mingw_22 => Gem::Platform::X64_MINGW, + :x64_mingw_23 => Gem::Platform::X64_MINGW, + :x64_mingw_24 => Gem::Platform::X64_MINGW, + :x64_mingw_25 => Gem::Platform::X64_MINGW, + }.freeze + + REVERSE_PLATFORM_MAP = {}.tap do |reverse_platform_map| + PLATFORM_MAP.each do |key, value| + reverse_platform_map[value] ||= [] + reverse_platform_map[value] << key + end + + reverse_platform_map.each {|_, platforms| platforms.freeze } + end.freeze + + def initialize(name, version, options = {}, &blk) + type = options["type"] || :runtime + super(name, version, type) + + @autorequire = nil + @groups = Array(options["group"] || :default).map(&:to_sym) + @source = options["source"] + @platforms = Array(options["platforms"]) + @env = options["env"] + @should_include = options.fetch("should_include", true) + @gemfile = options["gemfile"] + + @autorequire = Array(options["require"] || []) if options.key?("require") + end + + # Returns the platforms this dependency is valid for, in the same order as + # passed in the `valid_platforms` parameter + def gem_platforms(valid_platforms) + return valid_platforms if @platforms.empty? + + @gem_platforms ||= @platforms.map {|pl| PLATFORM_MAP[pl] }.compact.uniq + + valid_platforms & @gem_platforms + end + + def should_include? + @should_include && current_env? && current_platform? + end + + def current_env? + return true unless @env + if @env.is_a?(Hash) + @env.all? do |key, val| + ENV[key.to_s] && (val.is_a?(String) ? ENV[key.to_s] == val : ENV[key.to_s] =~ val) + end + else + ENV[@env.to_s] + end + end + + def current_platform? + return true if @platforms.empty? + @platforms.any? do |p| + Bundler.current_ruby.send("#{p}?") + end + end + + def to_lock + out = super + out << "!" if source + out << "\n" + end + + def specific? + super + rescue NoMethodError + requirement != ">= 0" + end + end +end diff --git a/lib/bundler/deployment.rb b/lib/bundler/deployment.rb new file mode 100644 index 00000000000000..4c8f48d4056246 --- /dev/null +++ b/lib/bundler/deployment.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "bundler/shared_helpers" +Bundler::SharedHelpers.major_deprecation 3, "Bundler no longer integrates with " \ + "Capistrano, but Capistrano provides its own integration with " \ + "Bundler via the capistrano-bundler gem. Use it instead." + +module Bundler + class Deployment + def self.define_task(context, task_method = :task, opts = {}) + if defined?(Capistrano) && context.is_a?(Capistrano::Configuration) + context_name = "capistrano" + role_default = "{:except => {:no_release => true}}" + error_type = ::Capistrano::CommandError + else + context_name = "vlad" + role_default = "[:app]" + error_type = ::Rake::CommandFailedError + end + + roles = context.fetch(:bundle_roles, false) + opts[:roles] = roles if roles + + context.send :namespace, :bundle do + send :desc, <<-DESC + Install the current Bundler environment. By default, gems will be \ + installed to the shared/bundle path. Gems in the development and \ + test group will not be installed. The install command is executed \ + with the --deployment and --quiet flags. If the bundle cmd cannot \ + be found then you can override the bundle_cmd variable to specify \ + which one it should use. The base path to the app is fetched from \ + the :latest_release variable. Set it for custom deploy layouts. + + You can override any of these defaults by setting the variables shown below. + + N.B. bundle_roles must be defined before you require 'bundler/#{context_name}' \ + in your deploy.rb file. + + set :bundle_gemfile, "Gemfile" + set :bundle_dir, File.join(fetch(:shared_path), 'bundle') + set :bundle_flags, "--deployment --quiet" + set :bundle_without, [:development, :test] + set :bundle_with, [:mysql] + set :bundle_cmd, "bundle" # e.g. "/opt/ruby/bin/bundle" + set :bundle_roles, #{role_default} # e.g. [:app, :batch] + DESC + send task_method, :install, opts do + bundle_cmd = context.fetch(:bundle_cmd, "bundle") + bundle_flags = context.fetch(:bundle_flags, "--deployment --quiet") + bundle_dir = context.fetch(:bundle_dir, File.join(context.fetch(:shared_path), "bundle")) + bundle_gemfile = context.fetch(:bundle_gemfile, "Gemfile") + bundle_without = [*context.fetch(:bundle_without, [:development, :test])].compact + bundle_with = [*context.fetch(:bundle_with, [])].compact + app_path = context.fetch(:latest_release) + if app_path.to_s.empty? + raise error_type.new("Cannot detect current release path - make sure you have deployed at least once.") + end + args = ["--gemfile #{File.join(app_path, bundle_gemfile)}"] + args << "--path #{bundle_dir}" unless bundle_dir.to_s.empty? + args << bundle_flags.to_s + args << "--without #{bundle_without.join(" ")}" unless bundle_without.empty? + args << "--with #{bundle_with.join(" ")}" unless bundle_with.empty? + + run "cd #{app_path} && #{bundle_cmd} install #{args.join(" ")}" + end + end + end + end +end diff --git a/lib/bundler/deprecate.rb b/lib/bundler/deprecate.rb new file mode 100644 index 00000000000000..f59533630e99f9 --- /dev/null +++ b/lib/bundler/deprecate.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +begin + require "rubygems/deprecate" +rescue LoadError + # it's fine if it doesn't exist on the current RubyGems... + nil +end + +module Bundler + # If Bundler::Deprecate is an autoload constant, we need to define it + if defined?(Bundler::Deprecate) && !autoload?(:Deprecate) + # nothing to do! + elsif defined? ::Deprecate + Deprecate = ::Deprecate + elsif defined? Gem::Deprecate + Deprecate = Gem::Deprecate + else + class Deprecate + end + end + + unless Deprecate.respond_to?(:skip_during) + def Deprecate.skip_during + original = skip + self.skip = true + yield + ensure + self.skip = original + end + end + + unless Deprecate.respond_to?(:skip) + def Deprecate.skip + @skip ||= false + end + end + + unless Deprecate.respond_to?(:skip=) + def Deprecate.skip=(skip) + @skip = skip + end + end +end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb new file mode 100644 index 00000000000000..90ac073c36d264 --- /dev/null +++ b/lib/bundler/dsl.rb @@ -0,0 +1,615 @@ +# frozen_string_literal: true + +require "bundler/dependency" +require "bundler/ruby_dsl" + +module Bundler + class Dsl + include RubyDsl + + def self.evaluate(gemfile, lockfile, unlock) + builder = new + builder.eval_gemfile(gemfile) + builder.to_definition(lockfile, unlock) + end + + VALID_PLATFORMS = Bundler::Dependency::PLATFORM_MAP.keys.freeze + + VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules + platform platforms type source install_if gemfile].freeze + + attr_reader :gemspecs + attr_accessor :dependencies + + def initialize + @source = nil + @sources = SourceList.new + @git_sources = {} + @dependencies = [] + @groups = [] + @install_conditionals = [] + @optional_groups = [] + @platforms = [] + @env = nil + @ruby_version = nil + @gemspecs = [] + @gemfile = nil + @gemfiles = [] + add_git_sources + end + + def eval_gemfile(gemfile, contents = nil) + expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile && @gemfile.parent) + original_gemfile = @gemfile + @gemfile = expanded_gemfile_path + @gemfiles << expanded_gemfile_path + contents ||= Bundler.read_file(@gemfile.to_s) + instance_eval(contents.dup.untaint, gemfile.to_s, 1) + rescue Exception => e + message = "There was an error " \ + "#{e.is_a?(GemfileEvalError) ? "evaluating" : "parsing"} " \ + "`#{File.basename gemfile.to_s}`: #{e.message}" + + raise DSLError.new(message, gemfile, e.backtrace, contents) + ensure + @gemfile = original_gemfile + end + + def gemspec(opts = nil) + opts ||= {} + path = opts[:path] || "." + glob = opts[:glob] + name = opts[:name] + development_group = opts[:development_group] || :development + expanded_path = gemfile_root.join(path) + + gemspecs = Dir[File.join(expanded_path, "{,*}.gemspec")].map {|g| Bundler.load_gemspec(g) }.compact + gemspecs.reject! {|s| s.name != name } if name + Index.sort_specs(gemspecs) + specs_by_name_and_version = gemspecs.group_by {|s| [s.name, s.version] } + + case specs_by_name_and_version.size + when 1 + specs = specs_by_name_and_version.values.first + spec = specs.find {|s| s.match_platform(Bundler.local_platform) } || specs.first + + @gemspecs << spec + + gem_platforms = Bundler::Dependency::REVERSE_PLATFORM_MAP[Bundler::GemHelpers.generic_local_platform] + gem spec.name, :name => spec.name, :path => path, :glob => glob, :platforms => gem_platforms + + group(development_group) do + spec.development_dependencies.each do |dep| + gem dep.name, *(dep.requirement.as_list + [:type => :development]) + end + end + when 0 + raise InvalidOption, "There are no gemspecs at #{expanded_path}" + else + raise InvalidOption, "There are multiple gemspecs at #{expanded_path}. " \ + "Please use the :name option to specify which one should be used" + end + end + + def gem(name, *args) + options = args.last.is_a?(Hash) ? args.pop.dup : {} + options["gemfile"] = @gemfile + version = args || [">= 0"] + + normalize_options(name, version, options) + + dep = Dependency.new(name, version, options) + + # if there's already a dependency with this name we try to prefer one + if current = @dependencies.find {|d| d.name == dep.name } + deleted_dep = @dependencies.delete(current) if current.type == :development + + if current.requirement != dep.requirement + unless deleted_dep + return if dep.type == :development + + update_prompt = "" + + if File.basename(@gemfile) == Injector::INJECTED_GEMS + if dep.requirements_list.include?(">= 0") && !current.requirements_list.include?(">= 0") + update_prompt = ". Gem already added" + else + update_prompt = ". If you want to update the gem version, run `bundle update #{current.name}`" + + update_prompt += ". You may also need to change the version requirement specified in the Gemfile if it's too restrictive." unless current.requirements_list.include?(">= 0") + end + end + + raise GemfileError, "You cannot specify the same gem twice with different version requirements.\n" \ + "You specified: #{current.name} (#{current.requirement}) and #{dep.name} (#{dep.requirement})" \ + "#{update_prompt}" + end + + else + Bundler.ui.warn "Your Gemfile lists the gem #{current.name} (#{current.requirement}) more than once.\n" \ + "You should probably keep only one of them.\n" \ + "Remove any duplicate entries and specify the gem only once (per group).\n" \ + "While it's not a problem now, it could cause errors if you change the version of one of them later." + end + + if current.source != dep.source + unless deleted_dep + return if dep.type == :development + raise GemfileError, "You cannot specify the same gem twice coming from different sources.\n" \ + "You specified that #{dep.name} (#{dep.requirement}) should come from " \ + "#{current.source || "an unspecified source"} and #{dep.source}\n" + end + end + end + + @dependencies << dep + end + + def source(source, *args, &blk) + options = args.last.is_a?(Hash) ? args.pop.dup : {} + options = normalize_hash(options) + source = normalize_source(source) + + if options.key?("type") + options["type"] = options["type"].to_s + unless Plugin.source?(options["type"]) + raise InvalidOption, "No plugin sources available for #{options["type"]}" + end + + unless block_given? + raise InvalidOption, "You need to pass a block to #source with :type option" + end + + source_opts = options.merge("uri" => source) + with_source(@sources.add_plugin_source(options["type"], source_opts), &blk) + elsif block_given? + with_source(@sources.add_rubygems_source("remotes" => source), &blk) + else + check_primary_source_safety(@sources) + @sources.global_rubygems_source = source + end + end + + def git_source(name, &block) + unless block_given? + raise InvalidOption, "You need to pass a block to #git_source" + end + + if valid_keys.include?(name.to_s) + raise InvalidOption, "You cannot use #{name} as a git source. It " \ + "is a reserved key. Reserved keys are: #{valid_keys.join(", ")}" + end + + @git_sources[name.to_s] = block + end + + def path(path, options = {}, &blk) + unless block_given? + msg = "You can no longer specify a path source by itself. Instead, \n" \ + "either use the :path option on a gem, or specify the gems that \n" \ + "bundler should find in the path source by passing a block to \n" \ + "the path method, like: \n\n" \ + " path 'dir/containing/rails' do\n" \ + " gem 'rails'\n" \ + " end\n\n" + + raise DeprecatedError, msg if Bundler.feature_flag.disable_multisource? + SharedHelpers.major_deprecation(3, msg.strip) + end + + source_options = normalize_hash(options).merge( + "path" => Pathname.new(path), + "root_path" => gemfile_root, + "gemspec" => gemspecs.find {|g| g.name == options["name"] } + ) + source = @sources.add_path_source(source_options) + with_source(source, &blk) + end + + def git(uri, options = {}, &blk) + unless block_given? + msg = "You can no longer specify a git source by itself. Instead, \n" \ + "either use the :git option on a gem, or specify the gems that \n" \ + "bundler should find in the git source by passing a block to \n" \ + "the git method, like: \n\n" \ + " git 'git://github.com/rails/rails.git' do\n" \ + " gem 'rails'\n" \ + " end" + raise DeprecatedError, msg + end + + with_source(@sources.add_git_source(normalize_hash(options).merge("uri" => uri)), &blk) + end + + def github(repo, options = {}) + raise ArgumentError, "GitHub sources require a block" unless block_given? + raise DeprecatedError, "The #github method has been removed" if Bundler.feature_flag.skip_default_git_sources? + github_uri = @git_sources["github"].call(repo) + git_options = normalize_hash(options).merge("uri" => github_uri) + git_source = @sources.add_git_source(git_options) + with_source(git_source) { yield } + end + + def to_definition(lockfile, unlock) + Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) + end + + def group(*args, &blk) + options = args.last.is_a?(Hash) ? args.pop.dup : {} + normalize_group_options(options, args) + + @groups.concat args + + if options["optional"] + optional_groups = args - @optional_groups + @optional_groups.concat optional_groups + end + + yield + ensure + args.each { @groups.pop } + end + + def install_if(*args) + @install_conditionals.concat args + yield + ensure + args.each { @install_conditionals.pop } + end + + def platforms(*platforms) + @platforms.concat platforms + yield + ensure + platforms.each { @platforms.pop } + end + alias_method :platform, :platforms + + def env(name) + old = @env + @env = name + yield + ensure + @env = old + end + + def plugin(*args) + # Pass on + end + + def method_missing(name, *args) + raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile" + end + + private + + def add_git_sources + return if Bundler.feature_flag.skip_default_git_sources? + + git_source(:github) do |repo_name| + warn_deprecated_git_source(:github, <<-'RUBY'.strip, 'Change any "reponame" :github sources to "username/reponame".') +"https://github.com/#{repo_name}.git" + RUBY + # It would be better to use https instead of the git protocol, but this + # can break deployment of existing locked bundles when switching between + # different versions of Bundler. The change will be made in 2.0, which + # does not guarantee compatibility with the 1.x series. + # + # See https://github.com/bundler/bundler/pull/2569 for discussion + # + # This can be overridden by adding this code to your Gemfiles: + # + # git_source(:github) do |repo_name| + # repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + # "https://github.com/#{repo_name}.git" + # end + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + # TODO: 2.0 upgrade this setting to the default + if Bundler.settings["github.https"] + Bundler::SharedHelpers.major_deprecation 3, "The `github.https` setting will be removed" + "https://github.com/#{repo_name}.git" + else + "git://github.com/#{repo_name}.git" + end + end + + # TODO: 2.0 remove this deprecated git source + git_source(:gist) do |repo_name| + warn_deprecated_git_source(:gist, '"https://gist.github.com/#{repo_name}.git"') + + "https://gist.github.com/#{repo_name}.git" + end + + # TODO: 2.0 remove this deprecated git source + git_source(:bitbucket) do |repo_name| + warn_deprecated_git_source(:bitbucket, <<-'RUBY'.strip) +user_name, repo_name = repo_name.split("/") +repo_name ||= user_name +"https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git" + RUBY + + user_name, repo_name = repo_name.split("/") + repo_name ||= user_name + "https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git" + end + end + + def with_source(source) + old_source = @source + if block_given? + @source = source + yield + end + source + ensure + @source = old_source + end + + def normalize_hash(opts) + opts.keys.each do |k| + opts[k.to_s] = opts.delete(k) unless k.is_a?(String) + end + opts + end + + def valid_keys + @valid_keys ||= VALID_KEYS + end + + def normalize_options(name, version, opts) + if name.is_a?(Symbol) + raise GemfileError, %(You need to specify gem names as Strings. Use 'gem "#{name}"' instead) + end + if name =~ /\s/ + raise GemfileError, %('#{name}' is not a valid gem name because it contains whitespace) + end + if name.empty? + raise GemfileError, %(an empty gem name is not valid) + end + + normalize_hash(opts) + + git_names = @git_sources.keys.map(&:to_s) + validate_keys("gem '#{name}'", opts, valid_keys + git_names) + + groups = @groups.dup + opts["group"] = opts.delete("groups") || opts["group"] + groups.concat Array(opts.delete("group")) + groups = [:default] if groups.empty? + + install_if = @install_conditionals.dup + install_if.concat Array(opts.delete("install_if")) + install_if = install_if.reduce(true) do |memo, val| + memo && (val.respond_to?(:call) ? val.call : val) + end + + platforms = @platforms.dup + opts["platforms"] = opts["platform"] || opts["platforms"] + platforms.concat Array(opts.delete("platforms")) + platforms.map!(&:to_sym) + platforms.each do |p| + next if VALID_PLATFORMS.include?(p) + raise GemfileError, "`#{p}` is not a valid platform. The available options are: #{VALID_PLATFORMS.inspect}" + end + + # Save sources passed in a key + if opts.key?("source") + source = normalize_source(opts["source"]) + opts["source"] = @sources.add_rubygems_source("remotes" => source) + end + + git_name = (git_names & opts.keys).last + if @git_sources[git_name] + opts["git"] = @git_sources[git_name].call(opts[git_name]) + end + + %w[git path].each do |type| + next unless param = opts[type] + if version.first && version.first =~ /^\s*=?\s*(\d[^\s]*)\s*$/ + options = opts.merge("name" => name, "version" => $1) + else + options = opts.dup + end + source = send(type, param, options) {} + opts["source"] = source + end + + opts["source"] ||= @source + opts["env"] ||= @env + opts["platforms"] = platforms.dup + opts["group"] = groups + opts["should_include"] = install_if + end + + def normalize_group_options(opts, groups) + normalize_hash(opts) + + groups = groups.map {|group| ":#{group}" }.join(", ") + validate_keys("group #{groups}", opts, %w[optional]) + + opts["optional"] ||= false + end + + def validate_keys(command, opts, valid_keys) + invalid_keys = opts.keys - valid_keys + + git_source = opts.keys & @git_sources.keys.map(&:to_s) + if opts["branch"] && !(opts["git"] || opts["github"] || git_source.any?) + raise GemfileError, %(The `branch` option for `#{command}` is not allowed. Only gems with a git source can specify a branch) + end + + return true unless invalid_keys.any? + + message = String.new + message << "You passed #{invalid_keys.map {|k| ":" + k }.join(", ")} " + message << if invalid_keys.size > 1 + "as options for #{command}, but they are invalid." + else + "as an option for #{command}, but it is invalid." + end + + message << " Valid options are: #{valid_keys.join(", ")}." + message << " You may be able to resolve this by upgrading Bundler to the newest version." + raise InvalidOption, message + end + + def normalize_source(source) + case source + when :gemcutter, :rubygems, :rubyforge + Bundler::SharedHelpers.major_deprecation 3, "The source :#{source} is deprecated because HTTP " \ + "requests are insecure.\nPlease change your source to 'https://" \ + "rubygems.org' if possible, or 'http://rubygems.org' if not." + "http://rubygems.org" + when String + source + else + raise GemfileError, "Unknown source '#{source}'" + end + end + + def check_primary_source_safety(source_list) + return if source_list.rubygems_primary_remotes.empty? && source_list.global_rubygems_source.nil? + + if Bundler.feature_flag.disable_multisource? + msg = "This Gemfile contains multiple primary sources. " \ + "Each source after the first must include a block to indicate which gems " \ + "should come from that source" + unless Bundler.feature_flag.bundler_3_mode? + msg += ". To downgrade this error to a warning, run " \ + "`bundle config --delete disable_multisource`" + end + raise GemfileEvalError, msg + else + Bundler::SharedHelpers.major_deprecation 3, "Your Gemfile contains multiple primary sources. " \ + "Using `source` more than once without a block is a security risk, and " \ + "may result in installing unexpected gems. To resolve this warning, use " \ + "a block to indicate which gems should come from the secondary source. " \ + "To upgrade this warning to an error, run `bundle config " \ + "disable_multisource true`." + end + end + + def warn_deprecated_git_source(name, replacement, additional_message = nil) + # TODO: 2.0 remove deprecation + additional_message &&= " #{additional_message}" + replacement = if replacement.count("\n").zero? + "{|repo_name| #{replacement} }" + else + "do |repo_name|\n#{replacement.to_s.gsub(/^/, " ")}\n end" + end + + Bundler::SharedHelpers.major_deprecation 3, <<-EOS +The :#{name} git source is deprecated, and will be removed in Bundler 3.0.#{additional_message} Add this code to the top of your Gemfile to ensure it continues to work: + + git_source(:#{name}) #{replacement} + + EOS + end + + class DSLError < GemfileError + # @return [String] the description that should be presented to the user. + # + attr_reader :description + + # @return [String] the path of the dsl file that raised the exception. + # + attr_reader :dsl_path + + # @return [Exception] the backtrace of the exception raised by the + # evaluation of the dsl file. + # + attr_reader :backtrace + + # @param [Exception] backtrace @see backtrace + # @param [String] dsl_path @see dsl_path + # + def initialize(description, dsl_path, backtrace, contents = nil) + @status_code = $!.respond_to?(:status_code) && $!.status_code + + @description = description + @dsl_path = dsl_path + @backtrace = backtrace + @contents = contents + end + + def status_code + @status_code || super + end + + # @return [String] the contents of the DSL that cause the exception to + # be raised. + # + def contents + @contents ||= begin + dsl_path && File.exist?(dsl_path) && File.read(dsl_path) + end + end + + # The message of the exception reports the content of podspec for the + # line that generated the original exception. + # + # @example Output + # + # Invalid podspec at `RestKit.podspec` - undefined method + # `exclude_header_search_paths=' for # + # + # from spec-repos/master/RestKit/0.9.3/RestKit.podspec:36 + # ------------------------------------------- + # # because it would break: #import + # > ns.exclude_header_search_paths = 'Code/RestKit.h' + # end + # ------------------------------------------- + # + # @return [String] the message of the exception. + # + def to_s + @to_s ||= begin + trace_line, description = parse_line_number_from_description + + m = String.new("\n[!] ") + m << description + m << ". Bundler cannot continue.\n" + + return m unless backtrace && dsl_path && contents + + trace_line = backtrace.find {|l| l.include?(dsl_path.to_s) } || trace_line + return m unless trace_line + line_numer = trace_line.split(":")[1].to_i - 1 + return m unless line_numer + + lines = contents.lines.to_a + indent = " # " + indicator = indent.tr("#", ">") + first_line = line_numer.zero? + last_line = (line_numer == (lines.count - 1)) + + m << "\n" + m << "#{indent}from #{trace_line.gsub(/:in.*$/, "")}\n" + m << "#{indent}-------------------------------------------\n" + m << "#{indent}#{lines[line_numer - 1]}" unless first_line + m << "#{indicator}#{lines[line_numer]}" + m << "#{indent}#{lines[line_numer + 1]}" unless last_line + m << "\n" unless m.end_with?("\n") + m << "#{indent}-------------------------------------------\n" + end + end + + private + + def parse_line_number_from_description + description = self.description + if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path.to_s}):\d+)/ + trace_line = Regexp.last_match[1] + description = description.sub(/#{Regexp.quote trace_line}:\s*/, "").sub("\n", " - ") + end + [trace_line, description] + end + end + + def gemfile_root + @gemfile ||= Bundler.default_gemfile + @gemfile.dirname + end + end +end diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb new file mode 100644 index 00000000000000..9a00b64e0ede93 --- /dev/null +++ b/lib/bundler/endpoint_specification.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +module Bundler + # used for Creating Specifications from the Gemcutter Endpoint + class EndpointSpecification < Gem::Specification + ILLFORMED_MESSAGE = 'Ill-formed requirement ["# 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 + + def build_dependency(name, requirements) + Gem::Dependency.new(name, requirements) + rescue ArgumentError => e + raise unless e.message.include?(ILLFORMED_MESSAGE) + puts # we shouldn't print the error message on the "fetching info" status line + raise GemspecError, + "Unfortunately, the gem #{name} (#{version}) has an invalid " \ + "gemspec.\nPlease ask the gem author to yank the bad version to fix " \ + "this issue. For more information, see http://bit.ly/syck-defaultkey." + end + end +end diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb new file mode 100644 index 00000000000000..51738139fac815 --- /dev/null +++ b/lib/bundler/env.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require "bundler/rubygems_integration" +require "bundler/source/git/git_proxy" + +module Bundler + class Env + def self.write(io) + io.write report + end + + def self.report(options = {}) + print_gemfile = options.delete(:print_gemfile) { true } + print_gemspecs = options.delete(:print_gemspecs) { true } + + out = String.new + append_formatted_table("Environment", environment, out) + append_formatted_table("Bundler Build Metadata", BuildMetadata.to_h, out) + + unless Bundler.settings.all.empty? + out << "\n## Bundler settings\n\n```\n" + Bundler.settings.all.each do |setting| + out << setting << "\n" + Bundler.settings.pretty_values_for(setting).each do |line| + out << " " << line << "\n" + end + end + out << "```\n" + end + + return out unless SharedHelpers.in_bundle? + + if print_gemfile + gemfiles = [Bundler.default_gemfile] + begin + gemfiles = Bundler.definition.gemfiles + rescue GemfileNotFound + nil + end + + out << "\n## Gemfile\n" + gemfiles.each do |gemfile| + out << "\n### #{Pathname.new(gemfile).relative_path_from(SharedHelpers.pwd)}\n\n" + out << "```ruby\n" << read_file(gemfile).chomp << "\n```\n" + end + + out << "\n### #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}\n\n" + out << "```\n" << read_file(Bundler.default_lockfile).chomp << "\n```\n" + end + + if print_gemspecs + dsl = Dsl.new.tap {|d| d.eval_gemfile(Bundler.default_gemfile) } + out << "\n## Gemspecs\n" unless dsl.gemspecs.empty? + dsl.gemspecs.each do |gs| + out << "\n### #{File.basename(gs.loaded_from)}" + out << "\n\n```ruby\n" << read_file(gs.loaded_from).chomp << "\n```\n" + end + end + + out + end + + def self.read_file(filename) + Bundler.read_file(filename.to_s).strip + rescue Errno::ENOENT + "" + rescue RuntimeError => e + "#{e.class}: #{e.message}" + end + + def self.ruby_version + str = String.new("#{RUBY_VERSION}") + if RUBY_VERSION < "1.9" + str << " (#{RUBY_RELEASE_DATE}" + str << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL + str << ") [#{RUBY_PLATFORM}]" + else + str << "p#{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL + str << " (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{RUBY_PLATFORM}]" + end + end + + def self.git_version + Bundler::Source::Git::GitProxy.new(nil, nil, nil).full_version + rescue Bundler::Source::Git::GitNotInstalledError + "not installed" + end + + def self.version_of(script) + return "not installed" unless Bundler.which(script) + `#{script} --version`.chomp + end + + def self.chruby_version + return "not installed" unless Bundler.which("chruby-exec") + `chruby-exec -- chruby --version`. + sub(/.*^chruby: (#{Gem::Version::VERSION_PATTERN}).*/m, '\1') + end + + def self.environment + out = [] + + out << ["Bundler", Bundler::VERSION] + out << [" Platforms", Gem.platforms.join(", ")] + out << ["Ruby", ruby_version] + out << [" Full Path", Gem.ruby] + out << [" Config Dir", Pathname.new(Gem::ConfigFile::SYSTEM_WIDE_CONFIG_FILE).dirname] + out << ["RubyGems", Gem::VERSION] + out << [" Gem Home", ENV.fetch("GEM_HOME") { Gem.dir }] + out << [" Gem Path", ENV.fetch("GEM_PATH") { Gem.path.join(File::PATH_SEPARATOR) }] + out << [" User Path", Gem.user_dir] + out << [" Bin Dir", Gem.bindir] + if defined?(OpenSSL) + out << ["OpenSSL"] + out << [" Compiled", OpenSSL::OPENSSL_VERSION] if defined?(OpenSSL::OPENSSL_VERSION) + out << [" Loaded", OpenSSL::OPENSSL_LIBRARY_VERSION] if defined?(OpenSSL::OPENSSL_LIBRARY_VERSION) + out << [" Cert File", OpenSSL::X509::DEFAULT_CERT_FILE] if defined?(OpenSSL::X509::DEFAULT_CERT_FILE) + out << [" Cert Dir", OpenSSL::X509::DEFAULT_CERT_DIR] if defined?(OpenSSL::X509::DEFAULT_CERT_DIR) + end + out << ["Tools"] + out << [" Git", git_version] + out << [" RVM", ENV.fetch("rvm_version") { version_of("rvm") }] + out << [" rbenv", version_of("rbenv")] + out << [" chruby", chruby_version] + + %w[rubygems-bundler open_gem].each do |name| + specs = Bundler.rubygems.find_name(name) + out << [" #{name}", "(#{specs.map(&:version).join(",")})"] unless specs.empty? + end + if (exe = caller.last.split(":").first) && exe =~ %r{(exe|bin)/bundler?\z} + shebang = File.read(exe).lines.first + shebang.sub!(/^#!\s*/, "") + unless shebang.start_with?(Gem.ruby, "/usr/bin/env ruby") + out << ["Gem.ruby", Gem.ruby] + out << ["bundle #!", shebang] + end + end + + out + end + + def self.append_formatted_table(title, pairs, out) + return if pairs.empty? + out << "\n" unless out.empty? + out << "## #{title}\n\n```\n" + ljust = pairs.map {|k, _v| k.to_s.length }.max + pairs.each do |k, v| + out << "#{k.to_s.ljust(ljust)} #{v}\n" + end + out << "```\n" + end + + private_class_method :read_file, :ruby_version, :git_version, :append_formatted_table, :version_of, :chruby_version + end +end diff --git a/lib/bundler/environment_preserver.rb b/lib/bundler/environment_preserver.rb new file mode 100644 index 00000000000000..af7c1ef0a49897 --- /dev/null +++ b/lib/bundler/environment_preserver.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Bundler + class EnvironmentPreserver + INTENTIONALLY_NIL = "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL".freeze + BUNDLER_KEYS = %w[ + BUNDLE_BIN_PATH + BUNDLE_GEMFILE + BUNDLER_ORIG_MANPATH + BUNDLER_VERSION + GEM_HOME + GEM_PATH + MANPATH + PATH + RB_USER_INSTALL + RUBYLIB + RUBYOPT + ].map(&:freeze).freeze + BUNDLER_PREFIX = "BUNDLER_ORIG_".freeze + + # @param env [ENV] + # @param keys [Array] + def initialize(env, keys) + @original = env.to_hash + @keys = keys + @prefix = BUNDLER_PREFIX + end + + # @return [Hash] + def backup + env = @original.clone + @keys.each do |key| + value = env[key] + if !value.nil? && !value.empty? + env[@prefix + key] ||= value + elsif value.nil? + env[@prefix + key] ||= INTENTIONALLY_NIL + end + end + env + end + + # @return [Hash] + def restore + env = @original.clone + @keys.each do |key| + value_original = env[@prefix + key] + next if value_original.nil? || value_original.empty? + if value_original == INTENTIONALLY_NIL + env.delete(key) + else + env[key] = value_original + end + env.delete(@prefix + key) + end + env + end + end +end diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb new file mode 100644 index 00000000000000..e471bce0b6f78a --- /dev/null +++ b/lib/bundler/errors.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +module Bundler + class BundlerError < StandardError + def self.status_code(code) + define_method(:status_code) { code } + if match = BundlerError.all_errors.find {|_k, v| v == code } + error, _ = match + raise ArgumentError, + "Trying to register #{self} for status code #{code} but #{error} is already registered" + end + BundlerError.all_errors[self] = code + end + + def self.all_errors + @all_errors ||= {} + end + end + + class GemfileError < BundlerError; status_code(4); end + class InstallError < BundlerError; status_code(5); end + + # Internal error, should be rescued + class VersionConflict < BundlerError + attr_reader :conflicts + + def initialize(conflicts, msg = nil) + super(msg) + @conflicts = conflicts + end + + status_code(6) + end + + class GemNotFound < BundlerError; status_code(7); end + class InstallHookError < BundlerError; status_code(8); end + class GemfileNotFound < BundlerError; status_code(10); end + class GitError < BundlerError; status_code(11); end + class DeprecatedError < BundlerError; status_code(12); end + class PathError < BundlerError; status_code(13); end + class GemspecError < BundlerError; status_code(14); end + class InvalidOption < BundlerError; status_code(15); end + class ProductionError < BundlerError; status_code(16); end + class HTTPError < BundlerError + status_code(17) + def filter_uri(uri) + URICredentialsFilter.credential_filtered_uri(uri) + end + end + class RubyVersionMismatch < BundlerError; status_code(18); end + class SecurityError < BundlerError; status_code(19); end + class LockfileError < BundlerError; status_code(20); end + class CyclicDependencyError < BundlerError; status_code(21); end + class GemfileLockNotFound < BundlerError; status_code(22); end + class PluginError < BundlerError; status_code(29); end + class SudoNotPermittedError < BundlerError; status_code(30); end + class ThreadCreationError < BundlerError; status_code(33); end + class APIResponseMismatchError < BundlerError; status_code(34); end + class GemfileEvalError < GemfileError; end + class MarshalError < StandardError; end + + class PermissionError < BundlerError + def initialize(path, permission_type = :write) + @path = path + @permission_type = permission_type + end + + def action + case @permission_type + when :read then "read from" + when :write then "write to" + when :executable, :exec then "execute" + else @permission_type.to_s + end + end + + def message + "There was an error while trying to #{action} `#{@path}`. " \ + "It is likely that you need to grant #{@permission_type} permissions " \ + "for that path." + end + + status_code(23) + end + + class GemRequireError < BundlerError + attr_reader :orig_exception + + def initialize(orig_exception, msg) + full_message = msg + "\nGem Load Error is: #{orig_exception.message}\n"\ + "Backtrace for gem load error is:\n"\ + "#{orig_exception.backtrace.join("\n")}\n"\ + "Bundler Error Backtrace:\n" + super(full_message) + @orig_exception = orig_exception + end + + status_code(24) + end + + class YamlSyntaxError < BundlerError + attr_reader :orig_exception + + def initialize(orig_exception, msg) + super(msg) + @orig_exception = orig_exception + end + + status_code(25) + end + + class TemporaryResourceError < PermissionError + def message + "There was an error while trying to #{action} `#{@path}`. " \ + "Some resource was temporarily unavailable. It's suggested that you try" \ + "the operation again." + end + + status_code(26) + end + + class VirtualProtocolError < BundlerError + def message + "There was an error relating to virtualization and file access." \ + "It is likely that you need to grant access to or mount some file system correctly." + end + + status_code(27) + end + + class OperationNotSupportedError < PermissionError + def message + "Attempting to #{action} `#{@path}` is unsupported by your OS." + end + + status_code(28) + end + + class NoSpaceOnDeviceError < PermissionError + def message + "There was an error while trying to #{action} `#{@path}`. " \ + "There was insufficient space remaining on the device." + end + + status_code(31) + end + + class GenericSystemCallError < BundlerError + attr_reader :underlying_error + + def initialize(underlying_error, message) + @underlying_error = underlying_error + super("#{message}\nThe underlying system error is #{@underlying_error.class}: #{@underlying_error}") + end + + status_code(32) + end +end diff --git a/lib/bundler/feature_flag.rb b/lib/bundler/feature_flag.rb new file mode 100644 index 00000000000000..e5b4e84063ff5c --- /dev/null +++ b/lib/bundler/feature_flag.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Bundler + class FeatureFlag + def self.settings_flag(flag, &default) + unless Bundler::Settings::BOOL_KEYS.include?(flag.to_s) + raise "Cannot use `#{flag}` as a settings feature flag since it isn't a bool key" + end + + settings_method("#{flag}?", flag, &default) + end + private_class_method :settings_flag + + def self.settings_option(key, &default) + settings_method(key, key, &default) + end + private_class_method :settings_option + + def self.settings_method(name, key, &default) + define_method(name) do + value = Bundler.settings[key] + value = instance_eval(&default) if value.nil? && !default.nil? + value + end + end + private_class_method :settings_method + + (1..10).each {|v| define_method("bundler_#{v}_mode?") { major_version >= v } } + + settings_flag(:allow_bundler_dependency_conflicts) { bundler_3_mode? } + settings_flag(:allow_offline_install) { bundler_3_mode? } + settings_flag(:auto_clean_without_path) { bundler_3_mode? } + settings_flag(:auto_config_jobs) { bundler_3_mode? } + settings_flag(:cache_all) { bundler_3_mode? } + settings_flag(:cache_command_is_package) { bundler_3_mode? } + settings_flag(:console_command) { !bundler_3_mode? } + settings_flag(:default_install_uses_path) { bundler_3_mode? } + settings_flag(:deployment_means_frozen) { bundler_3_mode? } + settings_flag(:disable_multisource) { bundler_3_mode? } + settings_flag(:error_on_stderr) { bundler_2_mode? } + settings_flag(:forget_cli_options) { bundler_3_mode? } + settings_flag(:global_path_appends_ruby_scope) { bundler_3_mode? } + settings_flag(:global_gem_cache) { bundler_3_mode? } + settings_flag(:init_gems_rb) { bundler_3_mode? } + settings_flag(:list_command) { bundler_3_mode? } + settings_flag(:lockfile_uses_separate_rubygems_sources) { bundler_3_mode? } + settings_flag(:lockfile_upgrade_warning) { bundler_3_mode? } + settings_flag(:only_update_to_newer_versions) { bundler_3_mode? } + settings_flag(:path_relative_to_cwd) { bundler_3_mode? } + settings_flag(:plugins) { @bundler_version >= Gem::Version.new("1.14") } + settings_flag(:prefer_gems_rb) { bundler_3_mode? } + settings_flag(:print_only_version_number) { bundler_3_mode? } + settings_flag(:setup_makes_kernel_gem_public) { !bundler_3_mode? } + settings_flag(:skip_default_git_sources) { bundler_3_mode? } + settings_flag(:specific_platform) { bundler_3_mode? } + settings_flag(:suppress_install_using_messages) { bundler_3_mode? } + settings_flag(:unlock_source_unlocks_spec) { !bundler_3_mode? } + settings_flag(:update_requires_all_flag) { bundler_3_mode? } + settings_flag(:use_gem_version_promoter_for_major_updates) { bundler_3_mode? } + settings_flag(:viz_command) { !bundler_3_mode? } + + settings_option(:default_cli_command) { bundler_3_mode? ? :cli_help : :install } + + settings_method(:github_https?, "github.https") { bundler_2_mode? } + + def initialize(bundler_version) + @bundler_version = Gem::Version.create(bundler_version) + end + + def major_version + @bundler_version.segments.first + end + private :major_version + end +end diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb new file mode 100644 index 00000000000000..4dd42e42ffa750 --- /dev/null +++ b/lib/bundler/fetcher.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true + +require "bundler/vendored_persistent" +require "cgi" +require "securerandom" +require "zlib" + +module Bundler + # Handles all the fetching with the rubygems server + class Fetcher + autoload :CompactIndex, "bundler/fetcher/compact_index" + autoload :Downloader, "bundler/fetcher/downloader" + autoload :Dependency, "bundler/fetcher/dependency" + autoload :Index, "bundler/fetcher/index" + + # This error is raised when it looks like the network is down + class NetworkDownError < HTTPError; end + # This error is raised if the API returns a 413 (only printed in verbose) + class FallbackError < HTTPError; end + # This is the error raised if OpenSSL fails the cert verification + class CertificateFailureError < HTTPError + def initialize(remote_uri) + remote_uri = filter_uri(remote_uri) + super "Could not verify the SSL certificate for #{remote_uri}.\nThere" \ + " is a chance you are experiencing a man-in-the-middle attack, but" \ + " most likely your system doesn't have the CA certificates needed" \ + " for verification. For information about OpenSSL certificates, see" \ + " http://bit.ly/ruby-ssl. To connect without using SSL, edit your Gemfile" \ + " sources and change 'https' to 'http'." + end + end + # This is the error raised when a source is HTTPS and OpenSSL didn't load + class SSLError < HTTPError + def initialize(msg = nil) + super msg || "Could not load OpenSSL.\n" \ + "You must recompile Ruby with OpenSSL support or change the sources in your " \ + "Gemfile from 'https' to 'http'. Instructions for compiling with OpenSSL " \ + "using RVM are available at rvm.io/packages/openssl." + end + end + # This error is raised if HTTP authentication is required, but not provided. + class AuthenticationRequiredError < HTTPError + def initialize(remote_uri) + remote_uri = filter_uri(remote_uri) + super "Authentication is required for #{remote_uri}.\n" \ + "Please supply credentials for this source. You can do this by running:\n" \ + " bundle config #{remote_uri} username:password" + end + end + # This error is raised if HTTP authentication is provided, but incorrect. + class BadAuthenticationError < HTTPError + def initialize(remote_uri) + remote_uri = filter_uri(remote_uri) + super "Bad username or password for #{remote_uri}.\n" \ + "Please double-check your credentials and correct them." + end + end + + # Exceptions classes that should bypass retry attempts. If your password didn't work the + # first time, it's not going to the third time. + NET_ERRORS = [:HTTPBadGateway, :HTTPBadRequest, :HTTPFailedDependency, + :HTTPForbidden, :HTTPInsufficientStorage, :HTTPMethodNotAllowed, + :HTTPMovedPermanently, :HTTPNoContent, :HTTPNotFound, + :HTTPNotImplemented, :HTTPPreconditionFailed, :HTTPRequestEntityTooLarge, + :HTTPRequestURITooLong, :HTTPUnauthorized, :HTTPUnprocessableEntity, + :HTTPUnsupportedMediaType, :HTTPVersionNotSupported].freeze + FAIL_ERRORS = begin + fail_errors = [AuthenticationRequiredError, BadAuthenticationError, FallbackError] + fail_errors << Gem::Requirement::BadRequirementError if defined?(Gem::Requirement::BadRequirementError) + fail_errors.concat(NET_ERRORS.map {|e| SharedHelpers.const_get_safely(e, Net) }.compact) + end.freeze + + class << self + attr_accessor :disable_endpoint, :api_timeout, :redirect_limit, :max_retries + end + + self.redirect_limit = Bundler.settings[:redirect] # How many redirects to allow in one request + self.api_timeout = Bundler.settings[:timeout] # How long to wait for each API call + self.max_retries = Bundler.settings[:retry] # How many retries for the API call + + def initialize(remote) + @remote = remote + + Socket.do_not_reverse_lookup = true + connection # create persistent connection + end + + def uri + @remote.anonymized_uri + end + + # fetch a gem specification + def fetch_spec(spec) + spec -= [nil, "ruby", ""] + spec_file_name = "#{spec.join "-"}.gemspec" + + uri = URI.parse("#{remote_uri}#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}.rz") + if uri.scheme == "file" + Bundler.load_marshal Bundler.rubygems.inflate(Gem.read_binary(uri.path)) + elsif cached_spec_path = gemspec_cached_path(spec_file_name) + Bundler.load_gemspec(cached_spec_path) + else + Bundler.load_marshal Bundler.rubygems.inflate(downloader.fetch(uri).body) + end + rescue MarshalError + raise HTTPError, "Gemspec #{spec} contained invalid data.\n" \ + "Your network or your gem server is probably having issues right now." + end + + # return the specs in the bundler format as an index with retries + def specs_with_retry(gem_names, source) + Bundler::Retry.new("fetcher", FAIL_ERRORS).attempts do + specs(gem_names, source) + end + end + + # return the specs in the bundler format as an index + def specs(gem_names, source) + old = Bundler.rubygems.sources + index = Bundler::Index.new + + if Bundler::Fetcher.disable_endpoint + @use_api = false + specs = fetchers.last.specs(gem_names) + else + specs = [] + fetchers.shift until fetchers.first.available? || fetchers.empty? + fetchers.dup.each do |f| + break unless f.api_fetcher? && !gem_names || !specs = f.specs(gem_names) + fetchers.delete(f) + end + @use_api = false if fetchers.none?(&:api_fetcher?) + end + + specs.each do |name, version, platform, dependencies, metadata| + next if name == "bundler" + spec = if dependencies + EndpointSpecification.new(name, version, platform, dependencies, metadata) + else + RemoteSpecification.new(name, version, platform, self) + end + spec.source = source + spec.remote = @remote + index << spec + end + + index + rescue CertificateFailureError + Bundler.ui.info "" if gem_names && use_api # newline after dots + raise + ensure + Bundler.rubygems.sources = old + end + + def use_api + return @use_api if defined?(@use_api) + + fetchers.shift until fetchers.first.available? + + @use_api = if remote_uri.scheme == "file" || Bundler::Fetcher.disable_endpoint + false + else + fetchers.first.api_fetcher? + end + end + + def user_agent + @user_agent ||= begin + ruby = Bundler::RubyVersion.system + + agent = String.new("bundler/#{Bundler::VERSION}") + agent << " rubygems/#{Gem::VERSION}" + agent << " ruby/#{ruby.versions_string(ruby.versions)}" + agent << " (#{ruby.host})" + agent << " command/#{ARGV.first}" + + if ruby.engine != "ruby" + # engine_version raises on unknown engines + engine_version = begin + ruby.engine_versions + rescue RuntimeError + "???" + end + agent << " #{ruby.engine}/#{ruby.versions_string(engine_version)}" + end + + agent << " options/#{Bundler.settings.all.join(",")}" + + agent << " ci/#{cis.join(",")}" if cis.any? + + # add a random ID so we can consolidate runs server-side + agent << " " << SecureRandom.hex(8) + + # add any user agent strings set in the config + extra_ua = Bundler.settings[:user_agent] + agent << " " << extra_ua if extra_ua + + agent + end + end + + def fetchers + @fetchers ||= FETCHERS.map {|f| f.new(downloader, @remote, uri) } + end + + def http_proxy + return unless uri = connection.proxy_uri + uri.to_s + end + + def inspect + "#<#{self.class}:0x#{object_id} uri=#{uri}>" + end + + private + + FETCHERS = [CompactIndex, Dependency, Index].freeze + + def cis + env_cis = { + "TRAVIS" => "travis", + "CIRCLECI" => "circle", + "SEMAPHORE" => "semaphore", + "JENKINS_URL" => "jenkins", + "BUILDBOX" => "buildbox", + "GO_SERVER_URL" => "go", + "SNAP_CI" => "snap", + "CI_NAME" => ENV["CI_NAME"], + "CI" => "ci" + } + env_cis.find_all {|env, _| ENV[env] }.map {|_, ci| ci } + end + + def connection + @connection ||= begin + needs_ssl = remote_uri.scheme == "https" || + Bundler.settings[:ssl_verify_mode] || + Bundler.settings[:ssl_client_cert] + raise SSLError if needs_ssl && !defined?(OpenSSL::SSL) + + con = PersistentHTTP.new "bundler", :ENV + if gem_proxy = Bundler.rubygems.configuration[:http_proxy] + con.proxy = URI.parse(gem_proxy) if gem_proxy != :no_proxy + end + + if remote_uri.scheme == "https" + con.verify_mode = (Bundler.settings[:ssl_verify_mode] || + OpenSSL::SSL::VERIFY_PEER) + con.cert_store = bundler_cert_store + end + + ssl_client_cert = Bundler.settings[:ssl_client_cert] || + (Bundler.rubygems.configuration.ssl_client_cert if + Bundler.rubygems.configuration.respond_to?(:ssl_client_cert)) + if ssl_client_cert + pem = File.read(ssl_client_cert) + con.cert = OpenSSL::X509::Certificate.new(pem) + con.key = OpenSSL::PKey::RSA.new(pem) + end + + con.read_timeout = Fetcher.api_timeout + con.open_timeout = Fetcher.api_timeout + con.override_headers["User-Agent"] = user_agent + con.override_headers["X-Gemfile-Source"] = @remote.original_uri.to_s if @remote.original_uri + con + end + end + + # cached gem specification path, if one exists + def gemspec_cached_path(spec_file_name) + paths = Bundler.rubygems.spec_cache_dirs.map {|dir| File.join(dir, spec_file_name) } + paths = paths.select {|path| File.file? path } + paths.first + end + + HTTP_ERRORS = [ + Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH, + Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN, + Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, + PersistentHTTP::Error, Zlib::BufError, Errno::EHOSTUNREACH + ].freeze + + def bundler_cert_store + store = OpenSSL::X509::Store.new + ssl_ca_cert = Bundler.settings[:ssl_ca_cert] || + (Bundler.rubygems.configuration.ssl_ca_cert if + Bundler.rubygems.configuration.respond_to?(:ssl_ca_cert)) + if ssl_ca_cert + if File.directory? ssl_ca_cert + store.add_path ssl_ca_cert + else + store.add_file ssl_ca_cert + end + else + store.set_default_paths + certs = File.expand_path("../ssl_certs/*/*.pem", __FILE__) + Dir.glob(certs).each {|c| store.add_file c } + end + store + end + + private + + def remote_uri + @remote.uri + end + + def downloader + @downloader ||= Downloader.new(connection, self.class.redirect_limit) + end + end +end diff --git a/lib/bundler/fetcher/base.rb b/lib/bundler/fetcher/base.rb new file mode 100644 index 00000000000000..27987f670ae3ca --- /dev/null +++ b/lib/bundler/fetcher/base.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Bundler + class Fetcher + class Base + attr_reader :downloader + attr_reader :display_uri + attr_reader :remote + + def initialize(downloader, remote, display_uri) + raise "Abstract class" if self.class == Base + @downloader = downloader + @remote = remote + @display_uri = display_uri + end + + def remote_uri + @remote.uri + end + + def fetch_uri + @fetch_uri ||= begin + if remote_uri.host == "rubygems.org" + uri = remote_uri.dup + uri.host = "index.rubygems.org" + uri + else + remote_uri + end + end + end + + def available? + true + end + + def api_fetcher? + false + end + + private + + def log_specs(debug_msg) + if Bundler.ui.debug? + Bundler.ui.debug debug_msg + else + Bundler.ui.info ".", false + end + end + end + end +end diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb new file mode 100644 index 00000000000000..cfc74d642cf040 --- /dev/null +++ b/lib/bundler/fetcher/compact_index.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require "bundler/fetcher/base" +require "bundler/worker" + +module Bundler + autoload :CompactIndexClient, "bundler/compact_index_client" + + class Fetcher + class CompactIndex < Base + def self.compact_index_request(method_name) + method = instance_method(method_name) + undef_method(method_name) + define_method(method_name) do |*args, &blk| + begin + method.bind(self).call(*args, &blk) + rescue NetworkDownError, CompactIndexClient::Updater::MisMatchedChecksumError => e + raise HTTPError, e.message + rescue AuthenticationRequiredError + # Fail since we got a 401 from the server. + raise + rescue HTTPError => e + Bundler.ui.trace(e) + nil + end + end + end + + def specs(gem_names) + specs_for_names(gem_names) + end + compact_index_request :specs + + def specs_for_names(gem_names) + gem_info = [] + complete_gems = [] + remaining_gems = gem_names.dup + + until remaining_gems.empty? + log_specs "Looking up gems #{remaining_gems.inspect}" + + deps = compact_index_client.dependencies(remaining_gems) + next_gems = deps.map {|d| d[3].map(&:first).flatten(1) }.flatten(1).uniq + deps.each {|dep| gem_info << dep } + complete_gems.concat(deps.map(&:first)).uniq! + remaining_gems = next_gems - complete_gems + end + @bundle_worker.stop if @bundle_worker + @bundle_worker = nil # reset it. Not sure if necessary + + gem_info + end + + def fetch_spec(spec) + spec -= [nil, "ruby", ""] + contents = compact_index_client.spec(*spec) + return nil if contents.nil? + contents.unshift(spec.first) + contents[3].map! {|d| Gem::Dependency.new(*d) } + EndpointSpecification.new(*contents) + end + compact_index_request :fetch_spec + + def available? + return nil unless SharedHelpers.md5_available? + user_home = Bundler.user_home + return nil unless user_home.directory? && user_home.writable? + # Read info file checksums out of /versions, so we can know if gems are up to date + fetch_uri.scheme != "file" && compact_index_client.update_and_parse_checksums! + rescue CompactIndexClient::Updater::MisMatchedChecksumError => e + Bundler.ui.debug(e.message) + nil + end + compact_index_request :available? + + def api_fetcher? + true + end + + private + + def compact_index_client + @compact_index_client ||= begin + SharedHelpers.filesystem_access(cache_path) do + CompactIndexClient.new(cache_path, client_fetcher) + end.tap do |client| + client.in_parallel = lambda do |inputs, &blk| + func = lambda {|object, _index| blk.call(object) } + worker = bundle_worker(func) + inputs.each {|input| worker.enq(input) } + inputs.map { worker.deq } + end + end + end + end + + def bundle_worker(func = nil) + @bundle_worker ||= begin + worker_name = "Compact Index (#{display_uri.host})" + Bundler::Worker.new(Bundler.current_ruby.rbx? ? 1 : 25, worker_name, func) + end + @bundle_worker.tap do |worker| + worker.instance_variable_set(:@func, func) if func + end + end + + def cache_path + Bundler.user_cache.join("compact_index", remote.cache_slug) + end + + def client_fetcher + ClientFetcher.new(self, Bundler.ui) + end + + ClientFetcher = Struct.new(:fetcher, :ui) do + def call(path, headers) + fetcher.downloader.fetch(fetcher.fetch_uri + path, headers) + rescue NetworkDownError => e + raise unless Bundler.feature_flag.allow_offline_install? && headers["If-None-Match"] + ui.warn "Using the cached data for the new index because of a network error: #{e}" + Net::HTTPNotModified.new(nil, nil, nil) + end + end + end + end +end diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb new file mode 100644 index 00000000000000..1430d1ebeb9494 --- /dev/null +++ b/lib/bundler/fetcher/dependency.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require "bundler/fetcher/base" +require "cgi" + +module Bundler + class Fetcher + class Dependency < Base + def available? + @available ||= fetch_uri.scheme != "file" && downloader.fetch(dependency_api_uri) + rescue NetworkDownError => e + raise HTTPError, e.message + rescue AuthenticationRequiredError + # Fail since we got a 401 from the server. + raise + rescue HTTPError + false + end + + def api_fetcher? + true + end + + def specs(gem_names, full_dependency_list = [], last_spec_list = []) + query_list = gem_names.uniq - full_dependency_list + + log_specs "Query List: #{query_list.inspect}" + + return last_spec_list if query_list.empty? + + spec_list, deps_list = Bundler::Retry.new("dependency api", FAIL_ERRORS).attempts do + dependency_specs(query_list) + end + + returned_gems = spec_list.map(&:first).uniq + specs(deps_list, full_dependency_list + returned_gems, spec_list + last_spec_list) + rescue MarshalError + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + Bundler.ui.debug "could not fetch from the dependency API, trying the full index" + nil + rescue HTTPError, GemspecError + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + Bundler.ui.debug "could not fetch from the dependency API\nit's suggested to retry using the full index via `bundle install --full-index`" + nil + end + + def dependency_specs(gem_names) + Bundler.ui.debug "Query Gemcutter Dependency Endpoint API: #{gem_names.join(",")}" + + gem_list = unmarshalled_dep_gems(gem_names) + get_formatted_specs_and_deps(gem_list) + end + + def unmarshalled_dep_gems(gem_names) + gem_list = [] + gem_names.each_slice(Source::Rubygems::API_REQUEST_SIZE) do |names| + marshalled_deps = downloader.fetch(dependency_api_uri(names)).body + gem_list.concat(Bundler.load_marshal(marshalled_deps)) + end + gem_list + end + + def get_formatted_specs_and_deps(gem_list) + deps_list = [] + spec_list = [] + + gem_list.each do |s| + deps_list.concat(s[:dependencies].map(&:first)) + deps = s[:dependencies].map {|n, d| [n, d.split(", ")] } + spec_list.push([s[:name], s[:number], s[:platform], deps]) + end + [spec_list, deps_list] + end + + def dependency_api_uri(gem_names = []) + uri = fetch_uri + "api/v1/dependencies" + uri.query = "gems=#{CGI.escape(gem_names.sort.join(","))}" if gem_names.any? + uri + end + end + end +end diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb new file mode 100644 index 00000000000000..e0e0cbf1c9afb3 --- /dev/null +++ b/lib/bundler/fetcher/downloader.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Bundler + class Fetcher + class Downloader + attr_reader :connection + attr_reader :redirect_limit + + def initialize(connection, redirect_limit) + @connection = connection + @redirect_limit = redirect_limit + end + + def fetch(uri, headers = {}, counter = 0) + raise HTTPError, "Too many redirects" if counter >= redirect_limit + + response = request(uri, headers) + Bundler.ui.debug("HTTP #{response.code} #{response.message} #{uri}") + + case response + when Net::HTTPSuccess, Net::HTTPNotModified + response + when Net::HTTPRedirection + new_uri = URI.parse(response["location"]) + if new_uri.host == uri.host + new_uri.user = uri.user + new_uri.password = uri.password + end + fetch(new_uri, headers, counter + 1) + when Net::HTTPRequestedRangeNotSatisfiable + new_headers = headers.dup + new_headers.delete("Range") + new_headers["Accept-Encoding"] = "gzip" + fetch(uri, new_headers) + when Net::HTTPRequestEntityTooLarge + raise FallbackError, response.body + when Net::HTTPUnauthorized + raise AuthenticationRequiredError, uri.host + when Net::HTTPNotFound + raise FallbackError, "Net::HTTPNotFound" + else + raise HTTPError, "#{response.class}#{": #{response.body}" unless response.body.empty?}" + end + end + + def request(uri, headers) + validate_uri_scheme!(uri) + + Bundler.ui.debug "HTTP GET #{uri}" + req = Net::HTTP::Get.new uri.request_uri, headers + if uri.user + user = CGI.unescape(uri.user) + password = uri.password ? CGI.unescape(uri.password) : nil + req.basic_auth(user, password) + end + connection.request(uri, req) + rescue NoMethodError => e + raise unless ["undefined method", "use_ssl="].all? {|snippet| e.message.include? snippet } + raise LoadError.new("cannot load such file -- openssl") + rescue OpenSSL::SSL::SSLError + raise CertificateFailureError.new(uri) + rescue *HTTP_ERRORS => e + Bundler.ui.trace e + case e.message + when /host down:/, /getaddrinfo: nodename nor servname provided/ + raise NetworkDownError, "Could not reach host #{uri.host}. Check your network " \ + "connection and try again." + else + raise HTTPError, "Network error while fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" \ + " (#{e})" + end + end + + private + + def validate_uri_scheme!(uri) + return if uri.scheme =~ /\Ahttps?\z/ + raise InvalidOption, + "The request uri `#{uri}` has an invalid scheme (`#{uri.scheme}`). " \ + "Did you mean `http` or `https`?" + end + end + end +end diff --git a/lib/bundler/fetcher/index.rb b/lib/bundler/fetcher/index.rb new file mode 100644 index 00000000000000..1a8064624d8046 --- /dev/null +++ b/lib/bundler/fetcher/index.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "bundler/fetcher/base" +require "rubygems/remote_fetcher" + +module Bundler + class Fetcher + class Index < Base + def specs(_gem_names) + Bundler.rubygems.fetch_all_remote_specs(remote) + rescue Gem::RemoteFetcher::FetchError, OpenSSL::SSL::SSLError, Net::HTTPFatalError => e + case e.message + when /certificate verify failed/ + raise CertificateFailureError.new(display_uri) + when /401/ + raise AuthenticationRequiredError, remote_uri + when /403/ + raise BadAuthenticationError, remote_uri if remote_uri.userinfo + raise AuthenticationRequiredError, remote_uri + else + Bundler.ui.trace e + raise HTTPError, "Could not fetch specs from #{display_uri}" + end + end + + def fetch_spec(spec) + spec -= [nil, "ruby", ""] + spec_file_name = "#{spec.join "-"}.gemspec" + + uri = URI.parse("#{remote_uri}#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}.rz") + if uri.scheme == "file" + Bundler.load_marshal Bundler.rubygems.inflate(Gem.read_binary(uri.path)) + elsif cached_spec_path = gemspec_cached_path(spec_file_name) + Bundler.load_gemspec(cached_spec_path) + else + Bundler.load_marshal Bundler.rubygems.inflate(downloader.fetch(uri).body) + end + rescue MarshalError + raise HTTPError, "Gemspec #{spec} contained invalid data.\n" \ + "Your network or your gem server is probably having issues right now." + end + + private + + # cached gem specification path, if one exists + def gemspec_cached_path(spec_file_name) + paths = Bundler.rubygems.spec_cache_dirs.map {|dir| File.join(dir, spec_file_name) } + paths.find {|path| File.file? path } + end + end + end +end diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb new file mode 100644 index 00000000000000..ae3299a7c8c110 --- /dev/null +++ b/lib/bundler/friendly_errors.rb @@ -0,0 +1,131 @@ +# encoding: utf-8 +# frozen_string_literal: true + +require "cgi" +require "bundler/vendored_thor" + +module Bundler + module FriendlyErrors + module_function + + def log_error(error) + case error + when YamlSyntaxError + Bundler.ui.error error.message + Bundler.ui.trace error.orig_exception + when Dsl::DSLError, GemspecError + Bundler.ui.error error.message + when GemRequireError + Bundler.ui.error error.message + Bundler.ui.trace error.orig_exception, nil, true + when BundlerError + Bundler.ui.error error.message, :wrap => true + Bundler.ui.trace error + when Thor::Error + Bundler.ui.error error.message + when LoadError + raise error unless error.message =~ /cannot load such file -- openssl|openssl.so|libcrypto.so/ + Bundler.ui.error "\nCould not load OpenSSL." + Bundler.ui.warn <<-WARN, :wrap => true + You must recompile Ruby with OpenSSL support or change the sources in your \ + Gemfile from 'https' to 'http'. Instructions for compiling with OpenSSL \ + using RVM are available at http://rvm.io/packages/openssl. + WARN + Bundler.ui.trace error + when Interrupt + Bundler.ui.error "\nQuitting..." + Bundler.ui.trace error + when Gem::InvalidSpecificationException + Bundler.ui.error error.message, :wrap => true + when SystemExit + when *[defined?(Java::JavaLang::OutOfMemoryError) && Java::JavaLang::OutOfMemoryError].compact + Bundler.ui.error "\nYour JVM has run out of memory, and Bundler cannot continue. " \ + "You can decrease the amount of memory Bundler needs by removing gems from your Gemfile, " \ + "especially large gems. (Gems can be as large as hundreds of megabytes, and Bundler has to read those files!). " \ + "Alternatively, you can increase the amount of memory the JVM is able to use by running Bundler with jruby -J-Xmx1024m -S bundle (JRuby defaults to 500MB)." + else request_issue_report_for(error) + end + rescue + raise error + end + + def exit_status(error) + case error + when BundlerError then error.status_code + when Thor::Error then 15 + when SystemExit then error.status + else 1 + end + end + + def request_issue_report_for(e) + Bundler.ui.info <<-EOS.gsub(/^ {8}/, "") + --- ERROR REPORT TEMPLATE ------------------------------------------------------- + # Error Report + + ## Questions + + Please fill out answers to these questions, it'll help us figure out + why things are going wrong. + + - **What did you do?** + + I ran the command `#{$PROGRAM_NAME} #{ARGV.join(" ")}` + + - **What did you expect to happen?** + + I expected Bundler to... + + - **What happened instead?** + + Instead, what happened was... + + - **Have you tried any solutions posted on similar issues in our issue tracker, stack overflow, or google?** + + I tried... + + - **Have you read our issues document, https://github.com/bundler/bundler/blob/master/doc/contributing/ISSUES.md?** + + ... + + ## Backtrace + + ``` + #{e.class}: #{e.message} + #{e.backtrace && e.backtrace.join("\n ").chomp} + ``` + + #{Bundler::Env.report} + --- TEMPLATE END ---------------------------------------------------------------- + + EOS + + Bundler.ui.error "Unfortunately, an unexpected error occurred, and Bundler cannot continue." + + Bundler.ui.warn <<-EOS.gsub(/^ {8}/, "") + + First, try this link to see if there are any existing issue reports for this error: + #{issues_url(e)} + + If there aren't any reports for this error yet, please create copy and paste the report template above into a new issue. Don't forget to anonymize any private data! The new issue form is located at: + https://github.com/bundler/bundler/issues/new + EOS + end + + def issues_url(exception) + message = exception.message.lines.first.tr(":", " ").chomp + message = message.split("-").first if exception.is_a?(Errno) + "https://github.com/bundler/bundler/search?q=" \ + "#{CGI.escape(message)}&type=Issues" + end + end + + def self.with_friendly_errors + yield + rescue SignalException + raise + rescue Exception => e + FriendlyErrors.log_error(e) + exit FriendlyErrors.exit_status(e) + end +end diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb new file mode 100644 index 00000000000000..e7673cba88347e --- /dev/null +++ b/lib/bundler/gem_helper.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require "bundler/vendored_thor" unless defined?(Thor) +require "bundler" + +module Bundler + class GemHelper + include Rake::DSL if defined? Rake::DSL + + class << self + # set when install'd. + attr_accessor :instance + + def install_tasks(opts = {}) + new(opts[:dir], opts[:name]).install + end + + def gemspec(&block) + gemspec = instance.gemspec + block.call(gemspec) if block + gemspec + end + end + + attr_reader :spec_path, :base, :gemspec + + def initialize(base = nil, name = nil) + Bundler.ui = UI::Shell.new + @base = (base ||= SharedHelpers.pwd) + gemspecs = name ? [File.join(base, "#{name}.gemspec")] : Dir[File.join(base, "{,*}.gemspec")] + 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) + end + + def install + built_gem_path = nil + + desc "Build #{name}-#{version}.gem into the pkg directory." + task "build" do + built_gem_path = build_gem + end + + desc "Build and install #{name}-#{version}.gem into system gems." + task "install" => "build" do + install_gem(built_gem_path) + end + + desc "Build and install #{name}-#{version}.gem into system gems without network access." + task "install:local" => "build" do + install_gem(built_gem_path, :local) + end + + desc "Create tag #{version_tag} and build and push #{name}-#{version}.gem to #{gem_push_host}\n" \ + "To prevent publishing in RubyGems use `gem_push=no rake release`" + task "release", [:remote] => ["build", "release:guard_clean", + "release:source_control_push", "release:rubygem_push"] do + end + + task "release:guard_clean" do + guard_clean + end + + task "release:source_control_push", [:remote] do |_, args| + tag_version { git_push(args[:remote]) } unless already_tagged? + end + + task "release:rubygem_push" do + rubygem_push(built_gem_path) if gem_push? + end + + GemHelper.instance = self + end + + def build_gem + file_name = nil + sh("gem build -V '#{spec_path}'") do + file_name = File.basename(built_gem_path) + SharedHelpers.filesystem_access(File.join(base, "pkg")) {|p| FileUtils.mkdir_p(p) } + FileUtils.mv(built_gem_path, "pkg") + Bundler.ui.confirm "#{name} #{version} built to pkg/#{file_name}." + end + File.join(base, "pkg", file_name) + end + + def install_gem(built_gem_path = nil, local = false) + built_gem_path ||= build_gem + out, _ = sh_with_code("gem install '#{built_gem_path}'#{" --local" if local}") + raise "Couldn't install gem, run `gem install #{built_gem_path}' for more detailed output" unless out[/Successfully installed/] + Bundler.ui.confirm "#{name} (#{version}) installed." + end + + protected + + def rubygem_push(path) + gem_command = "gem push '#{path}'" + gem_command += " --key #{gem_key}" if gem_key + gem_command += " --host #{allowed_push_host}" if allowed_push_host + unless allowed_push_host || Bundler.user_home.join(".gem/credentials").file? + raise "Your rubygems.org credentials aren't set. Run `gem push` to set them." + end + sh(gem_command) + Bundler.ui.confirm "Pushed #{name} #{version} to #{gem_push_host}" + end + + def built_gem_path + Dir[File.join(base, "#{name}-*.gem")].sort_by {|f| File.mtime(f) }.last + end + + def git_push(remote = "") + perform_git_push remote + perform_git_push "#{remote} --tags" + Bundler.ui.confirm "Pushed git commits and tags." + end + + def allowed_push_host + @gemspec.metadata["allowed_push_host"] if @gemspec.respond_to?(:metadata) + end + + def gem_push_host + env_rubygems_host = ENV["RUBYGEMS_HOST"] + env_rubygems_host = nil if + env_rubygems_host && env_rubygems_host.empty? + + allowed_push_host || env_rubygems_host || "rubygems.org" + end + + def perform_git_push(options = "") + cmd = "git push #{options}" + out, code = sh_with_code(cmd) + raise "Couldn't git push. `#{cmd}' failed with the following output:\n\n#{out}\n" unless code == 0 + end + + def already_tagged? + return false unless sh("git tag").split(/\n/).include?(version_tag) + Bundler.ui.confirm "Tag #{version_tag} has already been created." + true + end + + def guard_clean + clean? && committed? || raise("There are files that need to be committed first.") + end + + def clean? + sh_with_code("git diff --exit-code")[1] == 0 + end + + def committed? + sh_with_code("git diff-index --quiet --cached HEAD")[1] == 0 + end + + def tag_version + sh "git tag -m \"Version #{version}\" #{version_tag}" + Bundler.ui.confirm "Tagged #{version_tag}." + yield if block_given? + rescue RuntimeError + Bundler.ui.error "Untagging #{version_tag} due to error." + sh_with_code "git tag -d #{version_tag}" + raise + end + + def version + gemspec.version + end + + def version_tag + "v#{version}" + end + + def name + gemspec.name + end + + def sh(cmd, &block) + out, code = sh_with_code(cmd, &block) + unless code.zero? + raise(out.empty? ? "Running `#{cmd}` failed. Run this command directly for more detailed output." : out) + end + out + end + + def sh_with_code(cmd, &block) + cmd += " 2>&1" + outbuf = String.new + Bundler.ui.debug(cmd) + SharedHelpers.chdir(base) do + outbuf = `#{cmd}` + status = $?.exitstatus + block.call(outbuf) if status.zero? && block + [outbuf, status] + end + end + + def gem_key + Bundler.settings["gem.push_key"].to_s.downcase if Bundler.settings["gem.push_key"] + end + + def gem_push? + !%w[n no nil false off 0].include?(ENV["gem_push"].to_s.downcase) + end + end +end diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb new file mode 100644 index 00000000000000..019ae10c66cb67 --- /dev/null +++ b/lib/bundler/gem_helpers.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Bundler + module GemHelpers + GENERIC_CACHE = {} # rubocop:disable MutableConstant + GENERICS = [ + [Gem::Platform.new("java"), Gem::Platform.new("java")], + [Gem::Platform.new("mswin32"), Gem::Platform.new("mswin32")], + [Gem::Platform.new("mswin64"), Gem::Platform.new("mswin64")], + [Gem::Platform.new("universal-mingw32"), Gem::Platform.new("universal-mingw32")], + [Gem::Platform.new("x64-mingw32"), Gem::Platform.new("x64-mingw32")], + [Gem::Platform.new("x86_64-mingw32"), Gem::Platform.new("x64-mingw32")], + [Gem::Platform.new("mingw32"), Gem::Platform.new("x86-mingw32")] + ].freeze + + def generic(p) + return p if p == Gem::Platform::RUBY + + GENERIC_CACHE[p] ||= begin + _, found = GENERICS.find do |match, _generic| + p.os == match.os && (!match.cpu || p.cpu == match.cpu) + end + found || Gem::Platform::RUBY + end + end + module_function :generic + + def generic_local_platform + generic(Bundler.local_platform) + end + module_function :generic_local_platform + + def platform_specificity_match(spec_platform, user_platform) + spec_platform = Gem::Platform.new(spec_platform) + return PlatformMatch::EXACT_MATCH if spec_platform == user_platform + return PlatformMatch::WORST_MATCH if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY + + PlatformMatch.new( + PlatformMatch.os_match(spec_platform, user_platform), + PlatformMatch.cpu_match(spec_platform, user_platform), + PlatformMatch.platform_version_match(spec_platform, user_platform) + ) + end + module_function :platform_specificity_match + + def select_best_platform_match(specs, platform) + specs.select {|spec| spec.match_platform(platform) }. + min_by {|spec| platform_specificity_match(spec.platform, platform) } + end + module_function :select_best_platform_match + + PlatformMatch = Struct.new(:os_match, :cpu_match, :platform_version_match) + class PlatformMatch + def <=>(other) + return nil unless other.is_a?(PlatformMatch) + + m = os_match <=> other.os_match + return m unless m.zero? + + m = cpu_match <=> other.cpu_match + return m unless m.zero? + + m = platform_version_match <=> other.platform_version_match + m + end + + EXACT_MATCH = new(-1, -1, -1).freeze + WORST_MATCH = new(1_000_000, 1_000_000, 1_000_000).freeze + + def self.os_match(spec_platform, user_platform) + if spec_platform.os == user_platform.os + 0 + else + 1 + end + end + + def self.cpu_match(spec_platform, user_platform) + if spec_platform.cpu == user_platform.cpu + 0 + elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm") + 0 + elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal" + 1 + else + 2 + end + end + + def self.platform_version_match(spec_platform, user_platform) + if spec_platform.version == user_platform.version + 0 + elsif spec_platform.version.nil? + 1 + else + 2 + end + end + end + end +end diff --git a/lib/bundler/gem_remote_fetcher.rb b/lib/bundler/gem_remote_fetcher.rb new file mode 100644 index 00000000000000..9577535d63c71e --- /dev/null +++ b/lib/bundler/gem_remote_fetcher.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "rubygems/remote_fetcher" + +module Bundler + # Adds support for setting custom HTTP headers when fetching gems from the + # server. + # + # TODO: Get rid of this when and if gemstash only supports RubyGems versions + # that contain https://github.com/rubygems/rubygems/commit/3db265cc20b2f813. + class GemRemoteFetcher < Gem::RemoteFetcher + attr_accessor :headers + + # Extracted from RubyGems 2.4. + def fetch_http(uri, last_modified = nil, head = false, depth = 0) + fetch_type = head ? Net::HTTP::Head : Net::HTTP::Get + # beginning of change + response = request uri, fetch_type, last_modified do |req| + headers.each {|k, v| req.add_field(k, v) } if headers + end + # end of change + + case response + when Net::HTTPOK, Net::HTTPNotModified then + response.uri = uri if response.respond_to? :uri + head ? response : response.body + when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther, + Net::HTTPTemporaryRedirect then + raise FetchError.new("too many redirects", uri) if depth > 10 + + location = URI.parse response["Location"] + + if https?(uri) && !https?(location) + raise FetchError.new("redirecting to non-https resource: #{location}", uri) + end + + fetch_http(location, last_modified, head, depth + 1) + else + raise FetchError.new("bad response #{response.message} #{response.code}", uri) + end + end + end +end diff --git a/lib/bundler/gem_tasks.rb b/lib/bundler/gem_tasks.rb new file mode 100644 index 00000000000000..f736517bd76a48 --- /dev/null +++ b/lib/bundler/gem_tasks.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rake/clean" +CLOBBER.include "pkg" + +require "bundler/gem_helper" +Bundler::GemHelper.install_tasks diff --git a/lib/bundler/gem_version_promoter.rb b/lib/bundler/gem_version_promoter.rb new file mode 100644 index 00000000000000..adb951a7a0cc1d --- /dev/null +++ b/lib/bundler/gem_version_promoter.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +module Bundler + # This class contains all of the logic for determining the next version of a + # Gem to update to based on the requested level (patch, minor, major). + # Primarily designed to work with Resolver which will provide it the list of + # available dependency versions as found in its index, before returning it to + # to the resolution engine to select the best version. + class GemVersionPromoter + DEBUG = ENV["DEBUG_RESOLVER"] + + attr_reader :level, :locked_specs, :unlock_gems + + # By default, strict is false, meaning every available version of a gem + # is returned from sort_versions. The order gives preference to the + # requested level (:patch, :minor, :major) but in complicated requirement + # cases some gems will by necessity by promoted past the requested level, + # or even reverted to older versions. + # + # If strict is set to true, the results from sort_versions will be + # truncated, eliminating any version outside the current level scope. + # This can lead to unexpected outcomes or even VersionConflict exceptions + # that report a version of a gem not existing for versions that indeed do + # existing in the referenced source. + attr_accessor :strict + + attr_accessor :prerelease_specified + + # Given a list of locked_specs and a list of gems to unlock creates a + # GemVersionPromoter instance. + # + # @param locked_specs [SpecSet] All current locked specs. Unlike Definition + # where this list is empty if all gems are being updated, this should + # always be populated for all gems so this class can properly function. + # @param unlock_gems [String] List of gem names being unlocked. If empty, + # all gems will be considered unlocked. + # @return [GemVersionPromoter] + def initialize(locked_specs = SpecSet.new([]), unlock_gems = []) + @level = :major + @strict = false + @locked_specs = locked_specs + @unlock_gems = unlock_gems + @sort_versions = {} + @prerelease_specified = {} + end + + # @param value [Symbol] One of three Symbols: :major, :minor or :patch. + def level=(value) + v = case value + when String, Symbol + value.to_sym + end + + raise ArgumentError, "Unexpected level #{v}. Must be :major, :minor or :patch" unless [:major, :minor, :patch].include?(v) + @level = v + end + + # Given a Dependency and an Array of SpecGroups of available versions for a + # gem, this method will return the Array of SpecGroups sorted (and possibly + # truncated if strict is true) in an order to give preference to the current + # level (:major, :minor or :patch) when resolution is deciding what versions + # best resolve all dependencies in the bundle. + # @param dep [Dependency] The Dependency of the gem. + # @param spec_groups [SpecGroup] An array of SpecGroups for the same gem + # named in the @dep param. + # @return [SpecGroup] A new instance of the SpecGroup Array sorted and + # possibly filtered. + def sort_versions(dep, spec_groups) + before_result = "before sort_versions: #{debug_format_result(dep, spec_groups).inspect}" if DEBUG + + @sort_versions[dep] ||= begin + gem_name = dep.name + + # An Array per version returned, different entries for different platforms. + # We only need the version here so it's ok to hard code this to the first instance. + locked_spec = locked_specs[gem_name].first + + if strict + filter_dep_specs(spec_groups, locked_spec) + else + sort_dep_specs(spec_groups, locked_spec) + end.tap do |specs| + if DEBUG + STDERR.puts before_result + STDERR.puts " after sort_versions: #{debug_format_result(dep, specs).inspect}" + end + end + end + end + + # @return [bool] Convenience method for testing value of level variable. + def major? + level == :major + end + + # @return [bool] Convenience method for testing value of level variable. + def minor? + level == :minor + end + + private + + def filter_dep_specs(spec_groups, locked_spec) + res = spec_groups.select do |spec_group| + if locked_spec && !major? + gsv = spec_group.version + lsv = locked_spec.version + + must_match = minor? ? [0] : [0, 1] + + matches = must_match.map {|idx| gsv.segments[idx] == lsv.segments[idx] } + (matches.uniq == [true]) ? (gsv >= lsv) : false + else + true + end + end + + sort_dep_specs(res, locked_spec) + end + + def sort_dep_specs(spec_groups, locked_spec) + return spec_groups unless locked_spec + @gem_name = locked_spec.name + @locked_version = locked_spec.version + + result = spec_groups.sort do |a, b| + @a_ver = a.version + @b_ver = b.version + + unless @prerelease_specified[@gem_name] + a_pre = @a_ver.prerelease? + b_pre = @b_ver.prerelease? + + next -1 if a_pre && !b_pre + next 1 if b_pre && !a_pre + end + + if major? + @a_ver <=> @b_ver + elsif either_version_older_than_locked + @a_ver <=> @b_ver + elsif segments_do_not_match(:major) + @b_ver <=> @a_ver + elsif !minor? && segments_do_not_match(:minor) + @b_ver <=> @a_ver + else + @a_ver <=> @b_ver + end + end + post_sort(result) + end + + def either_version_older_than_locked + @a_ver < @locked_version || @b_ver < @locked_version + end + + def segments_do_not_match(level) + index = [:major, :minor].index(level) + @a_ver.segments[index] != @b_ver.segments[index] + end + + def unlocking_gem? + unlock_gems.empty? || unlock_gems.include?(@gem_name) + end + + # Specific version moves can't always reliably be done during sorting + # as not all elements are compared against each other. + def post_sort(result) + # default :major behavior in Bundler does not do this + return result if major? + if unlocking_gem? + result + else + move_version_to_end(result, @locked_version) + end + end + + def move_version_to_end(result, version) + move, keep = result.partition {|s| s.version.to_s == version.to_s } + keep.concat(move) + end + + def debug_format_result(dep, spec_groups) + a = [dep.to_s, + spec_groups.map {|sg| [sg.version, sg.dependencies_for_activated_platforms.map {|dp| [dp.name, dp.requirement.to_s] }] }] + last_map = a.last.map {|sg_data| [sg_data.first.version, sg_data.last.map {|aa| aa.join(" ") }] } + [a.first, last_map, level, strict ? :strict : :not_strict] + end + end +end diff --git a/lib/bundler/gemdeps.rb b/lib/bundler/gemdeps.rb new file mode 100644 index 00000000000000..cd4b25d0e698f1 --- /dev/null +++ b/lib/bundler/gemdeps.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Bundler + class Gemdeps + def initialize(runtime) + @runtime = runtime + end + + def requested_specs + @runtime.requested_specs + end + + def specs + @runtime.specs + end + + def dependencies + @runtime.dependencies + end + + def current_dependencies + @runtime.current_dependencies + end + + def requires + @runtime.requires + end + end +end diff --git a/lib/bundler/graph.rb b/lib/bundler/graph.rb new file mode 100644 index 00000000000000..de6bba021484ce --- /dev/null +++ b/lib/bundler/graph.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require "set" +module Bundler + class Graph + GRAPH_NAME = :Gemfile + + def initialize(env, output_file, show_version = false, show_requirements = false, output_format = "png", without = []) + @env = env + @output_file = output_file + @show_version = show_version + @show_requirements = show_requirements + @output_format = output_format + @without_groups = without.map(&:to_sym) + + @groups = [] + @relations = Hash.new {|h, k| h[k] = Set.new } + @node_options = {} + @edge_options = {} + + _populate_relations + end + + attr_reader :groups, :relations, :node_options, :edge_options, :output_file, :output_format + + def viz + GraphVizClient.new(self).run + end + + private + + def _populate_relations + parent_dependencies = _groups.values.to_set.flatten + loop do + break if parent_dependencies.empty? + + tmp = Set.new + parent_dependencies.each do |dependency| + child_dependencies = spec_for_dependency(dependency).runtime_dependencies.to_set + @relations[dependency.name] += child_dependencies.map(&:name).to_set + tmp += child_dependencies + + @node_options[dependency.name] = _make_label(dependency, :node) + child_dependencies.each do |c_dependency| + @edge_options["#{dependency.name}_#{c_dependency.name}"] = _make_label(c_dependency, :edge) + end + end + parent_dependencies = tmp + end + end + + def _groups + relations = Hash.new {|h, k| h[k] = Set.new } + @env.current_dependencies.each do |dependency| + dependency.groups.each do |group| + next if @without_groups.include?(group) + + relations[group.to_s].add(dependency) + @relations[group.to_s].add(dependency.name) + + @node_options[group.to_s] ||= _make_label(group, :node) + @edge_options["#{group}_#{dependency.name}"] = _make_label(dependency, :edge) + end + end + @groups = relations.keys + relations + end + + def _make_label(symbol_or_string_or_dependency, element_type) + case element_type.to_sym + when :node + if symbol_or_string_or_dependency.is_a?(Gem::Dependency) + label = symbol_or_string_or_dependency.name.dup + label << "\n#{spec_for_dependency(symbol_or_string_or_dependency).version}" if @show_version + else + label = symbol_or_string_or_dependency.to_s + end + when :edge + label = nil + if symbol_or_string_or_dependency.respond_to?(:requirements_list) && @show_requirements + tmp = symbol_or_string_or_dependency.requirements_list.join(", ") + label = tmp if tmp != ">= 0" + end + else + raise ArgumentError, "2nd argument is invalid" + end + label.nil? ? {} : { :label => label } + end + + def spec_for_dependency(dependency) + @env.requested_specs.find {|s| s.name == dependency.name } + end + + class GraphVizClient + def initialize(graph_instance) + @graph_name = graph_instance.class::GRAPH_NAME + @groups = graph_instance.groups + @relations = graph_instance.relations + @node_options = graph_instance.node_options + @edge_options = graph_instance.edge_options + @output_file = graph_instance.output_file + @output_format = graph_instance.output_format + end + + def g + @g ||= ::GraphViz.digraph(@graph_name, :concentrate => true, :normalize => true, :nodesep => 0.55) do |g| + g.edge[:weight] = 2 + g.edge[:fontname] = g.node[:fontname] = "Arial, Helvetica, SansSerif" + g.edge[:fontsize] = 12 + end + end + + def run + @groups.each do |group| + g.add_nodes( + group, { + :style => "filled", + :fillcolor => "#B9B9D5", + :shape => "box3d", + :fontsize => 16 + }.merge(@node_options[group]) + ) + end + + @relations.each do |parent, children| + children.each do |child| + if @groups.include?(parent) + g.add_nodes(child, { :style => "filled", :fillcolor => "#B9B9D5" }.merge(@node_options[child])) + g.add_edges(parent, child, { :constraint => false }.merge(@edge_options["#{parent}_#{child}"])) + else + g.add_nodes(child, @node_options[child]) + g.add_edges(parent, child, @edge_options["#{parent}_#{child}"]) + end + end + end + + if @output_format.to_s == "debug" + $stdout.puts g.output :none => String + Bundler.ui.info "debugging bundle viz..." + else + begin + g.output @output_format.to_sym => "#{@output_file}.#{@output_format}" + Bundler.ui.info "#{@output_file}.#{@output_format}" + rescue ArgumentError => e + $stderr.puts "Unsupported output format. See Ruby-Graphviz/lib/graphviz/constants.rb" + raise e + end + end + end + end + end +end diff --git a/lib/bundler/index.rb b/lib/bundler/index.rb new file mode 100644 index 00000000000000..9166a927388fa6 --- /dev/null +++ b/lib/bundler/index.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require "set" + +module Bundler + class Index + include Enumerable + + def self.build + i = new + yield i + i + end + + attr_reader :specs, :all_specs, :sources + protected :specs, :all_specs + + RUBY = "ruby".freeze + NULL = "\0".freeze + + def initialize + @sources = [] + @cache = {} + @specs = Hash.new {|h, k| h[k] = {} } + @all_specs = Hash.new {|h, k| h[k] = EMPTY_SEARCH } + end + + def initialize_copy(o) + @sources = o.sources.dup + @cache = {} + @specs = Hash.new {|h, k| h[k] = {} } + @all_specs = Hash.new {|h, k| h[k] = EMPTY_SEARCH } + + o.specs.each do |name, hash| + @specs[name] = hash.dup + end + o.all_specs.each do |name, array| + @all_specs[name] = array.dup + end + end + + def inspect + "#<#{self.class}:0x#{object_id} sources=#{sources.map(&:inspect)} specs.size=#{specs.size}>" + end + + def empty? + each { return false } + true + end + + def search_all(name) + all_matches = local_search(name) + @all_specs[name] + @sources.each do |source| + all_matches.concat(source.search_all(name)) + end + all_matches + end + + # Search this index's specs, and any source indexes that this index knows + # about, returning all of the results. + def search(query, base = nil) + sort_specs(unsorted_search(query, base)) + end + + def unsorted_search(query, base) + results = local_search(query, base) + + seen = results.map(&:full_name).to_set unless @sources.empty? + + @sources.each do |source| + source.unsorted_search(query, base).each do |spec| + results << spec if seen.add?(spec.full_name) + end + end + + results + end + protected :unsorted_search + + def self.sort_specs(specs) + specs.sort_by do |s| + platform_string = s.platform.to_s + [s.version, platform_string == RUBY ? NULL : platform_string] + end + end + + def sort_specs(specs) + self.class.sort_specs(specs) + end + + def local_search(query, base = nil) + case query + when Gem::Specification, RemoteSpecification, LazySpecification, EndpointSpecification then search_by_spec(query) + when String then specs_by_name(query) + when Gem::Dependency then search_by_dependency(query, base) + when DepProxy then search_by_dependency(query.dep, base) + else + raise "You can't search for a #{query.inspect}." + end + end + + alias_method :[], :search + + def <<(spec) + @specs[spec.name][spec.full_name] = spec + spec + end + + def each(&blk) + return enum_for(:each) unless blk + specs.values.each do |spec_sets| + spec_sets.values.each(&blk) + end + sources.each {|s| s.each(&blk) } + self + end + + def spec_names + names = specs.keys + sources.map(&:spec_names) + names.uniq! + names + end + + # returns a list of the dependencies + def unmet_dependency_names + dependency_names.select do |name| + name != "bundler" && search(name).empty? + end + end + + def dependency_names + names = [] + each do |spec| + spec.dependencies.each do |dep| + next if dep.type == :development + names << dep.name + end + end + names.uniq + end + + def use(other, override_dupes = false) + return unless other + other.each do |s| + if (dupes = search_by_spec(s)) && !dupes.empty? + # safe to << since it's a new array when it has contents + @all_specs[s.name] = dupes << s + next unless override_dupes + end + self << s + end + self + end + + def size + @sources.inject(@specs.size) do |size, source| + size += source.size + end + end + + # Whether all the specs in self are in other + # TODO: rename to #include? + def ==(other) + all? do |spec| + other_spec = other[spec].first + other_spec && dependencies_eql?(spec, other_spec) && spec.source == other_spec.source + end + end + + def dependencies_eql?(spec, other_spec) + deps = spec.dependencies.select {|d| d.type != :development } + other_deps = other_spec.dependencies.select {|d| d.type != :development } + Set.new(deps) == Set.new(other_deps) + end + + def add_source(index) + raise ArgumentError, "Source must be an index, not #{index.class}" unless index.is_a?(Index) + @sources << index + @sources.uniq! # need to use uniq! here instead of checking for the item before adding + end + + private + + def specs_by_name(name) + @specs[name].values + end + + def search_by_dependency(dependency, base = nil) + @cache[base || false] ||= {} + @cache[base || false][dependency] ||= begin + specs = specs_by_name(dependency.name) + specs += base if base + found = specs.select do |spec| + next true if spec.source.is_a?(Source::Gemspec) + if base # allow all platforms when searching from a lockfile + dependency.matches_spec?(spec) + else + dependency.matches_spec?(spec) && Gem::Platform.match(spec.platform) + end + end + + found + end + end + + EMPTY_SEARCH = [].freeze + + def search_by_spec(spec) + spec = @specs[spec.name][spec.full_name] + spec ? [spec] : EMPTY_SEARCH + end + end +end diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb new file mode 100644 index 00000000000000..1bb29f0b3673a2 --- /dev/null +++ b/lib/bundler/injector.rb @@ -0,0 +1,253 @@ +# frozen_string_literal: true + +module Bundler + class Injector + INJECTED_GEMS = "injected gems".freeze + + def self.inject(new_deps, options = {}) + injector = new(new_deps, options) + injector.inject(Bundler.default_gemfile, Bundler.default_lockfile) + end + + def self.remove(gems, options = {}) + injector = new(gems, options) + injector.remove(Bundler.default_gemfile, Bundler.default_lockfile) + end + + def initialize(deps, options = {}) + @deps = deps + @options = options + end + + # @param [Pathname] gemfile_path The Gemfile in which to inject the new dependency. + # @param [Pathname] lockfile_path The lockfile in which to inject the new dependency. + # @return [Array] + def inject(gemfile_path, lockfile_path) + if Bundler.frozen_bundle? + # ensure the lock and Gemfile are synced + Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) + end + + # temporarily unfreeze + Bundler.settings.temporary(:deployment => false, :frozen => false) do + # evaluate the Gemfile we have now + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + # don't inject any gems that are already in the Gemfile + @deps -= builder.dependencies + + # add new deps to the end of the in-memory Gemfile + # Set conservative versioning to false because + # we want to let the resolver resolve the version first + builder.eval_gemfile(INJECTED_GEMS, build_gem_lines(false)) if @deps.any? + + # resolve to see if the new deps broke anything + @definition = builder.to_definition(lockfile_path, {}) + @definition.resolve_remotely! + + # since nothing broke, we can add those gems to the gemfile + append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @deps.any? + + # since we resolved successfully, write out the lockfile + @definition.lock(Bundler.default_lockfile) + + # invalidate the cached Bundler.definition + Bundler.reset_paths! + + # return an array of the deps that we added + @deps + end + end + + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + # @param [Pathname] lockfile_path The lockfile from which to remove dependencies. + # @return [Array] + def remove(gemfile_path, lockfile_path) + # remove gems from each gemfiles we have + Bundler.definition.gemfiles.each do |path| + deps = remove_deps(path) + + show_warning("No gems were removed from the gemfile.") if deps.empty? + + deps.each {|dep| Bundler.ui.confirm "#{SharedHelpers.pretty_dependency(dep, false)} was removed." } + end + end + + private + + def conservative_version(spec) + version = spec.version + return ">= 0" if version.nil? + segments = version.segments + seg_end_index = version >= Gem::Version.new("1.0") ? 1 : 2 + + prerelease_suffix = version.to_s.gsub(version.release.to_s, "") if version.prerelease? + "#{version_prefix}#{segments[0..seg_end_index].join(".")}#{prerelease_suffix}" + end + + def version_prefix + if @options[:strict] + "= " + elsif @options[:optimistic] + ">= " + else + "~> " + end + end + + def build_gem_lines(conservative_versioning) + @deps.map do |d| + name = d.name.dump + + requirement = if conservative_versioning + ", \"#{conservative_version(@definition.specs[d.name][0])}\"" + else + ", #{d.requirement.as_list.map(&:dump).join(", ")}" + end + + if d.groups != Array(:default) + group = d.groups.size == 1 ? ", :group => #{d.groups.first.inspect}" : ", :groups => #{d.groups.inspect}" + end + + source = ", :source => \"#{d.source}\"" unless d.source.nil? + + %(gem #{name}#{requirement}#{group}#{source}) + end.join("\n") + end + + def append_to(gemfile_path, new_gem_lines) + gemfile_path.open("a") do |f| + f.puts + f.puts new_gem_lines + end + end + + # evalutes a gemfile to remove the specified gem + # from it. + def remove_deps(gemfile_path) + initial_gemfile = IO.readlines(gemfile_path) + + Bundler.ui.info "Removing gems from #{gemfile_path}" + + # evaluate the Gemfile we have + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + removed_deps = remove_gems_from_dependencies(builder, @deps, gemfile_path) + + # abort the opertion if no gems were removed + # no need to operate on gemfile furthur + return [] if removed_deps.empty? + + cleaned_gemfile = remove_gems_from_gemfile(@deps, gemfile_path) + + SharedHelpers.write_to_gemfile(gemfile_path, cleaned_gemfile) + + # check for errors + # including extra gems being removed + # or some gems not being removed + # and return the actual removed deps + cross_check_for_errors(gemfile_path, builder.dependencies, removed_deps, initial_gemfile) + end + + # @param [Dsl] builder Dsl object of current Gemfile. + # @param [Array] gems Array of names of gems to be removed. + # @param [Pathname] path of the Gemfile + # @return [Array] removed_deps Array of removed dependencies. + def remove_gems_from_dependencies(builder, gems, gemfile_path) + removed_deps = [] + + gems.each do |gem_name| + deleted_dep = builder.dependencies.find {|d| d.name == gem_name } + + if deleted_dep.nil? + raise GemfileError, "`#{gem_name}` is not specified in #{gemfile_path} so it could not be removed." + end + + builder.dependencies.delete(deleted_dep) + + removed_deps << deleted_dep + end + + removed_deps + end + + # @param [Array] gems Array of names of gems to be removed. + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + def remove_gems_from_gemfile(gems, gemfile_path) + patterns = /gem\s+(['"])#{Regexp.union(gems)}\1|gem\s*\((['"])#{Regexp.union(gems)}\2\)/ + + # remove lines which match the regex + new_gemfile = IO.readlines(gemfile_path).reject {|line| line.match(patterns) } + + # remove lone \n and append them with other strings + new_gemfile.each_with_index do |_line, index| + if new_gemfile[index + 1] == "\n" + new_gemfile[index] += new_gemfile[index + 1] + new_gemfile.delete_at(index + 1) + end + end + + %w[group source env install_if].each {|block| remove_nested_blocks(new_gemfile, block) } + + new_gemfile.join.chomp + end + + # @param [Array] gemfile Array of gemfile contents. + # @param [String] block_name Name of block name to look for. + def remove_nested_blocks(gemfile, block_name) + nested_blocks = 0 + + # count number of nested blocks + gemfile.each_with_index {|line, index| nested_blocks += 1 if !gemfile[index + 1].nil? && gemfile[index + 1].include?(block_name) && line.include?(block_name) } + + while nested_blocks >= 0 + nested_blocks -= 1 + + gemfile.each_with_index do |line, index| + next unless !line.nil? && line.include?(block_name) + if gemfile[index + 1] =~ /^\s*end\s*$/ + gemfile[index] = nil + gemfile[index + 1] = nil + end + end + + gemfile.compact! + end + end + + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + # @param [Array] original_deps Array of original dependencies. + # @param [Array] removed_deps Array of removed dependencies. + # @param [Array] initial_gemfile Contents of original Gemfile before any operation. + def cross_check_for_errors(gemfile_path, original_deps, removed_deps, initial_gemfile) + # evalute the new gemfile to look for any failure cases + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + # record gems which were removed but not requested + extra_removed_gems = original_deps - builder.dependencies + + # if some extra gems were removed then raise error + # and revert Gemfile to original + unless extra_removed_gems.empty? + SharedHelpers.write_to_gemfile(gemfile_path, initial_gemfile.join) + + raise InvalidOption, "Gems could not be removed. #{extra_removed_gems.join(", ")} would also have been removed. Bundler cannot continue." + end + + # record gems which could not be removed due to some reasons + errored_deps = builder.dependencies.select {|d| d.gemfile == gemfile_path } & removed_deps.select {|d| d.gemfile == gemfile_path } + + show_warning "#{errored_deps.map(&:name).join(", ")} could not be removed." unless errored_deps.empty? + + # return actual removed dependencies + removed_deps - errored_deps + end + + def show_warning(message) + Bundler.ui.info Bundler.ui.add_color(message, :yellow) + end + end +end diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb new file mode 100644 index 00000000000000..9d25f3261a19a0 --- /dev/null +++ b/lib/bundler/inline.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "bundler/compatibility_guard" + +# Allows for declaring a Gemfile inline in a ruby script, optionally installing +# any gems that aren't already installed on the user's system. +# +# @note Every gem that is specified in this 'Gemfile' will be `require`d, as if +# the user had manually called `Bundler.require`. To avoid a requested gem +# being automatically required, add the `:require => false` option to the +# `gem` dependency declaration. +# +# @param install [Boolean] whether gems that aren't already installed on the +# user's system should be installed. +# Defaults to `false`. +# +# @param gemfile [Proc] a block that is evaluated as a `Gemfile`. +# +# @example Using an inline Gemfile +# +# #!/usr/bin/env ruby +# +# require 'bundler/inline' +# +# gemfile do +# source 'https://rubygems.org' +# gem 'json', require: false +# gem 'nap', require: 'rest' +# gem 'cocoapods', '~> 0.34.1' +# end +# +# puts Pod::VERSION # => "0.34.4" +# +def gemfile(install = false, options = {}, &gemfile) + require "bundler" + + opts = options.dup + ui = opts.delete(:ui) { Bundler::UI::Shell.new } + raise ArgumentError, "Unknown options: #{opts.keys.join(", ")}" unless opts.empty? + + old_root = Bundler.method(:root) + def Bundler.root + Bundler::SharedHelpers.pwd.expand_path + end + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", "Gemfile" + + Bundler::Plugin.gemfile_install(&gemfile) if Bundler.feature_flag.plugins? + builder = Bundler::Dsl.new + builder.instance_eval(&gemfile) + + definition = builder.to_definition(nil, true) + def definition.lock(*); end + definition.validate_runtime! + + missing_specs = proc do + definition.missing_specs? + end + + Bundler.ui = ui if install + if install || missing_specs.call + Bundler.settings.temporary(:inline => true) do + installer = Bundler::Installer.install(Bundler.root, definition, :system => true) + installer.post_install_messages.each do |name, message| + Bundler.ui.info "Post-install message from #{name}:\n#{message}" + end + end + end + + runtime = Bundler::Runtime.new(nil, definition) + runtime.setup.require +ensure + bundler_module = class << Bundler; self; end + bundler_module.send(:define_method, :root, old_root) if old_root +end diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb new file mode 100644 index 00000000000000..b49cfb67031b5d --- /dev/null +++ b/lib/bundler/installer.rb @@ -0,0 +1,318 @@ +# frozen_string_literal: true + +require "erb" +require "rubygems/dependency_installer" +require "bundler/worker" +require "bundler/installer/parallel_installer" +require "bundler/installer/standalone" +require "bundler/installer/gem_installer" + +module Bundler + class Installer + class << self + attr_accessor :ambiguous_gems + + Installer.ambiguous_gems = [] + end + + attr_reader :post_install_messages + + # Begins the installation process for Bundler. + # For more information see the #run method on this class. + def self.install(root, definition, options = {}) + installer = new(root, definition) + Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL_ALL, definition.dependencies) + installer.run(options) + Plugin.hook(Plugin::Events::GEM_AFTER_INSTALL_ALL, definition.dependencies) + installer + end + + def initialize(root, definition) + @root = root + @definition = definition + @post_install_messages = {} + end + + # Runs the install procedures for a specific Gemfile. + # + # Firstly, this method will check to see if `Bundler.bundle_path` exists + # and if not then Bundler will create the directory. This is usually the same + # location as RubyGems which typically is the `~/.gem` directory + # unless other specified. + # + # Secondly, it checks if Bundler has been configured to be "frozen". + # Frozen ensures that the Gemfile and the Gemfile.lock file are matching. + # This stops a situation where a developer may update the Gemfile but may not run + # `bundle install`, which leads to the Gemfile.lock file not being correctly updated. + # If this file is not correctly updated then any other developer running + # `bundle install` will potentially not install the correct gems. + # + # Thirdly, Bundler checks if there are any dependencies specified in the Gemfile. + # If there are no dependencies specified then Bundler returns a warning message stating + # so and this method returns. + # + # Fourthly, Bundler checks if the Gemfile.lock exists, and if so + # then proceeds to set up a definition based on the Gemfile and the Gemfile.lock. + # During this step Bundler will also download information about any new gems + # that are not in the Gemfile.lock and resolve any dependencies if needed. + # + # Fifthly, Bundler resolves the dependencies either through a cache of gems or by remote. + # This then leads into the gems being installed, along with stubs for their executables, + # but only if the --binstubs option has been passed or Bundler.options[:bin] has been set + # earlier. + # + # Sixthly, a new Gemfile.lock is created from the installed gems to ensure that the next time + # that a user runs `bundle install` they will receive any updates from this process. + # + # Finally, if the user has specified the standalone flag, Bundler will generate the needed + # require paths and save them in a `setup.rb` file. See `bundle standalone --help` for more + # information. + def run(options) + create_bundle_path + + ProcessLock.lock do + if Bundler.frozen_bundle? + @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) + end + + if @definition.dependencies.empty? + Bundler.ui.warn "The Gemfile specifies no dependencies" + lock + return + end + + if resolve_if_needed(options) + ensure_specs_are_compatible! + warn_on_incompatible_bundler_deps + load_plugins + options.delete(:jobs) + else + options[:jobs] = 1 # to avoid the overhead of Bundler::Worker + end + install(options) + + lock unless Bundler.frozen_bundle? + Standalone.new(options[:standalone], @definition).generate if options[:standalone] + end + end + + def generate_bundler_executable_stubs(spec, options = {}) + if options[:binstubs_cmd] && spec.executables.empty? + options = {} + spec.runtime_dependencies.each do |dep| + bins = @definition.specs[dep].first.executables + options[dep.name] = bins unless bins.empty? + end + if options.any? + Bundler.ui.warn "#{spec.name} has no executables, but you may want " \ + "one from a gem it depends on." + options.each {|name, bins| Bundler.ui.warn " #{name} has: #{bins.join(", ")}" } + else + Bundler.ui.warn "There are no executables for the gem #{spec.name}." + end + return + end + + # double-assignment to avoid warnings about variables that will be used by ERB + bin_path = Bundler.bin_path + bin_path = bin_path + relative_gemfile_path = Bundler.default_gemfile.relative_path_from(bin_path) + relative_gemfile_path = relative_gemfile_path + ruby_command = Thor::Util.ruby_command + ruby_command = ruby_command + template_path = File.expand_path("../templates/Executable", __FILE__) + if spec.name == "bundler" + template_path += ".bundler" + spec.executables = %(bundle) + end + template = File.read(template_path) + + exists = [] + spec.executables.each do |executable| + binstub_path = "#{bin_path}/#{executable}" + if File.exist?(binstub_path) && !options[:force] + exists << executable + next + end + + File.open(binstub_path, "w", 0o777 & ~File.umask) do |f| + if RUBY_VERSION >= "2.6" + f.puts ERB.new(template, :trim_mode => "-").result(binding) + else + f.puts ERB.new(template, nil, "-").result(binding) + end + end + end + + if options[:binstubs_cmd] && exists.any? + case exists.size + when 1 + Bundler.ui.warn "Skipped #{exists[0]} since it already exists." + when 2 + Bundler.ui.warn "Skipped #{exists.join(" and ")} since they already exist." + else + items = exists[0...-1].empty? ? nil : exists[0...-1].join(", ") + skipped = [items, exists[-1]].compact.join(" and ") + Bundler.ui.warn "Skipped #{skipped} since they already exist." + end + Bundler.ui.warn "If you want to overwrite skipped stubs, use --force." + end + end + + def generate_standalone_bundler_executable_stubs(spec) + # double-assignment to avoid warnings about variables that will be used by ERB + bin_path = Bundler.bin_path + unless path = Bundler.settings[:path] + raise "Can't standalone without an explicit path set" + end + standalone_path = Bundler.root.join(path).relative_path_from(bin_path) + standalone_path = standalone_path + template = File.read(File.expand_path("../templates/Executable.standalone", __FILE__)) + ruby_command = Thor::Util.ruby_command + ruby_command = ruby_command + + spec.executables.each do |executable| + next if executable == "bundle" + executable_path = Pathname(spec.full_gem_path).join(spec.bindir, executable).relative_path_from(bin_path) + executable_path = executable_path + File.open "#{bin_path}/#{executable}", "w", 0o755 do |f| + if RUBY_VERSION >= "2.6" + f.puts ERB.new(template, :trim_mode => "-").result(binding) + else + f.puts ERB.new(template, nil, "-").result(binding) + end + end + end + end + + private + + # the order that the resolver provides is significant, since + # dependencies might affect the installation of a gem. + # that said, it's a rare situation (other than rake), and parallel + # installation is SO MUCH FASTER. so we let people opt in. + def install(options) + force = options["force"] + jobs = installation_parallelization(options) + install_in_parallel jobs, options[:standalone], force + end + + def installation_parallelization(options) + if jobs = options.delete(:jobs) + return jobs + end + + return 1 unless can_install_in_parallel? + + auto_config_jobs = Bundler.feature_flag.auto_config_jobs? + if jobs = Bundler.settings[:jobs] + if auto_config_jobs + jobs + else + [jobs.pred, 1].max + end + elsif auto_config_jobs + processor_count + else + 1 + end + end + + def processor_count + require "etc" + Etc.nprocessors + rescue + 1 + end + + def load_plugins + Bundler.rubygems.load_plugins + + requested_path_gems = @definition.requested_specs.select {|s| s.source.is_a?(Source::Path) } + path_plugin_files = requested_path_gems.map do |spec| + begin + Bundler.rubygems.spec_matches_for_glob(spec, "rubygems_plugin#{Bundler.rubygems.suffix_pattern}") + rescue TypeError + error_message = "#{spec.name} #{spec.version} has an invalid gemspec" + raise Gem::InvalidSpecificationException, error_message + end + end.flatten + Bundler.rubygems.load_plugin_files(path_plugin_files) + end + + def ensure_specs_are_compatible! + system_ruby = Bundler::RubyVersion.system + rubygems_version = Gem::Version.create(Gem::VERSION) + @definition.specs.each do |spec| + if required_ruby_version = spec.required_ruby_version + unless required_ruby_version.satisfied_by?(system_ruby.gem_version) + raise InstallError, "#{spec.full_name} requires ruby version #{required_ruby_version}, " \ + "which is incompatible with the current version, #{system_ruby}" + end + end + next unless required_rubygems_version = spec.required_rubygems_version + unless required_rubygems_version.satisfied_by?(rubygems_version) + raise InstallError, "#{spec.full_name} requires rubygems version #{required_rubygems_version}, " \ + "which is incompatible with the current version, #{rubygems_version}" + end + end + end + + def warn_on_incompatible_bundler_deps + bundler_version = Gem::Version.create(Bundler::VERSION) + @definition.specs.each do |spec| + spec.dependencies.each do |dep| + next if dep.type == :development + next unless dep.name == "bundler".freeze + next if dep.requirement.satisfied_by?(bundler_version) + + Bundler.ui.warn "#{spec.name} (#{spec.version}) has dependency" \ + " #{SharedHelpers.pretty_dependency(dep)}" \ + ", which is unsatisfied by the current bundler version #{VERSION}" \ + ", so the dependency is being ignored" + end + end + end + + def can_install_in_parallel? + if Bundler.rubygems.provides?(">= 2.1.0") + true + else + Bundler.ui.warn "RubyGems #{Gem::VERSION} is not threadsafe, so your "\ + "gems will be installed one at a time. Upgrade to RubyGems 2.1.0 " \ + "or higher to enable parallel gem installation." + false + end + end + + def install_in_parallel(size, standalone, force = false) + spec_installations = ParallelInstaller.call(self, @definition.specs, size, standalone, force) + spec_installations.each do |installation| + post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message? + end + end + + def create_bundle_path + SharedHelpers.filesystem_access(Bundler.bundle_path.to_s) do |p| + Bundler.mkdir_p(p) + end unless Bundler.bundle_path.exist? + rescue Errno::EEXIST + raise PathError, "Could not install to path `#{Bundler.bundle_path}` " \ + "because a file already exists at that path. Either remove or rename the file so the directory can be created." + end + + # returns whether or not a re-resolve was needed + def resolve_if_needed(options) + if !@definition.unlocking? && !options["force"] && !Bundler.settings[:inline] && Bundler.default_lockfile.file? + return false if @definition.nothing_changed? && !@definition.missing_specs? + end + + options["local"] ? @definition.resolve_with_cache! : @definition.resolve_remotely! + true + end + + def lock(opts = {}) + @definition.lock(Bundler.default_lockfile, opts[:preserve_unknown_sections]) + end + end +end diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb new file mode 100644 index 00000000000000..e5e245f9706b6f --- /dev/null +++ b/lib/bundler/installer/gem_installer.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Bundler + class GemInstaller + attr_reader :spec, :standalone, :worker, :force, :installer + + def initialize(spec, installer, standalone = false, worker = 0, force = false) + @spec = spec + @installer = installer + @standalone = standalone + @worker = worker + @force = force + end + + def install_from_spec + post_install_message = spec_settings ? install_with_settings : install + Bundler.ui.debug "#{worker}: #{spec.name} (#{spec.version}) from #{spec.loaded_from}" + generate_executable_stubs + return true, post_install_message + rescue Bundler::InstallHookError, Bundler::SecurityError, APIResponseMismatchError + raise + rescue Errno::ENOSPC + return false, out_of_space_message + rescue StandardError => e + return false, specific_failure_message(e) + end + + private + + def specific_failure_message(e) + message = "#{e.class}: #{e.message}\n" + message += " " + e.backtrace.join("\n ") + "\n\n" if Bundler.ui.debug? + message = message.lines.first + Bundler.ui.add_color(message.lines.drop(1).join, :clear) + message + Bundler.ui.add_color(failure_message, :red) + end + + def failure_message + return install_error_message if spec.source.options["git"] + "#{install_error_message}\n#{gem_install_message}" + end + + def install_error_message + "An error occurred while installing #{spec.name} (#{spec.version}), and Bundler cannot continue." + end + + def gem_install_message + source = spec.source + return unless source.respond_to?(:remotes) + + if source.remotes.size == 1 + "Make sure that `gem install #{spec.name} -v '#{spec.version}' --source '#{source.remotes.first}'` succeeds before bundling." + else + "Make sure that `gem install #{spec.name} -v '#{spec.version}'` succeeds before bundling." + end + end + + def spec_settings + # Fetch the build settings, if there are any + Bundler.settings["build.#{spec.name}"] + end + + def install + spec.source.install(spec, :force => force, :ensure_builtin_gems_cached => standalone, :build_args => Array(spec_settings)) + end + + def install_with_settings + # Build arguments are global, so this is mutexed + Bundler.rubygems.install_with_build_args([spec_settings]) { install } + end + + def out_of_space_message + "#{install_error_message}\nYour disk is out of space. Free some space to be able to install your bundle." + end + + def generate_executable_stubs + return if Bundler.feature_flag.forget_cli_options? + return if Bundler.settings[:inline] + if Bundler.settings[:bin] && standalone + installer.generate_standalone_bundler_executable_stubs(spec) + elsif Bundler.settings[:bin] + installer.generate_bundler_executable_stubs(spec, :force => true) + end + end + end +end diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb new file mode 100644 index 00000000000000..f8a849ccfcaaf8 --- /dev/null +++ b/lib/bundler/installer/parallel_installer.rb @@ -0,0 +1,233 @@ +# frozen_string_literal: true + +require "bundler/worker" +require "bundler/installer/gem_installer" + +module Bundler + class ParallelInstaller + class SpecInstallation + attr_accessor :spec, :name, :post_install_message, :state, :error + def initialize(spec) + @spec = spec + @name = spec.name + @state = :none + @post_install_message = "" + @error = nil + end + + def installed? + state == :installed + end + + def enqueued? + state == :enqueued + end + + def failed? + state == :failed + end + + def installation_attempted? + installed? || failed? + end + + # Only true when spec in neither installed nor already enqueued + def ready_to_enqueue? + !enqueued? && !installation_attempted? + end + + def has_post_install_message? + !post_install_message.empty? + end + + def ignorable_dependency?(dep) + dep.type == :development || dep.name == @name + end + + # Checks installed dependencies against spec's dependencies to make + # sure needed dependencies have been installed. + def dependencies_installed?(all_specs) + installed_specs = all_specs.select(&:installed?).map(&:name) + dependencies.all? {|d| installed_specs.include? d.name } + end + + # Represents only the non-development dependencies, the ones that are + # itself and are in the total list. + def dependencies + @dependencies ||= begin + all_dependencies.reject {|dep| ignorable_dependency? dep } + end + end + + def missing_lockfile_dependencies(all_spec_names) + deps = all_dependencies.reject {|dep| ignorable_dependency? dep } + deps.reject {|dep| all_spec_names.include? dep.name } + end + + # Represents all dependencies + def all_dependencies + @spec.dependencies + end + + def to_s + "#<#{self.class} #{@spec.full_name} (#{state})>" + end + end + + def self.call(*args) + new(*args).call + end + + attr_reader :size + + def initialize(installer, all_specs, size, standalone, force) + @installer = installer + @size = size + @standalone = standalone + @force = force + @specs = all_specs.map {|s| SpecInstallation.new(s) } + @spec_set = all_specs + @rake = @specs.find {|s| s.name == "rake" } + end + + def call + # Since `autoload` has the potential for threading issues on 1.8.7 + # TODO: remove in bundler 2.0 + require "bundler/gem_remote_fetcher" if RUBY_VERSION < "1.9" + + check_for_corrupt_lockfile + + if @size > 1 + install_with_worker + else + install_serially + end + + handle_error if @specs.any?(&:failed?) + @specs + ensure + worker_pool && worker_pool.stop + end + + def check_for_corrupt_lockfile + missing_dependencies = @specs.map do |s| + [ + s, + s.missing_lockfile_dependencies(@specs.map(&:name)), + ] + end.reject { |a| a.last.empty? } + return if missing_dependencies.empty? + + warning = [] + warning << "Your lockfile was created by an old Bundler that left some things out." + if @size != 1 + warning << "Because of the missing DEPENDENCIES, we can only install gems one at a time, instead of installing #{@size} at a time." + @size = 1 + end + warning << "You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile." + warning << "The missing gems are:" + + missing_dependencies.each do |spec, missing| + warning << "* #{missing.map(&:name).join(", ")} depended upon by #{spec.name}" + end + + Bundler.ui.warn(warning.join("\n")) + end + + private + + def install_with_worker + enqueue_specs + process_specs until finished_installing? + end + + def install_serially + until finished_installing? + raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?) + spec_install.state = :enqueued + do_install(spec_install, 0) + end + end + + def worker_pool + @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda { |spec_install, worker_num| + do_install(spec_install, worker_num) + } + end + + def do_install(spec_install, worker_num) + Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL, spec_install) + gem_installer = Bundler::GemInstaller.new( + spec_install.spec, @installer, @standalone, worker_num, @force + ) + success, message = begin + gem_installer.install_from_spec + rescue RuntimeError => e + raise e, "#{e}\n\n#{require_tree_for_spec(spec_install.spec)}" + end + if success + spec_install.state = :installed + spec_install.post_install_message = message unless message.nil? + else + spec_install.state = :failed + spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}" + end + Plugin.hook(Plugin::Events::GEM_AFTER_INSTALL, spec_install) + spec_install + end + + # Dequeue a spec and save its post-install message and then enqueue the + # remaining specs. + # Some specs might've had to wait til this spec was installed to be + # processed so the call to `enqueue_specs` is important after every + # dequeue. + def process_specs + worker_pool.deq + enqueue_specs + end + + def finished_installing? + @specs.all? do |spec| + return true if spec.failed? + spec.installed? + end + end + + def handle_error + errors = @specs.select(&:failed?).map(&:error) + if exception = errors.find {|e| e.is_a?(Bundler::BundlerError) } + raise exception + end + raise Bundler::InstallError, errors.map(&:to_s).join("\n\n") + end + + def require_tree_for_spec(spec) + tree = @spec_set.what_required(spec) + t = String.new("In #{File.basename(SharedHelpers.default_gemfile)}:\n") + tree.each_with_index do |s, depth| + t << " " * depth.succ << s.name + unless tree.last == s + t << %( was resolved to #{s.version}, which depends on) + end + t << %(\n) + end + t + end + + # Keys in the remains hash represent uninstalled gems specs. + # We enqueue all gem specs that do not have any dependencies. + # Later we call this lambda again to install specs that depended on + # previously installed specifications. We continue until all specs + # are installed. + def enqueue_specs + @specs.select(&:ready_to_enqueue?).each do |spec| + next if @rake && !@rake.installed? && spec.name != @rake.name + + if spec.dependencies_installed? @specs + spec.state = :enqueued + worker_pool.enq spec + end + end + end + end +end diff --git a/lib/bundler/installer/standalone.rb b/lib/bundler/installer/standalone.rb new file mode 100644 index 00000000000000..ce0c9df1ebcb13 --- /dev/null +++ b/lib/bundler/installer/standalone.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Bundler + class Standalone + def initialize(groups, definition) + @specs = groups.empty? ? definition.requested_specs : definition.specs_for(groups.map(&:to_sym)) + end + + def generate + SharedHelpers.filesystem_access(bundler_path) do |p| + FileUtils.mkdir_p(p) + end + File.open File.join(bundler_path, "setup.rb"), "w" do |file| + file.puts "require 'rbconfig'" + file.puts "# ruby 1.8.7 doesn't define RUBY_ENGINE" + file.puts "ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'" + file.puts "ruby_version = RbConfig::CONFIG[\"ruby_version\"]" + file.puts "path = File.expand_path('..', __FILE__)" + paths.each do |path| + file.puts %($:.unshift "\#{path}/#{path}") + end + end + end + + private + + def paths + @specs.map do |spec| + next if spec.name == "bundler" + Array(spec.require_paths).map do |path| + gem_path(path, spec).sub(version_dir, '#{ruby_engine}/#{ruby_version}') + # This is a static string intentionally. It's interpolated at a later time. + end + end.flatten + end + + def version_dir + "#{Bundler::RubyVersion.system.engine}/#{RbConfig::CONFIG["ruby_version"]}" + end + + def bundler_path + Bundler.root.join(Bundler.settings[:path], "bundler") + end + + def gem_path(path, spec) + full_path = Pathname.new(path).absolute? ? path : File.join(spec.full_gem_path, path) + Pathname.new(full_path).relative_path_from(Bundler.root.join(bundler_path)).to_s + rescue TypeError + error_message = "#{spec.name} #{spec.version} has an invalid gemspec" + raise Gem::InvalidSpecificationException.new(error_message) + end + end +end diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb new file mode 100644 index 00000000000000..d9cb01f810e8db --- /dev/null +++ b/lib/bundler/lazy_specification.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require "uri" +require "bundler/match_platform" + +module Bundler + class LazySpecification + Identifier = Struct.new(:name, :version, :source, :platform, :dependencies) + class Identifier + include Comparable + def <=>(other) + return unless other.is_a?(Identifier) + [name, version, platform_string] <=> [other.name, other.version, other.platform_string] + end + + protected + + def platform_string + platform_string = platform.to_s + platform_string == Index::RUBY ? Index::NULL : platform_string + end + end + + include MatchPlatform + + attr_reader :name, :version, :dependencies, :platform + attr_accessor :source, :remote + + def initialize(name, version, platform, source = nil) + @name = name + @version = version + @dependencies = [] + @platform = platform || Gem::Platform::RUBY + @source = source + @specification = nil + end + + def full_name + if platform == Gem::Platform::RUBY || platform.nil? + "#{@name}-#{@version}" + else + "#{@name}-#{@version}-#{platform}" + end + end + + def ==(other) + identifier == other.identifier + end + + def satisfies?(dependency) + @name == dependency.name && dependency.requirement.satisfied_by?(Gem::Version.new(@version)) + end + + def to_lock + out = String.new + + if platform == Gem::Platform::RUBY || platform.nil? + out << " #{name} (#{version})\n" + else + out << " #{name} (#{version}-#{platform})\n" + end + + dependencies.sort_by(&:to_s).uniq.each do |dep| + next if dep.type == :development + out << " #{dep.to_lock}\n" + end + + out + end + + def __materialize__ + search_object = Bundler.feature_flag.specific_platform? || Bundler.settings[:force_ruby_platform] ? self : Dependency.new(name, version) + @specification = if source.is_a?(Source::Gemspec) && source.gemspec.name == name + source.gemspec.tap {|s| s.source = source } + else + search = source.specs.search(search_object).last + if search && Gem::Platform.new(search.platform) != Gem::Platform.new(platform) && !search.runtime_dependencies.-(dependencies.reject {|d| d.type == :development }).empty? + Bundler.ui.warn "Unable to use the platform-specific (#{search.platform}) version of #{name} (#{version}) " \ + "because it has different dependencies from the #{platform} version. " \ + "To use the platform-specific version of the gem, run `bundle config specific_platform true` and install again." + search = source.specs.search(self).last + end + search.dependencies = dependencies if search && (search.is_a?(RemoteSpecification) || search.is_a?(EndpointSpecification)) + search + end + end + + def respond_to?(*args) + super || @specification ? @specification.respond_to?(*args) : nil + end + + def to_s + @__to_s ||= if platform == Gem::Platform::RUBY || platform.nil? + "#{name} (#{version})" + else + "#{name} (#{version}-#{platform})" + end + end + + def identifier + @__identifier ||= Identifier.new(name, version, source, platform, dependencies) + end + + def git_version + return unless source.is_a?(Bundler::Source::Git) + " #{source.revision[0..6]}" + end + + private + + def to_ary + nil + end + + def method_missing(method, *args, &blk) + raise "LazySpecification has not been materialized yet (calling :#{method} #{args.inspect})" unless @specification + + return super unless respond_to?(method) + + @specification.send(method, *args, &blk) + end + end +end diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb new file mode 100644 index 00000000000000..585077d18dc4cb --- /dev/null +++ b/lib/bundler/lockfile_generator.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Bundler + class LockfileGenerator + attr_reader :definition + attr_reader :out + + # @private + def initialize(definition) + @definition = definition + @out = String.new + end + + def self.generate(definition) + new(definition).generate! + end + + def generate! + add_sources + add_platforms + add_dependencies + add_locked_ruby_version + add_bundled_with + + out + end + + private + + def add_sources + definition.send(:sources).lock_sources.each_with_index do |source, idx| + out << "\n" unless idx.zero? + + # Add the source header + out << source.to_lock + + # Find all specs for this source + specs = definition.resolve.select {|s| source.can_lock?(s) } + add_specs(specs) + end + end + + def add_specs(specs) + # This needs to be sorted by full name so that + # gems with the same name, but different platform + # are ordered consistently + specs.sort_by(&:full_name).each do |spec| + next if spec.name == "bundler".freeze + out << spec.to_lock + end + end + + def add_platforms + add_section("PLATFORMS", definition.platforms) + end + + def add_dependencies + out << "\nDEPENDENCIES\n" + + handled = [] + definition.dependencies.sort_by(&:to_s).each do |dep| + next if handled.include?(dep.name) + out << dep.to_lock + handled << dep.name + end + end + + def add_locked_ruby_version + return unless locked_ruby_version = definition.locked_ruby_version + add_section("RUBY VERSION", locked_ruby_version.to_s) + end + + def add_bundled_with + add_section("BUNDLED WITH", definition.locked_bundler_version.to_s) + end + + def add_section(name, value) + out << "\n#{name}\n" + case value + when Array + value.map(&:to_s).sort.each do |val| + out << " #{val}\n" + end + when Hash + value.to_a.sort_by {|k, _| k.to_s }.each do |key, val| + out << " #{key}: #{val}\n" + end + when String + out << " #{value}\n" + else + raise ArgumentError, "#{value.inspect} can't be serialized in a lockfile" + end + end + end +end diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb new file mode 100644 index 00000000000000..ff706fca1d5194 --- /dev/null +++ b/lib/bundler/lockfile_parser.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +# Some versions of the Bundler 1.1 RC series introduced corrupted +# lockfiles. There were two major problems: +# +# * multiple copies of the same GIT section appeared in the lockfile +# * when this happened, those sections got multiple copies of gems +# in those sections. +# +# As a result, Bundler 1.1 contains code that fixes the earlier +# corruption. We will remove this fix-up code in Bundler 1.2. + +module Bundler + class LockfileParser + attr_reader :sources, :dependencies, :specs, :platforms, :bundler_version, :ruby_version + + BUNDLED = "BUNDLED WITH".freeze + DEPENDENCIES = "DEPENDENCIES".freeze + PLATFORMS = "PLATFORMS".freeze + RUBY = "RUBY VERSION".freeze + GIT = "GIT".freeze + GEM = "GEM".freeze + PATH = "PATH".freeze + PLUGIN = "PLUGIN SOURCE".freeze + SPECS = " specs:".freeze + OPTIONS = /^ ([a-z]+): (.*)$/i + SOURCE = [GIT, GEM, PATH, PLUGIN].freeze + + SECTIONS_BY_VERSION_INTRODUCED = { + # The strings have to be dup'ed for old RG on Ruby 2.3+ + # TODO: remove dup in Bundler 2.0 + Gem::Version.create("1.0".dup) => [DEPENDENCIES, PLATFORMS, GIT, GEM, PATH].freeze, + Gem::Version.create("1.10".dup) => [BUNDLED].freeze, + Gem::Version.create("1.12".dup) => [RUBY].freeze, + Gem::Version.create("1.13".dup) => [PLUGIN].freeze, + }.freeze + + KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten.freeze + + ENVIRONMENT_VERSION_SECTIONS = [BUNDLED, RUBY].freeze + + def self.sections_in_lockfile(lockfile_contents) + lockfile_contents.scan(/^\w[\w ]*$/).uniq + end + + def self.unknown_sections_in_lockfile(lockfile_contents) + sections_in_lockfile(lockfile_contents) - KNOWN_SECTIONS + end + + def self.sections_to_ignore(base_version = nil) + base_version &&= base_version.release + base_version ||= Gem::Version.create("1.0".dup) + attributes = [] + SECTIONS_BY_VERSION_INTRODUCED.each do |version, introduced| + next if version <= base_version + attributes += introduced + end + attributes + end + + def initialize(lockfile) + @platforms = [] + @sources = [] + @dependencies = {} + @state = nil + @specs = {} + + @rubygems_aggregate = Source::Rubygems.new + + if lockfile.match(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/) + raise LockfileError, "Your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} contains merge conflicts.\n" \ + "Run `git checkout HEAD -- #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}` first to get a clean lock." + end + + lockfile.split(/(?:\r?\n)+/).each do |line| + if SOURCE.include?(line) + @state = :source + parse_source(line) + elsif line == DEPENDENCIES + @state = :dependency + elsif line == PLATFORMS + @state = :platform + elsif line == RUBY + @state = :ruby + elsif line == BUNDLED + @state = :bundled_with + elsif line =~ /^[^\s]/ + @state = nil + elsif @state + send("parse_#{@state}", line) + end + end + @sources << @rubygems_aggregate unless Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + @specs = @specs.values.sort_by(&:identifier) + warn_for_outdated_bundler_version + rescue ArgumentError => e + Bundler.ui.debug(e) + raise LockfileError, "Your lockfile is unreadable. Run `rm #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}` " \ + "and then `bundle install` to generate a new lockfile." + end + + def warn_for_outdated_bundler_version + return unless bundler_version + prerelease_text = bundler_version.prerelease? ? " --pre" : "" + current_version = Gem::Version.create(Bundler::VERSION) + case current_version.segments.first <=> bundler_version.segments.first + when -1 + raise LockfileError, "You must use Bundler #{bundler_version.segments.first} or greater with this lockfile." + when 0 + if current_version < bundler_version + Bundler.ui.warn "Warning: the running version of Bundler (#{current_version}) is older " \ + "than the version that created the lockfile (#{bundler_version}). We suggest you " \ + "upgrade to the latest version of Bundler by running `gem " \ + "install bundler#{prerelease_text}`.\n" + end + end + end + + private + + TYPES = { + GIT => Bundler::Source::Git, + GEM => Bundler::Source::Rubygems, + PATH => Bundler::Source::Path, + PLUGIN => Bundler::Plugin, + }.freeze + + def parse_source(line) + case line + when SPECS + case @type + when PATH + @current_source = TYPES[@type].from_lock(@opts) + @sources << @current_source + when GIT + @current_source = TYPES[@type].from_lock(@opts) + # Strip out duplicate GIT sections + if @sources.include?(@current_source) + @current_source = @sources.find {|s| s == @current_source } + else + @sources << @current_source + end + when GEM + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + @opts["remotes"] = @opts.delete("remote") + @current_source = TYPES[@type].from_lock(@opts) + @sources << @current_source + else + Array(@opts["remote"]).each do |url| + @rubygems_aggregate.add_remote(url) + end + @current_source = @rubygems_aggregate + end + when PLUGIN + @current_source = Plugin.source_from_lock(@opts) + @sources << @current_source + end + when OPTIONS + value = $2 + value = true if value == "true" + value = false if value == "false" + + key = $1 + + if @opts[key] + @opts[key] = Array(@opts[key]) + @opts[key] << value + else + @opts[key] = value + end + when *SOURCE + @current_source = nil + @opts = {} + @type = line + else + parse_spec(line) + end + end + + space = / / + NAME_VERSION = / + ^(#{space}{2}|#{space}{4}|#{space}{6})(?!#{space}) # Exactly 2, 4, or 6 spaces at the start of the line + (.*?) # Name + (?:#{space}\(([^-]*) # Space, followed by version + (?:-(.*))?\))? # Optional platform + (!)? # Optional pinned marker + $ # Line end + /xo + + def parse_dependency(line) + return unless line =~ NAME_VERSION + spaces = $1 + return unless spaces.size == 2 + name = $2 + version = $3 + pinned = $5 + + version = version.split(",").map(&:strip) if version + + dep = Bundler::Dependency.new(name, version) + + if pinned && dep.name != "bundler" + spec = @specs.find {|_, v| v.name == dep.name } + dep.source = spec.last.source if spec + + # Path sources need to know what the default name / version + # to use in the case that there are no gemspecs present. A fake + # gemspec is created based on the version set on the dependency + # TODO: Use the version from the spec instead of from the dependency + if version && version.size == 1 && version.first =~ /^\s*= (.+)\s*$/ && dep.source.is_a?(Bundler::Source::Path) + dep.source.name = name + dep.source.version = $1 + end + end + + @dependencies[dep.name] = dep + end + + def parse_spec(line) + return unless line =~ NAME_VERSION + spaces = $1 + name = $2 + version = $3 + platform = $4 + + if spaces.size == 4 + version = Gem::Version.new(version) + platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY + @current_spec = LazySpecification.new(name, version, platform) + @current_spec.source = @current_source + + # Avoid introducing multiple copies of the same spec (caused by + # duplicate GIT sections) + @specs[@current_spec.identifier] ||= @current_spec + elsif spaces.size == 6 + version = version.split(",").map(&:strip) if version + dep = Gem::Dependency.new(name, version) + @current_spec.dependencies << dep + end + end + + def parse_platform(line) + @platforms << Gem::Platform.new($1) if line =~ /^ (.*)$/ + end + + def parse_bundled_with(line) + line = line.strip + return unless Gem::Version.correct?(line) + @bundler_version = Gem::Version.create(line) + end + + def parse_ruby(line) + @ruby_version = line.strip + end + end +end diff --git a/lib/bundler/match_platform.rb b/lib/bundler/match_platform.rb new file mode 100644 index 00000000000000..56cbbfb95dcc6c --- /dev/null +++ b/lib/bundler/match_platform.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "bundler/gem_helpers" + +module Bundler + module MatchPlatform + include GemHelpers + + def match_platform(p) + MatchPlatform.platforms_match?(platform, p) + end + + def self.platforms_match?(gemspec_platform, local_platform) + return true if gemspec_platform.nil? + return true if Gem::Platform::RUBY == gemspec_platform + return true if local_platform == gemspec_platform + gemspec_platform = Gem::Platform.new(gemspec_platform) + return true if GemHelpers.generic(gemspec_platform) === local_platform + return true if gemspec_platform === local_platform + + false + end + end +end diff --git a/lib/bundler/mirror.rb b/lib/bundler/mirror.rb new file mode 100644 index 00000000000000..b15190e7e579e2 --- /dev/null +++ b/lib/bundler/mirror.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +require "socket" + +module Bundler + class Settings + # Class used to build the mirror set and then find a mirror for a given URI + # + # @param prober [Prober object, nil] by default a TCPSocketProbe, this object + # will be used to probe the mirror address to validate that the mirror replies. + class Mirrors + def initialize(prober = nil) + @all = Mirror.new + @prober = prober || TCPSocketProbe.new + @mirrors = {} + end + + # Returns a mirror for the given uri. + # + # Depending on the uri having a valid mirror or not, it may be a + # mirror that points to the provided uri + def for(uri) + if @all.validate!(@prober).valid? + @all + else + fetch_valid_mirror_for(Settings.normalize_uri(uri)) + end + end + + def each + @mirrors.each do |k, v| + yield k, v.uri.to_s + end + end + + def parse(key, value) + config = MirrorConfig.new(key, value) + mirror = if config.all? + @all + else + @mirrors[config.uri] ||= Mirror.new + end + config.update_mirror(mirror) + end + + private + + def fetch_valid_mirror_for(uri) + downcased = uri.to_s.downcase + mirror = @mirrors[downcased] || @mirrors[URI(downcased).host] || Mirror.new(uri) + mirror.validate!(@prober) + mirror = Mirror.new(uri) unless mirror.valid? + mirror + end + end + + # A mirror + # + # Contains both the uri that should be used as a mirror and the + # fallback timeout which will be used for probing if the mirror + # replies on time or not. + class Mirror + DEFAULT_FALLBACK_TIMEOUT = 0.1 + + attr_reader :uri, :fallback_timeout + + def initialize(uri = nil, fallback_timeout = 0) + self.uri = uri + self.fallback_timeout = fallback_timeout + @valid = nil + end + + def uri=(uri) + @uri = if uri.nil? + nil + else + URI(uri.to_s) + end + @valid = nil + end + + def fallback_timeout=(timeout) + case timeout + when true, "true" + @fallback_timeout = DEFAULT_FALLBACK_TIMEOUT + when false, "false" + @fallback_timeout = 0 + else + @fallback_timeout = timeout.to_i + end + @valid = nil + end + + def ==(other) + !other.nil? && uri == other.uri && fallback_timeout == other.fallback_timeout + end + + def valid? + return false if @uri.nil? + return @valid unless @valid.nil? + false + end + + def validate!(probe = nil) + @valid = false if uri.nil? + if @valid.nil? + @valid = fallback_timeout == 0 || (probe || TCPSocketProbe.new).replies?(self) + end + self + end + end + + # Class used to parse one configuration line + # + # Gets the configuration line and the value. + # This object provides a `update_mirror` method + # used to setup the given mirror value. + class MirrorConfig + attr_accessor :uri, :value + + def initialize(config_line, value) + uri, fallback = + config_line.match(%r{\Amirror\.(all|.+?)(\.fallback_timeout)?\/?\z}).captures + @fallback = !fallback.nil? + @all = false + if uri == "all" + @all = true + else + @uri = URI(uri).absolute? ? Settings.normalize_uri(uri) : uri + end + @value = value + end + + def all? + @all + end + + def update_mirror(mirror) + if @fallback + mirror.fallback_timeout = @value + else + mirror.uri = Settings.normalize_uri(@value) + end + end + end + + # Class used for probing TCP availability for a given mirror. + class TCPSocketProbe + def replies?(mirror) + MirrorSockets.new(mirror).any? do |socket, address, timeout| + begin + socket.connect_nonblock(address) + rescue Errno::EINPROGRESS + wait_for_writtable_socket(socket, address, timeout) + rescue RuntimeError # Connection failed somehow, again + false + end + end + end + + private + + def wait_for_writtable_socket(socket, address, timeout) + if IO.select(nil, [socket], nil, timeout) + probe_writtable_socket(socket, address) + else # TCP Handshake timed out, or there is something dropping packets + false + end + end + + def probe_writtable_socket(socket, address) + socket.connect_nonblock(address) + rescue Errno::EISCONN + true + rescue StandardError # Connection failed + false + end + end + end + + # Class used to build the list of sockets that correspond to + # a given mirror. + # + # One mirror may correspond to many different addresses, both + # because of it having many dns entries or because + # the network interface is both ipv4 and ipv5 + class MirrorSockets + def initialize(mirror) + @timeout = mirror.fallback_timeout + @addresses = Socket.getaddrinfo(mirror.uri.host, mirror.uri.port).map do |address| + SocketAddress.new(address[0], address[3], address[1]) + end + end + + def any? + @addresses.any? do |address| + socket = Socket.new(Socket.const_get(address.type), Socket::SOCK_STREAM, 0) + socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + value = yield socket, address.to_socket_address, @timeout + socket.close unless socket.closed? + value + end + end + end + + # Socket address builder. + # + # Given a socket type, a host and a port, + # provides a method to build sockaddr string + class SocketAddress + attr_reader :type, :host, :port + + def initialize(type, host, port) + @type = type + @host = host + @port = port + end + + def to_socket_address + Socket.pack_sockaddr_in(@port, @host) + end + end +end diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb new file mode 100644 index 00000000000000..53f9806b73d403 --- /dev/null +++ b/lib/bundler/plugin.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +require "bundler/plugin/api" + +module Bundler + module Plugin + autoload :DSL, "bundler/plugin/dsl" + autoload :Events, "bundler/plugin/events" + autoload :Index, "bundler/plugin/index" + autoload :Installer, "bundler/plugin/installer" + autoload :SourceList, "bundler/plugin/source_list" + + class MalformattedPlugin < PluginError; end + class UndefinedCommandError < PluginError; end + class UnknownSourceError < PluginError; end + + PLUGIN_FILE_NAME = "plugins.rb".freeze + + module_function + + def reset! + instance_variables.each {|i| remove_instance_variable(i) } + + @sources = {} + @commands = {} + @hooks_by_event = Hash.new {|h, k| h[k] = [] } + @loaded_plugin_names = [] + end + + reset! + + # Installs a new plugin by the given name + # + # @param [Array] names the name of plugin to be installed + # @param [Hash] options various parameters as described in description. + # Refer to cli/plugin for available options + def install(names, options) + specs = Installer.new.install(names, options) + + save_plugins names, specs + rescue PluginError => e + if specs + specs_to_delete = Hash[specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) }] + specs_to_delete.values.each {|spec| Bundler.rm_rf(spec.full_gem_path) } + end + + Bundler.ui.error "Failed to install plugin #{name}: #{e.message}\n #{e.backtrace.join("\n ")}" + end + + # Evaluates the Gemfile with a limited DSL and installs the plugins + # specified by plugin method + # + # @param [Pathname] gemfile path + # @param [Proc] block that can be evaluated for (inline) Gemfile + def gemfile_install(gemfile = nil, &inline) + builder = DSL.new + if block_given? + builder.instance_eval(&inline) + else + builder.eval_gemfile(gemfile) + end + definition = builder.to_definition(nil, true) + + return if definition.dependencies.empty? + + plugins = definition.dependencies.map(&:name).reject {|p| index.installed? p } + installed_specs = Installer.new.install_definition(definition) + + save_plugins plugins, installed_specs, builder.inferred_plugins + rescue RuntimeError => e + unless e.is_a?(GemfileError) + Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}" + end + raise + end + + # The index object used to store the details about the plugin + def index + @index ||= Index.new + end + + # The directory root for all plugin related data + # + # If run in an app, points to local root, in app_config_path + # Otherwise, points to global root, in Bundler.user_bundle_path("plugin") + def root + @root ||= if SharedHelpers.in_bundle? + local_root + else + global_root + end + end + + def local_root + Bundler.app_config_path.join("plugin") + end + + # The global directory root for all plugin related data + def global_root + Bundler.user_bundle_path("plugin") + end + + # The cache directory for plugin stuffs + def cache + @cache ||= root.join("cache") + end + + # To be called via the API to register to handle a command + def add_command(command, cls) + @commands[command] = cls + end + + # Checks if any plugin handles the command + def command?(command) + !index.command_plugin(command).nil? + end + + # To be called from Cli class to pass the command and argument to + # approriate plugin class + def exec_command(command, args) + raise UndefinedCommandError, "Command `#{command}` not found" unless command? command + + load_plugin index.command_plugin(command) unless @commands.key? command + + @commands[command].new.exec(command, args) + end + + # To be called via the API to register to handle a source plugin + def add_source(source, cls) + @sources[source] = cls + end + + # Checks if any plugin declares the source + def source?(name) + !index.source_plugin(name.to_s).nil? + end + + # @return [Class] that handles the source. The calss includes API::Source + def source(name) + raise UnknownSourceError, "Source #{name} not found" unless source? name + + load_plugin(index.source_plugin(name)) unless @sources.key? name + + @sources[name] + end + + # @param [Hash] The options that are present in the lock file + # @return [API::Source] the instance of the class that handles the source + # type passed in locked_opts + def source_from_lock(locked_opts) + src = source(locked_opts["type"]) + + src.new(locked_opts.merge("uri" => locked_opts["remote"])) + end + + # To be called via the API to register a hooks and corresponding block that + # will be called to handle the hook + def add_hook(event, &block) + unless Events.defined_event?(event) + raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events" + end + @hooks_by_event[event.to_s] << block + end + + # Runs all the hooks that are registered for the passed event + # + # It passes the passed arguments and block to the block registered with + # the api. + # + # @param [String] event + def hook(event, *args, &arg_blk) + return unless Bundler.feature_flag.plugins? + unless Events.defined_event?(event) + raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events" + end + + plugins = index.hook_plugins(event) + return unless plugins.any? + + (plugins - @loaded_plugin_names).each {|name| load_plugin(name) } + + @hooks_by_event[event].each {|blk| blk.call(*args, &arg_blk) } + end + + # currently only intended for specs + # + # @return [String, nil] installed path + def installed?(plugin) + Index.new.installed?(plugin) + end + + # Post installation processing and registering with index + # + # @param [Array] plugins list to be installed + # @param [Hash] specs of plugins mapped to installation path (currently they + # contain all the installed specs, including plugins) + # @param [Array] names of inferred source plugins that can be ignored + def save_plugins(plugins, specs, optional_plugins = []) + plugins.each do |name| + spec = specs[name] + validate_plugin! Pathname.new(spec.full_gem_path) + installed = register_plugin(name, spec, optional_plugins.include?(name)) + Bundler.ui.info "Installed plugin #{name}" if installed + end + end + + # Checks if the gem is good to be a plugin + # + # At present it only checks whether it contains plugins.rb file + # + # @param [Pathname] plugin_path the path plugin is installed at + # @raise [MalformattedPlugin] if plugins.rb file is not found + def validate_plugin!(plugin_path) + plugin_file = plugin_path.join(PLUGIN_FILE_NAME) + raise MalformattedPlugin, "#{PLUGIN_FILE_NAME} was not found in the plugin." unless plugin_file.file? + end + + # Runs the plugins.rb file in an isolated namespace, records the plugin + # actions it registers for and then passes the data to index to be stored. + # + # @param [String] name the name of the plugin + # @param [Specification] spec of installed plugin + # @param [Boolean] optional_plugin, removed if there is conflict with any + # other plugin (used for default source plugins) + # + # @raise [MalformattedPlugin] if plugins.rb raises any error + def register_plugin(name, spec, optional_plugin = false) + commands = @commands + sources = @sources + hooks = @hooks_by_event + + @commands = {} + @sources = {} + @hooks_by_event = Hash.new {|h, k| h[k] = [] } + + load_paths = spec.load_paths + add_to_load_path(load_paths) + path = Pathname.new spec.full_gem_path + + begin + load path.join(PLUGIN_FILE_NAME), true + rescue StandardError => e + raise MalformattedPlugin, "#{e.class}: #{e.message}" + end + + if optional_plugin && @sources.keys.any? {|s| source? s } + Bundler.rm_rf(path) + false + else + index.register_plugin(name, path.to_s, load_paths, @commands.keys, + @sources.keys, @hooks_by_event.keys) + true + end + ensure + @commands = commands + @sources = sources + @hooks_by_event = hooks + end + + # Executes the plugins.rb file + # + # @param [String] name of the plugin + def load_plugin(name) + # Need to ensure before this that plugin root where the rest of gems + # are installed to be on load path to support plugin deps. Currently not + # done to avoid conflicts + path = index.plugin_path(name) + + add_to_load_path(index.load_paths(name)) + + load path.join(PLUGIN_FILE_NAME) + + @loaded_plugin_names << name + rescue RuntimeError => e + Bundler.ui.error "Failed loading plugin #{name}: #{e.message}" + raise + end + + def add_to_load_path(load_paths) + if insert_index = Bundler.rubygems.load_path_insert_index + $LOAD_PATH.insert(insert_index, *load_paths) + else + $LOAD_PATH.unshift(*load_paths) + end + end + + class << self + private :load_plugin, :register_plugin, :save_plugins, :validate_plugin!, + :add_to_load_path + end + end +end diff --git a/lib/bundler/plugin/api.rb b/lib/bundler/plugin/api.rb new file mode 100644 index 00000000000000..a2d5cbb4ac5af1 --- /dev/null +++ b/lib/bundler/plugin/api.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Bundler + # This is the interfacing class represents the API that we intend to provide + # the plugins to use. + # + # For plugins to be independent of the Bundler internals they shall limit their + # interactions to methods of this class only. This will save them from breaking + # when some internal change. + # + # Currently we are delegating the methods defined in Bundler class to + # itself. So, this class acts as a buffer. + # + # If there is some change in the Bundler class that is incompatible to its + # previous behavior or if otherwise desired, we can reimplement(or implement) + # the method to preserve compatibility. + # + # To use this, either the class can inherit this class or use it directly. + # For example of both types of use, refer the file `spec/plugins/command.rb` + # + # To use it without inheriting, you will have to create an object of this + # to use the functions (except for declaration functions like command, source, + # and hooks). + module Plugin + class API + autoload :Source, "bundler/plugin/api/source" + + # The plugins should declare that they handle a command through this helper. + # + # @param [String] command being handled by them + # @param [Class] (optional) class that handles the command. If not + # provided, the `self` class will be used. + def self.command(command, cls = self) + Plugin.add_command command, cls + end + + # The plugins should declare that they provide a installation source + # through this helper. + # + # @param [String] the source type they provide + # @param [Class] (optional) class that handles the source. If not + # provided, the `self` class will be used. + def self.source(source, cls = self) + cls.send :include, Bundler::Plugin::API::Source + Plugin.add_source source, cls + end + + def self.hook(event, &block) + Plugin.add_hook(event, &block) + end + + # The cache dir to be used by the plugins for storage + # + # @return [Pathname] path of the cache dir + def cache_dir + Plugin.cache.join("plugins") + end + + # A tmp dir to be used by plugins + # Accepts names that get concatenated as suffix + # + # @return [Pathname] object for the new directory created + def tmp(*names) + Bundler.tmp(["plugin", *names].join("-")) + end + + def method_missing(name, *args, &blk) + return Bundler.send(name, *args, &blk) if Bundler.respond_to?(name) + + return SharedHelpers.send(name, *args, &blk) if SharedHelpers.respond_to?(name) + + super + end + + def respond_to_missing?(name, include_private = false) + SharedHelpers.respond_to?(name, include_private) || + Bundler.respond_to?(name, include_private) || super + end + end + end +end diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb new file mode 100644 index 00000000000000..586477efb55b74 --- /dev/null +++ b/lib/bundler/plugin/api/source.rb @@ -0,0 +1,306 @@ +# frozen_string_literal: true + +require "uri" + +module Bundler + module Plugin + class API + # This class provides the base to build source plugins + # All the method here are required to build a source plugin (except + # `uri_hash`, `gem_install_dir`; they are helpers). + # + # Defaults for methods, where ever possible are provided which is + # expected to work. But, all source plugins have to override + # `fetch_gemspec_files` and `install`. Defaults are also not provided for + # `remote!`, `cache!` and `unlock!`. + # + # The defaults shall work for most situations but nevertheless they can + # be (preferably should be) overridden as per the plugins' needs safely + # (as long as they behave as expected). + # On overriding `initialize` you should call super first. + # + # If required plugin should override `hash`, `==` and `eql?` methods to be + # able to match objects representing same sources, but may be created in + # different situation (like form gemfile and lockfile). The default ones + # checks only for class and uri, but elaborate source plugins may need + # more comparisons (e.g. git checking on branch or tag). + # + # @!attribute [r] uri + # @return [String] the remote specified with `source` block in Gemfile + # + # @!attribute [r] options + # @return [String] options passed during initialization (either from + # lockfile or Gemfile) + # + # @!attribute [r] name + # @return [String] name that can be used to uniquely identify a source + # + # @!attribute [rw] dependency_names + # @return [Array] Names of dependencies that the source should + # try to resolve. It is not necessary to use this list intenally. This + # is present to be compatible with `Definition` and is used by + # rubygems source. + module Source + attr_reader :uri, :options, :name + attr_accessor :dependency_names + + def initialize(opts) + @options = opts + @dependency_names = [] + @uri = opts["uri"] + @type = opts["type"] + @name = opts["name"] || "#{@type} at #{@uri}" + end + + # This is used by the default `spec` method to constructs the + # Specification objects for the gems and versions that can be installed + # by this source plugin. + # + # Note: If the spec method is overridden, this function is not necessary + # + # @return [Array] paths of the gemspec files for gems that can + # be installed + def fetch_gemspec_files + [] + end + + # Options to be saved in the lockfile so that the source plugin is able + # to check out same version of gem later. + # + # There options are passed when the source plugin is created from the + # lock file. + # + # @return [Hash] + def options_to_lock + {} + end + + # Install the gem specified by the spec at appropriate path. + # `install_path` provides a sufficient default, if the source can only + # satisfy one gem, but is not binding. + # + # @return [String] post installation message (if any) + def install(spec, opts) + raise MalformattedPlugin, "Source plugins need to override the install method." + end + + # It builds extensions, generates bins and installs them for the spec + # provided. + # + # It depends on `spec.loaded_from` to get full_gem_path. The source + # plugins should set that. + # + # It should be called in `install` after the plugin is done placing the + # gem at correct install location. + # + # It also runs Gem hooks `pre_install`, `post_build` and `post_install` + # + # Note: Do not override if you don't know what you are doing. + def post_install(spec, disable_exts = false) + opts = { :env_shebang => false, :disable_extensions => disable_exts } + installer = Bundler::Source::Path::Installer.new(spec, opts) + installer.post_install + end + + # A default installation path to install a single gem. If the source + # servers multiple gems, it's not of much use and the source should one + # of its own. + def install_path + @install_path ||= + begin + base_name = File.basename(URI.parse(uri).normalize.path) + + gem_install_dir.join("#{base_name}-#{uri_hash[0..11]}") + end + end + + # Parses the gemspec files to find the specs for the gems that can be + # satisfied by the source. + # + # Few important points to keep in mind: + # - If the gems are not installed then it shall return specs for all + # the gems it can satisfy + # - If gem is installed (that is to be detected by the plugin itself) + # then it shall return at least the specs that are installed. + # - The `loaded_from` for each of the specs shall be correct (it is + # used to find the load path) + # + # @return [Bundler::Index] index containing the specs + def specs + files = fetch_gemspec_files + + Bundler::Index.build do |index| + files.each do |file| + next unless spec = Bundler.load_gemspec(file) + Bundler.rubygems.set_installed_by_version(spec) + + spec.source = self + Bundler.rubygems.validate(spec) + + index << spec + end + end + end + + # Set internal representation to fetch the gems/specs from remote. + # + # When this is called, the source should try to fetch the specs and + # install from remote path. + def remote! + end + + # Set internal representation to fetch the gems/specs from app cache. + # + # When this is called, the source should try to fetch the specs and + # install from the path provided by `app_cache_path`. + def cached! + end + + # This is called to update the spec and installation. + # + # If the source plugin is loaded from lockfile or otherwise, it shall + # refresh the cache/specs (e.g. git sources can make a fresh clone). + def unlock! + end + + # Name of directory where plugin the is expected to cache the gems when + # #cache is called. + # + # Also this name is matched against the directories in cache for pruning + # + # This is used by `app_cache_path` + def app_cache_dirname + base_name = File.basename(URI.parse(uri).normalize.path) + "#{base_name}-#{uri_hash}" + end + + # This method is called while caching to save copy of the gems that the + # source can resolve to path provided by `app_cache_app`so that they can + # be reinstalled from the cache without querying the remote (i.e. an + # alternative to remote) + # + # This is stored with the app and source plugins should try to provide + # specs and install only from this cache when `cached!` is called. + # + # This cache is different from the internal caching that can be done + # at sub paths of `cache_path` (from API). This can be though as caching + # by bundler. + def cache(spec, custom_path = nil) + new_cache_path = app_cache_path(custom_path) + + FileUtils.rm_rf(new_cache_path) + FileUtils.cp_r(install_path, new_cache_path) + FileUtils.touch(app_cache_path.join(".bundlecache")) + end + + # This shall check if two source object represent the same source. + # + # The comparison shall take place only on the attribute that can be + # inferred from the options passed from Gemfile and not on attibutes + # that are used to pin down the gem to specific version (e.g. Git + # sources should compare on branch and tag but not on commit hash) + # + # The sources objects are constructed from Gemfile as well as from + # lockfile. To converge the sources, it is necessary that they match. + # + # The same applies for `eql?` and `hash` + def ==(other) + other.is_a?(self.class) && uri == other.uri + end + + # When overriding `eql?` please preserve the behaviour as mentioned in + # docstring for `==` method. + alias_method :eql?, :== + + # When overriding `hash` please preserve the behaviour as mentioned in + # docstring for `==` method, i.e. two methods equal by above comparison + # should have same hash. + def hash + [self.class, uri].hash + end + + # A helper method, not necessary if not used internally. + def installed? + File.directory?(install_path) + end + + # The full path where the plugin should cache the gem so that it can be + # installed latter. + # + # Note: Do not override if you don't know what you are doing. + def app_cache_path(custom_path = nil) + @app_cache_path ||= Bundler.app_cache(custom_path).join(app_cache_dirname) + end + + # Used by definition. + # + # Note: Do not override if you don't know what you are doing. + def unmet_deps + specs.unmet_dependency_names + end + + # Note: Do not override if you don't know what you are doing. + def can_lock?(spec) + spec.source == self + end + + # Generates the content to be entered into the lockfile. + # Saves type and remote and also calls to `options_to_lock`. + # + # Plugin should use `options_to_lock` to save information in lockfile + # and not override this. + # + # Note: Do not override if you don't know what you are doing. + def to_lock + out = String.new("#{LockfileParser::PLUGIN}\n") + out << " remote: #{@uri}\n" + out << " type: #{@type}\n" + options_to_lock.each do |opt, value| + out << " #{opt}: #{value}\n" + end + out << " specs:\n" + end + + def to_s + "plugin source for #{options[:type]} with uri #{uri}" + end + + # Note: Do not override if you don't know what you are doing. + def include?(other) + other == self + end + + def uri_hash + SharedHelpers.digest(:SHA1).hexdigest(uri) + end + + # Note: Do not override if you don't know what you are doing. + def gem_install_dir + Bundler.install_path + end + + # It is used to obtain the full_gem_path. + # + # spec's loaded_from path is expanded against this to get full_gem_path + # + # Note: Do not override if you don't know what you are doing. + def root + Bundler.root + end + + # @private + # Returns true + def bundler_plugin_api_source? + true + end + + # @private + # This API on source might not be stable, and for now we expect plugins + # to download all specs in `#specs`, so we implement the method for + # compatibility purposes and leave it undocumented (and don't support) + # overriding it) + def double_check_for(*); end + end + end + end +end diff --git a/lib/bundler/plugin/dsl.rb b/lib/bundler/plugin/dsl.rb new file mode 100644 index 00000000000000..4bfc8437e0e656 --- /dev/null +++ b/lib/bundler/plugin/dsl.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Bundler + module Plugin + # Dsl to parse the Gemfile looking for plugins to install + class DSL < Bundler::Dsl + class PluginGemfileError < PluginError; end + alias_method :_gem, :gem # To use for plugin installation as gem + + # So that we don't have to override all there methods to dummy ones + # explicitly. + # They will be handled by method_missing + [:gemspec, :gem, :path, :install_if, :platforms, :env].each {|m| undef_method m } + + # This lists the plugins that was added automatically and not specified by + # the user. + # + # When we encounter :type attribute with a source block, we add a plugin + # by name bundler-source- to list of plugins to be installed. + # + # These plugins are optional and are not installed when there is conflict + # with any other plugin. + attr_reader :inferred_plugins + + def initialize + super + @sources = Plugin::SourceList.new + @inferred_plugins = [] # The source plugins inferred from :type + end + + def plugin(name, *args) + _gem(name, *args) + end + + def method_missing(name, *args) + raise PluginGemfileError, "Undefined local variable or method `#{name}' for Gemfile" unless Bundler::Dsl.method_defined? name + end + + def source(source, *args, &blk) + options = args.last.is_a?(Hash) ? args.pop.dup : {} + options = normalize_hash(options) + return super unless options.key?("type") + + plugin_name = "bundler-source-#{options["type"]}" + + return if @dependencies.any? {|d| d.name == plugin_name } + + plugin(plugin_name) + @inferred_plugins << plugin_name + end + end + end +end diff --git a/lib/bundler/plugin/events.rb b/lib/bundler/plugin/events.rb new file mode 100644 index 00000000000000..bc037d1af507b7 --- /dev/null +++ b/lib/bundler/plugin/events.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Bundler + module Plugin + module Events + def self.define(const, event) + const = const.to_sym.freeze + if const_defined?(const) && const_get(const) != event + raise ArgumentError, "Attempting to reassign #{const} to a different value" + end + const_set(const, event) unless const_defined?(const) + @events ||= {} + @events[event] = const + end + private_class_method :define + + def self.reset + @events.each_value do |const| + remove_const(const) + end + @events = nil + end + private_class_method :reset + + # Check if an event has been defined + # @param event [String] An event to check + # @return [Boolean] A boolean indicating if the event has been defined + def self.defined_event?(event) + @events ||= {} + @events.key?(event) + end + + # @!parse + # A hook called before each individual gem is installed + # Includes a Bundler::ParallelInstaller::SpecInstallation. + # No state, error, post_install_message will be present as nothing has installed yet + # GEM_BEFORE_INSTALL = "before-install" + define :GEM_BEFORE_INSTALL, "before-install" + + # @!parse + # A hook called after each individual gem is installed + # Includes a Bundler::ParallelInstaller::SpecInstallation. + # - If state is failed, an error will be present. + # - If state is success, a post_install_message may be present. + # GEM_AFTER_INSTALL = "after-install" + define :GEM_AFTER_INSTALL, "after-install" + + # @!parse + # A hook called before any gems install + # Includes an Array of Bundler::Dependency objects + # GEM_BEFORE_INSTALL_ALL = "before-install-all" + define :GEM_BEFORE_INSTALL_ALL, "before-install-all" + + # @!parse + # A hook called after any gems install + # Includes an Array of Bundler::Dependency objects + # GEM_AFTER_INSTALL_ALL = "after-install-all" + define :GEM_AFTER_INSTALL_ALL, "after-install-all" + end + end +end diff --git a/lib/bundler/plugin/index.rb b/lib/bundler/plugin/index.rb new file mode 100644 index 00000000000000..f09587dfda7bb5 --- /dev/null +++ b/lib/bundler/plugin/index.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +module Bundler + # Manages which plugins are installed and their sources. This also is supposed to map + # which plugin does what (currently the features are not implemented so this class is + # now a stub class). + module Plugin + class Index + class CommandConflict < PluginError + def initialize(plugin, commands) + msg = "Command(s) `#{commands.join("`, `")}` declared by #{plugin} are already registered." + super msg + end + end + + class SourceConflict < PluginError + def initialize(plugin, sources) + msg = "Source(s) `#{sources.join("`, `")}` declared by #{plugin} are already registered." + super msg + end + end + + attr_reader :commands + + def initialize + @plugin_paths = {} + @commands = {} + @sources = {} + @hooks = {} + @load_paths = {} + + begin + load_index(global_index_file, true) + rescue GenericSystemCallError + # no need to fail when on a read-only FS, for example + nil + end + load_index(local_index_file) if SharedHelpers.in_bundle? + end + + # This function is to be called when a new plugin is installed. This + # function shall add the functions of the plugin to existing maps and also + # the name to source location. + # + # @param [String] name of the plugin to be registered + # @param [String] path where the plugin is installed + # @param [Array] load_paths for the plugin + # @param [Array] commands that are handled by the plugin + # @param [Array] sources that are handled by the plugin + def register_plugin(name, path, load_paths, commands, sources, hooks) + old_commands = @commands.dup + + common = commands & @commands.keys + raise CommandConflict.new(name, common) unless common.empty? + commands.each {|c| @commands[c] = name } + + common = sources & @sources.keys + raise SourceConflict.new(name, common) unless common.empty? + sources.each {|k| @sources[k] = name } + + hooks.each {|e| (@hooks[e] ||= []) << name } + + @plugin_paths[name] = path + @load_paths[name] = load_paths + save_index + rescue StandardError + @commands = old_commands + raise + end + + # Path of default index file + def index_file + Plugin.root.join("index") + end + + # Path where the global index file is stored + def global_index_file + Plugin.global_root.join("index") + end + + # Path where the local index file is stored + def local_index_file + Plugin.local_root.join("index") + end + + def plugin_path(name) + Pathname.new @plugin_paths[name] + end + + def load_paths(name) + @load_paths[name] + end + + # Fetch the name of plugin handling the command + def command_plugin(command) + @commands[command] + end + + def installed?(name) + @plugin_paths[name] + end + + def source?(source) + @sources.key? source + end + + def source_plugin(name) + @sources[name] + end + + # Returns the list of plugin names handling the passed event + def hook_plugins(event) + @hooks[event] || [] + end + + private + + # Reads the index file from the directory and initializes the instance + # variables. + # + # It skips the sources if the second param is true + # @param [Pathname] index file path + # @param [Boolean] is the index file global index + def load_index(index_file, global = false) + SharedHelpers.filesystem_access(index_file, :read) do |index_f| + valid_file = index_f && index_f.exist? && !index_f.size.zero? + break unless valid_file + + data = index_f.read + + require "bundler/yaml_serializer" + index = YAMLSerializer.load(data) + + @commands.merge!(index["commands"]) + @hooks.merge!(index["hooks"]) + @load_paths.merge!(index["load_paths"]) + @plugin_paths.merge!(index["plugin_paths"]) + @sources.merge!(index["sources"]) unless global + end + end + + # Should be called when any of the instance variables change. Stores the + # instance variables in YAML format. (The instance variables are supposed + # to be only String key value pairs) + def save_index + index = { + "commands" => @commands, + "hooks" => @hooks, + "load_paths" => @load_paths, + "plugin_paths" => @plugin_paths, + "sources" => @sources, + } + + require "bundler/yaml_serializer" + SharedHelpers.filesystem_access(index_file) do |index_f| + FileUtils.mkdir_p(index_f.dirname) + File.open(index_f, "w") {|f| f.puts YAMLSerializer.dump(index) } + end + end + end + end +end diff --git a/lib/bundler/plugin/installer.rb b/lib/bundler/plugin/installer.rb new file mode 100644 index 00000000000000..5379c38979c97d --- /dev/null +++ b/lib/bundler/plugin/installer.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module Bundler + # Handles the installation of plugin in appropriate directories. + # + # This class is supposed to be wrapper over the existing gem installation infra + # but currently it itself handles everything as the Source's subclasses (e.g. Source::RubyGems) + # are heavily dependent on the Gemfile. + module Plugin + class Installer + autoload :Rubygems, "bundler/plugin/installer/rubygems" + autoload :Git, "bundler/plugin/installer/git" + + def install(names, options) + version = options[:version] || [">= 0"] + Bundler.settings.temporary(:lockfile_uses_separate_rubygems_sources => false, :disable_multisource => false) do + if options[:git] + install_git(names, version, options) + else + sources = options[:source] || Bundler.rubygems.sources + install_rubygems(names, version, sources) + end + end + end + + # Installs the plugin from Definition object created by limited parsing of + # Gemfile searching for plugins to be installed + # + # @param [Definition] definition object + # @return [Hash] map of names to their specs they are installed with + def install_definition(definition) + def definition.lock(*); end + definition.resolve_remotely! + specs = definition.specs + + install_from_specs specs + end + + private + + def install_git(names, version, options) + uri = options.delete(:git) + options["uri"] = uri + + source_list = SourceList.new + source_list.add_git_source(options) + + # To support both sources + if options[:source] + source_list.add_rubygems_source("remotes" => options[:source]) + end + + deps = names.map {|name| Dependency.new name, version } + + definition = Definition.new(nil, deps, source_list, true) + install_definition(definition) + end + + # Installs the plugin from rubygems source and returns the path where the + # plugin was installed + # + # @param [String] name of the plugin gem to search in the source + # @param [Array] version of the gem to install + # @param [String, Array] source(s) to resolve the gem + # + # @return [Hash] map of names to the specs of plugins installed + def install_rubygems(names, version, sources) + deps = names.map {|name| Dependency.new name, version } + + source_list = SourceList.new + source_list.add_rubygems_source("remotes" => sources) + + definition = Definition.new(nil, deps, source_list, true) + install_definition(definition) + end + + # Installs the plugins and deps from the provided specs and returns map of + # gems to their paths + # + # @param specs to install + # + # @return [Hash] map of names to the specs + def install_from_specs(specs) + paths = {} + + specs.each do |spec| + spec.source.install spec + + paths[spec.name] = spec + end + + paths + end + end + end +end diff --git a/lib/bundler/plugin/installer/git.rb b/lib/bundler/plugin/installer/git.rb new file mode 100644 index 00000000000000..fbb6c5e40e6f3e --- /dev/null +++ b/lib/bundler/plugin/installer/git.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Bundler + module Plugin + class Installer + class Git < Bundler::Source::Git + def cache_path + @cache_path ||= begin + git_scope = "#{base_name}-#{uri_hash}" + + Plugin.cache.join("bundler", "git", git_scope) + end + end + + def install_path + @install_path ||= begin + git_scope = "#{base_name}-#{shortref_for_path(revision)}" + + Plugin.root.join("bundler", "gems", git_scope) + end + end + + def version_message(spec) + "#{spec.name} #{spec.version}" + end + + def root + Plugin.root + end + + def generate_bin(spec, disable_extensions = false) + # Need to find a way without code duplication + # For now, we can ignore this + end + end + end + end +end diff --git a/lib/bundler/plugin/installer/rubygems.rb b/lib/bundler/plugin/installer/rubygems.rb new file mode 100644 index 00000000000000..7ae74fa93b3bf5 --- /dev/null +++ b/lib/bundler/plugin/installer/rubygems.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Bundler + module Plugin + class Installer + class Rubygems < Bundler::Source::Rubygems + def version_message(spec) + "#{spec.name} #{spec.version}" + end + + private + + def requires_sudo? + false # Will change on implementation of project level plugins + end + + def rubygems_dir + Plugin.root + end + + def cache_path + Plugin.cache + end + end + end + end +end diff --git a/lib/bundler/plugin/source_list.rb b/lib/bundler/plugin/source_list.rb new file mode 100644 index 00000000000000..f0e212205ff7a8 --- /dev/null +++ b/lib/bundler/plugin/source_list.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Bundler + # SourceList object to be used while parsing the Gemfile, setting the + # approptiate options to be used with Source classes for plugin installation + module Plugin + class SourceList < Bundler::SourceList + def add_git_source(options = {}) + add_source_to_list Plugin::Installer::Git.new(options), git_sources + end + + def add_rubygems_source(options = {}) + add_source_to_list Plugin::Installer::Rubygems.new(options), @rubygems_sources + end + + def all_sources + path_sources + git_sources + rubygems_sources + [metadata_source] + end + + private + + def rubygems_aggregate_class + Plugin::Installer::Rubygems + end + end + end +end diff --git a/lib/bundler/process_lock.rb b/lib/bundler/process_lock.rb new file mode 100644 index 00000000000000..cba4fcdba5f66f --- /dev/null +++ b/lib/bundler/process_lock.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Bundler + class ProcessLock + def self.lock(bundle_path = Bundler.bundle_path) + lock_file_path = File.join(bundle_path, "bundler.lock") + has_lock = false + + File.open(lock_file_path, "w") do |f| + f.flock(File::LOCK_EX) + has_lock = true + yield + f.flock(File::LOCK_UN) + end + rescue Errno::EACCES, Errno::ENOLCK, *[SharedHelpers.const_get_safely(:ENOTSUP, Errno)].compact + # In the case the user does not have access to + # create the lock file or is using NFS where + # locks are not available we skip locking. + yield + ensure + FileUtils.rm_f(lock_file_path) if has_lock + end + end +end diff --git a/lib/bundler/psyched_yaml.rb b/lib/bundler/psyched_yaml.rb new file mode 100644 index 00000000000000..e654416a5a296f --- /dev/null +++ b/lib/bundler/psyched_yaml.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Psych could be a gem, so try to ask for it +begin + gem "psych" +rescue LoadError +end if defined?(gem) + +# Psych could be in the stdlib +# but it's too late if Syck is already loaded +begin + require "psych" unless defined?(Syck) +rescue LoadError + # Apparently Psych wasn't available. Oh well. +end + +# At least load the YAML stdlib, whatever that may be +require "yaml" unless defined?(YAML.dump) + +module Bundler + # On encountering invalid YAML, + # Psych raises Psych::SyntaxError + if defined?(::Psych::SyntaxError) + YamlLibrarySyntaxError = ::Psych::SyntaxError + else # Syck raises ArgumentError + YamlLibrarySyntaxError = ::ArgumentError + end +end + +require "bundler/deprecate" +begin + Bundler::Deprecate.skip_during do + require "rubygems/safe_yaml" + end +rescue LoadError + # it's OK if the file isn't there +end diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb new file mode 100644 index 00000000000000..23e1234330773f --- /dev/null +++ b/lib/bundler/remote_specification.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require "uri" + +module Bundler + # Represents a lazily loaded gem specification, where the full specification + # is on the source server in rubygems' "quick" index. The proxy object is to + # be seeded with what we're given from the source's abbreviated index - the + # full specification will only be fetched when necessary. + class RemoteSpecification + include MatchPlatform + include Comparable + + attr_reader :name, :version, :platform + attr_writer :dependencies + attr_accessor :source, :remote + + def initialize(name, version, platform, spec_fetcher) + @name = name + @version = Gem::Version.create version + @platform = platform + @spec_fetcher = spec_fetcher + @dependencies = nil + end + + # Needed before installs, since the arch matters then and quick + # specs don't bother to include the arch in the platform string + def fetch_platform + @platform = _remote_specification.platform + end + + def full_name + if platform == Gem::Platform::RUBY || platform.nil? + "#{@name}-#{@version}" + else + "#{@name}-#{@version}-#{platform}" + end + end + + # Compare this specification against another object. Using sort_obj + # is compatible with Gem::Specification and other Bundler or RubyGems + # objects. Otherwise, use the default Object comparison. + def <=>(other) + if other.respond_to?(:sort_obj) + sort_obj <=> other.sort_obj + else + super + end + end + + # Because Rubyforge cannot be trusted to provide valid specifications + # once the remote gem is downloaded, the backend specification will + # be swapped out. + def __swap__(spec) + SharedHelpers.ensure_same_dependencies(self, dependencies, spec.dependencies) + @_remote_specification = spec + end + + # Create a delegate used for sorting. This strategy is copied from + # RubyGems 2.23 and ensures that Bundler's specifications can be + # compared and sorted with RubyGems' own specifications. + # + # @see #<=> + # @see Gem::Specification#sort_obj + # + # @return [Array] an object you can use to compare and sort this + # specification against other specifications + def sort_obj + [@name, @version, @platform == Gem::Platform::RUBY ? -1 : 1] + end + + def to_s + "#<#{self.class} name=#{name} version=#{version} platform=#{platform}>" + end + + def dependencies + @dependencies ||= begin + deps = method_missing(:dependencies) + + # allow us to handle when the specs dependencies are an array of array of string + # see https://github.com/bundler/bundler/issues/5797 + deps = deps.map {|d| d.is_a?(Gem::Dependency) ? d : Gem::Dependency.new(*d) } + + deps + end + end + + def git_version + return unless loaded_from && source.is_a?(Bundler::Source::Git) + " #{source.revision[0..6]}" + end + + private + + def to_ary + nil + end + + def _remote_specification + @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @platform]) + @_remote_specification || raise(GemspecError, "Gemspec data for #{full_name} was" \ + " missing from the server! Try installing with `--full-index` as a workaround.") + end + + def method_missing(method, *args, &blk) + _remote_specification.send(method, *args, &blk) + end + + def respond_to?(method, include_all = false) + super || _remote_specification.respond_to?(method, include_all) + end + public :respond_to? + end +end diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb new file mode 100644 index 00000000000000..545b4cc88ace86 --- /dev/null +++ b/lib/bundler/resolver.rb @@ -0,0 +1,373 @@ +# frozen_string_literal: true + +module Bundler + class Resolver + require "bundler/vendored_molinillo" + require "bundler/resolver/spec_group" + + # Figures out the best possible configuration of gems that satisfies + # the list of passed dependencies and any child dependencies without + # causing any gem activation errors. + # + # ==== Parameters + # *dependencies:: The list of dependencies to resolve + # + # ==== Returns + # ,nil:: If the list of dependencies can be resolved, a + # collection of gemspecs is returned. Otherwise, nil is returned. + def self.resolve(requirements, index, source_requirements = {}, base = [], gem_version_promoter = GemVersionPromoter.new, additional_base_requirements = [], platforms = nil) + platforms = Set.new(platforms) if platforms + base = SpecSet.new(base) unless base.is_a?(SpecSet) + resolver = new(index, source_requirements, base, gem_version_promoter, additional_base_requirements, platforms) + result = resolver.start(requirements) + SpecSet.new(result) + end + + def initialize(index, source_requirements, base, gem_version_promoter, additional_base_requirements, platforms) + @index = index + @source_requirements = source_requirements + @base = base + @resolver = Molinillo::Resolver.new(self, self) + @search_for = {} + @base_dg = Molinillo::DependencyGraph.new + @base.each do |ls| + dep = Dependency.new(ls.name, ls.version) + @base_dg.add_vertex(ls.name, DepProxy.new(dep, ls.platform), true) + end + additional_base_requirements.each {|d| @base_dg.add_vertex(d.name, d) } + @platforms = platforms + @gem_version_promoter = gem_version_promoter + @allow_bundler_dependency_conflicts = Bundler.feature_flag.allow_bundler_dependency_conflicts? + @lockfile_uses_separate_rubygems_sources = Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + @use_gvp = Bundler.feature_flag.use_gem_version_promoter_for_major_updates? || !@gem_version_promoter.major? + end + + def start(requirements) + @gem_version_promoter.prerelease_specified = @prerelease_specified = {} + requirements.each {|dep| @prerelease_specified[dep.name] ||= dep.prerelease? } + + verify_gemfile_dependencies_are_found!(requirements) + dg = @resolver.resolve(requirements, @base_dg) + dg.map(&:payload). + reject {|sg| sg.name.end_with?("\0") }. + map(&:to_specs).flatten + rescue Molinillo::VersionConflict => e + message = version_conflict_message(e) + raise VersionConflict.new(e.conflicts.keys.uniq, message) + rescue Molinillo::CircularDependencyError => e + names = e.dependencies.sort_by(&:name).map {|d| "gem '#{d.name}'" } + raise CyclicDependencyError, "Your bundle requires gems that depend" \ + " on each other, creating an infinite loop. Please remove" \ + " #{names.count > 1 ? "either " : ""}#{names.join(" or ")}" \ + " and try again." + end + + include Molinillo::UI + + # Conveys debug information to the user. + # + # @param [Integer] depth the current depth of the resolution process. + # @return [void] + def debug(depth = 0) + return unless debug? + debug_info = yield + debug_info = debug_info.inspect unless debug_info.is_a?(String) + STDERR.puts debug_info.split("\n").map {|s| " " * depth + s } + end + + def debug? + return @debug_mode if defined?(@debug_mode) + @debug_mode = ENV["DEBUG_RESOLVER"] || ENV["DEBUG_RESOLVER_TREE"] || false + end + + def before_resolution + Bundler.ui.info "Resolving dependencies...", debug? + end + + def after_resolution + Bundler.ui.info "" + end + + def indicate_progress + Bundler.ui.info ".", false unless debug? + end + + include Molinillo::SpecificationProvider + + def dependencies_for(specification) + specification.dependencies_for_activated_platforms + end + + def search_for(dependency) + platform = dependency.__platform + dependency = dependency.dep unless dependency.is_a? Gem::Dependency + search = @search_for[dependency] ||= begin + index = index_for(dependency) + results = index.search(dependency, @base[dependency.name]) + + if vertex = @base_dg.vertex_named(dependency.name) + locked_requirement = vertex.payload.requirement + end + + if !@prerelease_specified[dependency.name] && (!@use_gvp || locked_requirement.nil?) + # Move prereleases to the beginning of the list, so they're considered + # last during resolution. + pre, results = results.partition {|spec| spec.version.prerelease? } + results = pre + results + end + + spec_groups = if results.any? + nested = [] + results.each do |spec| + version, specs = nested.last + if version == spec.version + specs << spec + else + nested << [spec.version, [spec]] + end + end + nested.reduce([]) do |groups, (version, specs)| + next groups if locked_requirement && !locked_requirement.satisfied_by?(version) + spec_group = SpecGroup.new(specs) + spec_group.ignores_bundler_dependencies = @allow_bundler_dependency_conflicts + groups << spec_group + end + else + [] + end + # GVP handles major itself, but it's still a bit risky to trust it with it + # until we get it settled with new behavior. For 2.x it can take over all cases. + if !@use_gvp + spec_groups + else + @gem_version_promoter.sort_versions(dependency, spec_groups) + end + end + search.select {|sg| sg.for?(platform) }.each {|sg| sg.activate_platform!(platform) } + end + + def index_for(dependency) + source = @source_requirements[dependency.name] + if source + source.specs + elsif @lockfile_uses_separate_rubygems_sources + Index.build do |idx| + if dependency.all_sources + dependency.all_sources.each {|s| idx.add_source(s.specs) if s } + else + idx.add_source @source_requirements[:default].specs + end + end + else + @index + end + end + + def name_for(dependency) + dependency.name + end + + def name_for_explicit_dependency_source + Bundler.default_gemfile.basename.to_s + rescue + "Gemfile" + end + + def name_for_locking_dependency_source + Bundler.default_lockfile.basename.to_s + rescue + "Gemfile.lock" + end + + def requirement_satisfied_by?(requirement, activated, spec) + return false unless requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec) + spec.activate_platform!(requirement.__platform) if !@platforms || @platforms.include?(requirement.__platform) + true + end + + def relevant_sources_for_vertex(vertex) + if vertex.root? + [@source_requirements[vertex.name]] + elsif @lockfile_uses_separate_rubygems_sources + vertex.recursive_predecessors.map do |v| + @source_requirements[v.name] + end << @source_requirements[:default] + end + end + + def sort_dependencies(dependencies, activated, conflicts) + dependencies.sort_by do |dependency| + dependency.all_sources = relevant_sources_for_vertex(activated.vertex_named(dependency.name)) + name = name_for(dependency) + vertex = activated.vertex_named(name) + [ + @base_dg.vertex_named(name) ? 0 : 1, + vertex.payload ? 0 : 1, + vertex.root? ? 0 : 1, + amount_constrained(dependency), + conflicts[name] ? 0 : 1, + vertex.payload ? 0 : search_for(dependency).count, + self.class.platform_sort_key(dependency.__platform), + ] + end + end + + # Sort platforms from most general to most specific + def self.sort_platforms(platforms) + platforms.sort_by do |platform| + platform_sort_key(platform) + end + end + + def self.platform_sort_key(platform) + return ["", "", ""] if Gem::Platform::RUBY == platform + platform.to_a.map {|part| part || "" } + end + + private + + # returns an integer \in (-\infty, 0] + # a number closer to 0 means the dependency is less constraining + # + # dependencies w/ 0 or 1 possibilities (ignoring version requirements) + # are given very negative values, so they _always_ sort first, + # before dependencies that are unconstrained + def amount_constrained(dependency) + @amount_constrained ||= {} + @amount_constrained[dependency.name] ||= begin + if (base = @base[dependency.name]) && !base.empty? + dependency.requirement.satisfied_by?(base.first.version) ? 0 : 1 + else + all = index_for(dependency).search(dependency.name).size + + if all <= 1 + all - 1_000_000 + else + search = search_for(dependency) + search = @prerelease_specified[dependency.name] ? search.count : search.count {|s| !s.version.prerelease? } + search - all + end + end + end + end + + def verify_gemfile_dependencies_are_found!(requirements) + requirements.each do |requirement| + name = requirement.name + next if name == "bundler" + next unless search_for(requirement).empty? + + cache_message = begin + " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? + rescue GemfileNotFound + nil + end + + if (base = @base[name]) && !base.empty? + version = base.first.version + message = "You have requested:\n" \ + " #{name} #{requirement.requirement}\n\n" \ + "The bundle currently has #{name} locked at #{version}.\n" \ + "Try running `bundle update #{name}`\n\n" \ + "If you are updating multiple gems in your Gemfile at once,\n" \ + "try passing them all to `bundle update`" + elsif source = @source_requirements[name] + specs = source.specs[name] + versions_with_platforms = specs.map {|s| [s.version, s.platform] } + message = String.new("Could not find gem '#{SharedHelpers.pretty_dependency(requirement)}' in #{source}#{cache_message}.\n") + message << if versions_with_platforms.any? + "The source contains '#{name}' at: #{formatted_versions_with_platforms(versions_with_platforms)}" + else + "The source does not contain any versions of '#{name}'" + end + else + message = "Could not find gem '#{requirement}' in any of the gem sources " \ + "listed in your Gemfile#{cache_message}." + end + raise GemNotFound, message + end + end + + def formatted_versions_with_platforms(versions_with_platforms) + version_platform_strs = versions_with_platforms.map do |vwp| + version = vwp.first + platform = vwp.last + version_platform_str = String.new(version.to_s) + version_platform_str << " #{platform}" unless platform.nil? || platform == Gem::Platform::RUBY + version_platform_str + end + version_platform_strs.join(", ") + end + + def version_conflict_message(e) + e.message_with_trees( + :solver_name => "Bundler", + :possibility_type => "gem", + :reduce_trees => lambda do |trees| + # called first, because we want to reduce the amount of work required to find maximal empty sets + trees = trees.uniq {|t| t.flatten.map {|dep| [dep.name, dep.requirement] } } + + # bail out if tree size is too big for Array#combination to make any sense + return trees if trees.size > 15 + maximal = 1.upto(trees.size).map do |size| + trees.map(&:last).flatten(1).combination(size).to_a + end.flatten(1).select do |deps| + Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement))) + end.min_by(&:size) + trees.reject! {|t| !maximal.include?(t.last) } if maximal + + trees = trees.sort_by {|t| t.flatten.map(&:to_s) } + trees.uniq! {|t| t.flatten.map {|dep| [dep.name, dep.requirement] } } + + trees.sort_by {|t| t.reverse.map(&:name) } + end, + :printable_requirement => lambda {|req| SharedHelpers.pretty_dependency(req) }, + :additional_message_for_conflict => lambda do |o, name, conflict| + if name == "bundler" + o << %(\n Current Bundler version:\n bundler (#{Bundler::VERSION})) + other_bundler_required = !conflict.requirement.requirement.satisfied_by?(Gem::Version.new Bundler::VERSION) + end + + if name == "bundler" && other_bundler_required + o << "\n" + o << "This Gemfile requires a different version of Bundler.\n" + o << "Perhaps you need to update Bundler by running `gem install bundler`?\n" + end + if conflict.locked_requirement + o << "\n" + o << %(Running `bundle update` will rebuild your snapshot from scratch, using only\n) + o << %(the gems in your Gemfile, which may resolve the conflict.\n) + elsif !conflict.existing + o << "\n" + + relevant_sources = if conflict.requirement.source + [conflict.requirement.source] + elsif conflict.requirement.all_sources + conflict.requirement.all_sources + elsif @lockfile_uses_separate_rubygems_sources + # every conflict should have an explicit group of sources when we + # enforce strict pinning + raise "no source set for #{conflict}" + else + [] + end.compact.map(&:to_s).uniq.sort + + o << "Could not find gem '#{SharedHelpers.pretty_dependency(conflict.requirement)}'" + if conflict.requirement_trees.first.size > 1 + o << ", which is required by " + o << "gem '#{SharedHelpers.pretty_dependency(conflict.requirement_trees.first[-2])}'," + end + o << " " + + o << if relevant_sources.empty? + "in any of the sources.\n" + else + "in any of the relevant sources:\n #{relevant_sources * "\n "}\n" + end + end + end, + :version_for_spec => lambda {|spec| spec.version } + ) + end + end +end diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb new file mode 100644 index 00000000000000..34d043aed7fcac --- /dev/null +++ b/lib/bundler/resolver/spec_group.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Bundler + class Resolver + class SpecGroup + include GemHelpers + + attr_accessor :name, :version, :source + attr_accessor :ignores_bundler_dependencies + + def initialize(all_specs) + raise ArgumentError, "cannot initialize with an empty value" unless exemplary_spec = all_specs.first + @name = exemplary_spec.name + @version = exemplary_spec.version + @source = exemplary_spec.source + + @activated_platforms = [] + @dependencies = nil + @specs = Hash.new do |specs, platform| + specs[platform] = select_best_platform_match(all_specs, platform) + end + @ignores_bundler_dependencies = true + end + + def to_specs + @activated_platforms.map do |p| + next unless s = @specs[p] + lazy_spec = LazySpecification.new(name, version, s.platform, source) + lazy_spec.dependencies.replace s.dependencies + lazy_spec + end.compact + end + + def activate_platform!(platform) + return unless for?(platform) + return if @activated_platforms.include?(platform) + @activated_platforms << platform + end + + def for?(platform) + spec = @specs[platform] + !spec.nil? + end + + def to_s + @to_s ||= "#{name} (#{version})" + end + + def dependencies_for_activated_platforms + dependencies = @activated_platforms.map {|p| __dependencies[p] } + metadata_dependencies = @activated_platforms.map do |platform| + metadata_dependencies(@specs[platform], platform) + end + dependencies.concat(metadata_dependencies).flatten + end + + def ==(other) + return unless other.is_a?(SpecGroup) + name == other.name && + version == other.version && + source == other.source + end + + def eql?(other) + name.eql?(other.name) && + version.eql?(other.version) && + source.eql?(other.source) + end + + def hash + to_s.hash ^ source.hash + end + + private + + def __dependencies + @dependencies = Hash.new do |dependencies, platform| + dependencies[platform] = [] + if spec = @specs[platform] + spec.dependencies.each do |dep| + next if dep.type == :development + next if @ignores_bundler_dependencies && dep.name == "bundler".freeze + dependencies[platform] << DepProxy.new(dep, platform) + end + end + dependencies[platform] + end + end + + def metadata_dependencies(spec, platform) + return [] unless spec + # Only allow endpoint specifications since they won't hit the network to + # fetch the full gemspec when calling required_ruby_version + return [] if !spec.is_a?(EndpointSpecification) && !spec.is_a?(Gem::Specification) + dependencies = [] + if !spec.required_ruby_version.nil? && !spec.required_ruby_version.none? + dependencies << DepProxy.new(Gem::Dependency.new("ruby\0", spec.required_ruby_version), platform) + end + if !spec.required_rubygems_version.nil? && !spec.required_rubygems_version.none? + dependencies << DepProxy.new(Gem::Dependency.new("rubygems\0", spec.required_rubygems_version), platform) + end + dependencies + end + end + end +end diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb new file mode 100644 index 00000000000000..244606dcc93d37 --- /dev/null +++ b/lib/bundler/retry.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Bundler + # General purpose class for retrying code that may fail + class Retry + attr_accessor :name, :total_runs, :current_run + + class << self + def default_attempts + default_retries + 1 + end + alias_method :attempts, :default_attempts + + def default_retries + Bundler.settings[:retry] + end + end + + def initialize(name, exceptions = nil, retries = self.class.default_retries) + @name = name + @retries = retries + @exceptions = Array(exceptions) || [] + @total_runs = @retries + 1 # will run once, then upto attempts.times + end + + def attempt(&block) + @current_run = 0 + @failed = false + @error = nil + run(&block) while keep_trying? + @result + end + alias_method :attempts, :attempt + + private + + def run(&block) + @failed = false + @current_run += 1 + @result = block.call + rescue => e + fail_attempt(e) + end + + def fail_attempt(e) + @failed = true + if last_attempt? || @exceptions.any? {|k| e.is_a?(k) } + Bundler.ui.info "" unless Bundler.ui.debug? + raise e + end + return true unless name + Bundler.ui.info "" unless Bundler.ui.debug? # Add new line incase dots preceded this + Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", Bundler.ui.debug? + end + + def keep_trying? + return true if current_run.zero? + return false if last_attempt? + return true if @failed + end + + def last_attempt? + current_run >= total_runs + end + end +end diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb new file mode 100644 index 00000000000000..f6ba220cd55904 --- /dev/null +++ b/lib/bundler/ruby_dsl.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Bundler + module RubyDsl + def ruby(*ruby_version) + options = ruby_version.last.is_a?(Hash) ? ruby_version.pop : {} + ruby_version.flatten! + raise GemfileError, "Please define :engine_version" if options[:engine] && options[:engine_version].nil? + raise GemfileError, "Please define :engine" if options[:engine_version] && options[:engine].nil? + + if options[:engine] == "ruby" && options[:engine_version] && + ruby_version != Array(options[:engine_version]) + raise GemfileEvalError, "ruby_version must match the :engine_version for MRI" + end + @ruby_version = RubyVersion.new(ruby_version, options[:patchlevel], options[:engine], options[:engine_version]) + end + end +end diff --git a/lib/bundler/ruby_version.rb b/lib/bundler/ruby_version.rb new file mode 100644 index 00000000000000..e6c31a94c917cb --- /dev/null +++ b/lib/bundler/ruby_version.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +module Bundler + class RubyVersion + attr_reader :versions, + :patchlevel, + :engine, + :engine_versions, + :gem_version, + :engine_gem_version + + def initialize(versions, patchlevel, engine, engine_version) + # The parameters to this method must satisfy the + # following constraints, which are verified in + # the DSL: + # + # * If an engine is specified, an engine version + # must also be specified + # * If an engine version is specified, an engine + # must also be specified + # * If the engine is "ruby", the engine version + # must not be specified, or the engine version + # specified must match the version. + + @versions = Array(versions).map do |v| + op, v = Gem::Requirement.parse(v) + op == "=" ? v.to_s : "#{op} #{v}" + end + + @gem_version = Gem::Requirement.create(@versions.first).requirements.first.last + @input_engine = engine && engine.to_s + @engine = engine && engine.to_s || "ruby" + @engine_versions = (engine_version && Array(engine_version)) || @versions + @engine_gem_version = Gem::Requirement.create(@engine_versions.first).requirements.first.last + @patchlevel = patchlevel + end + + def to_s(versions = self.versions) + output = String.new("ruby #{versions_string(versions)}") + output << "p#{patchlevel}" if patchlevel + output << " (#{engine} #{versions_string(engine_versions)})" unless engine == "ruby" + + output + end + + # @private + PATTERN = / + ruby\s + ([\d.]+) # ruby version + (?:p(-?\d+))? # optional patchlevel + (?:\s\((\S+)\s(.+)\))? # optional engine info + /xo + + # Returns a RubyVersion from the given string. + # @param [String] the version string to match. + # @return [RubyVersion,Nil] The version if the string is a valid RubyVersion + # description, and nil otherwise. + def self.from_string(string) + new($1, $2, $3, $4) if string =~ PATTERN + end + + def single_version_string + to_s(gem_version) + end + + def ==(other) + versions == other.versions && + engine == other.engine && + engine_versions == other.engine_versions && + patchlevel == other.patchlevel + end + + def host + @host ||= [ + RbConfig::CONFIG["host_cpu"], + RbConfig::CONFIG["host_vendor"], + RbConfig::CONFIG["host_os"] + ].join("-") + end + + # Returns a tuple of these things: + # [diff, this, other] + # The priority of attributes are + # 1. engine + # 2. ruby_version + # 3. engine_version + def diff(other) + raise ArgumentError, "Can only diff with a RubyVersion, not a #{other.class}" unless other.is_a?(RubyVersion) + if engine != other.engine && @input_engine + [:engine, engine, other.engine] + elsif versions.empty? || !matches?(versions, other.gem_version) + [:version, versions_string(versions), versions_string(other.versions)] + elsif @input_engine && !matches?(engine_versions, other.engine_gem_version) + [:engine_version, versions_string(engine_versions), versions_string(other.engine_versions)] + elsif patchlevel && (!patchlevel.is_a?(String) || !other.patchlevel.is_a?(String) || !matches?(patchlevel, other.patchlevel)) + [:patchlevel, patchlevel, other.patchlevel] + end + end + + def versions_string(versions) + Array(versions).join(", ") + end + + def self.system + ruby_engine = if defined?(RUBY_ENGINE) && !RUBY_ENGINE.nil? + RUBY_ENGINE.dup + else + # not defined in ruby 1.8.7 + "ruby" + end + # :sob: mocking RUBY_VERSION breaks stuff on 1.8.7 + ruby_version = ENV.fetch("BUNDLER_SPEC_RUBY_VERSION") { RUBY_VERSION }.dup + ruby_engine_version = case ruby_engine + when "ruby" + ruby_version + when "rbx" + Rubinius::VERSION.dup + when "jruby" + JRUBY_VERSION.dup + else + RUBY_ENGINE_VERSION.dup + end + patchlevel = RUBY_PATCHLEVEL.to_s + + @ruby_version ||= RubyVersion.new(ruby_version, patchlevel, ruby_engine, ruby_engine_version) + end + + def to_gem_version_with_patchlevel + @gem_version_with_patch ||= begin + Gem::Version.create("#{@gem_version}.#{@patchlevel}") + rescue ArgumentError + @gem_version + end + end + + def exact? + return @exact if defined?(@exact) + @exact = versions.all? {|v| Gem::Requirement.create(v).exact? } + end + + private + + def matches?(requirements, version) + # Handles RUBY_PATCHLEVEL of -1 for instances like ruby-head + return requirements == version if requirements.to_s == "-1" || version.to_s == "-1" + + Array(requirements).all? do |requirement| + Gem::Requirement.create(requirement).satisfied_by?(Gem::Version.create(version)) + end + end + end +end diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb new file mode 100644 index 00000000000000..e9f0eac3558513 --- /dev/null +++ b/lib/bundler/rubygems_ext.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require "pathname" + +if defined?(Gem::QuickLoader) + # Gem Prelude makes me a sad panda :'( + Gem::QuickLoader.load_full_rubygems_library +end + +require "rubygems" +require "rubygems/specification" + +begin + # Possible use in Gem::Specification#source below and require + # shouldn't be deferred. + require "rubygems/source" +rescue LoadError + # Not available before RubyGems 2.0.0, ignore + nil +end + +require "bundler/match_platform" + +module Gem + @loaded_stacks = Hash.new {|h, k| h[k] = [] } + + class Specification + attr_accessor :remote, :location, :relative_loaded_from + + if instance_methods(false).map(&:to_sym).include?(:source) + remove_method :source + attr_writer :source + def source + (defined?(@source) && @source) || Gem::Source::Installed.new + end + else + attr_accessor :source + end + + alias_method :rg_full_gem_path, :full_gem_path + alias_method :rg_loaded_from, :loaded_from + + attr_writer :full_gem_path unless instance_methods.include?(:full_gem_path=) + + def full_gem_path + # this cannot check source.is_a?(Bundler::Plugin::API::Source) + # because that _could_ trip the autoload, and if there are unresolved + # gems at that time, this method could be called inside another require, + # thus raising with that constant being undefined. Better to check a method + if source.respond_to?(:path) || (source.respond_to?(:bundler_plugin_api_source?) && source.bundler_plugin_api_source?) + Pathname.new(loaded_from).dirname.expand_path(source.root).to_s.untaint + else + rg_full_gem_path + end + end + + def loaded_from + if relative_loaded_from + source.path.join(relative_loaded_from).to_s + else + rg_loaded_from + end + end + + def load_paths + return full_require_paths if respond_to?(:full_require_paths) + + require_paths.map do |require_path| + if require_path.include?(full_gem_path) + require_path + else + File.join(full_gem_path, require_path) + end + end + end + + if method_defined?(:extension_dir) + alias_method :rg_extension_dir, :extension_dir + def extension_dir + @bundler_extension_dir ||= if source.respond_to?(:extension_dir_name) + File.expand_path(File.join(extensions_dir, source.extension_dir_name)) + else + rg_extension_dir + end + end + end + + # RubyGems 1.8+ used only. + methods = instance_methods(false) + gem_dir = methods.first.is_a?(String) ? "gem_dir" : :gem_dir + remove_method :gem_dir if methods.include?(gem_dir) + def gem_dir + full_gem_path + end + + def groups + @groups ||= [] + end + + def git_version + return unless loaded_from && source.is_a?(Bundler::Source::Git) + " #{source.revision[0..6]}" + end + + def to_gemfile(path = nil) + gemfile = String.new("source 'https://rubygems.org'\n") + gemfile << dependencies_to_gemfile(nondevelopment_dependencies) + unless development_dependencies.empty? + gemfile << "\n" + gemfile << dependencies_to_gemfile(development_dependencies, :development) + end + gemfile + end + + def nondevelopment_dependencies + dependencies - development_dependencies + end + + private + + def dependencies_to_gemfile(dependencies, group = nil) + gemfile = String.new + if dependencies.any? + gemfile << "group :#{group} do\n" if group + dependencies.each do |dependency| + gemfile << " " if group + gemfile << %(gem "#{dependency.name}") + req = dependency.requirements_list.first + gemfile << %(, "#{req}") if req + gemfile << "\n" + end + gemfile << "end\n" if group + end + gemfile + end + end + + class Dependency + attr_accessor :source, :groups, :all_sources + + alias_method :eql?, :== + + def encode_with(coder) + to_yaml_properties.each do |ivar| + coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar) + end + end + + def to_yaml_properties + instance_variables.reject {|p| ["@source", "@groups", "@all_sources"].include?(p.to_s) } + end + + def to_lock + out = String.new(" #{name}") + unless requirement.none? + reqs = requirement.requirements.map {|o, v| "#{o} #{v}" }.sort.reverse + out << " (#{reqs.join(", ")})" + end + out + end + + # Backport of performance enhancement added to RubyGems 1.4 + def matches_spec?(spec) + # name can be a Regexp, so use === + return false unless name === spec.name + return true if requirement.none? + + requirement.satisfied_by?(spec.version) + end unless allocate.respond_to?(:matches_spec?) + end + + class Requirement + # Backport of performance enhancement added to RubyGems 1.4 + def none? + # note that it might be tempting to replace with with RubyGems 2.0's + # improved implementation. Don't. It requires `DefaultRequirement` to be + # defined, and more importantantly, these overrides are not used when the + # running RubyGems defines these methods + to_s == ">= 0" + end unless allocate.respond_to?(:none?) + + # Backport of performance enhancement added to RubyGems 2.2 + def exact? + return false unless @requirements.size == 1 + @requirements[0][0] == "=" + end unless allocate.respond_to?(:exact?) + end + + class Platform + JAVA = Gem::Platform.new("java") unless defined?(JAVA) + MSWIN = Gem::Platform.new("mswin32") unless defined?(MSWIN) + MSWIN64 = Gem::Platform.new("mswin64") unless defined?(MSWIN64) + MINGW = Gem::Platform.new("x86-mingw32") unless defined?(MINGW) + X64_MINGW = Gem::Platform.new("x64-mingw32") unless defined?(X64_MINGW) + + undef_method :hash if method_defined? :hash + def hash + @cpu.hash ^ @os.hash ^ @version.hash + end + + undef_method :eql? if method_defined? :eql? + alias_method :eql?, :== + end +end + +module Gem + class Specification + include ::Bundler::MatchPlatform + end +end diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb new file mode 100644 index 00000000000000..2b7fa8e0f6f250 --- /dev/null +++ b/lib/bundler/rubygems_gem_installer.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require "rubygems/installer" + +module Bundler + class RubyGemsGemInstaller < Gem::Installer + unless respond_to?(:at) + def self.at(*args) + new(*args) + end + end + + def check_executable_overwrite(filename) + # Bundler needs to install gems regardless of binstub overwriting + end + + def pre_install_checks + super && validate_bundler_checksum(options[:bundler_expected_checksum]) + end + + def build_extensions + extension_cache_path = options[:bundler_extension_cache_path] + return super unless extension_cache_path && extension_dir = Bundler.rubygems.spec_extension_dir(spec) + + extension_dir = Pathname.new(extension_dir) + build_complete = SharedHelpers.filesystem_access(extension_cache_path.join("gem.build_complete"), :read, &:file?) + if build_complete && !options[:force] + SharedHelpers.filesystem_access(extension_dir.parent, &:mkpath) + SharedHelpers.filesystem_access(extension_cache_path) do + FileUtils.cp_r extension_cache_path, spec.extension_dir + end + else + super + if extension_dir.directory? # not made for gems without extensions + SharedHelpers.filesystem_access(extension_cache_path.parent, &:mkpath) + SharedHelpers.filesystem_access(extension_cache_path) do + FileUtils.cp_r extension_dir, extension_cache_path + end + end + end + end + + private + + def validate_bundler_checksum(checksum) + return true if Bundler.settings[:disable_checksum_validation] + return true unless checksum + return true unless source = @package.instance_variable_get(:@gem) + return true unless source.respond_to?(:with_read_io) + digest = source.with_read_io do |io| + digest = SharedHelpers.digest(:SHA256).new + digest << io.read(16_384) until io.eof? + io.rewind + send(checksum_type(checksum), digest) + end + unless digest == checksum + raise SecurityError, <<-MESSAGE + Bundler cannot continue installing #{spec.name} (#{spec.version}). + The checksum for the downloaded `#{spec.full_name}.gem` does not match \ + the checksum given by the server. This means the contents of the downloaded \ + gem is different from what was uploaded to the server, and could be a potential security issue. + + To resolve this issue: + 1. delete the downloaded gem located at: `#{spec.gem_dir}/#{spec.full_name}.gem` + 2. run `bundle install` + + If you wish to continue installing the downloaded gem, and are certain it does not pose a \ + security issue despite the mismatching checksum, do the following: + 1. run `bundle config disable_checksum_validation true` to turn off checksum verification + 2. run `bundle install` + + (More info: The expected SHA256 checksum was #{checksum.inspect}, but the \ + checksum for the downloaded gem was #{digest.inspect}.) + MESSAGE + end + true + end + + def checksum_type(checksum) + case checksum.length + when 64 then :hexdigest! + when 44 then :base64digest! + else raise InstallError, "The given checksum for #{spec.full_name} (#{checksum.inspect}) is not a valid SHA256 hexdigest nor base64digest" + end + end + + def hexdigest!(digest) + digest.hexdigest! + end + + def base64digest!(digest) + if digest.respond_to?(:base64digest!) + digest.base64digest! + else + [digest.digest!].pack("m0") + end + end + end +end diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb new file mode 100644 index 00000000000000..f088c2fdfba3c5 --- /dev/null +++ b/lib/bundler/rubygems_integration.rb @@ -0,0 +1,898 @@ +# frozen_string_literal: true + +require "monitor" +require "rubygems" +require "rubygems/config_file" + +module Bundler + class RubygemsIntegration + if defined?(Gem::Ext::Builder::CHDIR_MONITOR) + EXT_LOCK = Gem::Ext::Builder::CHDIR_MONITOR + else + EXT_LOCK = Monitor.new + end + + def self.version + @version ||= Gem::Version.new(Gem::VERSION) + end + + def self.provides?(req_str) + Gem::Requirement.new(req_str).satisfied_by?(version) + end + + def initialize + @replaced_methods = {} + end + + def version + self.class.version + end + + def provides?(req_str) + self.class.provides?(req_str) + end + + def build_args + Gem::Command.build_args + end + + def build_args=(args) + Gem::Command.build_args = args + end + + def load_path_insert_index + Gem.load_path_insert_index + end + + def loaded_specs(name) + Gem.loaded_specs[name] + end + + def mark_loaded(spec) + if spec.respond_to?(:activated=) + current = Gem.loaded_specs[spec.name] + current.activated = false if current + spec.activated = true + end + Gem.loaded_specs[spec.name] = spec + end + + def validate(spec) + Bundler.ui.silence { spec.validate(false) } + rescue Gem::InvalidSpecificationException => e + error_message = "The gemspec at #{spec.loaded_from} is not valid. Please fix this gemspec.\n" \ + "The validation error was '#{e.message}'\n" + raise Gem::InvalidSpecificationException.new(error_message) + rescue Errno::ENOENT + nil + end + + def set_installed_by_version(spec, installed_by_version = Gem::VERSION) + return unless spec.respond_to?(:installed_by_version=) + spec.installed_by_version = Gem::Version.create(installed_by_version) + end + + def spec_missing_extensions?(spec, default = true) + return spec.missing_extensions? if spec.respond_to?(:missing_extensions?) + + return false if spec_default_gem?(spec) + return false if spec.extensions.empty? + + default + end + + def spec_default_gem?(spec) + spec.respond_to?(:default_gem?) && spec.default_gem? + end + + def spec_matches_for_glob(spec, glob) + return spec.matches_for_glob(glob) if spec.respond_to?(:matches_for_glob) + + spec.load_paths.map do |lp| + Dir["#{lp}/#{glob}#{suffix_pattern}"] + end.flatten(1) + end + + def spec_extension_dir(spec) + return unless spec.respond_to?(:extension_dir) + spec.extension_dir + end + + def stub_set_spec(stub, spec) + stub.instance_variable_set(:@spec, spec) + end + + def path(obj) + obj.to_s + end + + def platforms + return [Gem::Platform::RUBY] if Bundler.settings[:force_ruby_platform] + Gem.platforms + end + + def configuration + require "bundler/psyched_yaml" + Gem.configuration + rescue Gem::SystemExitException, LoadError => e + Bundler.ui.error "#{e.class}: #{e.message}" + Bundler.ui.trace e + raise + rescue YamlLibrarySyntaxError => e + raise YamlSyntaxError.new(e, "Your RubyGems configuration, which is " \ + "usually located in ~/.gemrc, contains invalid YAML syntax.") + end + + def ruby_engine + Gem.ruby_engine + end + + def read_binary(path) + Gem.read_binary(path) + end + + def inflate(obj) + if defined?(Gem::Util) + Gem::Util.inflate(obj) + else + Gem.inflate(obj) + end + end + + def sources=(val) + # Gem.configuration creates a new Gem::ConfigFile, which by default will read ~/.gemrc + # If that file exists, its settings (including sources) will overwrite the values we + # are about to set here. In order to avoid that, we force memoizing the config file now. + configuration + + Gem.sources = val + end + + def sources + Gem.sources + end + + def gem_dir + Gem.dir + end + + def gem_bindir + Gem.bindir + end + + def user_home + Gem.user_home + end + + def gem_path + Gem.path + end + + def reset + Gem::Specification.reset + end + + def post_reset_hooks + Gem.post_reset_hooks + end + + def suffix_pattern + Gem.suffix_pattern + end + + def gem_cache + gem_path.map {|p| File.expand_path("cache", p) } + end + + def spec_cache_dirs + @spec_cache_dirs ||= begin + dirs = gem_path.map {|dir| File.join(dir, "specifications") } + dirs << Gem.spec_cache_dir if Gem.respond_to?(:spec_cache_dir) # Not in RubyGems 2.0.3 or earlier + dirs.uniq.select {|dir| File.directory? dir } + end + end + + def marshal_spec_dir + Gem::MARSHAL_SPEC_DIR + end + + def config_map + Gem::ConfigMap + end + + def repository_subdirectories + %w[cache doc gems specifications] + end + + def clear_paths + Gem.clear_paths + end + + def bin_path(gem, bin, ver) + Gem.bin_path(gem, bin, ver) + end + + def path_separator + File::PATH_SEPARATOR + end + + def preserve_paths + # this is a no-op outside of RubyGems 1.8 + yield + end + + def loaded_gem_paths + # RubyGems 2.2+ can put binary extension into dedicated folders, + # therefore use RubyGems facilities to obtain their load paths. + if Gem::Specification.method_defined? :full_require_paths + loaded_gem_paths = Gem.loaded_specs.map {|_, s| s.full_require_paths } + loaded_gem_paths.flatten + else + $LOAD_PATH.select do |p| + Bundler.rubygems.gem_path.any? {|gp| p =~ /^#{Regexp.escape(gp)}/ } + end + end + end + + def load_plugins + Gem.load_plugins if Gem.respond_to?(:load_plugins) + end + + def load_plugin_files(files) + Gem.load_plugin_files(files) if Gem.respond_to?(:load_plugin_files) + end + + def ui=(obj) + Gem::DefaultUserInteraction.ui = obj + end + + def ext_lock + EXT_LOCK + end + + def fetch_specs(all, pre, &blk) + require "rubygems/spec_fetcher" + specs = Gem::SpecFetcher.new.list(all, pre) + specs.each { yield } if block_given? + specs + end + + def fetch_prerelease_specs + fetch_specs(false, true) + rescue Gem::RemoteFetcher::FetchError + {} # if we can't download them, there aren't any + end + + # TODO: This is for older versions of RubyGems... should we support the + # X-Gemfile-Source header on these old versions? + # Maybe the newer implementation will work on older RubyGems? + # It seems difficult to keep this implementation and still send the header. + def fetch_all_remote_specs(remote) + old_sources = Bundler.rubygems.sources + Bundler.rubygems.sources = [remote.uri.to_s] + # Fetch all specs, minus prerelease specs + spec_list = fetch_specs(true, false) + # Then fetch the prerelease specs + fetch_prerelease_specs.each {|k, v| spec_list[k].concat(v) } + + spec_list.values.first + ensure + Bundler.rubygems.sources = old_sources + end + + def with_build_args(args) + ext_lock.synchronize do + old_args = build_args + begin + self.build_args = args + yield + ensure + self.build_args = old_args + end + end + end + + def install_with_build_args(args) + with_build_args(args) { yield } + end + + def gem_from_path(path, policy = nil) + require "rubygems/format" + Gem::Format.from_file_by_path(path, policy) + end + + def spec_from_gem(path, policy = nil) + require "rubygems/security" + require "bundler/psyched_yaml" + gem_from_path(path, security_policies[policy]).spec + rescue Gem::Package::FormatError + raise GemspecError, "Could not read gem at #{path}. It may be corrupted." + rescue Exception, Gem::Exception, Gem::Security::Exception => e + if e.is_a?(Gem::Security::Exception) || + e.message =~ /unknown trust policy|unsigned gem/i || + e.message =~ /couldn't verify (meta)?data signature/i + raise SecurityError, + "The gem #{File.basename(path, ".gem")} can't be installed because " \ + "the security policy didn't allow it, with the message: #{e.message}" + else + raise e + end + end + + def build(spec, skip_validation = false) + require "rubygems/builder" + Gem::Builder.new(spec).build + end + + def build_gem(gem_dir, spec) + build(spec) + end + + def download_gem(spec, uri, path) + uri = Bundler.settings.mirror_for(uri) + fetcher = Gem::RemoteFetcher.new(configuration[:http_proxy]) + Bundler::Retry.new("download gem from #{uri}").attempts do + fetcher.download(spec, uri, path) + end + end + + def security_policy_keys + %w[High Medium Low AlmostNo No].map {|level| "#{level}Security" } + end + + def security_policies + @security_policies ||= begin + require "rubygems/security" + Gem::Security::Policies + rescue LoadError, NameError + {} + end + end + + def reverse_rubygems_kernel_mixin + # Disable rubygems' gem activation system + kernel = (class << ::Kernel; self; end) + [kernel, ::Kernel].each do |k| + if k.private_method_defined?(:gem_original_require) + redefine_method(k, :require, k.instance_method(:gem_original_require)) + end + end + end + + def binstubs_call_gem? + true + end + + def stubs_provide_full_functionality? + false + end + + def replace_gem(specs, specs_by_name) + reverse_rubygems_kernel_mixin + + executables = nil + + kernel = (class << ::Kernel; self; end) + [kernel, ::Kernel].each do |kernel_class| + redefine_method(kernel_class, :gem) do |dep, *reqs| + executables ||= specs.map(&:executables).flatten if ::Bundler.rubygems.binstubs_call_gem? + if executables && executables.include?(File.basename(caller.first.split(":").first)) + break + end + + reqs.pop if reqs.last.is_a?(Hash) + + unless dep.respond_to?(:name) && dep.respond_to?(:requirement) + dep = Gem::Dependency.new(dep, reqs) + end + + if spec = specs_by_name[dep.name] + return true if dep.matches_spec?(spec) + end + + message = if spec.nil? + "#{dep.name} is not part of the bundle." \ + " Add it to your #{Bundler.default_gemfile.basename}." + else + "can't activate #{dep}, already activated #{spec.full_name}. " \ + "Make sure all dependencies are added to Gemfile." + end + + e = Gem::LoadError.new(message) + e.name = dep.name + if e.respond_to?(:requirement=) + e.requirement = dep.requirement + elsif e.respond_to?(:version_requirement=) + e.version_requirement = dep.requirement + end + raise e + end + + # backwards compatibility shim, see https://github.com/bundler/bundler/issues/5102 + kernel_class.send(:public, :gem) if Bundler.feature_flag.setup_makes_kernel_gem_public? + end + end + + def stub_source_index(specs) + Gem::SourceIndex.send(:alias_method, :old_initialize, :initialize) + redefine_method(Gem::SourceIndex, :initialize) do |*args| + @gems = {} + # You're looking at this thinking: Oh! This is how I make those + # rubygems deprecations go away! + # + # You'd be correct BUT using of this method in production code + # must be approved by the rubygems team itself! + # + # This is your warning. If you use this and don't have approval + # we can't protect you. + # + Deprecate.skip_during do + self.spec_dirs = *args + add_specs(*specs) + end + end + end + + # Used to make bin stubs that are not created by bundler work + # under bundler. The new Gem.bin_path only considers gems in + # +specs+ + def replace_bin_path(specs, specs_by_name) + gem_class = (class << Gem; self; end) + + redefine_method(gem_class, :find_spec_for_exe) do |gem_name, *args| + exec_name = args.first + + spec_with_name = specs_by_name[gem_name] + spec = if exec_name + if spec_with_name && spec_with_name.executables.include?(exec_name) + spec_with_name + else + specs.find {|s| s.executables.include?(exec_name) } + end + else + spec_with_name + end + + unless spec + message = "can't find executable #{exec_name} for gem #{gem_name}" + if !exec_name || spec_with_name.nil? + message += ". #{gem_name} is not currently included in the bundle, " \ + "perhaps you meant to add it to your #{Bundler.default_gemfile.basename}?" + end + raise Gem::Exception, message + end + + raise Gem::Exception, "no default executable for #{spec.full_name}" unless exec_name ||= spec.default_executable + + unless spec.name == gem_name + Bundler::SharedHelpers.major_deprecation 3, + "Bundler is using a binstub that was created for a different gem (#{spec.name}).\n" \ + "You should run `bundle binstub #{gem_name}` " \ + "to work around a system/bundle conflict." + end + spec + end + + redefine_method(gem_class, :activate_bin_path) do |name, *args| + exec_name = args.first + return ENV["BUNDLE_BIN_PATH"] if exec_name == "bundle" + + # Copy of Rubygems activate_bin_path impl + requirement = args.last + spec = find_spec_for_exe name, exec_name, [requirement] + + gem_bin = File.join(spec.full_gem_path, spec.bindir, exec_name) + gem_from_path_bin = File.join(File.dirname(spec.loaded_from), spec.bindir, exec_name) + File.exist?(gem_bin) ? gem_bin : gem_from_path_bin + end + + redefine_method(gem_class, :bin_path) do |name, *args| + exec_name = args.first + return ENV["BUNDLE_BIN_PATH"] if exec_name == "bundle" + + spec = find_spec_for_exe(name, *args) + exec_name ||= spec.default_executable + + gem_bin = File.join(spec.full_gem_path, spec.bindir, exec_name) + gem_from_path_bin = File.join(File.dirname(spec.loaded_from), spec.bindir, exec_name) + File.exist?(gem_bin) ? gem_bin : gem_from_path_bin + end + end + + # Because Bundler has a static view of what specs are available, + # we don't #refresh, so stub it out. + def replace_refresh + gem_class = (class << Gem; self; end) + redefine_method(gem_class, :refresh) {} + end + + # Replace or hook into RubyGems to provide a bundlerized view + # of the world. + def replace_entrypoints(specs) + specs_by_name = specs.reduce({}) do |h, s| + h[s.name] = s + h + end + + replace_gem(specs, specs_by_name) + stub_rubygems(specs) + replace_bin_path(specs, specs_by_name) + replace_refresh + + Gem.clear_paths + end + + # This backports the correct segment generation code from RubyGems 1.4+ + # by monkeypatching it into the method in RubyGems 1.3.6 and 1.3.7. + def backport_segment_generation + redefine_method(Gem::Version, :segments) do + @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s| + /^\d+$/ =~ s ? s.to_i : s + end + end + end + + # This backport fixes the marshaling of @segments. + def backport_yaml_initialize + redefine_method(Gem::Version, :yaml_initialize) do |_, map| + @version = map["version"] + @segments = nil + @hash = nil + end + end + + # This backports base_dir which replaces installation path + # RubyGems 1.8+ + def backport_base_dir + redefine_method(Gem::Specification, :base_dir) do + return Gem.dir unless loaded_from + File.dirname File.dirname loaded_from + end + end + + def backport_cache_file + redefine_method(Gem::Specification, :cache_dir) do + @cache_dir ||= File.join base_dir, "cache" + end + + redefine_method(Gem::Specification, :cache_file) do + @cache_file ||= File.join cache_dir, "#{full_name}.gem" + end + end + + def backport_spec_file + redefine_method(Gem::Specification, :spec_dir) do + @spec_dir ||= File.join base_dir, "specifications" + end + + redefine_method(Gem::Specification, :spec_file) do + @spec_file ||= File.join spec_dir, "#{full_name}.gemspec" + end + end + + def undo_replacements + @replaced_methods.each do |(sym, klass), method| + redefine_method(klass, sym, method) + end + if Binding.public_method_defined?(:source_location) + post_reset_hooks.reject! {|proc| proc.binding.source_location[0] == __FILE__ } + else + post_reset_hooks.reject! {|proc| proc.binding.eval("__FILE__") == __FILE__ } + end + @replaced_methods.clear + end + + def redefine_method(klass, method, unbound_method = nil, &block) + visibility = method_visibility(klass, method) + begin + if (instance_method = klass.instance_method(method)) && method != :initialize + # doing this to ensure we also get private methods + klass.send(:remove_method, method) + end + rescue NameError + # method isn't defined + nil + end + @replaced_methods[[method, klass]] = instance_method + if unbound_method + klass.send(:define_method, method, unbound_method) + klass.send(visibility, method) + elsif block + klass.send(:define_method, method, &block) + klass.send(visibility, method) + end + end + + def method_visibility(klass, method) + if klass.private_method_defined?(method) + :private + elsif klass.protected_method_defined?(method) + :protected + else + :public + end + end + + # RubyGems 1.4 through 1.6 + class Legacy < RubygemsIntegration + def initialize + super + backport_base_dir + backport_cache_file + backport_spec_file + backport_yaml_initialize + end + + def stub_rubygems(specs) + # RubyGems versions lower than 1.7 use SourceIndex#from_gems_in + source_index_class = (class << Gem::SourceIndex; self; end) + redefine_method(source_index_class, :from_gems_in) do |*args| + Gem::SourceIndex.new.tap do |source_index| + source_index.spec_dirs = *args + source_index.add_specs(*specs) + end + end + end + + def all_specs + Gem.source_index.gems.values + end + + def find_name(name) + Gem.source_index.find_name(name) + end + + def validate(spec) + # These versions of RubyGems always validate in "packaging" mode, + # which is too strict for the kinds of checks we care about. As a + # result, validation is disabled on versions of RubyGems below 1.7. + end + + def post_reset_hooks + [] + end + + def reset + end + end + + # RubyGems versions 1.3.6 and 1.3.7 + class Ancient < Legacy + def initialize + super + backport_segment_generation + end + end + + # RubyGems 1.7 + class Transitional < Legacy + def stub_rubygems(specs) + stub_source_index(specs) + end + + def validate(spec) + # Missing summary is downgraded to a warning in later versions, + # so we set it to an empty string to prevent an exception here. + spec.summary ||= "" + RubygemsIntegration.instance_method(:validate).bind(self).call(spec) + end + end + + # RubyGems 1.8.5-1.8.19 + class Modern < RubygemsIntegration + def stub_rubygems(specs) + Gem::Specification.all = specs + + Gem.post_reset do + Gem::Specification.all = specs + end + + stub_source_index(specs) + end + + def all_specs + Gem::Specification.to_a + end + + def find_name(name) + Gem::Specification.find_all_by_name name + end + end + + # RubyGems 1.8.0 to 1.8.4 + class AlmostModern < Modern + # RubyGems [>= 1.8.0, < 1.8.5] has a bug that changes Gem.dir whenever + # you call Gem::Installer#install with an :install_dir set. We have to + # change it back for our sudo mode to work. + def preserve_paths + old_dir = gem_dir + old_path = gem_path + yield + Gem.use_paths(old_dir, old_path) + end + end + + # RubyGems 1.8.20+ + class MoreModern < Modern + # RubyGems 1.8.20 and adds the skip_validation parameter, so that's + # when we start passing it through. + def build(spec, skip_validation = false) + require "rubygems/builder" + Gem::Builder.new(spec).build(skip_validation) + end + end + + # RubyGems 2.0 + class Future < RubygemsIntegration + def stub_rubygems(specs) + Gem::Specification.all = specs + + Gem.post_reset do + Gem::Specification.all = specs + end + + redefine_method((class << Gem; self; end), :finish_resolve) do |*| + [] + end + end + + def all_specs + Gem::Specification.to_a + end + + def find_name(name) + Gem::Specification.find_all_by_name name + end + + def fetch_specs(source, remote, name) + path = source + "#{name}.#{Gem.marshal_version}.gz" + fetcher = gem_remote_fetcher + fetcher.headers = { "X-Gemfile-Source" => remote.original_uri.to_s } if remote.original_uri + string = fetcher.fetch_path(path) + Bundler.load_marshal(string) + rescue Gem::RemoteFetcher::FetchError => e + # it's okay for prerelease to fail + raise e unless name == "prerelease_specs" + end + + def fetch_all_remote_specs(remote) + source = remote.uri.is_a?(URI) ? remote.uri : URI.parse(source.to_s) + + specs = fetch_specs(source, remote, "specs") + pres = fetch_specs(source, remote, "prerelease_specs") || [] + + specs.concat(pres) + end + + def download_gem(spec, uri, path) + uri = Bundler.settings.mirror_for(uri) + fetcher = gem_remote_fetcher + fetcher.headers = { "X-Gemfile-Source" => spec.remote.original_uri.to_s } if spec.remote.original_uri + Bundler::Retry.new("download gem from #{uri}").attempts do + fetcher.download(spec, uri, path) + end + end + + def gem_remote_fetcher + require "resolv" + proxy = configuration[:http_proxy] + dns = Resolv::DNS.new + Bundler::GemRemoteFetcher.new(proxy, dns) + end + + def gem_from_path(path, policy = nil) + require "rubygems/package" + p = Gem::Package.new(path) + p.security_policy = policy if policy + p + end + + def build(spec, skip_validation = false) + require "rubygems/package" + Gem::Package.build(spec, skip_validation) + end + + def repository_subdirectories + Gem::REPOSITORY_SUBDIRECTORIES + end + + def install_with_build_args(args) + yield + end + + def path_separator + Gem.path_separator + end + end + + # RubyGems 2.1.0 + class MoreFuture < Future + def initialize + super + backport_ext_builder_monitor + end + + def all_specs + require "bundler/remote_specification" + Gem::Specification.stubs.map do |stub| + StubSpecification.from_stub(stub) + end + end + + def backport_ext_builder_monitor + # So we can avoid requiring "rubygems/ext" in its entirety + Gem.module_eval <<-RB, __FILE__, __LINE__ + 1 + module Ext + end + RB + + require "rubygems/ext/builder" + + Gem::Ext::Builder.class_eval do + unless const_defined?(:CHDIR_MONITOR) + const_set(:CHDIR_MONITOR, EXT_LOCK) + end + + remove_const(:CHDIR_MUTEX) if const_defined?(:CHDIR_MUTEX) + const_set(:CHDIR_MUTEX, const_get(:CHDIR_MONITOR)) + end + end + + if Gem::Specification.respond_to?(:stubs_for) + def find_name(name) + Gem::Specification.stubs_for(name).map(&:to_spec) + end + else + def find_name(name) + Gem::Specification.stubs.find_all do |spec| + spec.name == name + end.map(&:to_spec) + end + end + + def use_gemdeps(gemfile) + ENV["BUNDLE_GEMFILE"] ||= File.expand_path(gemfile) + require "bundler/gemdeps" + runtime = Bundler.setup + Bundler.ui = nil + activated_spec_names = runtime.requested_specs.map(&:to_spec).sort_by(&:name) + [Gemdeps.new(runtime), activated_spec_names] + end + + if provides?(">= 2.5.2") + # RubyGems-generated binstubs call Kernel#gem + def binstubs_call_gem? + false + end + + # only 2.5.2+ has all of the stub methods we want to use, and since this + # is a performance optimization _only_, + # we'll restrict ourselves to the most + # recent RG versions instead of all versions that have stubs + def stubs_provide_full_functionality? + true + end + end + end + end + + def self.rubygems + @rubygems ||= if RubygemsIntegration.provides?(">= 2.1.0") + RubygemsIntegration::MoreFuture.new + elsif RubygemsIntegration.provides?(">= 1.99.99") + RubygemsIntegration::Future.new + elsif RubygemsIntegration.provides?(">= 1.8.20") + RubygemsIntegration::MoreModern.new + elsif RubygemsIntegration.provides?(">= 1.8.5") + RubygemsIntegration::Modern.new + elsif RubygemsIntegration.provides?(">= 1.8.0") + RubygemsIntegration::AlmostModern.new + elsif RubygemsIntegration.provides?(">= 1.7.0") + RubygemsIntegration::Transitional.new + elsif RubygemsIntegration.provides?(">= 1.4.0") + RubygemsIntegration::Legacy.new + else # RubyGems 1.3.6 and 1.3.7 + RubygemsIntegration::Ancient.new + end + end +end diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb new file mode 100644 index 00000000000000..762e7b3ec66ea5 --- /dev/null +++ b/lib/bundler/runtime.rb @@ -0,0 +1,322 @@ +# frozen_string_literal: true + +module Bundler + class Runtime + include SharedHelpers + + def initialize(root, definition) + @root = root + @definition = definition + end + + def setup(*groups) + @definition.ensure_equivalent_gemfile_and_lockfile if Bundler.frozen_bundle? + + groups.map!(&:to_sym) + + # Has to happen first + clean_load_path + + specs = groups.any? ? @definition.specs_for(groups) : requested_specs + + SharedHelpers.set_bundle_environment + Bundler.rubygems.replace_entrypoints(specs) + + # Activate the specs + load_paths = specs.map do |spec| + unless spec.loaded_from + raise GemNotFound, "#{spec.full_name} is missing. Run `bundle install` to get it." + end + + check_for_activated_spec!(spec) + + Bundler.rubygems.mark_loaded(spec) + spec.load_paths.reject {|path| $LOAD_PATH.include?(path) } + end.reverse.flatten + + # See Gem::Specification#add_self_to_load_path (since RubyGems 1.8) + if insert_index = Bundler.rubygems.load_path_insert_index + # Gem directories must come after -I and ENV['RUBYLIB'] + $LOAD_PATH.insert(insert_index, *load_paths) + else + # We are probably testing in core, -I and RUBYLIB don't apply + $LOAD_PATH.unshift(*load_paths) + end + + setup_manpath + + lock(:preserve_unknown_sections => true) + + self + end + + REQUIRE_ERRORS = [ + /^no such file to load -- (.+)$/i, + /^Missing \w+ (?:file\s*)?([^\s]+.rb)$/i, + /^Missing API definition file in (.+)$/i, + /^cannot load such file -- (.+)$/i, + /^dlopen\([^)]*\): Library not loaded: (.+)$/i, + ].freeze + + def require(*groups) + groups.map!(&:to_sym) + groups = [:default] if groups.empty? + + @definition.dependencies.each do |dep| + # Skip the dependency if it is not in any of the requested groups, or + # not for the current platform, or doesn't match the gem constraints. + next unless (dep.groups & groups).any? && dep.should_include? + + required_file = nil + + begin + # Loop through all the specified autorequires for the + # dependency. If there are none, use the dependency's name + # as the autorequire. + Array(dep.autorequire || dep.name).each do |file| + # Allow `require: true` as an alias for `require: ` + file = dep.name if file == true + required_file = file + begin + Kernel.require file + rescue RuntimeError => e + raise e if e.is_a?(LoadError) # we handle this a little later + raise Bundler::GemRequireError.new e, + "There was an error while trying to load the gem '#{file}'." + end + end + rescue LoadError => e + REQUIRE_ERRORS.find {|r| r =~ e.message } + raise if dep.autorequire || $1 != required_file + + if dep.autorequire.nil? && dep.name.include?("-") + begin + namespaced_file = dep.name.tr("-", "/") + Kernel.require namespaced_file + rescue LoadError => e + REQUIRE_ERRORS.find {|r| r =~ e.message } + raise if $1 != namespaced_file + end + end + end + end + end + + def self.definition_method(meth) + define_method(meth) do + raise ArgumentError, "no definition when calling Runtime##{meth}" unless @definition + @definition.send(meth) + end + end + private_class_method :definition_method + + definition_method :requested_specs + definition_method :specs + definition_method :dependencies + definition_method :current_dependencies + definition_method :requires + + def lock(opts = {}) + return if @definition.nothing_changed? && !@definition.unlocking? + @definition.lock(Bundler.default_lockfile, opts[:preserve_unknown_sections]) + end + + alias_method :gems, :specs + + def cache(custom_path = nil) + cache_path = Bundler.app_cache(custom_path) + SharedHelpers.filesystem_access(cache_path) do |p| + FileUtils.mkdir_p(p) + end unless File.exist?(cache_path) + + Bundler.ui.info "Updating files in #{Bundler.settings.app_cache_path}" + + specs_to_cache = Bundler.settings[:cache_all_platforms] ? @definition.resolve.materialized_for_all_platforms : specs + specs_to_cache.each do |spec| + next if spec.name == "bundler" + next if spec.source.is_a?(Source::Gemspec) + spec.source.send(:fetch_gem, spec) if Bundler.settings[:cache_all_platforms] && spec.source.respond_to?(:fetch_gem, true) + spec.source.cache(spec, custom_path) if spec.source.respond_to?(:cache) + end + + Dir[cache_path.join("*/.git")].each do |git_dir| + FileUtils.rm_rf(git_dir) + FileUtils.touch(File.expand_path("../.bundlecache", git_dir)) + end + + prune_cache(cache_path) unless Bundler.settings[:no_prune] + end + + def prune_cache(cache_path) + SharedHelpers.filesystem_access(cache_path) do |p| + FileUtils.mkdir_p(p) + end unless File.exist?(cache_path) + resolve = @definition.resolve + prune_gem_cache(resolve, cache_path) + prune_git_and_path_cache(resolve, cache_path) + end + + def clean(dry_run = false) + gem_bins = Dir["#{Gem.dir}/bin/*"] + git_dirs = Dir["#{Gem.dir}/bundler/gems/*"] + git_cache_dirs = Dir["#{Gem.dir}/cache/bundler/git/*"] + gem_dirs = Dir["#{Gem.dir}/gems/*"] + gem_files = Dir["#{Gem.dir}/cache/*.gem"] + gemspec_files = Dir["#{Gem.dir}/specifications/*.gemspec"] + extension_dirs = Dir["#{Gem.dir}/extensions/*/*/*"] + spec_gem_paths = [] + # need to keep git sources around + spec_git_paths = @definition.spec_git_paths + spec_git_cache_dirs = [] + spec_gem_executables = [] + spec_cache_paths = [] + spec_gemspec_paths = [] + spec_extension_paths = [] + specs.each do |spec| + spec_gem_paths << spec.full_gem_path + # need to check here in case gems are nested like for the rails git repo + md = %r{(.+bundler/gems/.+-[a-f0-9]{7,12})}.match(spec.full_gem_path) + spec_git_paths << md[1] if md + spec_gem_executables << spec.executables.collect do |executable| + e = "#{Bundler.rubygems.gem_bindir}/#{executable}" + [e, "#{e}.bat"] + end + spec_cache_paths << spec.cache_file + spec_gemspec_paths << spec.spec_file + spec_extension_paths << spec.extension_dir if spec.respond_to?(:extension_dir) + spec_git_cache_dirs << spec.source.cache_path.to_s if spec.source.is_a?(Bundler::Source::Git) + end + spec_gem_paths.uniq! + spec_gem_executables.flatten! + + stale_gem_bins = gem_bins - spec_gem_executables + stale_git_dirs = git_dirs - spec_git_paths - ["#{Gem.dir}/bundler/gems/extensions"] + stale_git_cache_dirs = git_cache_dirs - spec_git_cache_dirs + stale_gem_dirs = gem_dirs - spec_gem_paths + stale_gem_files = gem_files - spec_cache_paths + stale_gemspec_files = gemspec_files - spec_gemspec_paths + stale_extension_dirs = extension_dirs - spec_extension_paths + + removed_stale_gem_dirs = stale_gem_dirs.collect {|dir| remove_dir(dir, dry_run) } + removed_stale_git_dirs = stale_git_dirs.collect {|dir| remove_dir(dir, dry_run) } + output = removed_stale_gem_dirs + removed_stale_git_dirs + + unless dry_run + stale_files = stale_gem_bins + stale_gem_files + stale_gemspec_files + stale_files.each do |file| + SharedHelpers.filesystem_access(File.dirname(file)) do |_p| + FileUtils.rm(file) if File.exist?(file) + end + end + + stale_dirs = stale_git_cache_dirs + stale_extension_dirs + stale_dirs.each do |stale_dir| + SharedHelpers.filesystem_access(stale_dir) do |dir| + FileUtils.rm_rf(dir) if File.exist?(dir) + end + end + end + + output + end + + private + + def prune_gem_cache(resolve, cache_path) + cached = Dir["#{cache_path}/*.gem"] + + cached = cached.delete_if do |path| + spec = Bundler.rubygems.spec_from_gem path + + resolve.any? do |s| + s.name == spec.name && s.version == spec.version && !s.source.is_a?(Bundler::Source::Git) + end + end + + if cached.any? + Bundler.ui.info "Removing outdated .gem files from #{Bundler.settings.app_cache_path}" + + cached.each do |path| + Bundler.ui.info " * #{File.basename(path)}" + File.delete(path) + end + end + end + + def prune_git_and_path_cache(resolve, cache_path) + cached = Dir["#{cache_path}/*/.bundlecache"] + + cached = cached.delete_if do |path| + name = File.basename(File.dirname(path)) + + resolve.any? do |s| + source = s.source + source.respond_to?(:app_cache_dirname) && source.app_cache_dirname == name + end + end + + if cached.any? + Bundler.ui.info "Removing outdated git and path gems from #{Bundler.settings.app_cache_path}" + + cached.each do |path| + path = File.dirname(path) + Bundler.ui.info " * #{File.basename(path)}" + FileUtils.rm_rf(path) + end + end + end + + def setup_manpath + # Add man/ subdirectories from activated bundles to MANPATH for man(1) + manuals = $LOAD_PATH.map do |path| + man_subdir = path.sub(/lib$/, "man") + man_subdir unless Dir[man_subdir + "/man?/"].empty? + end.compact + + return if manuals.empty? + Bundler::SharedHelpers.set_env "MANPATH", manuals.concat( + ENV["MANPATH"].to_s.split(File::PATH_SEPARATOR) + ).uniq.join(File::PATH_SEPARATOR) + end + + def remove_dir(dir, dry_run) + full_name = Pathname.new(dir).basename.to_s + + parts = full_name.split("-") + name = parts[0..-2].join("-") + version = parts.last + output = "#{name} (#{version})" + + if dry_run + Bundler.ui.info "Would have removed #{output}" + else + Bundler.ui.info "Removing #{output}" + FileUtils.rm_rf(dir) + end + + output + end + + def check_for_activated_spec!(spec) + return unless activated_spec = Bundler.rubygems.loaded_specs(spec.name) + return if activated_spec.version == spec.version + + suggestion = if Bundler.rubygems.spec_default_gem?(activated_spec) + "Since #{spec.name} is a default gem, you can either remove your dependency on it" \ + " or try updating to a newer version of bundler that supports #{spec.name} as a default gem." + else + "Prepending `bundle exec` to your command may solve this." + end + + e = Gem::LoadError.new "You have already activated #{activated_spec.name} #{activated_spec.version}, " \ + "but your Gemfile requires #{spec.name} #{spec.version}. #{suggestion}" + e.name = spec.name + if e.respond_to?(:requirement=) + e.requirement = Gem::Requirement.new(spec.version.to_s) + else + e.version_requirement = Gem::Requirement.new(spec.version.to_s) + end + raise e + end + end +end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb new file mode 100644 index 00000000000000..66af31dab25787 --- /dev/null +++ b/lib/bundler/settings.rb @@ -0,0 +1,464 @@ +# frozen_string_literal: true + +require "uri" + +module Bundler + class Settings + autoload :Mirror, "bundler/mirror" + autoload :Mirrors, "bundler/mirror" + autoload :Validator, "bundler/settings/validator" + + BOOL_KEYS = %w[ + allow_bundler_dependency_conflicts + allow_deployment_source_credential_changes + allow_offline_install + auto_clean_without_path + auto_install + auto_config_jobs + cache_all + cache_all_platforms + cache_command_is_package + console_command + default_install_uses_path + deployment + deployment_means_frozen + disable_checksum_validation + disable_exec_load + disable_local_branch_check + disable_multisource + disable_platform_warnings + disable_shared_gems + disable_version_check + error_on_stderr + force_ruby_platform + forget_cli_options + frozen + gem.coc + gem.mit + global_path_appends_ruby_scope + global_gem_cache + ignore_messages + init_gems_rb + list_command + lockfile_upgrade_warning + lockfile_uses_separate_rubygems_sources + major_deprecations + no_install + no_prune + only_update_to_newer_versions + path_relative_to_cwd + path.system + plugins + prefer_gems_rb + print_only_version_number + setup_makes_kernel_gem_public + silence_root_warning + skip_default_git_sources + specific_platform + suppress_install_using_messages + unlock_source_unlocks_spec + update_requires_all_flag + use_gem_version_promoter_for_major_updates + viz_command + ].freeze + + NUMBER_KEYS = %w[ + jobs + redirect + retry + ssl_verify_mode + timeout + ].freeze + + ARRAY_KEYS = %w[ + with + without + ].freeze + + DEFAULT_CONFIG = { + :disable_version_check => true, + :redirect => 5, + :retry => 3, + :timeout => 10, + }.freeze + + def initialize(root = nil) + @root = root + @local_config = load_config(local_config_file) + @global_config = load_config(global_config_file) + @temporary = {} + end + + def [](name) + key = key_for(name) + value = @temporary.fetch(key) do + @local_config.fetch(key) do + ENV.fetch(key) do + @global_config.fetch(key) do + DEFAULT_CONFIG.fetch(name) do + nil + end end end end end + + converted_value(value, name) + end + + def set_command_option(key, value) + if Bundler.feature_flag.forget_cli_options? + temporary(key => value) + value + else + command = if value.nil? + "bundle config --delete #{key}" + else + "bundle config #{key} #{Array(value).join(":")}" + end + + Bundler::SharedHelpers.major_deprecation 3,\ + "flags passed to commands " \ + "will no longer be automatically remembered. Instead please set flags " \ + "you want remembered between commands using `bundle config " \ + " `, i.e. `#{command}`" + + set_local(key, value) + end + end + + def set_command_option_if_given(key, value) + return if value.nil? + set_command_option(key, value) + end + + def set_local(key, value) + local_config_file || raise(GemfileNotFound, "Could not locate Gemfile") + + set_key(key, value, @local_config, local_config_file) + end + + def temporary(update) + existing = Hash[update.map {|k, _| [k, @temporary[key_for(k)]] }] + update.each do |k, v| + set_key(k, v, @temporary, nil) + end + return unless block_given? + begin + yield + ensure + existing.each {|k, v| set_key(k, v, @temporary, nil) } + end + end + + def set_global(key, value) + set_key(key, value, @global_config, global_config_file) + end + + def all + env_keys = ENV.keys.grep(/\ABUNDLE_.+/) + + keys = @temporary.keys | @global_config.keys | @local_config.keys | env_keys + + keys.map do |key| + key.sub(/^BUNDLE_/, "").gsub(/__/, ".").downcase + end + end + + def local_overrides + repos = {} + all.each do |k| + repos[$'] = self[k] if k =~ /^local\./ + end + repos + end + + def mirror_for(uri) + uri = URI(uri.to_s) unless uri.is_a?(URI) + gem_mirrors.for(uri.to_s).uri + end + + def credentials_for(uri) + self[uri.to_s] || self[uri.host] + end + + def gem_mirrors + all.inject(Mirrors.new) do |mirrors, k| + mirrors.parse(k, self[k]) if k.start_with?("mirror.") + mirrors + end + end + + def locations(key) + key = key_for(key) + locations = {} + locations[:temporary] = @temporary[key] if @temporary.key?(key) + locations[:local] = @local_config[key] if @local_config.key?(key) + locations[:env] = ENV[key] if ENV[key] + locations[:global] = @global_config[key] if @global_config.key?(key) + locations[:default] = DEFAULT_CONFIG[key] if DEFAULT_CONFIG.key?(key) + locations + end + + def pretty_values_for(exposed_key) + key = key_for(exposed_key) + + locations = [] + + if @temporary.key?(key) + locations << "Set for the current command: #{converted_value(@temporary[key], exposed_key).inspect}" + end + + if @local_config.key?(key) + locations << "Set for your local app (#{local_config_file}): #{converted_value(@local_config[key], exposed_key).inspect}" + end + + if value = ENV[key] + locations << "Set via #{key}: #{converted_value(value, exposed_key).inspect}" + end + + if @global_config.key?(key) + locations << "Set for the current user (#{global_config_file}): #{converted_value(@global_config[key], exposed_key).inspect}" + end + + return ["You have not configured a value for `#{exposed_key}`"] if locations.empty? + locations + end + + # for legacy reasons, in Bundler 1, the ruby scope isnt appended when the setting comes from ENV or the global config, + # nor do we respect :disable_shared_gems + def path + key = key_for(:path) + path = ENV[key] || @global_config[key] + if path && !@temporary.key?(key) && !@local_config.key?(key) + return Path.new(path, Bundler.feature_flag.global_path_appends_ruby_scope?, false, false) + end + + system_path = self["path.system"] || (self[:disable_shared_gems] == false) + Path.new(self[:path], true, system_path, Bundler.feature_flag.default_install_uses_path?) + end + + Path = Struct.new(:explicit_path, :append_ruby_scope, :system_path, :default_install_uses_path) do + def path + path = base_path + path = File.join(path, Bundler.ruby_scope) if append_ruby_scope && !use_system_gems? + path + end + + def use_system_gems? + return true if system_path + return false if explicit_path + !default_install_uses_path + end + + def base_path + path = explicit_path + path ||= ".bundle" unless use_system_gems? + path ||= Bundler.rubygems.gem_dir + path + end + + def base_path_relative_to_pwd + base_path = Pathname.new(self.base_path) + expanded_base_path = base_path.expand_path(Bundler.root) + relative_path = expanded_base_path.relative_path_from(Pathname.pwd) + if relative_path.to_s.start_with?("..") + relative_path = base_path if base_path.absolute? + else + relative_path = Pathname.new(File.join(".", relative_path)) + end + relative_path + rescue ArgumentError + expanded_base_path + end + + def validate! + return unless explicit_path && system_path + path = Bundler.settings.pretty_values_for(:path) + path.unshift(nil, "path:") unless path.empty? + system_path = Bundler.settings.pretty_values_for("path.system") + system_path.unshift(nil, "path.system:") unless system_path.empty? + disable_shared_gems = Bundler.settings.pretty_values_for(:disable_shared_gems) + disable_shared_gems.unshift(nil, "disable_shared_gems:") unless disable_shared_gems.empty? + raise InvalidOption, + "Using a custom path while using system gems is unsupported.\n#{path.join("\n")}\n#{system_path.join("\n")}\n#{disable_shared_gems.join("\n")}" + end + end + + def allow_sudo? + key = key_for(:path) + path_configured = @temporary.key?(key) || @local_config.key?(key) + !path_configured + end + + def ignore_config? + ENV["BUNDLE_IGNORE_CONFIG"] + end + + def app_cache_path + @app_cache_path ||= self[:cache_path] || "vendor/cache" + end + + def validate! + all.each do |raw_key| + [@local_config, ENV, @global_config].each do |settings| + value = converted_value(settings[key_for(raw_key)], raw_key) + Validator.validate!(raw_key, value, settings.to_hash.dup) + end + end + end + + def key_for(key) + key = Settings.normalize_uri(key).to_s if key.is_a?(String) && /https?:/ =~ key + key = key.to_s.gsub(".", "__").upcase + "BUNDLE_#{key}" + end + + private + + def parent_setting_for(name) + split_specific_setting_for(name)[0] + end + + def specific_gem_for(name) + split_specific_setting_for(name)[1] + end + + def split_specific_setting_for(name) + name.split(".") + end + + def is_bool(name) + BOOL_KEYS.include?(name.to_s) || BOOL_KEYS.include?(parent_setting_for(name.to_s)) + end + + def to_bool(value) + case value + when nil, /\A(false|f|no|n|0|)\z/i, false + false + else + true + end + end + + def is_num(key) + NUMBER_KEYS.include?(key.to_s) + end + + def is_array(key) + ARRAY_KEYS.include?(key.to_s) + end + + def to_array(value) + return [] unless value + value.split(":").map(&:to_sym) + end + + def array_to_s(array) + array = Array(array) + return nil if array.empty? + array.join(":").tr(" ", ":") + end + + def set_key(raw_key, value, hash, file) + raw_key = raw_key.to_s + value = array_to_s(value) if is_array(raw_key) + + key = key_for(raw_key) + + return if hash[key] == value + + hash[key] = value + hash.delete(key) if value.nil? + + Validator.validate!(raw_key, converted_value(value, raw_key), hash) + + return unless file + SharedHelpers.filesystem_access(file) do |p| + FileUtils.mkdir_p(p.dirname) + require "bundler/yaml_serializer" + p.open("w") {|f| f.write(YAMLSerializer.dump(hash)) } + end + end + + def converted_value(value, key) + if is_array(key) + to_array(value) + elsif value.nil? + nil + elsif is_bool(key) || value == "false" + to_bool(value) + elsif is_num(key) + value.to_i + else + value.to_s + end + end + + def global_config_file + if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty? + Pathname.new(ENV["BUNDLE_CONFIG"]) + else + begin + Bundler.user_bundle_path("config") + rescue PermissionError, GenericSystemCallError + nil + end + end + end + + def local_config_file + Pathname.new(@root).join("config") if @root + end + + CONFIG_REGEX = %r{ # rubocop:disable Style/RegexpLiteral + ^ + (BUNDLE_.+):\s # the key + (?: !\s)? # optional exclamation mark found with ruby 1.9.3 + (['"]?) # optional opening quote + (.* # contents of the value + (?: # optionally, up until the next key + (\n(?!BUNDLE).+)* + ) + ) + \2 # matching closing quote + $ + }xo + + def load_config(config_file) + return {} if !config_file || ignore_config? + SharedHelpers.filesystem_access(config_file, :read) do |file| + valid_file = file.exist? && !file.size.zero? + return {} unless valid_file + require "bundler/yaml_serializer" + YAMLSerializer.load file.read + end + end + + PER_URI_OPTIONS = %w[ + fallback_timeout + ].freeze + + NORMALIZE_URI_OPTIONS_PATTERN = + / + \A + (\w+\.)? # optional prefix key + (https?.*?) # URI + (\.#{Regexp.union(PER_URI_OPTIONS)})? # optional suffix key + \z + /ix + + # TODO: duplicates Rubygems#normalize_uri + # TODO: is this the correct place to validate mirror URIs? + def self.normalize_uri(uri) + uri = uri.to_s + if uri =~ NORMALIZE_URI_OPTIONS_PATTERN + prefix = $1 + uri = $2 + suffix = $3 + end + uri = "#{uri}/" unless uri.end_with?("/") + uri = URI(uri) + unless uri.absolute? + raise ArgumentError, format("Gem sources must be absolute. You provided '%s'.", uri) + end + "#{prefix}#{uri}#{suffix}" + end + end +end diff --git a/lib/bundler/settings/validator.rb b/lib/bundler/settings/validator.rb new file mode 100644 index 00000000000000..0a57ea7f03894f --- /dev/null +++ b/lib/bundler/settings/validator.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Bundler + class Settings + class Validator + class Rule + attr_reader :description + + def initialize(keys, description, &validate) + @keys = keys + @description = description + @validate = validate + end + + def validate!(key, value, settings) + instance_exec(key, value, settings, &@validate) + end + + def fail!(key, value, *reasons) + reasons.unshift @description + raise InvalidOption, "Setting `#{key}` to #{value.inspect} failed:\n#{reasons.map {|r| " - #{r}" }.join("\n")}" + end + + def set(settings, key, value, *reasons) + hash_key = k(key) + return if settings[hash_key] == value + reasons.unshift @description + Bundler.ui.info "Setting `#{key}` to #{value.inspect}, since #{reasons.join(", ")}" + if value.nil? + settings.delete(hash_key) + else + settings[hash_key] = value + end + end + + def k(key) + Bundler.settings.key_for(key) + end + end + + def self.rules + @rules ||= Hash.new {|h, k| h[k] = [] } + end + private_class_method :rules + + def self.rule(keys, description, &blk) + rule = Rule.new(keys, description, &blk) + keys.each {|k| rules[k] << rule } + end + private_class_method :rule + + def self.validate!(key, value, settings) + rules_to_validate = rules[key] + rules_to_validate.each {|rule| rule.validate!(key, value, settings) } + end + + rule %w[path path.system], "path and path.system are mutually exclusive" do |key, value, settings| + if key == "path" && value + set(settings, "path.system", nil) + elsif key == "path.system" && value + set(settings, :path, nil) + end + end + + rule %w[with without], "a group cannot be in both `with` & `without` simultaneously" do |key, value, settings| + with = settings.fetch(k(:with), "").split(":").map(&:to_sym) + without = settings.fetch(k(:without), "").split(":").map(&:to_sym) + + other_key = key == "with" ? :without : :with + other_setting = key == "with" ? without : with + + conflicting = with & without + if conflicting.any? + fail!(key, value, "`#{other_key}` is current set to #{other_setting.inspect}", "the `#{conflicting.join("`, `")}` groups conflict") + end + end + + rule %w[path], "relative paths are expanded relative to the current working directory" do |key, value, settings| + next if value.nil? + + path = Pathname.new(value) + next if !path.relative? || !Bundler.feature_flag.path_relative_to_cwd? + + path = path.expand_path + + root = begin + Bundler.root + rescue GemfileNotFound + Pathname.pwd.expand_path + end + + path = begin + path.relative_path_from(root) + rescue ArgumentError + path + end + + set(settings, key, path.to_s) + end + end + end +end diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb new file mode 100644 index 00000000000000..0ab582c2a2398e --- /dev/null +++ b/lib/bundler/setup.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +return unless defined?(Gem) + +require "bundler/shared_helpers" + +if Bundler::SharedHelpers.in_bundle? + require "bundler" + + if STDOUT.tty? || ENV["BUNDLER_FORCE_TTY"] + begin + Bundler.setup + rescue Bundler::BundlerError => e + puts "\e[31m#{e.message}\e[0m" + puts e.backtrace.join("\n") if ENV["DEBUG"] + if e.is_a?(Bundler::GemNotFound) + puts "\e[33mRun `bundle install` to install missing gems.\e[0m" + end + exit e.status_code + end + else + Bundler.setup + end + + # Add bundler to the load path after disabling system gems + bundler_lib = File.expand_path("../..", __FILE__) + $LOAD_PATH.unshift(bundler_lib) unless $LOAD_PATH.include?(bundler_lib) + + Bundler.ui = nil +end diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb new file mode 100644 index 00000000000000..50214901c5c323 --- /dev/null +++ b/lib/bundler/shared_helpers.rb @@ -0,0 +1,384 @@ +# frozen_string_literal: true + +require "bundler/compatibility_guard" + +require "pathname" +require "rubygems" + +require "bundler/version" +require "bundler/constants" +require "bundler/rubygems_integration" +require "bundler/current_ruby" + +module Gem + class Dependency + # This is only needed for RubyGems < 1.4 + unless method_defined? :requirement + def requirement + version_requirements + end + end + end +end + +module Bundler + module SharedHelpers + def root + gemfile = find_gemfile + raise GemfileNotFound, "Could not locate Gemfile" unless gemfile + Pathname.new(gemfile).untaint.expand_path.parent + end + + def default_gemfile + gemfile = find_gemfile(:order_matters) + raise GemfileNotFound, "Could not locate Gemfile" unless gemfile + Pathname.new(gemfile).untaint.expand_path + end + + def default_lockfile + gemfile = default_gemfile + + case gemfile.basename.to_s + when "gems.rb" then Pathname.new(gemfile.sub(/.rb$/, ".locked")) + else Pathname.new("#{gemfile}.lock") + end.untaint + end + + def default_bundle_dir + bundle_dir = find_directory(".bundle") + return nil unless bundle_dir + + bundle_dir = Pathname.new(bundle_dir) + + global_bundle_dir = Bundler.user_home.join(".bundle") + return nil if bundle_dir == global_bundle_dir + + bundle_dir + end + + def in_bundle? + find_gemfile + end + + def chdir(dir, &blk) + Bundler.rubygems.ext_lock.synchronize do + Dir.chdir dir, &blk + end + end + + def pwd + Bundler.rubygems.ext_lock.synchronize do + Pathname.pwd + end + end + + def with_clean_git_env(&block) + keys = %w[GIT_DIR GIT_WORK_TREE] + old_env = keys.inject({}) do |h, k| + h.update(k => ENV[k]) + end + + keys.each {|key| ENV.delete(key) } + + block.call + ensure + keys.each {|key| ENV[key] = old_env[key] } + end + + def set_bundle_environment + set_bundle_variables + set_path + set_rubyopt + set_rubylib + end + + # Rescues permissions errors raised by file system operations + # (ie. Errno:EACCESS, Errno::EAGAIN) and raises more friendly errors instead. + # + # @param path [String] the path that the action will be attempted to + # @param action [Symbol, #to_s] the type of operation that will be + # performed. For example: :write, :read, :exec + # + # @yield path + # + # @raise [Bundler::PermissionError] if Errno:EACCES is raised in the + # given block + # @raise [Bundler::TemporaryResourceError] if Errno:EAGAIN is raised in the + # given block + # + # @example + # filesystem_access("vendor/cache", :write) do + # FileUtils.mkdir_p("vendor/cache") + # end + # + # @see {Bundler::PermissionError} + def filesystem_access(path, action = :write, &block) + # Use block.call instead of yield because of a bug in Ruby 2.2.2 + # See https://github.com/bundler/bundler/issues/5341 for details + block.call(path.dup.untaint) + rescue Errno::EACCES + raise PermissionError.new(path, action) + rescue Errno::EAGAIN + raise TemporaryResourceError.new(path, action) + rescue Errno::EPROTO + raise VirtualProtocolError.new + rescue Errno::ENOSPC + raise NoSpaceOnDeviceError.new(path, action) + rescue *[const_get_safely(:ENOTSUP, Errno)].compact + raise OperationNotSupportedError.new(path, action) + rescue Errno::EEXIST, Errno::ENOENT + raise + rescue SystemCallError => e + raise GenericSystemCallError.new(e, "There was an error accessing `#{path}`.") + end + + def const_get_safely(constant_name, namespace) + const_in_namespace = namespace.constants.include?(constant_name.to_s) || + namespace.constants.include?(constant_name.to_sym) + return nil unless const_in_namespace + namespace.const_get(constant_name) + end + + def major_deprecation(major_version, message) + if Bundler.bundler_major_version >= major_version + require "bundler/errors" + raise DeprecatedError, "[REMOVED FROM #{major_version}.0] #{message}" + end + + return unless prints_major_deprecations? + @major_deprecation_ui ||= Bundler::UI::Shell.new("no-color" => true) + ui = Bundler.ui.is_a?(@major_deprecation_ui.class) ? Bundler.ui : @major_deprecation_ui + ui.warn("[DEPRECATED FOR #{major_version}.0] #{message}") + end + + def print_major_deprecations! + multiple_gemfiles = search_up(".") do |dir| + gemfiles = gemfile_names.select {|gf| File.file? File.expand_path(gf, dir) } + next if gemfiles.empty? + break false if gemfiles.size == 1 + end + if multiple_gemfiles && Bundler.bundler_major_version == 2 + Bundler::SharedHelpers.major_deprecation 3, \ + "gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock." + end + + if RUBY_VERSION < "2" + major_deprecation(2, "Bundler will only support ruby >= 2.0, you are running #{RUBY_VERSION}") + end + return if Bundler.rubygems.provides?(">= 2") + major_deprecation(2, "Bundler will only support rubygems >= 2.0, you are running #{Bundler.rubygems.version}") + end + + def trap(signal, override = false, &block) + prior = Signal.trap(signal) do + block.call + prior.call unless override + end + end + + def ensure_same_dependencies(spec, old_deps, new_deps) + new_deps = new_deps.reject {|d| d.type == :development } + old_deps = old_deps.reject {|d| d.type == :development } + + without_type = proc {|d| Gem::Dependency.new(d.name, d.requirements_list.sort) } + new_deps.map!(&without_type) + old_deps.map!(&without_type) + + extra_deps = new_deps - old_deps + return if extra_deps.empty? + + Bundler.ui.debug "#{spec.full_name} from #{spec.remote} has either corrupted API or lockfile dependencies" \ + " (was expecting #{old_deps.map(&:to_s)}, but the real spec has #{new_deps.map(&:to_s)})" + raise APIResponseMismatchError, + "Downloading #{spec.full_name} revealed dependencies not in the API or the lockfile (#{extra_deps.join(", ")})." \ + "\nEither installing with `--full-index` or running `bundle update #{spec.name}` should fix the problem." + end + + def pretty_dependency(dep, print_source = false) + msg = String.new(dep.name) + msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default + + if dep.is_a?(Bundler::Dependency) + platform_string = dep.platforms.join(", ") + msg << " " << platform_string if !platform_string.empty? && platform_string != Gem::Platform::RUBY + end + + msg << " from the `#{dep.source}` source" if print_source && dep.source + msg + end + + def md5_available? + return @md5_available if defined?(@md5_available) + @md5_available = begin + require "openssl" + OpenSSL::Digest::MD5.digest("") + true + rescue LoadError + true + rescue OpenSSL::Digest::DigestError + false + end + end + + def digest(name) + require "digest" + Digest(name) + end + + def write_to_gemfile(gemfile_path, contents) + filesystem_access(gemfile_path) {|g| File.open(g, "w") {|file| file.puts contents } } + end + + private + + def validate_bundle_path + path_separator = Bundler.rubygems.path_separator + return unless Bundler.bundle_path.to_s.split(path_separator).size > 1 + message = "Your bundle path contains text matching #{path_separator.inspect}, " \ + "which is the path separator for your system. Bundler cannot " \ + "function correctly when the Bundle path contains the " \ + "system's PATH separator. Please change your " \ + "bundle path to not match #{path_separator.inspect}." \ + "\nYour current bundle path is '#{Bundler.bundle_path}'." + raise Bundler::PathError, message + end + + def find_gemfile(order_matters = false) + given = ENV["BUNDLE_GEMFILE"] + return given if given && !given.empty? + names = gemfile_names + names.reverse! if order_matters && Bundler.feature_flag.prefer_gems_rb? + find_file(*names) + end + + def gemfile_names + ["Gemfile", "gems.rb"] + end + + def find_file(*names) + search_up(*names) do |filename| + return filename if File.file?(filename) + end + end + + def find_directory(*names) + search_up(*names) do |dirname| + return dirname if File.directory?(dirname) + end + end + + def search_up(*names) + previous = nil + current = File.expand_path(SharedHelpers.pwd).untaint + + until !File.directory?(current) || current == previous + if ENV["BUNDLE_SPEC_RUN"] + # avoid stepping above the tmp directory when testing + gemspec = if ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"] + # for Ruby Core + "lib/bundler.gemspec" + else + "bundler.gemspec" + end + + # avoid stepping above the tmp directory when testing + return nil if File.file?(File.join(current, gemspec)) + end + + names.each do |name| + filename = File.join(current, name) + yield filename + end + previous = current + current = File.expand_path("..", current) + end + end + + def set_env(key, value) + raise ArgumentError, "new key #{key}" unless EnvironmentPreserver::BUNDLER_KEYS.include?(key) + orig_key = "#{EnvironmentPreserver::BUNDLER_PREFIX}#{key}" + orig = ENV[key] + orig ||= EnvironmentPreserver::INTENTIONALLY_NIL + ENV[orig_key] ||= orig + + ENV[key] = value + end + public :set_env + + def set_bundle_variables + begin + exe_file = Bundler.rubygems.bin_path("bundler", "bundle", VERSION) + unless File.exist?(exe_file) + exe_file = File.expand_path("../../../exe/bundle", __FILE__) + end + Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", exe_file + rescue Gem::GemNotFoundException + exe_file = File.expand_path("../../../exe/bundle", __FILE__) + # for Ruby core repository + exe_file = File.expand_path("../../../../bin/bundle", __FILE__) unless File.exist?(exe_file) + Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", exe_file + end + + # Set BUNDLE_GEMFILE + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", find_gemfile(:order_matters).to_s + Bundler::SharedHelpers.set_env "BUNDLER_VERSION", Bundler::VERSION + end + + def set_path + validate_bundle_path + paths = (ENV["PATH"] || "").split(File::PATH_SEPARATOR) + paths.unshift "#{Bundler.bundle_path}/bin" + Bundler::SharedHelpers.set_env "PATH", paths.uniq.join(File::PATH_SEPARATOR) + end + + def set_rubyopt + rubyopt = [ENV["RUBYOPT"]].compact + return if !rubyopt.empty? && rubyopt.first =~ %r{-rbundler/setup} + rubyopt.unshift %(-rbundler/setup) + Bundler::SharedHelpers.set_env "RUBYOPT", rubyopt.join(" ") + end + + def set_rubylib + rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR) + rubylib.unshift bundler_ruby_lib + Bundler::SharedHelpers.set_env "RUBYLIB", rubylib.uniq.join(File::PATH_SEPARATOR) + end + + def bundler_ruby_lib + resolve_path File.expand_path("../..", __FILE__) + end + + def clean_load_path + # handle 1.9 where system gems are always on the load path + return unless defined?(::Gem) + + bundler_lib = bundler_ruby_lib + + loaded_gem_paths = Bundler.rubygems.loaded_gem_paths + + $LOAD_PATH.reject! do |p| + next if resolve_path(p).start_with?(bundler_lib) + loaded_gem_paths.delete(p) + end + $LOAD_PATH.uniq! + end + + def resolve_path(path) + expanded = File.expand_path(path) + return expanded unless File.respond_to?(:realpath) && File.exist?(expanded) + + File.realpath(expanded) + end + + def prints_major_deprecations? + require "bundler" + deprecation_release = Bundler::VERSION.split(".").drop(1).include?("99") + return false if !deprecation_release && !Bundler.settings[:major_deprecations] + require "bundler/deprecate" + return false if Bundler::Deprecate.skip + true + end + + extend self + end +end diff --git a/lib/bundler/similarity_detector.rb b/lib/bundler/similarity_detector.rb new file mode 100644 index 00000000000000..b7f3ee7afaddf6 --- /dev/null +++ b/lib/bundler/similarity_detector.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Bundler + class SimilarityDetector + SimilarityScore = Struct.new(:string, :distance) + + # initialize with an array of words to be matched against + def initialize(corpus) + @corpus = corpus + end + + # return an array of words similar to 'word' from the corpus + def similar_words(word, limit = 3) + words_by_similarity = @corpus.map {|w| SimilarityScore.new(w, levenshtein_distance(word, w)) } + words_by_similarity.select {|s| s.distance <= limit }.sort_by(&:distance).map(&:string) + end + + # return the result of 'similar_words', concatenated into a list + # (eg "a, b, or c") + def similar_word_list(word, limit = 3) + words = similar_words(word, limit) + if words.length == 1 + words[0] + elsif words.length > 1 + [words[0..-2].join(", "), words[-1]].join(" or ") + end + end + + protected + + # http://www.informit.com/articles/article.aspx?p=683059&seqNum=36 + def levenshtein_distance(this, that, ins = 2, del = 2, sub = 1) + # ins, del, sub are weighted costs + return nil if this.nil? + return nil if that.nil? + dm = [] # distance matrix + + # Initialize first row values + dm[0] = (0..this.length).collect {|i| i * ins } + fill = [0] * (this.length - 1) + + # Initialize first column values + (1..that.length).each do |i| + dm[i] = [i * del, fill.flatten] + end + + # populate matrix + (1..that.length).each do |i| + (1..this.length).each do |j| + # critical comparison + dm[i][j] = [ + dm[i - 1][j - 1] + (this[j - 1] == that[i - 1] ? 0 : sub), + dm[i][j - 1] + ins, + dm[i - 1][j] + del + ].min + end + end + + # The last value in matrix is the Levenshtein distance between the strings + dm[that.length][this.length] + end + end +end diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb new file mode 100644 index 00000000000000..26a3625bb17578 --- /dev/null +++ b/lib/bundler/source.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Bundler + class Source + autoload :Gemspec, "bundler/source/gemspec" + autoload :Git, "bundler/source/git" + autoload :Metadata, "bundler/source/metadata" + autoload :Path, "bundler/source/path" + autoload :Rubygems, "bundler/source/rubygems" + + attr_accessor :dependency_names + + def unmet_deps + specs.unmet_dependency_names + end + + def version_message(spec) + message = "#{spec.name} #{spec.version}" + message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil? + + if Bundler.locked_gems + locked_spec = Bundler.locked_gems.specs.find {|s| s.name == spec.name } + locked_spec_version = locked_spec.version if locked_spec + if locked_spec_version && spec.version != locked_spec_version + message += Bundler.ui.add_color(" (was #{locked_spec_version})", version_color(spec.version, locked_spec_version)) + end + end + + message + end + + def can_lock?(spec) + spec.source == self + end + + # it's possible that gems from one source depend on gems from some + # other source, so now we download gemspecs and iterate over those + # dependencies, looking for gems we don't have info on yet. + def double_check_for(*); end + + def dependency_names_to_double_check + specs.dependency_names + end + + def include?(other) + other == self + end + + def inspect + "#<#{self.class}:0x#{object_id} #{self}>" + end + + def path? + instance_of?(Bundler::Source::Path) + end + + def extension_cache_path(spec) + return unless Bundler.feature_flag.global_gem_cache? + return unless source_slug = extension_cache_slug(spec) + Bundler.user_cache.join( + "extensions", Gem::Platform.local.to_s, Bundler.ruby_scope, + source_slug, spec.full_name + ) + end + + private + + def version_color(spec_version, locked_spec_version) + if Gem::Version.correct?(spec_version) && Gem::Version.correct?(locked_spec_version) + # display yellow if there appears to be a regression + earlier_version?(spec_version, locked_spec_version) ? :yellow : :green + else + # default to green if the versions cannot be directly compared + :green + end + end + + def earlier_version?(spec_version, locked_spec_version) + Gem::Version.new(spec_version) < Gem::Version.new(locked_spec_version) + end + + def print_using_message(message) + if !message.include?("(was ") && Bundler.feature_flag.suppress_install_using_messages? + Bundler.ui.debug message + else + Bundler.ui.info message + end + end + + def extension_cache_slug(_) + nil + end + end +end diff --git a/lib/bundler/source/gemspec.rb b/lib/bundler/source/gemspec.rb new file mode 100644 index 00000000000000..7e3447e7765402 --- /dev/null +++ b/lib/bundler/source/gemspec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Bundler + class Source + class Gemspec < Path + attr_reader :gemspec + + def initialize(options) + super + @gemspec = options["gemspec"] + end + + def as_path_source + Path.new(options) + end + end + end +end diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb new file mode 100644 index 00000000000000..0b00608bdd6177 --- /dev/null +++ b/lib/bundler/source/git.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true + +require "bundler/vendored_fileutils" +require "uri" + +module Bundler + class Source + class Git < Path + autoload :GitProxy, "bundler/source/git/git_proxy" + + attr_reader :uri, :ref, :branch, :options, :submodules + + def initialize(options) + @options = options + @glob = options["glob"] || DEFAULT_GLOB + + @allow_cached = false + @allow_remote = false + + # Stringify options that could be set as symbols + %w[ref branch tag revision].each {|k| options[k] = options[k].to_s if options[k] } + + @uri = options["uri"] || "" + @safe_uri = URICredentialsFilter.credential_filtered_uri(@uri) + @branch = options["branch"] + @ref = options["ref"] || options["branch"] || options["tag"] || "master" + @submodules = options["submodules"] + @name = options["name"] + @version = options["version"].to_s.strip.gsub("-", ".pre.") + + @copied = false + @local = false + end + + def self.from_lock(options) + new(options.merge("uri" => options.delete("remote"))) + end + + def to_lock + out = String.new("GIT\n") + out << " remote: #{@uri}\n" + out << " revision: #{revision}\n" + %w[ref branch tag submodules].each do |opt| + out << " #{opt}: #{options[opt]}\n" if options[opt] + end + out << " glob: #{@glob}\n" unless @glob == DEFAULT_GLOB + out << " specs:\n" + end + + def hash + [self.class, uri, ref, branch, name, version, submodules].hash + end + + def eql?(other) + other.is_a?(Git) && uri == other.uri && ref == other.ref && + branch == other.branch && name == other.name && + version == other.version && submodules == other.submodules + end + + alias_method :==, :eql? + + def to_s + at = if local? + path + elsif user_ref = options["ref"] + if ref =~ /\A[a-z0-9]{4,}\z/i + shortref_for_display(user_ref) + else + user_ref + end + else + ref + end + + rev = begin + "@#{shortref_for_display(revision)}" + rescue GitError + nil + end + + "#{@safe_uri} (at #{at}#{rev})" + end + + def name + File.basename(@uri, ".git") + end + + # This is the path which is going to contain a specific + # checkout of the git repository. When using local git + # repos, this is set to the local repo. + def install_path + @install_path ||= begin + git_scope = "#{base_name}-#{shortref_for_path(revision)}" + + path = Bundler.install_path.join(git_scope) + + if !path.exist? && Bundler.requires_sudo? + Bundler.user_bundle_path.join(Bundler.ruby_scope).join(git_scope) + else + path + end + end + end + + alias_method :path, :install_path + + def extension_dir_name + "#{base_name}-#{shortref_for_path(revision)}" + end + + def unlock! + git_proxy.revision = nil + options["revision"] = nil + + @unlocked = true + end + + def local_override!(path) + return false if local? + + path = Pathname.new(path) + path = path.expand_path(Bundler.root) unless path.relative? + + unless options["branch"] || Bundler.settings[:disable_local_branch_check] + raise GitError, "Cannot use local override for #{name} at #{path} because " \ + ":branch is not specified in Gemfile. Specify a branch or use " \ + "`bundle config --delete` to remove the local override" + end + + unless path.exist? + raise GitError, "Cannot use local override for #{name} because #{path} " \ + "does not exist. Check `bundle config --delete` to remove the local override" + end + + set_local!(path) + + # Create a new git proxy without the cached revision + # so the Gemfile.lock always picks up the new revision. + @git_proxy = GitProxy.new(path, uri, ref) + + if git_proxy.branch != options["branch"] && !Bundler.settings[:disable_local_branch_check] + raise GitError, "Local override for #{name} at #{path} is using branch " \ + "#{git_proxy.branch} but Gemfile specifies #{options["branch"]}" + end + + changed = cached_revision && cached_revision != git_proxy.revision + + if changed && !@unlocked && !git_proxy.contains?(cached_revision) + raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(cached_revision)} " \ + "but the current branch in your local override for #{name} does not contain such commit. " \ + "Please make sure your branch is up to date." + end + + changed + end + + def specs(*) + set_local!(app_cache_path) if has_app_cache? && !local? + + if requires_checkout? && !@copied + fetch + git_proxy.copy_to(install_path, submodules) + serialize_gemspecs_in(install_path) + @copied = true + end + + local_specs + end + + def install(spec, options = {}) + force = options[:force] + + print_using_message "Using #{version_message(spec)} from #{self}" + + if (requires_checkout? && !@copied) || force + Bundler.ui.debug " * Checking out revision: #{ref}" + git_proxy.copy_to(install_path, submodules) + serialize_gemspecs_in(install_path) + @copied = true + end + + generate_bin_options = { :disable_extensions => !Bundler.rubygems.spec_missing_extensions?(spec), :build_args => options[:build_args] } + generate_bin(spec, generate_bin_options) + + requires_checkout? ? spec.post_install_message : nil + end + + def cache(spec, custom_path = nil) + app_cache_path = app_cache_path(custom_path) + return unless Bundler.feature_flag.cache_all? + return if path == app_cache_path + cached! + FileUtils.rm_rf(app_cache_path) + git_proxy.checkout if requires_checkout? + git_proxy.copy_to(app_cache_path, @submodules) + serialize_gemspecs_in(app_cache_path) + end + + def load_spec_files + super + rescue PathError => e + Bundler.ui.trace e + raise GitError, "#{self} is not yet checked out. Run `bundle install` first." + end + + # This is the path which is going to contain a cache + # of the git repository. When using the same git repository + # across different projects, this cache will be shared. + # When using local git repos, this is set to the local repo. + def cache_path + @cache_path ||= begin + if Bundler.requires_sudo? || Bundler.feature_flag.global_gem_cache? + Bundler.user_cache + else + Bundler.bundle_path.join("cache", "bundler") + end.join("git", git_scope) + end + end + + def app_cache_dirname + "#{base_name}-#{shortref_for_path(cached_revision || revision)}" + end + + def revision + git_proxy.revision + end + + def allow_git_ops? + @allow_remote || @allow_cached + end + + private + + def serialize_gemspecs_in(destination) + destination = destination.expand_path(Bundler.root) if destination.relative? + Dir["#{destination}/#{@glob}"].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. + spec = Bundler.load_gemspec(spec_path) + next unless spec + Bundler.rubygems.set_installed_by_version(spec) + Bundler.rubygems.validate(spec) + File.open(spec_path, "wb") {|file| file.write(spec.to_ruby) } + end + end + + def set_local!(path) + @local = true + @local_specs = @git_proxy = nil + @cache_path = @install_path = path + end + + def has_app_cache? + cached_revision && super + end + + def local? + @local + end + + def requires_checkout? + allow_git_ops? && !local? + end + + def base_name + File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git") + end + + def shortref_for_display(ref) + ref[0..6] + end + + def shortref_for_path(ref) + ref[0..11] + end + + def uri_hash + if uri =~ %r{^\w+://(\w+@)?} + # Downcase the domain component of the URI + # and strip off a trailing slash, if one is present + input = URI.parse(uri).normalize.to_s.sub(%r{/$}, "") + else + # If there is no URI scheme, assume it is an ssh/git URI + input = uri + end + SharedHelpers.digest(:SHA1).hexdigest(input) + end + + def cached_revision + options["revision"] + end + + def cached? + cache_path.exist? + end + + def git_proxy + @git_proxy ||= GitProxy.new(cache_path, uri, ref, cached_revision, self) + end + + def fetch + git_proxy.checkout + rescue GitError => e + raise unless Bundler.feature_flag.allow_offline_install? + Bundler.ui.warn "Using cached git data because of network errors:\n#{e}" + end + + # no-op, since we validate when re-serializing the gemspec + def validate_spec(_spec); end + + if Bundler.rubygems.stubs_provide_full_functionality? + def load_gemspec(file) + stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent) + stub.full_gem_path = Pathname.new(file).dirname.expand_path(root).to_s.untaint + StubSpecification.from_stub(stub) + end + end + + def git_scope + "#{base_name}-#{uri_hash}" + end + + def extension_cache_slug(_) + extension_dir_name + end + end + end +end diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb new file mode 100644 index 00000000000000..cd964f7e56491a --- /dev/null +++ b/lib/bundler/source/git/git_proxy.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require "shellwords" +require "tempfile" +module Bundler + class Source + class Git + class GitNotInstalledError < GitError + def initialize + msg = String.new + msg << "You need to install git to be able to use gems from git repositories. " + msg << "For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git" + super msg + end + end + + class GitNotAllowedError < GitError + def initialize(command) + msg = String.new + msg << "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, " + msg << "this error message could probably be more useful. Please submit a ticket at http://github.com/bundler/bundler/issues " + msg << "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}" + super msg + end + end + + class GitCommandError < GitError + def initialize(command, path = nil, extra_info = nil) + msg = String.new + msg << "Git error: command `git #{command}` in directory #{SharedHelpers.pwd} has failed." + msg << "\n#{extra_info}" if extra_info + msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path && path.exist? + super msg + end + end + + class MissingGitRevisionError < GitError + def initialize(ref, repo) + msg = "Revision #{ref} does not exist in the repository #{repo}. Maybe you misspelled it?" + super msg + end + end + + # The GitProxy is responsible to interact with git repositories. + # All actions required by the Git source is encapsulated in this + # object. + class GitProxy + attr_accessor :path, :uri, :ref + attr_writer :revision + + def initialize(path, uri, ref, revision = nil, git = nil) + @path = path + @uri = uri + @ref = ref + @revision = revision + @git = git + raise GitNotInstalledError.new if allow? && !Bundler.git_present? + end + + def revision + return @revision if @revision + + begin + @revision ||= find_local_revision + rescue GitCommandError + raise MissingGitRevisionError.new(ref, URICredentialsFilter.credential_filtered_uri(uri)) + end + + @revision + end + + def branch + @branch ||= allowed_in_path do + git("rev-parse --abbrev-ref HEAD").strip + end + end + + def contains?(commit) + allowed_in_path do + result = git_null("branch --contains #{commit}") + $? == 0 && result =~ /^\* (.*)$/ + end + end + + def version + git("--version").match(/(git version\s*)?((\.?\d+)+).*/)[2] + end + + def full_version + git("--version").sub("git version", "").strip + end + + def checkout + return if path.exist? && has_revision_cached? + extra_ref = "#{Shellwords.shellescape(ref)}:#{Shellwords.shellescape(ref)}" if ref && ref.start_with?("refs/") + + Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" + + unless path.exist? + SharedHelpers.filesystem_access(path.dirname) do |p| + FileUtils.mkdir_p(p) + end + git_retry %(clone #{uri_escaped_with_configured_credentials} "#{path}" --bare --no-hardlinks --quiet) + return unless extra_ref + end + + in_path do + git_retry %(fetch --force --quiet --tags #{uri_escaped_with_configured_credentials} "refs/heads/*:refs/heads/*" #{extra_ref}) + end + end + + def copy_to(destination, submodules = false) + # method 1 + unless File.exist?(destination.join(".git")) + begin + SharedHelpers.filesystem_access(destination.dirname) do |p| + FileUtils.mkdir_p(p) + end + SharedHelpers.filesystem_access(destination) do |p| + FileUtils.rm_rf(p) + end + git_retry %(clone --no-checkout --quiet "#{path}" "#{destination}") + File.chmod(((File.stat(destination).mode | 0o777) & ~File.umask), destination) + rescue Errno::EEXIST => e + file_path = e.message[%r{.*?(/.*)}, 1] + raise GitError, "Bundler could not install a gem because it needs to " \ + "create a directory, but a file exists - #{file_path}. Please delete " \ + "this file and try again." + end + end + # method 2 + SharedHelpers.chdir(destination) do + git_retry %(fetch --force --quiet --tags "#{path}") + + begin + git "reset --hard #{@revision}" + rescue GitCommandError + raise MissingGitRevisionError.new(@revision, URICredentialsFilter.credential_filtered_uri(uri)) + end + + if submodules + git_retry "submodule update --init --recursive" + elsif Gem::Version.create(version) >= Gem::Version.create("2.9.0") + git_retry "submodule deinit --all --force" + end + end + end + + private + + # TODO: Do not rely on /dev/null. + # Given that open3 is not cross platform until Ruby 1.9.3, + # the best solution is to pipe to /dev/null if it exists. + # If it doesn't, everything will work fine, but the user + # will get the $stderr messages as well. + def git_null(command) + git("#{command} 2>#{Bundler::NULL}", false) + end + + def git_retry(command) + Bundler::Retry.new("`git #{URICredentialsFilter.credential_filtered_string(command, uri)}`", GitNotAllowedError).attempts do + git(command) + end + end + + def git(command, check_errors = true, error_msg = nil) + command_with_no_credentials = URICredentialsFilter.credential_filtered_string(command, uri) + raise GitNotAllowedError.new(command_with_no_credentials) unless allow? + + out = SharedHelpers.with_clean_git_env do + capture_and_filter_stderr(uri) { `git #{command}` } + end + + stdout_with_no_credentials = URICredentialsFilter.credential_filtered_string(out, uri) + raise GitCommandError.new(command_with_no_credentials, path, error_msg) if check_errors && !$?.success? + stdout_with_no_credentials + end + + def has_revision_cached? + return unless @revision + in_path { git("cat-file -e #{@revision}") } + true + rescue GitError + false + end + + def remove_cache + FileUtils.rm_rf(path) + end + + def find_local_revision + allowed_in_path do + git("rev-parse --verify #{Shellwords.shellescape(ref)}", true).strip + end + end + + # Escape the URI for git commands + def uri_escaped_with_configured_credentials + remote = configured_uri_for(uri) + if Bundler::WINDOWS + # Windows quoting requires double quotes only, with double quotes + # inside the string escaped by being doubled. + '"' + remote.gsub('"') { '""' } + '"' + else + # Bash requires single quoted strings, with the single quotes escaped + # by ending the string, escaping the quote, and restarting the string. + "'" + remote.gsub("'") { "'\\''" } + "'" + end + end + + # Adds credentials to the URI as Fetcher#configured_uri_for does + def configured_uri_for(uri) + if /https?:/ =~ uri + remote = URI(uri) + config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host] + remote.userinfo ||= config_auth + remote.to_s + else + uri + end + end + + def allow? + @git ? @git.allow_git_ops? : true + end + + def in_path(&blk) + checkout unless path.exist? + _ = URICredentialsFilter # load it before we chdir + SharedHelpers.chdir(path, &blk) + end + + def allowed_in_path + return in_path { yield } if allow? + raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application" + end + + # TODO: Replace this with Open3 when upgrading to bundler 2 + # Similar to #git_null, as Open3 is not cross-platform, + # a temporary way is to use Tempfile to capture the stderr. + # When replacing this using Open3, make sure git_null is + # also replaced by Open3, so stdout and stderr all got handled properly. + def capture_and_filter_stderr(uri) + return_value, captured_err = "" + backup_stderr = STDERR.dup + begin + Tempfile.open("captured_stderr") do |f| + STDERR.reopen(f) + return_value = yield + f.rewind + captured_err = f.read + end + ensure + STDERR.reopen backup_stderr + end + $stderr.puts URICredentialsFilter.credential_filtered_string(captured_err, uri) if uri && !captured_err.empty? + return_value + end + end + end + end +end diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb new file mode 100644 index 00000000000000..9c5657eef61e73 --- /dev/null +++ b/lib/bundler/source/metadata.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Bundler + class Source + class Metadata < Source + def specs + @specs ||= Index.build do |idx| + idx << Gem::Specification.new("ruby\0", RubyVersion.system.to_gem_version_with_patchlevel) + idx << Gem::Specification.new("rubygems\0", Gem::VERSION) + + idx << Gem::Specification.new do |s| + s.name = "bundler" + s.version = VERSION + s.platform = Gem::Platform::RUBY + s.source = self + s.authors = ["bundler team"] + s.bindir = "exe" + s.executables = %w[bundle] + # can't point to the actual gemspec or else the require paths will be wrong + s.loaded_from = File.expand_path("..", __FILE__) + end + if loaded_spec = Bundler.rubygems.loaded_specs("bundler") + idx << loaded_spec # this has to come after the fake gemspec, to override it + elsif local_spec = Bundler.rubygems.find_name("bundler").find {|s| s.version.to_s == VERSION } + idx << local_spec + end + + idx.each {|s| s.source = self } + end + end + + def cached!; end + + def remote!; end + + def options + {} + end + + def install(spec, _opts = {}) + print_using_message "Using #{version_message(spec)}" + nil + end + + def to_s + "the local ruby installation" + end + + def ==(other) + self.class == other.class + end + alias_method :eql?, :== + + def hash + self.class.hash + end + + def version_message(spec) + "#{spec.name} #{spec.version}" + end + end + end +end diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb new file mode 100644 index 00000000000000..ed734bf549aae6 --- /dev/null +++ b/lib/bundler/source/path.rb @@ -0,0 +1,249 @@ +# frozen_string_literal: true + +module Bundler + class Source + class Path < Source + autoload :Installer, "bundler/source/path/installer" + + attr_reader :path, :options, :root_path, :original_path + attr_writer :name + attr_accessor :version + + protected :original_path + + DEFAULT_GLOB = "{,*,*/*}.gemspec".freeze + + def initialize(options) + @options = options.dup + @glob = options["glob"] || DEFAULT_GLOB + + @allow_cached = false + @allow_remote = false + + @root_path = options["root_path"] || Bundler.root + + if options["path"] + @path = Pathname.new(options["path"]) + @path = expand(@path) unless @path.relative? + end + + @name = options["name"] + @version = options["version"] + + # Stores the original path. If at any point we move to the + # cached directory, we still have the original path to copy from. + @original_path = @path + end + + def remote! + @local_specs = nil + @allow_remote = true + end + + def cached! + @local_specs = nil + @allow_cached = true + end + + def self.from_lock(options) + new(options.merge("path" => options.delete("remote"))) + end + + def to_lock + out = String.new("PATH\n") + out << " remote: #{lockfile_path}\n" + out << " glob: #{@glob}\n" unless @glob == DEFAULT_GLOB + out << " specs:\n" + end + + def to_s + "source at `#{@path}`" + end + + def hash + [self.class, expanded_path, version].hash + end + + def eql?(other) + return unless other.class == self.class + expanded_original_path == other.expanded_original_path && + version == other.version + end + + alias_method :==, :eql? + + def name + File.basename(expanded_path.to_s) + end + + def install(spec, options = {}) + print_using_message "Using #{version_message(spec)} from #{self}" + generate_bin(spec, :disable_extensions => true) + nil # no post-install message + end + + def cache(spec, custom_path = nil) + app_cache_path = app_cache_path(custom_path) + return unless Bundler.feature_flag.cache_all? + return if expand(@original_path).to_s.index(root_path.to_s + "/") == 0 + + unless @original_path.exist? + raise GemNotFound, "Can't cache gem #{version_message(spec)} because #{self} is missing!" + end + + FileUtils.rm_rf(app_cache_path) + FileUtils.cp_r("#{@original_path}/.", app_cache_path) + FileUtils.touch(app_cache_path.join(".bundlecache")) + end + + def local_specs(*) + @local_specs ||= load_spec_files + end + + def specs + if has_app_cache? + @path = app_cache_path + @expanded_path = nil # Invalidate + end + local_specs + end + + def app_cache_dirname + name + end + + def root + Bundler.root + end + + def expanded_original_path + @expanded_original_path ||= expand(original_path) + end + + private + + def expanded_path + @expanded_path ||= expand(path) + end + + def expand(somepath) + somepath.expand_path(root_path) + rescue ArgumentError => e + Bundler.ui.debug(e) + raise PathError, "There was an error while trying to use the path " \ + "`#{somepath}`.\nThe error message was: #{e.message}." + end + + def lockfile_path + return relative_path(original_path) if original_path.absolute? + expand(original_path).relative_path_from(Bundler.root) + end + + def app_cache_path(custom_path = nil) + @app_cache_path ||= Bundler.app_cache(custom_path).join(app_cache_dirname) + end + + def has_app_cache? + SharedHelpers.in_bundle? && app_cache_path.exist? + end + + def load_gemspec(file) + return unless spec = Bundler.load_gemspec(file) + Bundler.rubygems.set_installed_by_version(spec) + spec + end + + def validate_spec(spec) + Bundler.rubygems.validate(spec) + end + + def load_spec_files + index = Index.new + + if File.directory?(expanded_path) + # We sort depth-first since `<<` will override the earlier-found specs + Dir["#{expanded_path}/#{@glob}"].sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| + next unless spec = load_gemspec(file) + spec.source = self + + # Validation causes extension_dir to be calculated, which depends + # on #source, so we validate here instead of load_gemspec + validate_spec(spec) + index << spec + end + + if index.empty? && @name && @version + index << Gem::Specification.new do |s| + s.name = @name + s.source = self + s.version = Gem::Version.new(@version) + s.platform = Gem::Platform::RUBY + s.summary = "Fake gemspec for #{@name}" + s.relative_loaded_from = "#{@name}.gemspec" + s.authors = ["no one"] + if expanded_path.join("bin").exist? + executables = expanded_path.join("bin").children + executables.reject! {|p| File.directory?(p) } + s.executables = executables.map {|c| c.basename.to_s } + end + end + end + else + message = String.new("The path `#{expanded_path}` ") + message << if File.exist?(expanded_path) + "is not a directory." + else + "does not exist." + end + raise PathError, message + end + + index + end + + def relative_path(path = self.path) + if path.to_s.start_with?(root_path.to_s) + return path.relative_path_from(root_path) + end + path + end + + def generate_bin(spec, options = {}) + gem_dir = Pathname.new(spec.full_gem_path) + + # Some gem authors put absolute paths in their gemspec + # and we have to save them from themselves + spec.files = spec.files.map do |p| + next p unless p =~ /\A#{Pathname::SEPARATOR_PAT}/ + next if File.directory?(p) + begin + Pathname.new(p).relative_path_from(gem_dir).to_s + rescue ArgumentError + p + end + end.compact + + installer = Path::Installer.new( + spec, + :env_shebang => false, + :disable_extensions => options[:disable_extensions], + :build_args => options[:build_args], + :bundler_extension_cache_path => extension_cache_path(spec) + ) + installer.post_install + rescue Gem::InvalidSpecificationException => e + Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \ + "This prevents bundler from installing bins or native extensions, but " \ + "that may not affect its functionality." + + if !spec.extensions.empty? && !spec.email.empty? + Bundler.ui.warn "If you need to use this package without installing it from a gem " \ + "repository, please contact #{spec.email} and ask them " \ + "to modify their .gemspec so it can work with `gem build`." + end + + Bundler.ui.warn "The validation message from RubyGems was:\n #{e.message}" + end + end + end +end diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb new file mode 100644 index 00000000000000..a0357ffa391a3b --- /dev/null +++ b/lib/bundler/source/path/installer.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Bundler + class Source + class Path + class Installer < Bundler::RubyGemsGemInstaller + attr_reader :spec + + def initialize(spec, options = {}) + @options = options + @spec = spec + @gem_dir = Bundler.rubygems.path(spec.full_gem_path) + @wrappers = true + @env_shebang = true + @format_executable = options[:format_executable] || false + @build_args = options[:build_args] || Bundler.rubygems.build_args + @gem_bin_dir = "#{Bundler.rubygems.gem_dir}/bin" + @disable_extensions = options[:disable_extensions] + + if Bundler.requires_sudo? + @tmp_dir = Bundler.tmp(spec.full_name).to_s + @bin_dir = "#{@tmp_dir}/bin" + else + @bin_dir = @gem_bin_dir + end + end + + def post_install + SharedHelpers.chdir(@gem_dir) do + run_hooks(:pre_install) + + unless @disable_extensions + build_extensions + run_hooks(:post_build) + end + + generate_bin unless spec.executables.nil? || spec.executables.empty? + + run_hooks(:post_install) + end + ensure + Bundler.rm_rf(@tmp_dir) if Bundler.requires_sudo? + end + + private + + def generate_bin + super + + if Bundler.requires_sudo? + SharedHelpers.filesystem_access(@gem_bin_dir) do |p| + Bundler.mkdir_p(p) + end + spec.executables.each do |exe| + Bundler.sudo "cp -R #{@bin_dir}/#{exe} #{@gem_bin_dir}" + end + end + end + + def run_hooks(type) + hooks_meth = "#{type}_hooks" + return unless Gem.respond_to?(hooks_meth) + Gem.send(hooks_meth).each do |hook| + result = hook.call(self) + next unless result == false + location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ + message = "#{type} hook#{location} failed for #{spec.full_name}" + raise InstallHookError, message + end + end + end + end + end +end diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb new file mode 100644 index 00000000000000..485b388a3260e7 --- /dev/null +++ b/lib/bundler/source/rubygems.rb @@ -0,0 +1,539 @@ +# frozen_string_literal: true + +require "uri" +require "rubygems/user_interaction" + +module Bundler + class Source + class Rubygems < Source + autoload :Remote, "bundler/source/rubygems/remote" + + # Use the API when installing less than X gems + API_REQUEST_LIMIT = 500 + # Ask for X gems per API request + API_REQUEST_SIZE = 50 + + attr_reader :remotes, :caches + + def initialize(options = {}) + @options = options + @remotes = [] + @dependency_names = [] + @allow_remote = false + @allow_cached = false + @caches = [cache_path, *Bundler.rubygems.gem_cache] + + Array(options["remotes"] || []).reverse_each {|r| add_remote(r) } + end + + def remote! + @specs = nil + @allow_remote = true + end + + def cached! + @specs = nil + @allow_cached = true + end + + def hash + @remotes.hash + end + + def eql?(other) + other.is_a?(Rubygems) && other.credless_remotes == credless_remotes + end + + alias_method :==, :eql? + + def include?(o) + o.is_a?(Rubygems) && (o.credless_remotes - credless_remotes).empty? + end + + def can_lock?(spec) + return super if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + spec.source.is_a?(Rubygems) + end + + def options + { "remotes" => @remotes.map(&:to_s) } + end + + def self.from_lock(options) + new(options) + end + + def to_lock + out = String.new("GEM\n") + remotes.reverse_each do |remote| + out << " remote: #{suppress_configured_credentials remote}\n" + end + out << " specs:\n" + end + + def to_s + if remotes.empty? + "locally installed gems" + else + remote_names = remotes.map(&:to_s).join(", ") + "rubygems repository #{remote_names} or installed locally" + end + end + alias_method :name, :to_s + + def specs + @specs ||= begin + # remote_specs usually generates a way larger Index than the other + # sources, and large_idx.use small_idx is way faster than + # small_idx.use large_idx. + idx = @allow_remote ? remote_specs.dup : Index.new + idx.use(cached_specs, :override_dupes) if @allow_cached || @allow_remote + idx.use(installed_specs, :override_dupes) + idx + end + end + + def install(spec, opts = {}) + force = opts[:force] + ensure_builtin_gems_cached = opts[:ensure_builtin_gems_cached] + + if ensure_builtin_gems_cached && builtin_gem?(spec) + if !cached_path(spec) + cached_built_in_gem(spec) unless spec.remote + force = true + else + spec.loaded_from = loaded_from(spec) + end + end + + if installed?(spec) && !force + print_using_message "Using #{version_message(spec)}" + return nil # no post-install message + end + + # Download the gem to get the spec, because some specs that are returned + # by rubygems.org are broken and wrong. + if spec.remote + # Check for this spec from other sources + uris = [spec.remote.anonymized_uri] + uris += remotes_for_spec(spec).map(&:anonymized_uri) + uris.uniq! + Installer.ambiguous_gems << [spec.name, *uris] if uris.length > 1 + + s = Bundler.rubygems.spec_from_gem(fetch_gem(spec), Bundler.settings["trust-policy"]) + spec.__swap__(s) + end + + unless Bundler.settings[:no_install] + message = "Installing #{version_message(spec)}" + message += " with native extensions" if spec.extensions.any? + Bundler.ui.confirm message + + path = cached_gem(spec) + if requires_sudo? + install_path = Bundler.tmp(spec.full_name) + bin_path = install_path.join("bin") + else + install_path = rubygems_dir + bin_path = Bundler.system_bindir + end + + Bundler.mkdir_p bin_path, :no_sudo => true unless spec.executables.empty? || Bundler.rubygems.provides?(">= 2.7.5") + + installed_spec = nil + Bundler.rubygems.preserve_paths do + installed_spec = Bundler::RubyGemsGemInstaller.at( + path, + :install_dir => install_path.to_s, + :bin_dir => bin_path.to_s, + :ignore_dependencies => true, + :wrappers => true, + :env_shebang => true, + :build_args => opts[:build_args], + :bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum, + :bundler_extension_cache_path => extension_cache_path(spec) + ).install + end + spec.full_gem_path = installed_spec.full_gem_path + + # SUDO HAX + if requires_sudo? + Bundler.rubygems.repository_subdirectories.each do |name| + src = File.join(install_path, name, "*") + dst = File.join(rubygems_dir, name) + if name == "extensions" && Dir.glob(src).any? + src = File.join(src, "*/*") + ext_src = Dir.glob(src).first + ext_src.gsub!(src[0..-6], "") + dst = File.dirname(File.join(dst, ext_src)) + end + SharedHelpers.filesystem_access(dst) do |p| + Bundler.mkdir_p(p) + end + Bundler.sudo "cp -R #{src} #{dst}" if Dir[src].any? + end + + spec.executables.each do |exe| + SharedHelpers.filesystem_access(Bundler.system_bindir) do |p| + Bundler.mkdir_p(p) + end + Bundler.sudo "cp -R #{install_path}/bin/#{exe} #{Bundler.system_bindir}/" + end + end + installed_spec.loaded_from = loaded_from(spec) + end + spec.loaded_from = loaded_from(spec) + + spec.post_install_message + ensure + Bundler.rm_rf(install_path) if requires_sudo? + end + + def cache(spec, custom_path = nil) + if builtin_gem?(spec) + cached_path = cached_built_in_gem(spec) + else + cached_path = cached_gem(spec) + end + raise GemNotFound, "Missing gem file '#{spec.full_name}.gem'." unless cached_path + return if File.dirname(cached_path) == Bundler.app_cache.to_s + Bundler.ui.info " * #{File.basename(cached_path)}" + FileUtils.cp(cached_path, Bundler.app_cache(custom_path)) + rescue Errno::EACCES => e + Bundler.ui.debug(e) + raise InstallError, e.message + end + + def cached_built_in_gem(spec) + cached_path = cached_path(spec) + if cached_path.nil? + remote_spec = remote_specs.search(spec).first + if remote_spec + cached_path = fetch_gem(remote_spec) + else + Bundler.ui.warn "#{spec.full_name} is built in to Ruby, and can't be cached because your Gemfile doesn't have any sources that contain it." + end + end + cached_path + end + + def add_remote(source) + uri = normalize_uri(source) + @remotes.unshift(uri) unless @remotes.include?(uri) + end + + def equivalent_remotes?(other_remotes) + other_remotes.map(&method(:remove_auth)) == @remotes.map(&method(:remove_auth)) + end + + def replace_remotes(other_remotes, allow_equivalent = false) + return false if other_remotes == @remotes + + equivalent = allow_equivalent && equivalent_remotes?(other_remotes) + + @remotes = [] + other_remotes.reverse_each do |r| + add_remote r.to_s + end + + !equivalent + end + + def unmet_deps + if @allow_remote && api_fetchers.any? + remote_specs.unmet_dependency_names + else + [] + end + end + + def fetchers + @fetchers ||= remotes.map do |uri| + remote = Source::Rubygems::Remote.new(uri) + Bundler::Fetcher.new(remote) + end + end + + def double_check_for(unmet_dependency_names) + return unless @allow_remote + return unless api_fetchers.any? + + unmet_dependency_names = unmet_dependency_names.call + unless unmet_dependency_names.nil? + if api_fetchers.size <= 1 + # can't do this when there are multiple fetchers because then we might not fetch from _all_ + # of them + unmet_dependency_names -= remote_specs.spec_names # avoid re-fetching things we've already gotten + end + return if unmet_dependency_names.empty? + end + + Bundler.ui.debug "Double checking for #{unmet_dependency_names || "all specs (due to the size of the request)"} in #{self}" + + fetch_names(api_fetchers, unmet_dependency_names, specs, false) + end + + def dependency_names_to_double_check + names = [] + remote_specs.each do |spec| + case spec + when EndpointSpecification, Gem::Specification, StubSpecification, LazySpecification + names.concat(spec.runtime_dependencies) + when RemoteSpecification # from the full index + return nil + else + raise "unhandled spec type (#{spec.inspect})" + end + end + names.map!(&:name) if names + names + end + + protected + + def credless_remotes + remotes.map(&method(:suppress_configured_credentials)) + end + + def remotes_for_spec(spec) + specs.search_all(spec.name).inject([]) do |uris, s| + uris << s.remote if s.remote + uris + end + end + + def loaded_from(spec) + "#{rubygems_dir}/specifications/#{spec.full_name}.gemspec" + end + + def cached_gem(spec) + cached_gem = cached_path(spec) + unless cached_gem + raise Bundler::GemNotFound, "Could not find #{spec.file_name} for installation" + end + cached_gem + end + + def cached_path(spec) + possibilities = @caches.map {|p| "#{p}/#{spec.file_name}" } + possibilities.find {|p| File.exist?(p) } + end + + def normalize_uri(uri) + uri = uri.to_s + uri = "#{uri}/" unless uri =~ %r{/$} + uri = URI(uri) + raise ArgumentError, "The source must be an absolute URI. For example:\n" \ + "source 'https://rubygems.org'" if !uri.absolute? || (uri.is_a?(URI::HTTP) && uri.host.nil?) + uri + end + + def suppress_configured_credentials(remote) + remote_nouser = remove_auth(remote) + if remote.userinfo && remote.userinfo == Bundler.settings[remote_nouser] + remote_nouser + else + remote + end + end + + def remove_auth(remote) + if remote.user || remote.password + remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s + else + remote.to_s + end + end + + def installed_specs + @installed_specs ||= Index.build do |idx| + Bundler.rubygems.all_specs.reverse_each do |spec| + next if spec.name == "bundler" + spec.source = self + if Bundler.rubygems.spec_missing_extensions?(spec, false) + Bundler.ui.debug "Source #{self} is ignoring #{spec} because it is missing extensions" + next + end + idx << spec + end + end + end + + def cached_specs + @cached_specs ||= begin + idx = installed_specs.dup + + Dir["#{cache_path}/*.gem"].each do |gemfile| + next if gemfile =~ /^bundler\-[\d\.]+?\.gem/ + s ||= Bundler.rubygems.spec_from_gem(gemfile) + s.source = self + if Bundler.rubygems.spec_missing_extensions?(s, false) + Bundler.ui.debug "Source #{self} is ignoring #{s} because it is missing extensions" + next + end + idx << s + end + + idx + end + end + + def api_fetchers + fetchers.select {|f| f.use_api && f.fetchers.first.api_fetcher? } + end + + def remote_specs + @remote_specs ||= Index.build do |idx| + index_fetchers = fetchers - api_fetchers + + # gather lists from non-api sites + fetch_names(index_fetchers, nil, idx, false) + + # because ensuring we have all the gems we need involves downloading + # the gemspecs of those gems, if the non-api sites contain more than + # about 500 gems, we treat all sites as non-api for speed. + allow_api = idx.size < API_REQUEST_LIMIT && dependency_names.size < API_REQUEST_LIMIT + Bundler.ui.debug "Need to query more than #{API_REQUEST_LIMIT} gems." \ + " Downloading full index instead..." unless allow_api + + fetch_names(api_fetchers, allow_api && dependency_names, idx, false) + end + end + + def fetch_names(fetchers, dependency_names, index, override_dupes) + fetchers.each do |f| + if dependency_names + Bundler.ui.info "Fetching gem metadata from #{f.uri}", Bundler.ui.debug? + index.use f.specs_with_retry(dependency_names, self), override_dupes + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + else + Bundler.ui.info "Fetching source index from #{f.uri}" + index.use f.specs_with_retry(nil, self), override_dupes + end + end + end + + def fetch_gem(spec) + return false unless spec.remote + + spec.fetch_platform + + download_path = requires_sudo? ? Bundler.tmp(spec.full_name) : rubygems_dir + gem_path = "#{rubygems_dir}/cache/#{spec.full_name}.gem" + + SharedHelpers.filesystem_access("#{download_path}/cache") do |p| + FileUtils.mkdir_p(p) + end + download_gem(spec, download_path) + + if requires_sudo? + SharedHelpers.filesystem_access("#{rubygems_dir}/cache") do |p| + Bundler.mkdir_p(p) + end + Bundler.sudo "mv #{download_path}/cache/#{spec.full_name}.gem #{gem_path}" + end + + gem_path + ensure + Bundler.rm_rf(download_path) if requires_sudo? + end + + def builtin_gem?(spec) + # Ruby 2.1, where all included gems have this summary + return true if spec.summary =~ /is bundled with Ruby/ + + # Ruby 2.0, where gemspecs are stored in specifications/default/ + spec.loaded_from && spec.loaded_from.include?("specifications/default/") + end + + def installed?(spec) + installed_specs[spec].any? + end + + def requires_sudo? + Bundler.requires_sudo? + end + + def rubygems_dir + Bundler.rubygems.gem_dir + end + + def cache_path + Bundler.app_cache + end + + private + + # Checks if the requested spec exists in the global cache. If it does, + # we copy it to the download path, and if it does not, we download it. + # + # @param [Specification] spec + # the spec we want to download or retrieve from the cache. + # + # @param [String] download_path + # the local directory the .gem will end up in. + # + def download_gem(spec, download_path) + local_path = File.join(download_path, "cache/#{spec.full_name}.gem") + + if (cache_path = download_cache_path(spec)) && cache_path.file? + SharedHelpers.filesystem_access(local_path) do + FileUtils.cp(cache_path, local_path) + end + else + uri = spec.remote.uri + Bundler.ui.confirm("Fetching #{version_message(spec)}") + rubygems_local_path = Bundler.rubygems.download_gem(spec, uri, download_path) + if rubygems_local_path != local_path + FileUtils.mv(rubygems_local_path, local_path) + end + cache_globally(spec, local_path) + end + end + + # Checks if the requested spec exists in the global cache. If it does + # not, we create the relevant global cache subdirectory if it does not + # exist and copy the spec from the local cache to the global cache. + # + # @param [Specification] spec + # the spec we want to copy to the global cache. + # + # @param [String] local_cache_path + # the local directory from which we want to copy the .gem. + # + def cache_globally(spec, local_cache_path) + return unless cache_path = download_cache_path(spec) + return if cache_path.exist? + + SharedHelpers.filesystem_access(cache_path.dirname, &:mkpath) + SharedHelpers.filesystem_access(cache_path) do + FileUtils.cp(local_cache_path, cache_path) + end + end + + # Returns the global cache path of the calling Rubygems::Source object. + # + # Note that the Source determines the path's subdirectory. We use this + # subdirectory in the global cache path so that gems with the same name + # -- and possibly different versions -- from different sources are saved + # to their respective subdirectories and do not override one another. + # + # @param [Gem::Specification] specification + # + # @return [Pathname] The global cache path. + # + def download_cache_path(spec) + return unless Bundler.feature_flag.global_gem_cache? + return unless remote = spec.remote + return unless cache_slug = remote.cache_slug + + Bundler.user_cache.join("gems", cache_slug, spec.file_name) + end + + def extension_cache_slug(spec) + return unless remote = spec.remote + remote.cache_slug + end + end + end +end diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb new file mode 100644 index 00000000000000..b45f33770aea89 --- /dev/null +++ b/lib/bundler/source/rubygems/remote.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Bundler + class Source + class Rubygems + class Remote + attr_reader :uri, :anonymized_uri, :original_uri + + def initialize(uri) + orig_uri = uri + uri = Bundler.settings.mirror_for(uri) + @original_uri = orig_uri if orig_uri != uri + fallback_auth = Bundler.settings.credentials_for(uri) + + @uri = apply_auth(uri, fallback_auth).freeze + @anonymized_uri = remove_auth(@uri).freeze + end + + # @return [String] A slug suitable for use as a cache key for this + # remote. + # + def cache_slug + @cache_slug ||= begin + return nil unless SharedHelpers.md5_available? + + cache_uri = original_uri || uri + + # URI::File of Ruby 2.6 returns empty string when given "file://". + host = defined?(URI::File) && cache_uri.is_a?(URI::File) ? nil : cache_uri.host + + uri_parts = [host, cache_uri.user, cache_uri.port, cache_uri.path] + uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.compact.join(".")) + + uri_parts[-1] = uri_digest + uri_parts.compact.join(".") + end + end + + def to_s + "rubygems remote at #{anonymized_uri}" + end + + private + + def apply_auth(uri, auth) + if auth && uri.userinfo.nil? + uri = uri.dup + uri.userinfo = auth + end + + uri + rescue URI::InvalidComponentError + error_message = "Please CGI escape your usernames and passwords before " \ + "setting them for authentication." + raise HTTPError.new(error_message) + end + + def remove_auth(uri) + if uri.userinfo + uri = uri.dup + uri.user = uri.password = nil + end + + uri + end + end + end + end +end diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb new file mode 100644 index 00000000000000..ac2adacb3d45e9 --- /dev/null +++ b/lib/bundler/source_list.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +module Bundler + class SourceList + attr_reader :path_sources, + :git_sources, + :plugin_sources, + :global_rubygems_source, + :metadata_source + + def initialize + @path_sources = [] + @git_sources = [] + @plugin_sources = [] + @global_rubygems_source = nil + @rubygems_aggregate = rubygems_aggregate_class.new + @rubygems_sources = [] + @metadata_source = Source::Metadata.new + end + + def add_path_source(options = {}) + if options["gemspec"] + add_source_to_list Source::Gemspec.new(options), path_sources + else + add_source_to_list Source::Path.new(options), path_sources + end + end + + def add_git_source(options = {}) + add_source_to_list(Source::Git.new(options), git_sources).tap do |source| + warn_on_git_protocol(source) + end + end + + def add_rubygems_source(options = {}) + add_source_to_list Source::Rubygems.new(options), @rubygems_sources + end + + def add_plugin_source(source, options = {}) + add_source_to_list Plugin.source(source).new(options), @plugin_sources + end + + def global_rubygems_source=(uri) + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + @global_rubygems_source ||= rubygems_aggregate_class.new("remotes" => uri) + end + add_rubygems_remote(uri) + end + + def add_rubygems_remote(uri) + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + return if Bundler.feature_flag.disable_multisource? + raise InvalidOption, "`lockfile_uses_separate_rubygems_sources` cannot be set without `disable_multisource` being set" + end + @rubygems_aggregate.add_remote(uri) + @rubygems_aggregate + end + + def default_source + global_rubygems_source || @rubygems_aggregate + end + + def rubygems_sources + @rubygems_sources + [default_source] + end + + def rubygems_remotes + rubygems_sources.map(&:remotes).flatten.uniq + end + + def all_sources + path_sources + git_sources + plugin_sources + rubygems_sources + [metadata_source] + end + + def get(source) + source_list_for(source).find {|s| equal_source?(source, s) || equivalent_source?(source, s) } + end + + def lock_sources + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + [[default_source], @rubygems_sources, git_sources, path_sources, plugin_sources].map do |sources| + sources.sort_by(&:to_s) + end.flatten(1) + else + lock_sources = (path_sources + git_sources + plugin_sources).sort_by(&:to_s) + lock_sources << combine_rubygems_sources + end + end + + # Returns true if there are changes + def replace_sources!(replacement_sources) + return true if replacement_sources.empty? + + [path_sources, git_sources, plugin_sources].each do |source_list| + source_list.map! do |source| + replacement_sources.find {|s| s == source } || source + end + end + + replacement_rubygems = !Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? && + replacement_sources.detect {|s| s.is_a?(Source::Rubygems) } + @rubygems_aggregate = replacement_rubygems if replacement_rubygems + + return true if !equal_sources?(lock_sources, replacement_sources) && !equivalent_sources?(lock_sources, replacement_sources) + return true if replacement_rubygems && rubygems_remotes.to_set != replacement_rubygems.remotes.to_set + + false + end + + def cached! + all_sources.each(&:cached!) + end + + def remote! + all_sources.each(&:remote!) + end + + def rubygems_primary_remotes + @rubygems_aggregate.remotes + end + + private + + def rubygems_aggregate_class + Source::Rubygems + end + + def add_source_to_list(source, list) + list.unshift(source).uniq! + source + end + + def source_list_for(source) + case source + when Source::Git then git_sources + when Source::Path then path_sources + when Source::Rubygems then rubygems_sources + when Plugin::API::Source then plugin_sources + else raise ArgumentError, "Invalid source: #{source.inspect}" + end + end + + def combine_rubygems_sources + Source::Rubygems.new("remotes" => rubygems_remotes) + end + + def warn_on_git_protocol(source) + return if Bundler.settings["git.allow_insecure"] + + if source.uri =~ /^git\:/ + Bundler.ui.warn "The git source `#{source.uri}` uses the `git` protocol, " \ + "which transmits data without encryption. Disable this warning with " \ + "`bundle config git.allow_insecure true`, or switch to the `https` " \ + "protocol to keep your data secure." + end + end + + def equal_sources?(lock_sources, replacement_sources) + lock_sources.to_set == replacement_sources.to_set + end + + def equal_source?(source, other_source) + source == other_source + end + + def equivalent_source?(source, other_source) + return false unless Bundler.settings[:allow_deployment_source_credential_changes] && source.is_a?(Source::Rubygems) + + equivalent_rubygems_sources?([source], [other_source]) + end + + def equivalent_sources?(lock_sources, replacement_sources) + return false unless Bundler.settings[:allow_deployment_source_credential_changes] + + lock_rubygems_sources, lock_other_sources = lock_sources.partition {|s| s.is_a?(Source::Rubygems) } + replacement_rubygems_sources, replacement_other_sources = replacement_sources.partition {|s| s.is_a?(Source::Rubygems) } + + equivalent_rubygems_sources?(lock_rubygems_sources, replacement_rubygems_sources) && equal_sources?(lock_other_sources, replacement_other_sources) + end + + def equivalent_rubygems_sources?(lock_sources, replacement_sources) + actual_remotes = replacement_sources.map(&:remotes).flatten.uniq + lock_sources.all? {|s| s.equivalent_remotes?(actual_remotes) } + end + end +end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb new file mode 100644 index 00000000000000..5003b2cbecaf6e --- /dev/null +++ b/lib/bundler/spec_set.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "tsort" +require "forwardable" +require "set" + +module Bundler + class SpecSet + extend Forwardable + include TSort, Enumerable + + def_delegators :@specs, :<<, :length, :add, :remove, :size, :empty? + def_delegators :sorted, :each + + def initialize(specs) + @specs = specs + end + + def for(dependencies, skip = [], check = false, match_current_platform = false, raise_on_missing = true) + handled = Set.new + deps = dependencies.dup + specs = [] + skip += ["bundler"] + + loop do + break unless dep = deps.shift + next if !handled.add?(dep) || skip.include?(dep.name) + + if spec = spec_for_dependency(dep, match_current_platform) + specs << spec + + spec.dependencies.each do |d| + next if d.type == :development + d = DepProxy.new(d, dep.__platform) unless match_current_platform + deps << d + end + elsif check + return false + elsif raise_on_missing + others = lookup[dep.name] if match_current_platform + message = "Unable to find a spec satisfying #{dep} in the set. Perhaps the lockfile is corrupted?" + message += " Found #{others.join(", ")} that did not match the current platform." if others && !others.empty? + raise GemNotFound, message + end + end + + if spec = lookup["bundler"].first + specs << spec + end + + check ? true : SpecSet.new(specs) + end + + def valid_for?(deps) + self.for(deps, [], true) + end + + def [](key) + key = key.name if key.respond_to?(:name) + lookup[key].reverse + end + + def []=(key, value) + @specs << value + @lookup = nil + @sorted = nil + value + end + + def sort! + self + end + + def to_a + sorted.dup + end + + def to_hash + lookup.dup + end + + def materialize(deps, missing_specs = nil) + materialized = self.for(deps, [], false, true, !missing_specs).to_a + deps = materialized.map(&:name).uniq + materialized.map! do |s| + next s unless s.is_a?(LazySpecification) + s.source.dependency_names = deps if s.source.respond_to?(:dependency_names=) + spec = s.__materialize__ + unless spec + unless missing_specs + raise GemNotFound, "Could not find #{s.full_name} in any of the sources" + end + missing_specs << s + end + spec + end + SpecSet.new(missing_specs ? materialized.compact : materialized) + end + + # Materialize for all the specs in the spec set, regardless of what platform they're for + # This is in contrast to how for does platform filtering (and specifically different from how `materialize` calls `for` only for the current platform) + # @return [Array] + def materialized_for_all_platforms + names = @specs.map(&:name).uniq + @specs.map do |s| + next s unless s.is_a?(LazySpecification) + s.source.dependency_names = names if s.source.respond_to?(:dependency_names=) + spec = s.__materialize__ + raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec + spec + end + end + + def merge(set) + arr = sorted.dup + set.each do |set_spec| + full_name = set_spec.full_name + next if arr.any? {|spec| spec.full_name == full_name } + arr << set_spec + end + SpecSet.new(arr) + end + + def find_by_name_and_platform(name, platform) + @specs.detect {|spec| spec.name == name && spec.match_platform(platform) } + end + + def what_required(spec) + unless req = find {|s| s.dependencies.any? {|d| d.type == :runtime && d.name == spec.name } } + return [spec] + end + what_required(req) << spec + end + + private + + def sorted + rake = @specs.find {|s| s.name == "rake" } + begin + @sorted ||= ([rake] + tsort).compact.uniq + rescue TSort::Cyclic => error + cgems = extract_circular_gems(error) + raise CyclicDependencyError, "Your bundle requires gems that depend" \ + " on each other, creating an infinite loop. Please remove either" \ + " gem '#{cgems[1]}' or gem '#{cgems[0]}' and try again." + end + end + + def extract_circular_gems(error) + if Bundler.current_ruby.mri? && Bundler.current_ruby.on_19? + error.message.scan(/(\w+) \([^)]/).flatten + else + error.message.scan(/@name="(.*?)"/).flatten + end + end + + def lookup + @lookup ||= begin + lookup = Hash.new {|h, k| h[k] = [] } + Index.sort_specs(@specs).reverse_each do |s| + lookup[s.name] << s + end + lookup + end + end + + def tsort_each_node + # MUST sort by name for backwards compatibility + @specs.sort_by(&:name).each {|s| yield s } + end + + def spec_for_dependency(dep, match_current_platform) + specs_for_platforms = lookup[dep.name] + if match_current_platform + Bundler.rubygems.platforms.reverse_each do |pl| + match = GemHelpers.select_best_platform_match(specs_for_platforms, pl) + return match if match + end + nil + else + GemHelpers.select_best_platform_match(specs_for_platforms, dep.__platform) + end + end + + def tsort_each_child(s) + s.dependencies.sort_by(&:name).each do |d| + next if d.type == :development + lookup[d.name].each {|s2| yield s2 } + end + end + end +end diff --git a/lib/bundler/ssl_certs/.document b/lib/bundler/ssl_certs/.document new file mode 100644 index 00000000000000..fb66f13c330f04 --- /dev/null +++ b/lib/bundler/ssl_certs/.document @@ -0,0 +1 @@ +# Ignore all files in this directory diff --git a/lib/bundler/ssl_certs/certificate_manager.rb b/lib/bundler/ssl_certs/certificate_manager.rb new file mode 100644 index 00000000000000..26fc38ec185bde --- /dev/null +++ b/lib/bundler/ssl_certs/certificate_manager.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "bundler/vendored_fileutils" +require "net/https" +require "openssl" + +module Bundler + module SSLCerts + class CertificateManager + attr_reader :bundler_cert_path, :bundler_certs, :rubygems_certs + + def self.update_from!(rubygems_path) + new(rubygems_path).update! + end + + def initialize(rubygems_path = nil) + if rubygems_path + rubygems_cert_path = File.join(rubygems_path, "lib/rubygems/ssl_certs") + @rubygems_certs = certificates_in(rubygems_cert_path) + end + + @bundler_cert_path = File.expand_path("..", __FILE__) + @bundler_certs = certificates_in(bundler_cert_path) + end + + def up_to_date? + rubygems_certs.all? do |rc| + bundler_certs.find do |bc| + File.basename(bc) == File.basename(rc) && FileUtils.compare_file(bc, rc) + end + end + end + + def update! + return if up_to_date? + + FileUtils.rm bundler_certs + FileUtils.cp rubygems_certs, bundler_cert_path + end + + def connect_to(host) + http = Net::HTTP.new(host, 443) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + http.cert_store = store + http.head("/") + end + + private + + def certificates_in(path) + Dir[File.join(path, "**/*.pem")].sort + end + + def store + @store ||= begin + store = OpenSSL::X509::Store.new + bundler_certs.each do |cert| + store.add_file cert + end + store + end + end + end + end +end diff --git a/lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem b/lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem new file mode 100644 index 00000000000000..f4ce4ca43dc0a0 --- /dev/null +++ b/lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- diff --git a/lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem b/lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem new file mode 100644 index 00000000000000..9e6810ab70cfa1 --- /dev/null +++ b/lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- diff --git a/lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem b/lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem new file mode 100644 index 00000000000000..20585f1c01e187 --- /dev/null +++ b/lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- diff --git a/lib/bundler/stub_specification.rb b/lib/bundler/stub_specification.rb new file mode 100644 index 00000000000000..0dd024024a01ac --- /dev/null +++ b/lib/bundler/stub_specification.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require "bundler/remote_specification" + +module Bundler + class StubSpecification < RemoteSpecification + def self.from_stub(stub) + return stub if stub.is_a?(Bundler::StubSpecification) + spec = new(stub.name, stub.version, stub.platform, nil) + spec.stub = stub + spec + end + + attr_accessor :stub, :ignored + + # Pre 2.2.0 did not include extension_dir + # https://github.com/rubygems/rubygems/commit/9485ca2d101b82a946d6f327f4bdcdea6d4946ea + if Bundler.rubygems.provides?(">= 2.2.0") + def source=(source) + super + # Stub has no concept of source, which means that extension_dir may be wrong + # This is the case for git-based gems. So, instead manually assign the extension dir + return unless source.respond_to?(:extension_dir_name) + path = File.join(stub.extensions_dir, source.extension_dir_name) + stub.extension_dir = File.expand_path(path) + end + end + + def to_yaml + _remote_specification.to_yaml + end + + # @!group Stub Delegates + + if Bundler.rubygems.provides?(">= 2.3") + # This is defined directly to avoid having to load every installed spec + def missing_extensions? + stub.missing_extensions? + end + end + + def activated + stub.activated + end + + def activated=(activated) + stub.instance_variable_set(:@activated, activated) + end + + def default_gem + stub.default_gem + end + + def full_gem_path + # deleted gems can have their stubs return nil, so in that case grab the + # expired path from the full spec + stub.full_gem_path || method_missing(:full_gem_path) + end + + if Bundler.rubygems.provides?(">= 2.2.0") + def full_require_paths + stub.full_require_paths + end + + # This is what we do in bundler/rubygems_ext + # full_require_paths is always implemented in >= 2.2.0 + def load_paths + full_require_paths + end + end + + def loaded_from + stub.loaded_from + end + + if Bundler.rubygems.stubs_provide_full_functionality? + def matches_for_glob(glob) + stub.matches_for_glob(glob) + end + end + + def raw_require_paths + stub.raw_require_paths + end + + private + + def _remote_specification + @_remote_specification ||= begin + rs = stub.to_spec + if rs.equal?(self) # happens when to_spec gets the spec from Gem.loaded_specs + rs = Gem::Specification.load(loaded_from) + Bundler.rubygems.stub_set_spec(stub, rs) + end + + unless rs + raise GemspecError, "The gemspec for #{full_name} at #{loaded_from}" \ + " was missing or broken. Try running `gem pristine #{name} -v #{version}`" \ + " to fix the cached spec." + end + + rs.source = source + + rs + end + end + end +end diff --git a/lib/bundler/templates/.document b/lib/bundler/templates/.document new file mode 100644 index 00000000000000..fb66f13c330f04 --- /dev/null +++ b/lib/bundler/templates/.document @@ -0,0 +1 @@ +# Ignore all files in this directory diff --git a/lib/bundler/templates/Executable b/lib/bundler/templates/Executable new file mode 100644 index 00000000000000..3e8d5b317ad8b1 --- /dev/null +++ b/lib/bundler/templates/Executable @@ -0,0 +1,29 @@ +#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application '<%= executable %>' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../<%= relative_gemfile_path %>", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("<%= spec.name %>", "<%= executable %>") diff --git a/lib/bundler/templates/Executable.bundler b/lib/bundler/templates/Executable.bundler new file mode 100644 index 00000000000000..eeda90b584b647 --- /dev/null +++ b/lib/bundler/templates/Executable.bundler @@ -0,0 +1,105 @@ +#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application '<%= executable %>' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 || ">= 0.a" + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../<%= relative_gemfile_path %>", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= begin + env_var_version || cli_arg_version || + lockfile_version || "#{Gem::Requirement.default}.a" + end + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + # must dup string for RG < 1.8 compatibility + activate_bundler(bundler_version.dup) + end + + def activate_bundler(bundler_version) + if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") + bundler_version = "< 2" + end + gem_error = activation_error_handling do + gem "bundler", bundler_version + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("<%= spec.name %>", "<%= executable %>") +end diff --git a/lib/bundler/templates/Executable.standalone b/lib/bundler/templates/Executable.standalone new file mode 100644 index 00000000000000..4bf0753f44d53e --- /dev/null +++ b/lib/bundler/templates/Executable.standalone @@ -0,0 +1,14 @@ +#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> +# +# This file was generated by Bundler. +# +# The application '<%= executable %>' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +path = Pathname.new(__FILE__) +$:.unshift File.expand_path "../<%= standalone_path %>", path.realpath + +require "bundler/setup" +load File.expand_path "../<%= executable_path %>", path.realpath diff --git a/lib/bundler/templates/Gemfile b/lib/bundler/templates/Gemfile new file mode 100644 index 00000000000000..1afd2cce673bb2 --- /dev/null +++ b/lib/bundler/templates/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# gem "rails" diff --git a/lib/bundler/templates/gems.rb b/lib/bundler/templates/gems.rb new file mode 100644 index 00000000000000..547cd6e8d9bb76 --- /dev/null +++ b/lib/bundler/templates/gems.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# A sample gems.rb +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# gem "rails" diff --git a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt new file mode 100644 index 00000000000000..a3833d29d780ff --- /dev/null +++ b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at <%= config[:email] %>. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/lib/bundler/templates/newgem/Gemfile.tt b/lib/bundler/templates/newgem/Gemfile.tt new file mode 100644 index 00000000000000..4cd2e40f4f8a23 --- /dev/null +++ b/lib/bundler/templates/newgem/Gemfile.tt @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in <%= config[:name] %>.gemspec +gemspec diff --git a/lib/bundler/templates/newgem/LICENSE.txt.tt b/lib/bundler/templates/newgem/LICENSE.txt.tt new file mode 100644 index 00000000000000..76ef4b0191c829 --- /dev/null +++ b/lib/bundler/templates/newgem/LICENSE.txt.tt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) <%= Time.now.year %> <%= config[:author] %> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/bundler/templates/newgem/README.md.tt b/lib/bundler/templates/newgem/README.md.tt new file mode 100644 index 00000000000000..868a0afe67fa7b --- /dev/null +++ b/lib/bundler/templates/newgem/README.md.tt @@ -0,0 +1,47 @@ +# <%= config[:constant_name] %> + +Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/<%= config[:namespaced_path] %>`. To experiment with that code, run `bin/console` for an interactive prompt. + +TODO: Delete this and the text above, and describe your gem + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem '<%= config[:name] %>' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install <%= config[:name] %> + +## Usage + +TODO: Write usage instructions here + +## Development + +After checking out the repo, run `bin/setup` to install dependencies.<% if config[:test] %> Then, run `rake <%= config[:test].sub('mini', '').sub('rspec', 'spec') %>` to run the tests.<% end %> You can also run `bin/console` for an interactive prompt that will allow you to experiment.<% if config[:bin] %> Run `bundle exec <%= config[:name] %>` to use the gem in this directory, ignoring other installed copies of this gem.<% end %> + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/<%= config[:github_username] %>/<%= config[:name] %>.<% if config[:coc] %> This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.<% end %> +<% if config[:mit] -%> + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). +<% end -%> +<% if config[:coc] -%> + +## Code of Conduct + +Everyone interacting in the <%= config[:constant_name] %> project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/<%= config[:github_username] %>/<%= config[:name] %>/blob/master/CODE_OF_CONDUCT.md). +<% end -%> diff --git a/lib/bundler/templates/newgem/Rakefile.tt b/lib/bundler/templates/newgem/Rakefile.tt new file mode 100644 index 00000000000000..099da6f3ecde55 --- /dev/null +++ b/lib/bundler/templates/newgem/Rakefile.tt @@ -0,0 +1,29 @@ +require "bundler/gem_tasks" +<% if config[:test] == "minitest" -%> +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +<% elsif config[:test] == "rspec" -%> +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +<% end -%> +<% if config[:ext] -%> +require "rake/extensiontask" + +task :build => :compile + +Rake::ExtensionTask.new("<%= config[:underscored_name] %>") do |ext| + ext.lib_dir = "lib/<%= config[:namespaced_path] %>" +end + +task :default => [:clobber, :compile, :<%= config[:test_task] %>] +<% else -%> +task :default => :<%= config[:test_task] %> +<% end -%> diff --git a/lib/bundler/templates/newgem/bin/console.tt b/lib/bundler/templates/newgem/bin/console.tt new file mode 100644 index 00000000000000..a27f82430f9516 --- /dev/null +++ b/lib/bundler/templates/newgem/bin/console.tt @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "<%= config[:namespaced_path] %>" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/lib/bundler/templates/newgem/bin/setup.tt b/lib/bundler/templates/newgem/bin/setup.tt new file mode 100644 index 00000000000000..dce67d860af47a --- /dev/null +++ b/lib/bundler/templates/newgem/bin/setup.tt @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/lib/bundler/templates/newgem/exe/newgem.tt b/lib/bundler/templates/newgem/exe/newgem.tt new file mode 100644 index 00000000000000..a8339bb79f58b0 --- /dev/null +++ b/lib/bundler/templates/newgem/exe/newgem.tt @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby + +require "<%= config[:namespaced_path] %>" diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt new file mode 100644 index 00000000000000..8cfc828f94f93a --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt @@ -0,0 +1,3 @@ +require "mkmf" + +create_makefile(<%= config[:makefile_path].inspect %>) diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt new file mode 100644 index 00000000000000..8177c4d202147d --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt @@ -0,0 +1,9 @@ +#include "<%= config[:underscored_name] %>.h" + +VALUE rb_m<%= config[:constant_array].join %>; + +void +Init_<%= config[:underscored_name] %>(void) +{ + rb_m<%= config[:constant_array].join %> = rb_define_module(<%= config[:constant_name].inspect %>); +} diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.h.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.h.tt new file mode 100644 index 00000000000000..c6e420b66ecb0f --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem.h.tt @@ -0,0 +1,6 @@ +#ifndef <%= config[:underscored_name].upcase %>_H +#define <%= config[:underscored_name].upcase %>_H 1 + +#include "ruby.h" + +#endif /* <%= config[:underscored_name].upcase %>_H */ diff --git a/lib/bundler/templates/newgem/gitignore.tt b/lib/bundler/templates/newgem/gitignore.tt new file mode 100644 index 00000000000000..b1c9f9986cc4bb --- /dev/null +++ b/lib/bundler/templates/newgem/gitignore.tt @@ -0,0 +1,20 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +<%- if config[:ext] -%> +*.bundle +*.so +*.o +*.a +mkmf.log +<%- end -%> +<%- if config[:test] == "rspec" -%> + +# rspec failure tracking +.rspec_status +<%- end -%> diff --git a/lib/bundler/templates/newgem/lib/newgem.rb.tt b/lib/bundler/templates/newgem/lib/newgem.rb.tt new file mode 100644 index 00000000000000..fae6337c3e6e69 --- /dev/null +++ b/lib/bundler/templates/newgem/lib/newgem.rb.tt @@ -0,0 +1,13 @@ +require "<%= config[:namespaced_path] %>/version" +<%- if config[:ext] -%> +require "<%= config[:namespaced_path] %>/<%= config[:underscored_name] %>" +<%- end -%> + +<%- config[:constant_array].each_with_index do |c, i| -%> +<%= " " * i %>module <%= c %> +<%- end -%> +<%= " " * config[:constant_array].size %>class Error < StandardError; end +<%= " " * config[:constant_array].size %># Your code goes here... +<%- (config[:constant_array].size-1).downto(0) do |i| -%> +<%= " " * i %>end +<%- end -%> diff --git a/lib/bundler/templates/newgem/lib/newgem/version.rb.tt b/lib/bundler/templates/newgem/lib/newgem/version.rb.tt new file mode 100644 index 00000000000000..389daf5048d796 --- /dev/null +++ b/lib/bundler/templates/newgem/lib/newgem/version.rb.tt @@ -0,0 +1,7 @@ +<%- config[:constant_array].each_with_index do |c, i| -%> +<%= " " * i %>module <%= c %> +<%- end -%> +<%= " " * config[:constant_array].size %>VERSION = "0.1.0" +<%- (config[:constant_array].size-1).downto(0) do |i| -%> +<%= " " * i %>end +<%- end -%> diff --git a/lib/bundler/templates/newgem/newgem.gemspec.tt b/lib/bundler/templates/newgem/newgem.gemspec.tt new file mode 100644 index 00000000000000..faf6f7bbc5e55b --- /dev/null +++ b/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -0,0 +1,55 @@ +<%- if RUBY_VERSION < "2.0.0" -%> +# coding: utf-8 +<%- end -%> + +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "<%= config[:namespaced_path] %>/version" + +Gem::Specification.new do |spec| + spec.name = <%= config[:name].inspect %> + spec.version = <%= config[:constant_name] %>::VERSION + spec.authors = [<%= config[:author].inspect %>] + spec.email = [<%= config[:email].inspect %>] + + spec.summary = %q{TODO: Write a short summary, because RubyGems requires one.} + spec.description = %q{TODO: Write a longer description or delete this line.} + spec.homepage = "TODO: Put your gem's website or public repo URL here." +<%- if config[:mit] -%> + spec.license = "MIT" +<%- end -%> + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." + spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] +<%- if config[:ext] -%> + spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"] +<%- end -%> + + spec.add_development_dependency "bundler", "~> <%= config[:bundler_version] %>" + spec.add_development_dependency "rake", "~> 10.0" +<%- if config[:ext] -%> + spec.add_development_dependency "rake-compiler" +<%- end -%> +<%- if config[:test] -%> + spec.add_development_dependency "<%= config[:test] %>", "~> <%= config[:test_framework_version] %>" +<%- end -%> +end diff --git a/lib/bundler/templates/newgem/rspec.tt b/lib/bundler/templates/newgem/rspec.tt new file mode 100644 index 00000000000000..34c5164d9b56c7 --- /dev/null +++ b/lib/bundler/templates/newgem/rspec.tt @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt new file mode 100644 index 00000000000000..c63b48783060af --- /dev/null +++ b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt @@ -0,0 +1,9 @@ +RSpec.describe <%= config[:constant_name] %> do + it "has a version number" do + expect(<%= config[:constant_name] %>::VERSION).not_to be nil + end + + it "does something useful" do + expect(false).to eq(true) + end +end diff --git a/lib/bundler/templates/newgem/spec/spec_helper.rb.tt b/lib/bundler/templates/newgem/spec/spec_helper.rb.tt new file mode 100644 index 00000000000000..805cf57e0114ef --- /dev/null +++ b/lib/bundler/templates/newgem/spec/spec_helper.rb.tt @@ -0,0 +1,14 @@ +require "bundler/setup" +require "<%= config[:namespaced_path] %>" + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/lib/bundler/templates/newgem/test/newgem_test.rb.tt b/lib/bundler/templates/newgem/test/newgem_test.rb.tt new file mode 100644 index 00000000000000..f2af9f90e0bab0 --- /dev/null +++ b/lib/bundler/templates/newgem/test/newgem_test.rb.tt @@ -0,0 +1,11 @@ +require "test_helper" + +class <%= config[:constant_name] %>Test < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::<%= config[:constant_name] %>::VERSION + end + + def test_it_does_something_useful + assert false + end +end diff --git a/lib/bundler/templates/newgem/test/test_helper.rb.tt b/lib/bundler/templates/newgem/test/test_helper.rb.tt new file mode 100644 index 00000000000000..725e3e4647386f --- /dev/null +++ b/lib/bundler/templates/newgem/test/test_helper.rb.tt @@ -0,0 +1,4 @@ +$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) +require "<%= config[:namespaced_path] %>" + +require "minitest/autorun" diff --git a/lib/bundler/templates/newgem/travis.yml.tt b/lib/bundler/templates/newgem/travis.yml.tt new file mode 100644 index 00000000000000..7a3381a889097a --- /dev/null +++ b/lib/bundler/templates/newgem/travis.yml.tt @@ -0,0 +1,7 @@ +--- +sudo: false +language: ruby +cache: bundler +rvm: + - <%= RUBY_VERSION %> +before_install: gem install bundler -v <%= Bundler::VERSION %> diff --git a/lib/bundler/ui.rb b/lib/bundler/ui.rb new file mode 100644 index 00000000000000..8138b30d389e2b --- /dev/null +++ b/lib/bundler/ui.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Bundler + module UI + autoload :RGProxy, "bundler/ui/rg_proxy" + autoload :Shell, "bundler/ui/shell" + autoload :Silent, "bundler/ui/silent" + end +end diff --git a/lib/bundler/ui/rg_proxy.rb b/lib/bundler/ui/rg_proxy.rb new file mode 100644 index 00000000000000..e2f98481db3328 --- /dev/null +++ b/lib/bundler/ui/rg_proxy.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "bundler/ui" +require "rubygems/user_interaction" + +module Bundler + module UI + class RGProxy < ::Gem::SilentUI + def initialize(ui) + @ui = ui + super() + end + + def say(message) + @ui && @ui.debug(message) + end + end + end +end diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb new file mode 100644 index 00000000000000..16e3d1571373b0 --- /dev/null +++ b/lib/bundler/ui/shell.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require "bundler/vendored_thor" + +module Bundler + module UI + class Shell + LEVELS = %w[silent error warn confirm info debug].freeze + + attr_writer :shell + + def initialize(options = {}) + if options["no-color"] || !$stdout.tty? + Thor::Base.shell = Thor::Shell::Basic + end + @shell = Thor::Base.shell.new + @level = ENV["DEBUG"] ? "debug" : "info" + @warning_history = [] + end + + def add_color(string, *color) + @shell.set_color(string, *color) + end + + def info(msg, newline = nil) + tell_me(msg, nil, newline) if level("info") + end + + def confirm(msg, newline = nil) + tell_me(msg, :green, newline) if level("confirm") + end + + def warn(msg, newline = nil) + return unless level("warn") + return if @warning_history.include? msg + @warning_history << msg + + return tell_err(msg, :yellow, newline) if Bundler.feature_flag.error_on_stderr? + tell_me(msg, :yellow, newline) + end + + def error(msg, newline = nil) + return unless level("error") + return tell_err(msg, :red, newline) if Bundler.feature_flag.error_on_stderr? + tell_me(msg, :red, newline) + end + + def debug(msg, newline = nil) + tell_me(msg, nil, newline) if debug? + end + + def debug? + level("debug") + end + + def quiet? + level("quiet") + end + + def ask(msg) + @shell.ask(msg) + end + + def yes?(msg) + @shell.yes?(msg) + end + + def no? + @shell.no?(msg) + end + + def level=(level) + raise ArgumentError unless LEVELS.include?(level.to_s) + @level = level.to_s + end + + def level(name = nil) + return @level unless name + unless index = LEVELS.index(name) + raise "#{name.inspect} is not a valid level" + end + index <= LEVELS.index(@level) + end + + def trace(e, newline = nil, force = false) + return unless debug? || force + msg = "#{e.class}: #{e.message}\n#{e.backtrace.join("\n ")}" + tell_me(msg, nil, newline) + end + + def silence(&blk) + with_level("silent", &blk) + end + + def unprinted_warnings + [] + end + + private + + # valimism + def tell_me(msg, color = nil, newline = nil) + msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap] + if newline.nil? + @shell.say(msg, color) + else + @shell.say(msg, color, newline) + end + end + + def tell_err(message, color = nil, newline = nil) + return if @shell.send(:stderr).closed? + + newline ||= message.to_s !~ /( |\t)\Z/ + message = word_wrap(message) if newline.is_a?(Hash) && newline[:wrap] + + color = nil if color && !$stderr.tty? + + buffer = @shell.send(:prepare_message, message, *color) + buffer << "\n" if newline && !message.to_s.end_with?("\n") + + @shell.send(:stderr).print(buffer) + @shell.send(:stderr).flush + end + + def strip_leading_spaces(text) + spaces = text[/\A\s+/, 0] + spaces ? text.gsub(/#{spaces}/, "") : text + end + + def word_wrap(text, line_width = @shell.terminal_width) + strip_leading_spaces(text).split("\n").collect do |line| + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line + end * "\n" + end + + def with_level(level) + original = @level + @level = level + yield + ensure + @level = original + end + end + end +end diff --git a/lib/bundler/ui/silent.rb b/lib/bundler/ui/silent.rb new file mode 100644 index 00000000000000..dca1b2ac86de5a --- /dev/null +++ b/lib/bundler/ui/silent.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Bundler + module UI + class Silent + attr_writer :shell + + def initialize + @warnings = [] + end + + def add_color(string, color) + string + end + + def info(message, newline = nil) + end + + def confirm(message, newline = nil) + end + + def warn(message, newline = nil) + @warnings |= [message] + end + + def error(message, newline = nil) + end + + def debug(message, newline = nil) + end + + def debug? + false + end + + def quiet? + false + end + + def ask(message) + end + + def yes?(msg) + raise "Cannot ask yes? with a silent shell" + end + + def no? + raise "Cannot ask no? with a silent shell" + end + + def level=(name) + end + + def level(name = nil) + end + + def trace(message, newline = nil, force = false) + end + + def silence + yield + end + + def unprinted_warnings + @warnings + end + end + end +end diff --git a/lib/bundler/uri_credentials_filter.rb b/lib/bundler/uri_credentials_filter.rb new file mode 100644 index 00000000000000..ee3692268cfd17 --- /dev/null +++ b/lib/bundler/uri_credentials_filter.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Bundler + module URICredentialsFilter + module_function + + def credential_filtered_uri(uri_to_anonymize) + return uri_to_anonymize if uri_to_anonymize.nil? + uri = uri_to_anonymize.dup + uri = URI(uri.to_s) unless uri.is_a?(URI) + if uri.userinfo + # oauth authentication + if uri.password == "x-oauth-basic" || uri.password == "x" + # URI as string does not display with password if no user is set + oauth_designation = uri.password + uri.user = oauth_designation + end + uri.password = nil + end + return uri if uri_to_anonymize.is_a?(URI) + return uri.to_s if uri_to_anonymize.is_a?(String) + rescue URI::InvalidURIError # uri is not canonical uri scheme + uri + end + + def credential_filtered_string(str_to_filter, uri) + return str_to_filter if uri.nil? || str_to_filter.nil? + str_with_no_credentials = str_to_filter.dup + anonymous_uri_str = credential_filtered_uri(uri).to_s + uri_str = uri.to_s + if anonymous_uri_str != uri_str + str_with_no_credentials = str_with_no_credentials.gsub(uri_str, anonymous_uri_str) + end + str_with_no_credentials + end + end +end diff --git a/lib/bundler/vendor/fileutils/lib/fileutils.rb b/lib/bundler/vendor/fileutils/lib/fileutils.rb new file mode 100644 index 00000000000000..cc697408451573 --- /dev/null +++ b/lib/bundler/vendor/fileutils/lib/fileutils.rb @@ -0,0 +1,1638 @@ +# frozen_string_literal: true +# +# = fileutils.rb +# +# Copyright (c) 2000-2007 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the same terms of ruby. +# +# == module Bundler::FileUtils +# +# Namespace for several file utility methods for copying, moving, removing, etc. +# +# === Module Functions +# +# require 'bundler/vendor/fileutils/lib/fileutils' +# +# Bundler::FileUtils.cd(dir, options) +# Bundler::FileUtils.cd(dir, options) {|dir| block } +# Bundler::FileUtils.pwd() +# Bundler::FileUtils.mkdir(dir, options) +# Bundler::FileUtils.mkdir(list, options) +# Bundler::FileUtils.mkdir_p(dir, options) +# Bundler::FileUtils.mkdir_p(list, options) +# Bundler::FileUtils.rmdir(dir, options) +# Bundler::FileUtils.rmdir(list, options) +# Bundler::FileUtils.ln(target, link, options) +# Bundler::FileUtils.ln(targets, dir, options) +# Bundler::FileUtils.ln_s(target, link, options) +# Bundler::FileUtils.ln_s(targets, dir, options) +# Bundler::FileUtils.ln_sf(target, link, options) +# Bundler::FileUtils.cp(src, dest, options) +# Bundler::FileUtils.cp(list, dir, options) +# Bundler::FileUtils.cp_r(src, dest, options) +# Bundler::FileUtils.cp_r(list, dir, options) +# Bundler::FileUtils.mv(src, dest, options) +# Bundler::FileUtils.mv(list, dir, options) +# Bundler::FileUtils.rm(list, options) +# Bundler::FileUtils.rm_r(list, options) +# Bundler::FileUtils.rm_rf(list, options) +# Bundler::FileUtils.install(src, dest, options) +# Bundler::FileUtils.chmod(mode, list, options) +# Bundler::FileUtils.chmod_R(mode, list, options) +# Bundler::FileUtils.chown(user, group, list, options) +# Bundler::FileUtils.chown_R(user, group, list, options) +# Bundler::FileUtils.touch(list, options) +# +# The options parameter is a hash of options, taken from the list +# :force, :noop, :preserve, and :verbose. +# :noop means that no changes are made. The other three are obvious. +# Each method documents the options that it honours. +# +# All methods that have the concept of a "source" file or directory can take +# either one file or a list of files in that argument. See the method +# documentation for examples. +# +# There are some `low level' methods, which do not accept any option: +# +# Bundler::FileUtils.copy_entry(src, dest, preserve = false, dereference = false) +# Bundler::FileUtils.copy_file(src, dest, preserve = false, dereference = true) +# Bundler::FileUtils.copy_stream(srcstream, deststream) +# Bundler::FileUtils.remove_entry(path, force = false) +# Bundler::FileUtils.remove_entry_secure(path, force = false) +# Bundler::FileUtils.remove_file(path, force = false) +# Bundler::FileUtils.compare_file(path_a, path_b) +# Bundler::FileUtils.compare_stream(stream_a, stream_b) +# Bundler::FileUtils.uptodate?(file, cmp_list) +# +# == module Bundler::FileUtils::Verbose +# +# This module has all methods of Bundler::FileUtils module, but it outputs messages +# before acting. This equates to passing the :verbose flag to methods +# in Bundler::FileUtils. +# +# == module Bundler::FileUtils::NoWrite +# +# This module has all methods of Bundler::FileUtils module, but never changes +# files/directories. This equates to passing the :noop flag to methods +# in Bundler::FileUtils. +# +# == module Bundler::FileUtils::DryRun +# +# This module has all methods of Bundler::FileUtils module, but never changes +# files/directories. This equates to passing the :noop and +# :verbose flags to methods in Bundler::FileUtils. +# + +module Bundler::FileUtils + + def self.private_module_function(name) #:nodoc: + module_function name + private_class_method name + end + + # + # Returns the name of the current directory. + # + def pwd + Dir.pwd + end + module_function :pwd + + alias getwd pwd + module_function :getwd + + # + # Changes the current directory to the directory +dir+. + # + # If this method is called with block, resumes to the old + # working directory after the block execution finished. + # + # Bundler::FileUtils.cd('/', :verbose => true) # chdir and report it + # + # Bundler::FileUtils.cd('/') do # chdir + # # ... # do something + # end # return to original directory + # + def cd(dir, verbose: nil, &block) # :yield: dir + fu_output_message "cd #{dir}" if verbose + Dir.chdir(dir, &block) + fu_output_message 'cd -' if verbose and block + end + module_function :cd + + alias chdir cd + module_function :chdir + + # + # Returns true if +new+ is newer than all +old_list+. + # Non-existent files are older than any file. + # + # Bundler::FileUtils.uptodate?('hello.o', %w(hello.c hello.h)) or \ + # system 'make hello.o' + # + def uptodate?(new, old_list) + return false unless File.exist?(new) + new_time = File.mtime(new) + old_list.each do |old| + if File.exist?(old) + return false unless new_time > File.mtime(old) + end + end + true + end + module_function :uptodate? + + def remove_trailing_slash(dir) #:nodoc: + dir == '/' ? dir : dir.chomp(?/) + end + private_module_function :remove_trailing_slash + + # + # Creates one or more directories. + # + # Bundler::FileUtils.mkdir 'test' + # Bundler::FileUtils.mkdir %w( tmp data ) + # Bundler::FileUtils.mkdir 'notexist', :noop => true # Does not really create. + # Bundler::FileUtils.mkdir 'tmp', :mode => 0700 + # + def mkdir(list, mode: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "mkdir #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose + return if noop + + list.each do |dir| + fu_mkdir dir, mode + end + end + module_function :mkdir + + # + # Creates a directory and all its parent directories. + # For example, + # + # Bundler::FileUtils.mkdir_p '/usr/local/lib/ruby' + # + # causes to make following directories, if it does not exist. + # + # * /usr + # * /usr/local + # * /usr/local/lib + # * /usr/local/lib/ruby + # + # You can pass several directories at a time in a list. + # + def mkdir_p(list, mode: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "mkdir -p #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose + return *list if noop + + list.map {|path| remove_trailing_slash(path)}.each do |path| + # optimize for the most common case + begin + fu_mkdir path, mode + next + rescue SystemCallError + next if File.directory?(path) + end + + stack = [] + until path == stack.last # dirname("/")=="/", dirname("C:/")=="C:/" + stack.push path + path = File.dirname(path) + end + stack.pop # root directory should exist + stack.reverse_each do |dir| + begin + fu_mkdir dir, mode + rescue SystemCallError + raise unless File.directory?(dir) + end + end + end + + return *list + end + module_function :mkdir_p + + alias mkpath mkdir_p + alias makedirs mkdir_p + module_function :mkpath + module_function :makedirs + + def fu_mkdir(path, mode) #:nodoc: + path = remove_trailing_slash(path) + if mode + Dir.mkdir path, mode + File.chmod mode, path + else + Dir.mkdir path + end + end + private_module_function :fu_mkdir + + # + # Removes one or more directories. + # + # Bundler::FileUtils.rmdir 'somedir' + # Bundler::FileUtils.rmdir %w(somedir anydir otherdir) + # # Does not really remove directory; outputs message. + # Bundler::FileUtils.rmdir 'somedir', :verbose => true, :noop => true + # + def rmdir(list, parents: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "rmdir #{parents ? '-p ' : ''}#{list.join ' '}" if verbose + return if noop + list.each do |dir| + begin + Dir.rmdir(dir = remove_trailing_slash(dir)) + if parents + until (parent = File.dirname(dir)) == '.' or parent == dir + dir = parent + Dir.rmdir(dir) + end + end + rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT + end + end + end + module_function :rmdir + + # + # :call-seq: + # Bundler::FileUtils.ln(target, link, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln(target, dir, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln(targets, dir, force: nil, noop: nil, verbose: nil) + # + # In the first form, creates a hard link +link+ which points to +target+. + # If +link+ already exists, raises Errno::EEXIST. + # But if the :force option is set, overwrites +link+. + # + # Bundler::FileUtils.ln 'gcc', 'cc', verbose: true + # Bundler::FileUtils.ln '/usr/bin/emacs21', '/usr/bin/emacs' + # + # In the second form, creates a link +dir/target+ pointing to +target+. + # In the third form, creates several hard links in the directory +dir+, + # pointing to each item in +targets+. + # If +dir+ is not a directory, raises Errno::ENOTDIR. + # + # Bundler::FileUtils.cd '/sbin' + # Bundler::FileUtils.ln %w(cp mv mkdir), '/bin' # Now /sbin/cp and /bin/cp are linked. + # + def ln(src, dest, force: nil, noop: nil, verbose: nil) + fu_output_message "ln#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest0(src, dest) do |s,d| + remove_file d, true if force + File.link s, d + end + end + module_function :ln + + alias link ln + module_function :link + + # + # :call-seq: + # Bundler::FileUtils.ln_s(target, link, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln_s(target, dir, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln_s(targets, dir, force: nil, noop: nil, verbose: nil) + # + # In the first form, creates a symbolic link +link+ which points to +target+. + # If +link+ already exists, raises Errno::EEXIST. + # But if the :force option is set, overwrites +link+. + # + # Bundler::FileUtils.ln_s '/usr/bin/ruby', '/usr/local/bin/ruby' + # Bundler::FileUtils.ln_s 'verylongsourcefilename.c', 'c', force: true + # + # In the second form, creates a link +dir/target+ pointing to +target+. + # In the third form, creates several symbolic links in the directory +dir+, + # pointing to each item in +targets+. + # If +dir+ is not a directory, raises Errno::ENOTDIR. + # + # Bundler::FileUtils.ln_s Dir.glob('/bin/*.rb'), '/home/foo/bin' + # + def ln_s(src, dest, force: nil, noop: nil, verbose: nil) + fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest0(src, dest) do |s,d| + remove_file d, true if force + File.symlink s, d + end + end + module_function :ln_s + + alias symlink ln_s + module_function :symlink + + # + # :call-seq: + # Bundler::FileUtils.ln_sf(*args) + # + # Same as + # + # Bundler::FileUtils.ln_s(*args, force: true) + # + def ln_sf(src, dest, noop: nil, verbose: nil) + ln_s src, dest, force: true, noop: noop, verbose: verbose + end + module_function :ln_sf + + # + # Copies a file content +src+ to +dest+. If +dest+ is a directory, + # copies +src+ to +dest/src+. + # + # If +src+ is a list of files, then +dest+ must be a directory. + # + # Bundler::FileUtils.cp 'eval.c', 'eval.c.org' + # Bundler::FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6' + # Bundler::FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true + # Bundler::FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink + # + def cp(src, dest, preserve: nil, noop: nil, verbose: nil) + fu_output_message "cp#{preserve ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest(src, dest) do |s, d| + copy_file s, d, preserve + end + end + module_function :cp + + alias copy cp + module_function :copy + + # + # Copies +src+ to +dest+. If +src+ is a directory, this method copies + # all its contents recursively. If +dest+ is a directory, copies + # +src+ to +dest/src+. + # + # +src+ can be a list of files. + # + # # Installing Ruby library "mylib" under the site_ruby + # Bundler::FileUtils.rm_r site_ruby + '/mylib', :force + # Bundler::FileUtils.cp_r 'lib/', site_ruby + '/mylib' + # + # # Examples of copying several files to target directory. + # Bundler::FileUtils.cp_r %w(mail.rb field.rb debug/), site_ruby + '/tmail' + # Bundler::FileUtils.cp_r Dir.glob('*.rb'), '/home/foo/lib/ruby', :noop => true, :verbose => true + # + # # If you want to copy all contents of a directory instead of the + # # directory itself, c.f. src/x -> dest/x, src/y -> dest/y, + # # use following code. + # Bundler::FileUtils.cp_r 'src/.', 'dest' # cp_r('src', 'dest') makes dest/src, + # # but this doesn't. + # + def cp_r(src, dest, preserve: nil, noop: nil, verbose: nil, + dereference_root: true, remove_destination: nil) + fu_output_message "cp -r#{preserve ? 'p' : ''}#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest(src, dest) do |s, d| + copy_entry s, d, preserve, dereference_root, remove_destination + end + end + module_function :cp_r + + # + # Copies a file system entry +src+ to +dest+. + # If +src+ is a directory, this method copies its contents recursively. + # This method preserves file types, c.f. symlink, directory... + # (FIFO, device files and etc. are not supported yet) + # + # Both of +src+ and +dest+ must be a path name. + # +src+ must exist, +dest+ must not exist. + # + # If +preserve+ is true, this method preserves owner, group, and + # modified time. Permissions are copied regardless +preserve+. + # + # If +dereference_root+ is true, this method dereference tree root. + # + # If +remove_destination+ is true, this method removes each destination file before copy. + # + def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false) + Entry_.new(src, nil, dereference_root).wrap_traverse(proc do |ent| + destent = Entry_.new(dest, ent.rel, false) + File.unlink destent.path if remove_destination && File.file?(destent.path) + ent.copy destent.path + end, proc do |ent| + destent = Entry_.new(dest, ent.rel, false) + ent.copy_metadata destent.path if preserve + end) + end + module_function :copy_entry + + # + # Copies file contents of +src+ to +dest+. + # Both of +src+ and +dest+ must be a path name. + # + def copy_file(src, dest, preserve = false, dereference = true) + ent = Entry_.new(src, nil, dereference) + ent.copy_file dest + ent.copy_metadata dest if preserve + end + module_function :copy_file + + # + # Copies stream +src+ to +dest+. + # +src+ must respond to #read(n) and + # +dest+ must respond to #write(str). + # + def copy_stream(src, dest) + IO.copy_stream(src, dest) + end + module_function :copy_stream + + # + # Moves file(s) +src+ to +dest+. If +file+ and +dest+ exist on the different + # disk partition, the file is copied then the original file is removed. + # + # Bundler::FileUtils.mv 'badname.rb', 'goodname.rb' + # Bundler::FileUtils.mv 'stuff.rb', '/notexist/lib/ruby', :force => true # no error + # + # Bundler::FileUtils.mv %w(junk.txt dust.txt), '/home/foo/.trash/' + # Bundler::FileUtils.mv Dir.glob('test*.rb'), 'test', :noop => true, :verbose => true + # + def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil) + fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest(src, dest) do |s, d| + destent = Entry_.new(d, nil, true) + begin + if destent.exist? + if destent.directory? + raise Errno::EEXIST, d + else + destent.remove_file if rename_cannot_overwrite_file? + end + end + begin + File.rename s, d + rescue Errno::EXDEV + copy_entry s, d, true + if secure + remove_entry_secure s, force + else + remove_entry s, force + end + end + rescue SystemCallError + raise unless force + end + end + end + module_function :mv + + alias move mv + module_function :move + + def rename_cannot_overwrite_file? #:nodoc: + /emx/ =~ RUBY_PLATFORM + end + private_module_function :rename_cannot_overwrite_file? + + # + # Remove file(s) specified in +list+. This method cannot remove directories. + # All StandardErrors are ignored when the :force option is set. + # + # Bundler::FileUtils.rm %w( junk.txt dust.txt ) + # Bundler::FileUtils.rm Dir.glob('*.so') + # Bundler::FileUtils.rm 'NotExistFile', :force => true # never raises exception + # + def rm(list, force: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "rm#{force ? ' -f' : ''} #{list.join ' '}" if verbose + return if noop + + list.each do |path| + remove_file path, force + end + end + module_function :rm + + alias remove rm + module_function :remove + + # + # Equivalent to + # + # Bundler::FileUtils.rm(list, :force => true) + # + def rm_f(list, noop: nil, verbose: nil) + rm list, force: true, noop: noop, verbose: verbose + end + module_function :rm_f + + alias safe_unlink rm_f + module_function :safe_unlink + + # + # remove files +list+[0] +list+[1]... If +list+[n] is a directory, + # removes its all contents recursively. This method ignores + # StandardError when :force option is set. + # + # Bundler::FileUtils.rm_r Dir.glob('/tmp/*') + # Bundler::FileUtils.rm_r 'some_dir', :force => true + # + # WARNING: This method causes local vulnerability + # if one of parent directories or removing directory tree are world + # writable (including /tmp, whose permission is 1777), and the current + # process has strong privilege such as Unix super user (root), and the + # system has symbolic link. For secure removing, read the documentation + # of #remove_entry_secure carefully, and set :secure option to true. + # Default is :secure=>false. + # + # NOTE: This method calls #remove_entry_secure if :secure option is set. + # See also #remove_entry_secure. + # + def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil) + list = fu_list(list) + fu_output_message "rm -r#{force ? 'f' : ''} #{list.join ' '}" if verbose + return if noop + list.each do |path| + if secure + remove_entry_secure path, force + else + remove_entry path, force + end + end + end + module_function :rm_r + + # + # Equivalent to + # + # Bundler::FileUtils.rm_r(list, :force => true) + # + # WARNING: This method causes local vulnerability. + # Read the documentation of #rm_r first. + # + def rm_rf(list, noop: nil, verbose: nil, secure: nil) + rm_r list, force: true, noop: noop, verbose: verbose, secure: secure + end + module_function :rm_rf + + alias rmtree rm_rf + module_function :rmtree + + # + # This method removes a file system entry +path+. +path+ shall be a + # regular file, a directory, or something. If +path+ is a directory, + # remove it recursively. This method is required to avoid TOCTTOU + # (time-of-check-to-time-of-use) local security vulnerability of #rm_r. + # #rm_r causes security hole when: + # + # * Parent directory is world writable (including /tmp). + # * Removing directory tree includes world writable directory. + # * The system has symbolic link. + # + # To avoid this security hole, this method applies special preprocess. + # If +path+ is a directory, this method chown(2) and chmod(2) all + # removing directories. This requires the current process is the + # owner of the removing whole directory tree, or is the super user (root). + # + # WARNING: You must ensure that *ALL* parent directories cannot be + # moved by other untrusted users. For example, parent directories + # should not be owned by untrusted users, and should not be world + # writable except when the sticky bit set. + # + # WARNING: Only the owner of the removing directory tree, or Unix super + # user (root) should invoke this method. Otherwise this method does not + # work. + # + # For details of this security vulnerability, see Perl's case: + # + # * http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448 + # * http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452 + # + # For fileutils.rb, this vulnerability is reported in [ruby-dev:26100]. + # + def remove_entry_secure(path, force = false) + unless fu_have_symlink? + remove_entry path, force + return + end + fullpath = File.expand_path(path) + st = File.lstat(fullpath) + unless st.directory? + File.unlink fullpath + return + end + # is a directory. + parent_st = File.stat(File.dirname(fullpath)) + unless parent_st.world_writable? + remove_entry path, force + return + end + unless parent_st.sticky? + raise ArgumentError, "parent directory is world writable, Bundler::FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})" + end + # freeze tree root + euid = Process.euid + File.open(fullpath + '/.') {|f| + unless fu_stat_identical_entry?(st, f.stat) + # symlink (TOC-to-TOU attack?) + File.unlink fullpath + return + end + f.chown euid, -1 + f.chmod 0700 + unless fu_stat_identical_entry?(st, File.lstat(fullpath)) + # TOC-to-TOU attack? + File.unlink fullpath + return + end + } + # ---- tree root is frozen ---- + root = Entry_.new(path) + root.preorder_traverse do |ent| + if ent.directory? + ent.chown euid, -1 + ent.chmod 0700 + end + end + root.postorder_traverse do |ent| + begin + ent.remove + rescue + raise unless force + end + end + rescue + raise unless force + end + module_function :remove_entry_secure + + def fu_have_symlink? #:nodoc: + File.symlink nil, nil + rescue NotImplementedError + return false + rescue TypeError + return true + end + private_module_function :fu_have_symlink? + + def fu_stat_identical_entry?(a, b) #:nodoc: + a.dev == b.dev and a.ino == b.ino + end + private_module_function :fu_stat_identical_entry? + + # + # This method removes a file system entry +path+. + # +path+ might be a regular file, a directory, or something. + # If +path+ is a directory, remove it recursively. + # + # See also #remove_entry_secure. + # + def remove_entry(path, force = false) + Entry_.new(path).postorder_traverse do |ent| + begin + ent.remove + rescue + raise unless force + end + end + rescue + raise unless force + end + module_function :remove_entry + + # + # Removes a file +path+. + # This method ignores StandardError if +force+ is true. + # + def remove_file(path, force = false) + Entry_.new(path).remove_file + rescue + raise unless force + end + module_function :remove_file + + # + # Removes a directory +dir+ and its contents recursively. + # This method ignores StandardError if +force+ is true. + # + def remove_dir(path, force = false) + remove_entry path, force # FIXME?? check if it is a directory + end + module_function :remove_dir + + # + # Returns true if the contents of a file +a+ and a file +b+ are identical. + # + # Bundler::FileUtils.compare_file('somefile', 'somefile') #=> true + # Bundler::FileUtils.compare_file('/dev/null', '/dev/urandom') #=> false + # + def compare_file(a, b) + return false unless File.size(a) == File.size(b) + File.open(a, 'rb') {|fa| + File.open(b, 'rb') {|fb| + return compare_stream(fa, fb) + } + } + end + module_function :compare_file + + alias identical? compare_file + alias cmp compare_file + module_function :identical? + module_function :cmp + + # + # Returns true if the contents of a stream +a+ and +b+ are identical. + # + def compare_stream(a, b) + bsize = fu_stream_blksize(a, b) + sa = String.new(capacity: bsize) + sb = String.new(capacity: bsize) + begin + a.read(bsize, sa) + b.read(bsize, sb) + return true if sa.empty? && sb.empty? + end while sa == sb + false + end + module_function :compare_stream + + # + # If +src+ is not same as +dest+, copies it and changes the permission + # mode to +mode+. If +dest+ is a directory, destination is +dest+/+src+. + # This method removes destination before copy. + # + # Bundler::FileUtils.install 'ruby', '/usr/local/bin/ruby', :mode => 0755, :verbose => true + # Bundler::FileUtils.install 'lib.rb', '/usr/local/lib/ruby/site_ruby', :verbose => true + # + def install(src, dest, mode: nil, owner: nil, group: nil, preserve: nil, + noop: nil, verbose: nil) + if verbose + msg = +"install -c" + msg << ' -p' if preserve + msg << ' -m ' << mode_to_s(mode) if mode + msg << " -o #{owner}" if owner + msg << " -g #{group}" if group + msg << ' ' << [src,dest].flatten.join(' ') + fu_output_message msg + end + return if noop + uid = fu_get_uid(owner) + gid = fu_get_gid(group) + fu_each_src_dest(src, dest) do |s, d| + st = File.stat(s) + unless File.exist?(d) and compare_file(s, d) + remove_file d, true + copy_file s, d + File.utime st.atime, st.mtime, d if preserve + File.chmod fu_mode(mode, st), d if mode + File.chown uid, gid, d if uid or gid + end + end + end + module_function :install + + def user_mask(target) #:nodoc: + target.each_char.inject(0) do |mask, chr| + case chr + when "u" + mask | 04700 + when "g" + mask | 02070 + when "o" + mask | 01007 + when "a" + mask | 07777 + else + raise ArgumentError, "invalid `who' symbol in file mode: #{chr}" + end + end + end + private_module_function :user_mask + + def apply_mask(mode, user_mask, op, mode_mask) #:nodoc: + case op + when '=' + (mode & ~user_mask) | (user_mask & mode_mask) + when '+' + mode | (user_mask & mode_mask) + when '-' + mode & ~(user_mask & mode_mask) + end + end + private_module_function :apply_mask + + def symbolic_modes_to_i(mode_sym, path) #:nodoc: + mode = if File::Stat === path + path.mode + else + File.stat(path).mode + end + mode_sym.split(/,/).inject(mode & 07777) do |current_mode, clause| + target, *actions = clause.split(/([=+-])/) + raise ArgumentError, "invalid file mode: #{mode_sym}" if actions.empty? + target = 'a' if target.empty? + user_mask = user_mask(target) + actions.each_slice(2) do |op, perm| + need_apply = op == '=' + mode_mask = (perm || '').each_char.inject(0) do |mask, chr| + case chr + when "r" + mask | 0444 + when "w" + mask | 0222 + when "x" + mask | 0111 + when "X" + if FileTest.directory? path + mask | 0111 + else + mask + end + when "s" + mask | 06000 + when "t" + mask | 01000 + when "u", "g", "o" + if mask.nonzero? + current_mode = apply_mask(current_mode, user_mask, op, mask) + end + need_apply = false + copy_mask = user_mask(chr) + (current_mode & copy_mask) / (copy_mask & 0111) * (user_mask & 0111) + else + raise ArgumentError, "invalid `perm' symbol in file mode: #{chr}" + end + end + + if mode_mask.nonzero? || need_apply + current_mode = apply_mask(current_mode, user_mask, op, mode_mask) + end + end + current_mode + end + end + private_module_function :symbolic_modes_to_i + + def fu_mode(mode, path) #:nodoc: + mode.is_a?(String) ? symbolic_modes_to_i(mode, path) : mode + end + private_module_function :fu_mode + + def mode_to_s(mode) #:nodoc: + mode.is_a?(String) ? mode : "%o" % mode + end + private_module_function :mode_to_s + + # + # Changes permission bits on the named files (in +list+) to the bit pattern + # represented by +mode+. + # + # +mode+ is the symbolic and absolute mode can be used. + # + # Absolute mode is + # Bundler::FileUtils.chmod 0755, 'somecommand' + # Bundler::FileUtils.chmod 0644, %w(my.rb your.rb his.rb her.rb) + # Bundler::FileUtils.chmod 0755, '/usr/bin/ruby', :verbose => true + # + # Symbolic mode is + # Bundler::FileUtils.chmod "u=wrx,go=rx", 'somecommand' + # Bundler::FileUtils.chmod "u=wr,go=rr", %w(my.rb your.rb his.rb her.rb) + # Bundler::FileUtils.chmod "u=wrx,go=rx", '/usr/bin/ruby', :verbose => true + # + # "a" :: is user, group, other mask. + # "u" :: is user's mask. + # "g" :: is group's mask. + # "o" :: is other's mask. + # "w" :: is write permission. + # "r" :: is read permission. + # "x" :: is execute permission. + # "X" :: + # is execute permission for directories only, must be used in conjunction with "+" + # "s" :: is uid, gid. + # "t" :: is sticky bit. + # "+" :: is added to a class given the specified mode. + # "-" :: Is removed from a given class given mode. + # "=" :: Is the exact nature of the class will be given a specified mode. + + def chmod(mode, list, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message sprintf('chmod %s %s', mode_to_s(mode), list.join(' ')) if verbose + return if noop + list.each do |path| + Entry_.new(path).chmod(fu_mode(mode, path)) + end + end + module_function :chmod + + # + # Changes permission bits on the named files (in +list+) + # to the bit pattern represented by +mode+. + # + # Bundler::FileUtils.chmod_R 0700, "/tmp/app.#{$$}" + # Bundler::FileUtils.chmod_R "u=wrx", "/tmp/app.#{$$}" + # + def chmod_R(mode, list, noop: nil, verbose: nil, force: nil) + list = fu_list(list) + fu_output_message sprintf('chmod -R%s %s %s', + (force ? 'f' : ''), + mode_to_s(mode), list.join(' ')) if verbose + return if noop + list.each do |root| + Entry_.new(root).traverse do |ent| + begin + ent.chmod(fu_mode(mode, ent.path)) + rescue + raise unless force + end + end + end + end + module_function :chmod_R + + # + # Changes owner and group on the named files (in +list+) + # to the user +user+ and the group +group+. +user+ and +group+ + # may be an ID (Integer/String) or a name (String). + # If +user+ or +group+ is nil, this method does not change + # the attribute. + # + # Bundler::FileUtils.chown 'root', 'staff', '/usr/local/bin/ruby' + # Bundler::FileUtils.chown nil, 'bin', Dir.glob('/usr/bin/*'), :verbose => true + # + def chown(user, group, list, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message sprintf('chown %s %s', + (group ? "#{user}:#{group}" : user || ':'), + list.join(' ')) if verbose + return if noop + uid = fu_get_uid(user) + gid = fu_get_gid(group) + list.each do |path| + Entry_.new(path).chown uid, gid + end + end + module_function :chown + + # + # Changes owner and group on the named files (in +list+) + # to the user +user+ and the group +group+ recursively. + # +user+ and +group+ may be an ID (Integer/String) or + # a name (String). If +user+ or +group+ is nil, this + # method does not change the attribute. + # + # Bundler::FileUtils.chown_R 'www', 'www', '/var/www/htdocs' + # Bundler::FileUtils.chown_R 'cvs', 'cvs', '/var/cvs', :verbose => true + # + def chown_R(user, group, list, noop: nil, verbose: nil, force: nil) + list = fu_list(list) + fu_output_message sprintf('chown -R%s %s %s', + (force ? 'f' : ''), + (group ? "#{user}:#{group}" : user || ':'), + list.join(' ')) if verbose + return if noop + uid = fu_get_uid(user) + gid = fu_get_gid(group) + list.each do |root| + Entry_.new(root).traverse do |ent| + begin + ent.chown uid, gid + rescue + raise unless force + end + end + end + end + module_function :chown_R + + begin + require 'etc' + rescue LoadError # rescue LoadError for miniruby + end + + def fu_get_uid(user) #:nodoc: + return nil unless user + case user + when Integer + user + when /\A\d+\z/ + user.to_i + else + Etc.getpwnam(user) ? Etc.getpwnam(user).uid : nil + end + end + private_module_function :fu_get_uid + + def fu_get_gid(group) #:nodoc: + return nil unless group + case group + when Integer + group + when /\A\d+\z/ + group.to_i + else + Etc.getgrnam(group) ? Etc.getgrnam(group).gid : nil + end + end + private_module_function :fu_get_gid + + # + # Updates modification time (mtime) and access time (atime) of file(s) in + # +list+. Files are created if they don't exist. + # + # Bundler::FileUtils.touch 'timestamp' + # Bundler::FileUtils.touch Dir.glob('*.c'); system 'make' + # + def touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil) + list = fu_list(list) + t = mtime + if verbose + fu_output_message "touch #{nocreate ? '-c ' : ''}#{t ? t.strftime('-t %Y%m%d%H%M.%S ') : ''}#{list.join ' '}" + end + return if noop + list.each do |path| + created = nocreate + begin + File.utime(t, t, path) + rescue Errno::ENOENT + raise if created + File.open(path, 'a') { + ; + } + created = true + retry if t + end + end + end + module_function :touch + + private + + module StreamUtils_ + private + + def fu_windows? + /mswin|mingw|bccwin|emx/ =~ RUBY_PLATFORM + end + + def fu_copy_stream0(src, dest, blksize = nil) #:nodoc: + IO.copy_stream(src, dest) + end + + def fu_stream_blksize(*streams) + streams.each do |s| + next unless s.respond_to?(:stat) + size = fu_blksize(s.stat) + return size if size + end + fu_default_blksize() + end + + def fu_blksize(st) + s = st.blksize + return nil unless s + return nil if s == 0 + s + end + + def fu_default_blksize + 1024 + end + end + + include StreamUtils_ + extend StreamUtils_ + + class Entry_ #:nodoc: internal use only + include StreamUtils_ + + def initialize(a, b = nil, deref = false) + @prefix = @rel = @path = nil + if b + @prefix = a + @rel = b + else + @path = a + end + @deref = deref + @stat = nil + @lstat = nil + end + + def inspect + "\#<#{self.class} #{path()}>" + end + + def path + if @path + File.path(@path) + else + join(@prefix, @rel) + end + end + + def prefix + @prefix || @path + end + + def rel + @rel + end + + def dereference? + @deref + end + + def exist? + begin + lstat + true + rescue Errno::ENOENT + false + end + end + + def file? + s = lstat! + s and s.file? + end + + def directory? + s = lstat! + s and s.directory? + end + + def symlink? + s = lstat! + s and s.symlink? + end + + def chardev? + s = lstat! + s and s.chardev? + end + + def blockdev? + s = lstat! + s and s.blockdev? + end + + def socket? + s = lstat! + s and s.socket? + end + + def pipe? + s = lstat! + s and s.pipe? + end + + S_IF_DOOR = 0xD000 + + def door? + s = lstat! + s and (s.mode & 0xF000 == S_IF_DOOR) + end + + def entries + opts = {} + opts[:encoding] = ::Encoding::UTF_8 if fu_windows? + Dir.entries(path(), opts)\ + .reject {|n| n == '.' or n == '..' }\ + .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) } + end + + def stat + return @stat if @stat + if lstat() and lstat().symlink? + @stat = File.stat(path()) + else + @stat = lstat() + end + @stat + end + + def stat! + return @stat if @stat + if lstat! and lstat!.symlink? + @stat = File.stat(path()) + else + @stat = lstat! + end + @stat + rescue SystemCallError + nil + end + + def lstat + if dereference? + @lstat ||= File.stat(path()) + else + @lstat ||= File.lstat(path()) + end + end + + def lstat! + lstat() + rescue SystemCallError + nil + end + + def chmod(mode) + if symlink? + File.lchmod mode, path() if have_lchmod? + else + File.chmod mode, path() + end + end + + def chown(uid, gid) + if symlink? + File.lchown uid, gid, path() if have_lchown? + else + File.chown uid, gid, path() + end + end + + def copy(dest) + lstat + case + when file? + copy_file dest + when directory? + if !File.exist?(dest) and descendant_directory?(dest, path) + raise ArgumentError, "cannot copy directory %s to itself %s" % [path, dest] + end + begin + Dir.mkdir dest + rescue + raise unless File.directory?(dest) + end + when symlink? + File.symlink File.readlink(path()), dest + when chardev? + raise "cannot handle device file" unless File.respond_to?(:mknod) + mknod dest, ?c, 0666, lstat().rdev + when blockdev? + raise "cannot handle device file" unless File.respond_to?(:mknod) + mknod dest, ?b, 0666, lstat().rdev + when socket? + raise "cannot handle socket" unless File.respond_to?(:mknod) + mknod dest, nil, lstat().mode, 0 + when pipe? + raise "cannot handle FIFO" unless File.respond_to?(:mkfifo) + mkfifo dest, 0666 + when door? + raise "cannot handle door: #{path()}" + else + raise "unknown file type: #{path()}" + end + end + + def copy_file(dest) + File.open(path()) do |s| + File.open(dest, 'wb', s.stat.mode) do |f| + IO.copy_stream(s, f) + end + end + end + + def copy_metadata(path) + st = lstat() + if !st.symlink? + File.utime st.atime, st.mtime, path + end + mode = st.mode + begin + if st.symlink? + begin + File.lchown st.uid, st.gid, path + rescue NotImplementedError + end + else + File.chown st.uid, st.gid, path + end + rescue Errno::EPERM, Errno::EACCES + # clear setuid/setgid + mode &= 01777 + end + if st.symlink? + begin + File.lchmod mode, path + rescue NotImplementedError + end + else + File.chmod mode, path + end + end + + def remove + if directory? + remove_dir1 + else + remove_file + end + end + + def remove_dir1 + platform_support { + Dir.rmdir path().chomp(?/) + } + end + + def remove_file + platform_support { + File.unlink path + } + end + + def platform_support + return yield unless fu_windows? + first_time_p = true + begin + yield + rescue Errno::ENOENT + raise + rescue => err + if first_time_p + first_time_p = false + begin + File.chmod 0700, path() # Windows does not have symlink + retry + rescue SystemCallError + end + end + raise err + end + end + + def preorder_traverse + stack = [self] + while ent = stack.pop + yield ent + stack.concat ent.entries.reverse if ent.directory? + end + end + + alias traverse preorder_traverse + + def postorder_traverse + if directory? + entries().each do |ent| + ent.postorder_traverse do |e| + yield e + end + end + end + ensure + yield self + end + + def wrap_traverse(pre, post) + pre.call self + if directory? + entries.each do |ent| + ent.wrap_traverse pre, post + end + end + post.call self + end + + private + + $fileutils_rb_have_lchmod = nil + + def have_lchmod? + # This is not MT-safe, but it does not matter. + if $fileutils_rb_have_lchmod == nil + $fileutils_rb_have_lchmod = check_have_lchmod? + end + $fileutils_rb_have_lchmod + end + + def check_have_lchmod? + return false unless File.respond_to?(:lchmod) + File.lchmod 0 + return true + rescue NotImplementedError + return false + end + + $fileutils_rb_have_lchown = nil + + def have_lchown? + # This is not MT-safe, but it does not matter. + if $fileutils_rb_have_lchown == nil + $fileutils_rb_have_lchown = check_have_lchown? + end + $fileutils_rb_have_lchown + end + + def check_have_lchown? + return false unless File.respond_to?(:lchown) + File.lchown nil, nil + return true + rescue NotImplementedError + return false + end + + def join(dir, base) + return File.path(dir) if not base or base == '.' + return File.path(base) if not dir or dir == '.' + File.join(dir, base) + end + + if File::ALT_SEPARATOR + DIRECTORY_TERM = "(?=[/#{Regexp.quote(File::ALT_SEPARATOR)}]|\\z)" + else + DIRECTORY_TERM = "(?=/|\\z)" + end + SYSCASE = File::FNM_SYSCASE.nonzero? ? "-i" : "" + + def descendant_directory?(descendant, ascendant) + /\A(?#{SYSCASE}:#{Regexp.quote(ascendant)})#{DIRECTORY_TERM}/ =~ File.dirname(descendant) + end + end # class Entry_ + + def fu_list(arg) #:nodoc: + [arg].flatten.map {|path| File.path(path) } + end + private_module_function :fu_list + + def fu_each_src_dest(src, dest) #:nodoc: + fu_each_src_dest0(src, dest) do |s, d| + raise ArgumentError, "same file: #{s} and #{d}" if fu_same?(s, d) + yield s, d + end + end + private_module_function :fu_each_src_dest + + def fu_each_src_dest0(src, dest) #:nodoc: + if tmp = Array.try_convert(src) + tmp.each do |s| + s = File.path(s) + yield s, File.join(dest, File.basename(s)) + end + else + src = File.path(src) + if File.directory?(dest) + yield src, File.join(dest, File.basename(src)) + else + yield src, File.path(dest) + end + end + end + private_module_function :fu_each_src_dest0 + + def fu_same?(a, b) #:nodoc: + File.identical?(a, b) + end + private_module_function :fu_same? + + @fileutils_output = $stderr + @fileutils_label = '' + + def fu_output_message(msg) #:nodoc: + @fileutils_output ||= $stderr + @fileutils_label ||= '' + @fileutils_output.puts @fileutils_label + msg + end + private_module_function :fu_output_message + + # This hash table holds command options. + OPT_TABLE = {} #:nodoc: internal use only + (private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name| + (tbl[name.to_s] = instance_method(name).parameters).map! {|t, n| n if t == :key}.compact! + tbl + } + + # + # Returns an Array of method names which have any options. + # + # p Bundler::FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...] + # + def self.commands + OPT_TABLE.keys + end + + # + # Returns an Array of option names. + # + # p Bundler::FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"] + # + def self.options + OPT_TABLE.values.flatten.uniq.map {|sym| sym.to_s } + end + + # + # Returns true if the method +mid+ have an option +opt+. + # + # p Bundler::FileUtils.have_option?(:cp, :noop) #=> true + # p Bundler::FileUtils.have_option?(:rm, :force) #=> true + # p Bundler::FileUtils.have_option?(:rm, :preserve) #=> false + # + def self.have_option?(mid, opt) + li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}" + li.include?(opt) + end + + # + # Returns an Array of option names of the method +mid+. + # + # p Bundler::FileUtils.options_of(:rm) #=> ["noop", "verbose", "force"] + # + def self.options_of(mid) + OPT_TABLE[mid.to_s].map {|sym| sym.to_s } + end + + # + # Returns an Array of method names which have the option +opt+. + # + # p Bundler::FileUtils.collect_method(:preserve) #=> ["cp", "cp_r", "copy", "install"] + # + def self.collect_method(opt) + OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt) } + end + + LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) + module LowMethods + private + def _do_nothing(*)end + ::Bundler::FileUtils::LOW_METHODS.map {|name| alias_method name, :_do_nothing} + end + + METHODS = singleton_methods() - [:private_module_function, + :commands, :options, :have_option?, :options_of, :collect_method] + + # + # This module has all methods of Bundler::FileUtils module, but it outputs messages + # before acting. This equates to passing the :verbose flag to + # methods in Bundler::FileUtils. + # + module Verbose + include Bundler::FileUtils + @fileutils_output = $stderr + @fileutils_label = '' + names = ::Bundler::FileUtils.collect_method(:verbose) + names.each do |name| + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}(*args, **options) + super(*args, **options, verbose: true) + end + EOS + end + private(*names) + extend self + class << self + public(*::Bundler::FileUtils::METHODS) + end + end + + # + # This module has all methods of Bundler::FileUtils module, but never changes + # files/directories. This equates to passing the :noop flag + # to methods in Bundler::FileUtils. + # + module NoWrite + include Bundler::FileUtils + include LowMethods + @fileutils_output = $stderr + @fileutils_label = '' + names = ::Bundler::FileUtils.collect_method(:noop) + names.each do |name| + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}(*args, **options) + super(*args, **options, noop: true) + end + EOS + end + private(*names) + extend self + class << self + public(*::Bundler::FileUtils::METHODS) + end + end + + # + # This module has all methods of Bundler::FileUtils module, but never changes + # files/directories, with printing message before acting. + # This equates to passing the :noop and :verbose flag + # to methods in Bundler::FileUtils. + # + module DryRun + include Bundler::FileUtils + include LowMethods + @fileutils_output = $stderr + @fileutils_label = '' + names = ::Bundler::FileUtils.collect_method(:noop) + names.each do |name| + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}(*args, **options) + super(*args, **options, noop: true, verbose: true) + end + EOS + end + private(*names) + extend self + class << self + public(*::Bundler::FileUtils::METHODS) + end + end + +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo.rb b/lib/bundler/vendor/molinillo/lib/molinillo.rb new file mode 100644 index 00000000000000..9e2867144fb485 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/compatibility' +require 'bundler/vendor/molinillo/lib/molinillo/gem_metadata' +require 'bundler/vendor/molinillo/lib/molinillo/errors' +require 'bundler/vendor/molinillo/lib/molinillo/resolver' +require 'bundler/vendor/molinillo/lib/molinillo/modules/ui' +require 'bundler/vendor/molinillo/lib/molinillo/modules/specification_provider' + +# Bundler::Molinillo is a generic dependency resolution algorithm. +module Bundler::Molinillo +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb b/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb new file mode 100644 index 00000000000000..3eba8e4083fe7c --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # Hacks needed for old Ruby versions. + module Compatibility + module_function + + if [].respond_to?(:flat_map) + # Flat map + # @param [Enumerable] enum an enumerable object + # @block the block to flat-map with + # @return The enum, flat-mapped + def flat_map(enum, &blk) + enum.flat_map(&blk) + end + else + # Flat map + # @param [Enumerable] enum an enumerable object + # @block the block to flat-map with + # @return The enum, flat-mapped + def flat_map(enum, &blk) + enum.map(&blk).flatten(1) + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb new file mode 100644 index 00000000000000..bcacf35243bc01 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # @!visibility private + module Delegates + # Delegates all {Bundler::Molinillo::ResolutionState} methods to a `#state` property. + module ResolutionState + # (see Bundler::Molinillo::ResolutionState#name) + def name + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.name + end + + # (see Bundler::Molinillo::ResolutionState#requirements) + def requirements + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.requirements + end + + # (see Bundler::Molinillo::ResolutionState#activated) + def activated + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.activated + end + + # (see Bundler::Molinillo::ResolutionState#requirement) + def requirement + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.requirement + end + + # (see Bundler::Molinillo::ResolutionState#possibilities) + def possibilities + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.possibilities + end + + # (see Bundler::Molinillo::ResolutionState#depth) + def depth + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.depth + end + + # (see Bundler::Molinillo::ResolutionState#conflicts) + def conflicts + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.conflicts + end + + # (see Bundler::Molinillo::ResolutionState#unused_unwind_options) + def unused_unwind_options + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.unused_unwind_options + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb new file mode 100644 index 00000000000000..ec9c770a2889fd --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + module Delegates + # Delegates all {Bundler::Molinillo::SpecificationProvider} methods to a + # `#specification_provider` property. + module SpecificationProvider + # (see Bundler::Molinillo::SpecificationProvider#search_for) + def search_for(dependency) + with_no_such_dependency_error_handling do + specification_provider.search_for(dependency) + end + end + + # (see Bundler::Molinillo::SpecificationProvider#dependencies_for) + def dependencies_for(specification) + with_no_such_dependency_error_handling do + specification_provider.dependencies_for(specification) + end + end + + # (see Bundler::Molinillo::SpecificationProvider#requirement_satisfied_by?) + def requirement_satisfied_by?(requirement, activated, spec) + with_no_such_dependency_error_handling do + specification_provider.requirement_satisfied_by?(requirement, activated, spec) + end + end + + # (see Bundler::Molinillo::SpecificationProvider#name_for) + def name_for(dependency) + with_no_such_dependency_error_handling do + specification_provider.name_for(dependency) + end + end + + # (see Bundler::Molinillo::SpecificationProvider#name_for_explicit_dependency_source) + def name_for_explicit_dependency_source + with_no_such_dependency_error_handling do + specification_provider.name_for_explicit_dependency_source + end + end + + # (see Bundler::Molinillo::SpecificationProvider#name_for_locking_dependency_source) + def name_for_locking_dependency_source + with_no_such_dependency_error_handling do + specification_provider.name_for_locking_dependency_source + end + end + + # (see Bundler::Molinillo::SpecificationProvider#sort_dependencies) + def sort_dependencies(dependencies, activated, conflicts) + with_no_such_dependency_error_handling do + specification_provider.sort_dependencies(dependencies, activated, conflicts) + end + end + + # (see Bundler::Molinillo::SpecificationProvider#allow_missing?) + def allow_missing?(dependency) + with_no_such_dependency_error_handling do + specification_provider.allow_missing?(dependency) + end + end + + private + + # Ensures any raised {NoSuchDependencyError} has its + # {NoSuchDependencyError#required_by} set. + # @yield + def with_no_such_dependency_error_handling + yield + rescue NoSuchDependencyError => error + if state + vertex = activated.vertex_named(name_for(error.dependency)) + error.required_by += vertex.incoming_edges.map { |e| e.origin.name } + error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty? + end + raise + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb new file mode 100644 index 00000000000000..677a8bd916509e --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +require 'set' +require 'tsort' + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/log' +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex' + +module Bundler::Molinillo + # A directed acyclic graph that is tuned to hold named dependencies + class DependencyGraph + include Enumerable + + # Enumerates through the vertices of the graph. + # @return [Array] The graph's vertices. + def each + return vertices.values.each unless block_given? + vertices.values.each { |v| yield v } + end + + include TSort + + # @!visibility private + alias tsort_each_node each + + # @!visibility private + def tsort_each_child(vertex, &block) + vertex.successors.each(&block) + end + + # Topologically sorts the given vertices. + # @param [Enumerable] vertices the vertices to be sorted, which must + # all belong to the same graph. + # @return [Array] The sorted vertices. + def self.tsort(vertices) + TSort.tsort( + lambda { |b| vertices.each(&b) }, + lambda { |v, &b| (v.successors & vertices).each(&b) } + ) + end + + # A directed edge of a {DependencyGraph} + # @attr [Vertex] origin The origin of the directed edge + # @attr [Vertex] destination The destination of the directed edge + # @attr [Object] requirement The requirement the directed edge represents + Edge = Struct.new(:origin, :destination, :requirement) + + # @return [{String => Vertex}] the vertices of the dependency graph, keyed + # by {Vertex#name} + attr_reader :vertices + + # @return [Log] the op log for this graph + attr_reader :log + + # Initializes an empty dependency graph + def initialize + @vertices = {} + @log = Log.new + end + + # Tags the current state of the dependency as the given tag + # @param [Object] tag an opaque tag for the current state of the graph + # @return [Void] + def tag(tag) + log.tag(self, tag) + end + + # Rewinds the graph to the state tagged as `tag` + # @param [Object] tag the tag to rewind to + # @return [Void] + def rewind_to(tag) + log.rewind_to(self, tag) + end + + # Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices} + # are properly copied. + # @param [DependencyGraph] other the graph to copy. + def initialize_copy(other) + super + @vertices = {} + @log = other.log.dup + traverse = lambda do |new_v, old_v| + return if new_v.outgoing_edges.size == old_v.outgoing_edges.size + old_v.outgoing_edges.each do |edge| + destination = add_vertex(edge.destination.name, edge.destination.payload) + add_edge_no_circular(new_v, destination, edge.requirement) + traverse.call(destination, edge.destination) + end + end + other.vertices.each do |name, vertex| + new_vertex = add_vertex(name, vertex.payload, vertex.root?) + new_vertex.explicit_requirements.replace(vertex.explicit_requirements) + traverse.call(new_vertex, vertex) + end + end + + # @return [String] a string suitable for debugging + def inspect + "#{self.class}:#{vertices.values.inspect}" + end + + # @param [Hash] options options for dot output. + # @return [String] Returns a dot format representation of the graph + def to_dot(options = {}) + edge_label = options.delete(:edge_label) + raise ArgumentError, "Unknown options: #{options.keys}" unless options.empty? + + dot_vertices = [] + dot_edges = [] + vertices.each do |n, v| + dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]" + v.outgoing_edges.each do |e| + label = edge_label ? edge_label.call(e) : e.requirement + dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=#{label.to_s.dump}]" + end + end + + dot_vertices.uniq! + dot_vertices.sort! + dot_edges.uniq! + dot_edges.sort! + + dot = dot_vertices.unshift('digraph G {').push('') + dot_edges.push('}') + dot.join("\n") + end + + # @return [Boolean] whether the two dependency graphs are equal, determined + # by a recursive traversal of each {#root_vertices} and its + # {Vertex#successors} + def ==(other) + return false unless other + return true if equal?(other) + vertices.each do |name, vertex| + other_vertex = other.vertex_named(name) + return false unless other_vertex + return false unless vertex.payload == other_vertex.payload + return false unless other_vertex.successors.to_set == vertex.successors.to_set + end + end + + # @param [String] name + # @param [Object] payload + # @param [Array] parent_names + # @param [Object] requirement the requirement that is requiring the child + # @return [void] + def add_child_vertex(name, payload, parent_names, requirement) + root = !parent_names.delete(nil) { true } + vertex = add_vertex(name, payload, root) + vertex.explicit_requirements << requirement if root + parent_names.each do |parent_name| + parent_vertex = vertex_named(parent_name) + add_edge(parent_vertex, vertex, requirement) + end + vertex + end + + # Adds a vertex with the given name, or updates the existing one. + # @param [String] name + # @param [Object] payload + # @return [Vertex] the vertex that was added to `self` + def add_vertex(name, payload, root = false) + log.add_vertex(self, name, payload, root) + end + + # Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively + # removing any non-root vertices that were orphaned in the process + # @param [String] name + # @return [Array] the vertices which have been detached + def detach_vertex_named(name) + log.detach_vertex_named(self, name) + end + + # @param [String] name + # @return [Vertex,nil] the vertex with the given name + def vertex_named(name) + vertices[name] + end + + # @param [String] name + # @return [Vertex,nil] the root vertex with the given name + def root_vertex_named(name) + vertex = vertex_named(name) + vertex if vertex && vertex.root? + end + + # Adds a new {Edge} to the dependency graph + # @param [Vertex] origin + # @param [Vertex] destination + # @param [Object] requirement the requirement that this edge represents + # @return [Edge] the added edge + def add_edge(origin, destination, requirement) + if destination.path_to?(origin) + raise CircularDependencyError.new([origin, destination]) + end + add_edge_no_circular(origin, destination, requirement) + end + + # Deletes an {Edge} from the dependency graph + # @param [Edge] edge + # @return [Void] + def delete_edge(edge) + log.delete_edge(self, edge.origin.name, edge.destination.name, edge.requirement) + end + + # Sets the payload of the vertex with the given name + # @param [String] name the name of the vertex + # @param [Object] payload the payload + # @return [Void] + def set_payload(name, payload) + log.set_payload(self, name, payload) + end + + private + + # Adds a new {Edge} to the dependency graph without checking for + # circularity. + # @param (see #add_edge) + # @return (see #add_edge) + def add_edge_no_circular(origin, destination, requirement) + log.add_edge_no_circular(self, origin.name, destination.name, requirement) + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb new file mode 100644 index 00000000000000..c04c7eec9c783b --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + class DependencyGraph + # An action that modifies a {DependencyGraph} that is reversible. + # @abstract + class Action + # rubocop:disable Lint/UnusedMethodArgument + + # @return [Symbol] The name of the action. + def self.action_name + raise 'Abstract' + end + + # Performs the action on the given graph. + # @param [DependencyGraph] graph the graph to perform the action on. + # @return [Void] + def up(graph) + raise 'Abstract' + end + + # Reverses the action on the given graph. + # @param [DependencyGraph] graph the graph to reverse the action on. + # @return [Void] + def down(graph) + raise 'Abstract' + end + + # @return [Action,Nil] The previous action + attr_accessor :previous + + # @return [Action,Nil] The next action + attr_accessor :next + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb new file mode 100644 index 00000000000000..9849aea2fee2b0 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' +module Bundler::Molinillo + class DependencyGraph + # @!visibility private + # (see DependencyGraph#add_edge_no_circular) + class AddEdgeNoCircular < Action + # @!group Action + + # (see Action.action_name) + def self.action_name + :add_vertex + end + + # (see Action#up) + def up(graph) + edge = make_edge(graph) + edge.origin.outgoing_edges << edge + edge.destination.incoming_edges << edge + edge + end + + # (see Action#down) + def down(graph) + edge = make_edge(graph) + delete_first(edge.origin.outgoing_edges, edge) + delete_first(edge.destination.incoming_edges, edge) + end + + # @!group AddEdgeNoCircular + + # @return [String] the name of the origin of the edge + attr_reader :origin + + # @return [String] the name of the destination of the edge + attr_reader :destination + + # @return [Object] the requirement that the edge represents + attr_reader :requirement + + # @param [DependencyGraph] graph the graph to find vertices from + # @return [Edge] The edge this action adds + def make_edge(graph) + Edge.new(graph.vertex_named(origin), graph.vertex_named(destination), requirement) + end + + # Initialize an action to add an edge to a dependency graph + # @param [String] origin the name of the origin of the edge + # @param [String] destination the name of the destination of the edge + # @param [Object] requirement the requirement that the edge represents + def initialize(origin, destination, requirement) + @origin = origin + @destination = destination + @requirement = requirement + end + + private + + def delete_first(array, item) + return unless index = array.index(item) + array.delete_at(index) + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb new file mode 100644 index 00000000000000..0a1e08255bccd5 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' +module Bundler::Molinillo + class DependencyGraph + # @!visibility private + # (see DependencyGraph#add_vertex) + class AddVertex < Action # :nodoc: + # @!group Action + + # (see Action.action_name) + def self.action_name + :add_vertex + end + + # (see Action#up) + def up(graph) + if existing = graph.vertices[name] + @existing_payload = existing.payload + @existing_root = existing.root + end + vertex = existing || Vertex.new(name, payload) + graph.vertices[vertex.name] = vertex + vertex.payload ||= payload + vertex.root ||= root + vertex + end + + # (see Action#down) + def down(graph) + if defined?(@existing_payload) + vertex = graph.vertices[name] + vertex.payload = @existing_payload + vertex.root = @existing_root + else + graph.vertices.delete(name) + end + end + + # @!group AddVertex + + # @return [String] the name of the vertex + attr_reader :name + + # @return [Object] the payload for the vertex + attr_reader :payload + + # @return [Boolean] whether the vertex is root or not + attr_reader :root + + # Initialize an action to add a vertex to a dependency graph + # @param [String] name the name of the vertex + # @param [Object] payload the payload for the vertex + # @param [Boolean] root whether the vertex is root or not + def initialize(name, payload, root) + @name = name + @payload = payload + @root = root + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb new file mode 100644 index 00000000000000..1d9f4b327d10d3 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' +module Bundler::Molinillo + class DependencyGraph + # @!visibility private + # (see DependencyGraph#delete_edge) + class DeleteEdge < Action + # @!group Action + + # (see Action.action_name) + def self.action_name + :delete_edge + end + + # (see Action#up) + def up(graph) + edge = make_edge(graph) + edge.origin.outgoing_edges.delete(edge) + edge.destination.incoming_edges.delete(edge) + end + + # (see Action#down) + def down(graph) + edge = make_edge(graph) + edge.origin.outgoing_edges << edge + edge.destination.incoming_edges << edge + edge + end + + # @!group DeleteEdge + + # @return [String] the name of the origin of the edge + attr_reader :origin_name + + # @return [String] the name of the destination of the edge + attr_reader :destination_name + + # @return [Object] the requirement that the edge represents + attr_reader :requirement + + # @param [DependencyGraph] graph the graph to find vertices from + # @return [Edge] The edge this action adds + def make_edge(graph) + Edge.new( + graph.vertex_named(origin_name), + graph.vertex_named(destination_name), + requirement + ) + end + + # Initialize an action to add an edge to a dependency graph + # @param [String] origin_name the name of the origin of the edge + # @param [String] destination_name the name of the destination of the edge + # @param [Object] requirement the requirement that the edge represents + def initialize(origin_name, destination_name, requirement) + @origin_name = origin_name + @destination_name = destination_name + @requirement = requirement + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb new file mode 100644 index 00000000000000..385dcbdd064bd0 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' +module Bundler::Molinillo + class DependencyGraph + # @!visibility private + # @see DependencyGraph#detach_vertex_named + class DetachVertexNamed < Action + # @!group Action + + # (see Action#name) + def self.action_name + :add_vertex + end + + # (see Action#up) + def up(graph) + return [] unless @vertex = graph.vertices.delete(name) + + removed_vertices = [@vertex] + @vertex.outgoing_edges.each do |e| + v = e.destination + v.incoming_edges.delete(e) + if !v.root? && v.incoming_edges.empty? + removed_vertices.concat graph.detach_vertex_named(v.name) + end + end + + @vertex.incoming_edges.each do |e| + v = e.origin + v.outgoing_edges.delete(e) + end + + removed_vertices + end + + # (see Action#down) + def down(graph) + return unless @vertex + graph.vertices[@vertex.name] = @vertex + @vertex.outgoing_edges.each do |e| + e.destination.incoming_edges << e + end + @vertex.incoming_edges.each do |e| + e.origin.outgoing_edges << e + end + end + + # @!group DetachVertexNamed + + # @return [String] the name of the vertex to detach + attr_reader :name + + # Initialize an action to detach a vertex from a dependency graph + # @param [String] name the name of the vertex to detach + def initialize(name) + @name = name + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb new file mode 100644 index 00000000000000..8582dd19c16c63 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular' +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex' +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge' +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named' +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload' +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag' + +module Bundler::Molinillo + class DependencyGraph + # A log for dependency graph actions + class Log + # Initializes an empty log + def initialize + @current_action = @first_action = nil + end + + # @!macro [new] action + # {include:DependencyGraph#$0} + # @param [Graph] graph the graph to perform the action on + # @param (see DependencyGraph#$0) + # @return (see DependencyGraph#$0) + + # @macro action + def tag(graph, tag) + push_action(graph, Tag.new(tag)) + end + + # @macro action + def add_vertex(graph, name, payload, root) + push_action(graph, AddVertex.new(name, payload, root)) + end + + # @macro action + def detach_vertex_named(graph, name) + push_action(graph, DetachVertexNamed.new(name)) + end + + # @macro action + def add_edge_no_circular(graph, origin, destination, requirement) + push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement)) + end + + # {include:DependencyGraph#delete_edge} + # @param [Graph] graph the graph to perform the action on + # @param [String] origin_name + # @param [String] destination_name + # @param [Object] requirement + # @return (see DependencyGraph#delete_edge) + def delete_edge(graph, origin_name, destination_name, requirement) + push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement)) + end + + # @macro action + def set_payload(graph, name, payload) + push_action(graph, SetPayload.new(name, payload)) + end + + # Pops the most recent action from the log and undoes the action + # @param [DependencyGraph] graph + # @return [Action] the action that was popped off the log + def pop!(graph) + return unless action = @current_action + unless @current_action = action.previous + @first_action = nil + end + action.down(graph) + action + end + + extend Enumerable + + # @!visibility private + # Enumerates each action in the log + # @yield [Action] + def each + return enum_for unless block_given? + action = @first_action + loop do + break unless action + yield action + action = action.next + end + self + end + + # @!visibility private + # Enumerates each action in the log in reverse order + # @yield [Action] + def reverse_each + return enum_for(:reverse_each) unless block_given? + action = @current_action + loop do + break unless action + yield action + action = action.previous + end + self + end + + # @macro action + def rewind_to(graph, tag) + loop do + action = pop!(graph) + raise "No tag #{tag.inspect} found" unless action + break if action.class.action_name == :tag && action.tag == tag + end + end + + private + + # Adds the given action to the log, running the action + # @param [DependencyGraph] graph + # @param [Action] action + # @return The value returned by `action.up` + def push_action(graph, action) + action.previous = @current_action + @current_action.next = action if @current_action + @current_action = action + @first_action ||= action + action.up(graph) + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb new file mode 100644 index 00000000000000..37286d104a3995 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' +module Bundler::Molinillo + class DependencyGraph + # @!visibility private + # @see DependencyGraph#set_payload + class SetPayload < Action # :nodoc: + # @!group Action + + # (see Action.action_name) + def self.action_name + :set_payload + end + + # (see Action#up) + def up(graph) + vertex = graph.vertex_named(name) + @old_payload = vertex.payload + vertex.payload = payload + end + + # (see Action#down) + def down(graph) + graph.vertex_named(name).payload = @old_payload + end + + # @!group SetPayload + + # @return [String] the name of the vertex + attr_reader :name + + # @return [Object] the payload for the vertex + attr_reader :payload + + # Initialize an action to add set the payload for a vertex in a dependency + # graph + # @param [String] name the name of the vertex + # @param [Object] payload the payload for the vertex + def initialize(name, payload) + @name = name + @payload = payload + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb new file mode 100644 index 00000000000000..d6ad16e07a268e --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' +module Bundler::Molinillo + class DependencyGraph + # @!visibility private + # @see DependencyGraph#tag + class Tag < Action + # @!group Action + + # (see Action.action_name) + def self.action_name + :tag + end + + # (see Action#up) + def up(_graph) + end + + # (see Action#down) + def down(_graph) + end + + # @!group Tag + + # @return [Object] An opaque tag + attr_reader :tag + + # Initialize an action to tag a state of a dependency graph + # @param [Object] tag an opaque tag + def initialize(tag) + @tag = tag + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb new file mode 100644 index 00000000000000..7ecdc4b65a1a60 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + class DependencyGraph + # A vertex in a {DependencyGraph} that encapsulates a {#name} and a + # {#payload} + class Vertex + # @return [String] the name of the vertex + attr_accessor :name + + # @return [Object] the payload the vertex holds + attr_accessor :payload + + # @return [Array] the explicit requirements that required + # this vertex + attr_reader :explicit_requirements + + # @return [Boolean] whether the vertex is considered a root vertex + attr_accessor :root + alias root? root + + # Initializes a vertex with the given name and payload. + # @param [String] name see {#name} + # @param [Object] payload see {#payload} + def initialize(name, payload) + @name = name.frozen? ? name : name.dup.freeze + @payload = payload + @explicit_requirements = [] + @outgoing_edges = [] + @incoming_edges = [] + end + + # @return [Array] all of the requirements that required + # this vertex + def requirements + (incoming_edges.map(&:requirement) + explicit_requirements).uniq + end + + # @return [Array] the edges of {#graph} that have `self` as their + # {Edge#origin} + attr_accessor :outgoing_edges + + # @return [Array] the edges of {#graph} that have `self` as their + # {Edge#destination} + attr_accessor :incoming_edges + + # @return [Array] the vertices of {#graph} that have an edge with + # `self` as their {Edge#destination} + def predecessors + incoming_edges.map(&:origin) + end + + # @return [Array] the vertices of {#graph} where `self` is a + # {#descendent?} + def recursive_predecessors + vertices = predecessors + vertices += Compatibility.flat_map(vertices, &:recursive_predecessors) + vertices.uniq! + vertices + end + + # @return [Array] the vertices of {#graph} that have an edge with + # `self` as their {Edge#origin} + def successors + outgoing_edges.map(&:destination) + end + + # @return [Array] the vertices of {#graph} where `self` is an + # {#ancestor?} + def recursive_successors + vertices = successors + vertices += Compatibility.flat_map(vertices, &:recursive_successors) + vertices.uniq! + vertices + end + + # @return [String] a string suitable for debugging + def inspect + "#{self.class}:#{name}(#{payload.inspect})" + end + + # @return [Boolean] whether the two vertices are equal, determined + # by a recursive traversal of each {Vertex#successors} + def ==(other) + return true if equal?(other) + shallow_eql?(other) && + successors.to_set == other.successors.to_set + end + + # @param [Vertex] other the other vertex to compare to + # @return [Boolean] whether the two vertices are equal, determined + # solely by {#name} and {#payload} equality + def shallow_eql?(other) + return true if equal?(other) + other && + name == other.name && + payload == other.payload + end + + alias eql? == + + # @return [Fixnum] a hash for the vertex based upon its {#name} + def hash + name.hash + end + + # Is there a path from `self` to `other` following edges in the + # dependency graph? + # @return true iff there is a path following edges within this {#graph} + def path_to?(other) + _path_to?(other) + end + + alias descendent? path_to? + + # @param [Vertex] other the vertex to check if there's a path to + # @param [Set] visited the vertices of {#graph} that have been visited + # @return [Boolean] whether there is a path to `other` from `self` + def _path_to?(other, visited = Set.new) + return false unless visited.add?(self) + return true if equal?(other) + successors.any? { |v| v._path_to?(other, visited) } + end + protected :_path_to? + + # Is there a path from `other` to `self` following edges in the + # dependency graph? + # @return true iff there is a path following edges within this {#graph} + def ancestor?(other) + other.path_to?(self) + end + + alias is_reachable_from? ancestor? + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb new file mode 100644 index 00000000000000..ce0931f103b093 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # An error that occurred during the resolution process + class ResolverError < StandardError; end + + # An error caused by searching for a dependency that is completely unknown, + # i.e. has no versions available whatsoever. + class NoSuchDependencyError < ResolverError + # @return [Object] the dependency that could not be found + attr_accessor :dependency + + # @return [Array] the specifications that depended upon {#dependency} + attr_accessor :required_by + + # Initializes a new error with the given missing dependency. + # @param [Object] dependency @see {#dependency} + # @param [Array] required_by @see {#required_by} + def initialize(dependency, required_by = []) + @dependency = dependency + @required_by = required_by.uniq + super() + end + + # The error message for the missing dependency, including the specifications + # that had this dependency. + def message + sources = required_by.map { |r| "`#{r}`" }.join(' and ') + message = "Unable to find a specification for `#{dependency}`" + message += " depended upon by #{sources}" unless sources.empty? + message + end + end + + # An error caused by attempting to fulfil a dependency that was circular + # + # @note This exception will be thrown iff a {Vertex} is added to a + # {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an + # existing {DependencyGraph::Vertex} + class CircularDependencyError < ResolverError + # [Set] the dependencies responsible for causing the error + attr_reader :dependencies + + # Initializes a new error with the given circular vertices. + # @param [Array] vertices the vertices in the dependency + # that caused the error + def initialize(vertices) + super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}" + @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set + end + end + + # An error caused by conflicts in version + class VersionConflict < ResolverError + # @return [{String => Resolution::Conflict}] the conflicts that caused + # resolution to fail + attr_reader :conflicts + + # @return [SpecificationProvider] the specification provider used during + # resolution + attr_reader :specification_provider + + # Initializes a new error with the given version conflicts. + # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} + # @param [SpecificationProvider] specification_provider see {#specification_provider} + def initialize(conflicts, specification_provider) + pairs = [] + Compatibility.flat_map(conflicts.values.flatten, &:requirements).each do |conflicting| + conflicting.each do |source, conflict_requirements| + conflict_requirements.each do |c| + pairs << [c, source] + end + end + end + + super "Unable to satisfy the following requirements:\n\n" \ + "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" + + @conflicts = conflicts + @specification_provider = specification_provider + end + + require 'bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider' + include Delegates::SpecificationProvider + + # @return [String] An error message that includes requirement trees, + # which is much more detailed & customizable than the default message + # @param [Hash] opts the options to create a message with. + # @option opts [String] :solver_name The user-facing name of the solver + # @option opts [String] :possibility_type The generic name of a possibility + # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees + # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements + # @option opts [Proc] :additional_message_for_conflict A proc that appends additional + # messages for each conflict + # @option opts [Proc] :version_for_spec A proc that returns the version number for a + # possibility + def message_with_trees(opts = {}) + solver_name = opts.delete(:solver_name) { self.class.name.split('::').first } + possibility_type = opts.delete(:possibility_type) { 'possibility named' } + reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } + printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } + additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} } + version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } + incompatible_version_message_for_conflict = opts.delete(:incompatible_version_message_for_conflict) do + proc do |name, _conflict| + %(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":) + end + end + + conflicts.sort.reduce(''.dup) do |o, (name, conflict)| + o << "\n" << incompatible_version_message_for_conflict.call(name, conflict) << "\n" + if conflict.locked_requirement + o << %( In snapshot (#{name_for_locking_dependency_source}):\n) + o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) + o << %(\n) + end + o << %( In #{name_for_explicit_dependency_source}:\n) + trees = reduce_trees.call(conflict.requirement_trees) + + o << trees.map do |tree| + t = ''.dup + depth = 2 + tree.each do |req| + t << ' ' * depth << req.to_s + unless tree.last == req + if spec = conflict.activated_by_name[name_for(req)] + t << %( was resolved to #{version_for_spec.call(spec)}, which) + end + t << %( depends on) + end + t << %(\n) + depth += 1 + end + t + end.join("\n") + + additional_message_for_conflict.call(o, name, conflict) + + o + end.strip + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb new file mode 100644 index 00000000000000..73f8fbf2ac9504 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # The version of Bundler::Molinillo. + VERSION = '0.6.6'.freeze +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb new file mode 100644 index 00000000000000..fa094c19818006 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # Provides information about specifcations and dependencies to the resolver, + # allowing the {Resolver} class to remain generic while still providing power + # and flexibility. + # + # This module contains the methods that users of Bundler::Molinillo must to implement, + # using knowledge of their own model classes. + module SpecificationProvider + # Search for the specifications that match the given dependency. + # The specifications in the returned array will be considered in reverse + # order, so the latest version ought to be last. + # @note This method should be 'pure', i.e. the return value should depend + # only on the `dependency` parameter. + # + # @param [Object] dependency + # @return [Array] the specifications that satisfy the given + # `dependency`. + def search_for(dependency) + [] + end + + # Returns the dependencies of `specification`. + # @note This method should be 'pure', i.e. the return value should depend + # only on the `specification` parameter. + # + # @param [Object] specification + # @return [Array] the dependencies that are required by the given + # `specification`. + def dependencies_for(specification) + [] + end + + # Determines whether the given `requirement` is satisfied by the given + # `spec`, in the context of the current `activated` dependency graph. + # + # @param [Object] requirement + # @param [DependencyGraph] activated the current dependency graph in the + # resolution process. + # @param [Object] spec + # @return [Boolean] whether `requirement` is satisfied by `spec` in the + # context of the current `activated` dependency graph. + def requirement_satisfied_by?(requirement, activated, spec) + true + end + + # Returns the name for the given `dependency`. + # @note This method should be 'pure', i.e. the return value should depend + # only on the `dependency` parameter. + # + # @param [Object] dependency + # @return [String] the name for the given `dependency`. + def name_for(dependency) + dependency.to_s + end + + # @return [String] the name of the source of explicit dependencies, i.e. + # those passed to {Resolver#resolve} directly. + def name_for_explicit_dependency_source + 'user-specified dependency' + end + + # @return [String] the name of the source of 'locked' dependencies, i.e. + # those passed to {Resolver#resolve} directly as the `base` + def name_for_locking_dependency_source + 'Lockfile' + end + + # Sort dependencies so that the ones that are easiest to resolve are first. + # Easiest to resolve is (usually) defined by: + # 1) Is this dependency already activated? + # 2) How relaxed are the requirements? + # 3) Are there any conflicts for this dependency? + # 4) How many possibilities are there to satisfy this dependency? + # + # @param [Array] dependencies + # @param [DependencyGraph] activated the current dependency graph in the + # resolution process. + # @param [{String => Array}] conflicts + # @return [Array] a sorted copy of `dependencies`. + def sort_dependencies(dependencies, activated, conflicts) + dependencies.sort_by do |dependency| + name = name_for(dependency) + [ + activated.vertex_named(name).payload ? 0 : 1, + conflicts[name] ? 0 : 1, + ] + end + end + + # Returns whether this dependency, which has no possible matching + # specifications, can safely be ignored. + # + # @param [Object] dependency + # @return [Boolean] whether this dependency can safely be skipped. + def allow_missing?(dependency) + false + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb new file mode 100644 index 00000000000000..a166bc699184d2 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # Conveys information about the resolution process to a user. + module UI + # The {IO} object that should be used to print output. `STDOUT`, by default. + # + # @return [IO] + def output + STDOUT + end + + # Called roughly every {#progress_rate}, this method should convey progress + # to the user. + # + # @return [void] + def indicate_progress + output.print '.' unless debug? + end + + # How often progress should be conveyed to the user via + # {#indicate_progress}, in seconds. A third of a second, by default. + # + # @return [Float] + def progress_rate + 0.33 + end + + # Called before resolution begins. + # + # @return [void] + def before_resolution + output.print 'Resolving dependencies...' + end + + # Called after resolution ends (either successfully or with an error). + # By default, prints a newline. + # + # @return [void] + def after_resolution + output.puts + end + + # Conveys debug information to the user. + # + # @param [Integer] depth the current depth of the resolution process. + # @return [void] + def debug(depth = 0) + if debug? + debug_info = yield + debug_info = debug_info.inspect unless debug_info.is_a?(String) + debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } + output.puts debug_info + end + end + + # Whether or not debug messages should be printed. + # By default, whether or not the `MOLINILLO_DEBUG` environment variable is + # set. + # + # @return [Boolean] + def debug? + return @debug_mode if defined?(@debug_mode) + @debug_mode = ENV['MOLINILLO_DEBUG'] + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb new file mode 100644 index 00000000000000..0eb665d17af0a5 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb @@ -0,0 +1,837 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + class Resolver + # A specific resolution from a given {Resolver} + class Resolution + # A conflict that the resolution process encountered + # @attr [Object] requirement the requirement that immediately led to the conflict + # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict + # @attr [Object, nil] existing the existing spec that was in conflict with + # the {#possibility} + # @attr [Object] possibility_set the set of specs that was unable to be + # activated due to a conflict. + # @attr [Object] locked_requirement the relevant locking requirement. + # @attr [Array>] requirement_trees the different requirement + # trees that led to every requirement for the conflicting name. + # @attr [{String=>Object}] activated_by_name the already-activated specs. + # @attr [Object] underlying_error an error that has occurred during resolution, and + # will be raised at the end of it if no resolution is found. + Conflict = Struct.new( + :requirement, + :requirements, + :existing, + :possibility_set, + :locked_requirement, + :requirement_trees, + :activated_by_name, + :underlying_error + ) + + class Conflict + # @return [Object] a spec that was unable to be activated due to a conflict + def possibility + possibility_set && possibility_set.latest_version + end + end + + # A collection of possibility states that share the same dependencies + # @attr [Array] dependencies the dependencies for this set of possibilities + # @attr [Array] possibilities the possibilities + PossibilitySet = Struct.new(:dependencies, :possibilities) + + class PossibilitySet + # String representation of the possibility set, for debugging + def to_s + "[#{possibilities.join(', ')}]" + end + + # @return [Object] most up-to-date dependency in the possibility set + def latest_version + possibilities.last + end + end + + # Details of the state to unwind to when a conflict occurs, and the cause of the unwind + # @attr [Integer] state_index the index of the state to unwind to + # @attr [Object] state_requirement the requirement of the state we're unwinding to + # @attr [Array] requirement_tree for the requirement we're relaxing + # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict + # @attr [Array] requirement_trees for the conflict + # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind + UnwindDetails = Struct.new( + :state_index, + :state_requirement, + :requirement_tree, + :conflicting_requirements, + :requirement_trees, + :requirements_unwound_to_instead + ) + + class UnwindDetails + include Comparable + + # We compare UnwindDetails when choosing which state to unwind to. If + # two options have the same state_index we prefer the one most + # removed from a requirement that caused the conflict. Both options + # would unwind to the same state, but a `grandparent` option will + # filter out fewer of its possibilities after doing so - where a state + # is both a `parent` and a `grandparent` to requirements that have + # caused a conflict this is the correct behaviour. + # @param [UnwindDetail] other UnwindDetail to be compared + # @return [Integer] integer specifying ordering + def <=>(other) + if state_index > other.state_index + 1 + elsif state_index == other.state_index + reversed_requirement_tree_index <=> other.reversed_requirement_tree_index + else + -1 + end + end + + # @return [Integer] index of state requirement in reversed requirement tree + # (the conflicting requirement itself will be at position 0) + def reversed_requirement_tree_index + @reversed_requirement_tree_index ||= + if state_requirement + requirement_tree.reverse.index(state_requirement) + else + 999_999 + end + end + + # @return [Boolean] where the requirement of the state we're unwinding + # to directly caused the conflict. Note: in this case, it is + # impossible for the state we're unwinding to to be a parent of + # any of the other conflicting requirements (or we would have + # circularity) + def unwinding_to_primary_requirement? + requirement_tree.last == state_requirement + end + + # @return [Array] array of sub-dependencies to avoid when choosing a + # new possibility for the state we've unwound to. Only relevant for + # non-primary unwinds + def sub_dependencies_to_avoid + @requirements_to_avoid ||= + requirement_trees.map do |tree| + index = tree.index(state_requirement) + tree[index + 1] if index + end.compact + end + + # @return [Array] array of all the requirements that led to the need for + # this unwind + def all_requirements + @all_requirements ||= requirement_trees.flatten(1) + end + end + + # @return [SpecificationProvider] the provider that knows about + # dependencies, requirements, specifications, versions, etc. + attr_reader :specification_provider + + # @return [UI] the UI that knows how to communicate feedback about the + # resolution process back to the user + attr_reader :resolver_ui + + # @return [DependencyGraph] the base dependency graph to which + # dependencies should be 'locked' + attr_reader :base + + # @return [Array] the dependencies that were explicitly required + attr_reader :original_requested + + # Initializes a new resolution. + # @param [SpecificationProvider] specification_provider + # see {#specification_provider} + # @param [UI] resolver_ui see {#resolver_ui} + # @param [Array] requested see {#original_requested} + # @param [DependencyGraph] base see {#base} + def initialize(specification_provider, resolver_ui, requested, base) + @specification_provider = specification_provider + @resolver_ui = resolver_ui + @original_requested = requested + @base = base + @states = [] + @iteration_counter = 0 + @parents_of = Hash.new { |h, k| h[k] = [] } + end + + # Resolves the {#original_requested} dependencies into a full dependency + # graph + # @raise [ResolverError] if successful resolution is impossible + # @return [DependencyGraph] the dependency graph of successfully resolved + # dependencies + def resolve + start_resolution + + while state + break if !state.requirement && state.requirements.empty? + indicate_progress + if state.respond_to?(:pop_possibility_state) # DependencyState + debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } + state.pop_possibility_state.tap do |s| + if s + states.push(s) + activated.tag(s) + end + end + end + process_topmost_state + end + + resolve_activated_specs + ensure + end_resolution + end + + # @return [Integer] the number of resolver iterations in between calls to + # {#resolver_ui}'s {UI#indicate_progress} method + attr_accessor :iteration_rate + private :iteration_rate + + # @return [Time] the time at which resolution began + attr_accessor :started_at + private :started_at + + # @return [Array] the stack of states for the resolution + attr_accessor :states + private :states + + private + + # Sets up the resolution process + # @return [void] + def start_resolution + @started_at = Time.now + + handle_missing_or_push_dependency_state(initial_state) + + debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" } + resolver_ui.before_resolution + end + + def resolve_activated_specs + activated.vertices.each do |_, vertex| + next unless vertex.payload + + latest_version = vertex.payload.possibilities.reverse_each.find do |possibility| + vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) } + end + + activated.set_payload(vertex.name, latest_version) + end + activated.freeze + end + + # Ends the resolution process + # @return [void] + def end_resolution + resolver_ui.after_resolution + debug do + "Finished resolution (#{@iteration_counter} steps) " \ + "(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})" + end + debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state + debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state + end + + require 'bundler/vendor/molinillo/lib/molinillo/state' + require 'bundler/vendor/molinillo/lib/molinillo/modules/specification_provider' + + require 'bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state' + require 'bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider' + + include Bundler::Molinillo::Delegates::ResolutionState + include Bundler::Molinillo::Delegates::SpecificationProvider + + # Processes the topmost available {RequirementState} on the stack + # @return [void] + def process_topmost_state + if possibility + attempt_to_activate + else + create_conflict + unwind_for_conflict + end + rescue CircularDependencyError => underlying_error + create_conflict(underlying_error) + unwind_for_conflict + end + + # @return [Object] the current possibility that the resolution is trying + # to activate + def possibility + possibilities.last + end + + # @return [RequirementState] the current state the resolution is + # operating upon + def state + states.last + end + + # Creates the initial state for the resolution, based upon the + # {#requested} dependencies + # @return [DependencyState] the initial state for the resolution + def initial_state + graph = DependencyGraph.new.tap do |dg| + original_requested.each do |requested| + vertex = dg.add_vertex(name_for(requested), nil, true) + vertex.explicit_requirements << requested + end + dg.tag(:initial_state) + end + + requirements = sort_dependencies(original_requested, graph, {}) + initial_requirement = requirements.shift + DependencyState.new( + initial_requirement && name_for(initial_requirement), + requirements, + graph, + initial_requirement, + possibilities_for_requirement(initial_requirement, graph), + 0, + {}, + [] + ) + end + + # Unwinds the states stack because a conflict has been encountered + # @return [void] + def unwind_for_conflict + details_for_unwind = build_details_for_unwind + unwind_options = unused_unwind_options + debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } + conflicts.tap do |c| + sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1) + raise_error_unless_state(c) + activated.rewind_to(sliced_states.first || :initial_state) if sliced_states + state.conflicts = c + state.unused_unwind_options = unwind_options + filter_possibilities_after_unwind(details_for_unwind) + index = states.size - 1 + @parents_of.each { |_, a| a.reject! { |i| i >= index } } + state.unused_unwind_options.reject! { |uw| uw.state_index >= index } + end + end + + # Raises a VersionConflict error, or any underlying error, if there is no + # current state + # @return [void] + def raise_error_unless_state(conflicts) + return if state + + error = conflicts.values.map(&:underlying_error).compact.first + raise error || VersionConflict.new(conflicts, specification_provider) + end + + # @return [UnwindDetails] Details of the nearest index to which we could unwind + def build_details_for_unwind + # Get the possible unwinds for the current conflict + current_conflict = conflicts[name] + binding_requirements = binding_requirements_for_conflict(current_conflict) + unwind_details = unwind_options_for_requirements(binding_requirements) + + last_detail_for_current_unwind = unwind_details.sort.last + current_detail = last_detail_for_current_unwind + + # Look for past conflicts that could be unwound to affect the + # requirement tree for the current conflict + relevant_unused_unwinds = unused_unwind_options.select do |alternative| + intersecting_requirements = + last_detail_for_current_unwind.all_requirements & + alternative.requirements_unwound_to_instead + next if intersecting_requirements.empty? + # Find the highest index unwind whilst looping through + current_detail = alternative if alternative > current_detail + alternative + end + + # Add the current unwind options to the `unused_unwind_options` array. + # The "used" option will be filtered out during `unwind_for_conflict`. + state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } + + # Update the requirements_unwound_to_instead on any relevant unused unwinds + relevant_unused_unwinds.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } + unwind_details.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } + + current_detail + end + + # @param [Array] array of requirements that combine to create a conflict + # @return [Array] array of UnwindDetails that have a chance + # of resolving the passed requirements + def unwind_options_for_requirements(binding_requirements) + unwind_details = [] + + trees = [] + binding_requirements.reverse_each do |r| + partial_tree = [r] + trees << partial_tree + unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, []) + + # If this requirement has alternative possibilities, check if any would + # satisfy the other requirements that created this conflict + requirement_state = find_state_for(r) + if conflict_fixing_possibilities?(requirement_state, binding_requirements) + unwind_details << UnwindDetails.new( + states.index(requirement_state), + r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + + # Next, look at the parent of this requirement, and check if the requirement + # could have been avoided if an alternative PossibilitySet had been chosen + parent_r = parent_of(r) + next if parent_r.nil? + partial_tree.unshift(parent_r) + requirement_state = find_state_for(parent_r) + if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) } + unwind_details << UnwindDetails.new( + states.index(requirement_state), + parent_r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + + # Finally, look at the grandparent and up of this requirement, looking + # for any possibilities that wouldn't create their parent requirement + grandparent_r = parent_of(parent_r) + until grandparent_r.nil? + partial_tree.unshift(grandparent_r) + requirement_state = find_state_for(grandparent_r) + if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) } + unwind_details << UnwindDetails.new( + states.index(requirement_state), + grandparent_r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + parent_r = grandparent_r + grandparent_r = parent_of(parent_r) + end + end + + unwind_details + end + + # @param [DependencyState] state + # @param [Array] array of requirements + # @return [Boolean] whether or not the given state has any possibilities + # that could satisfy the given requirements + def conflict_fixing_possibilities?(state, binding_requirements) + return false unless state + + state.possibilities.any? do |possibility_set| + possibility_set.possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, binding_requirements) + end + end + end + + # Filter's a state's possibilities to remove any that would not fix the + # conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_after_unwind(unwind_details) + return unless state && !state.possibilities.empty? + + if unwind_details.unwinding_to_primary_requirement? + filter_possibilities_for_primary_unwind(unwind_details) + else + filter_possibilities_for_parent_unwind(unwind_details) + end + end + + # Filter's a state's possibilities to remove any that would not satisfy + # the requirements in the conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_for_primary_unwind(unwind_details) + unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } + unwinds_to_state << unwind_details + unwind_requirement_sets = unwinds_to_state.map(&:conflicting_requirements) + + state.possibilities.reject! do |possibility_set| + possibility_set.possibilities.none? do |poss| + unwind_requirement_sets.any? do |requirements| + possibility_satisfies_requirements?(poss, requirements) + end + end + end + end + + # @param [Object] possibility a single possibility + # @param [Array] requirements an array of requirements + # @return [Boolean] whether the possibility satisfies all of the + # given requirements + def possibility_satisfies_requirements?(possibility, requirements) + name = name_for(possibility) + + activated.tag(:swap) + activated.set_payload(name, possibility) if activated.vertex_named(name) + satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } + activated.rewind_to(:swap) + + satisfied + end + + # Filter's a state's possibilities to remove any that would (eventually) + # create a requirement in the conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_for_parent_unwind(unwind_details) + unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } + unwinds_to_state << unwind_details + + primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq + parent_unwinds = unwinds_to_state.uniq - primary_unwinds + + allowed_possibility_sets = Compatibility.flat_map(primary_unwinds) do |unwind| + states[unwind.state_index].possibilities.select do |possibility_set| + possibility_set.possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) + end + end + end + + requirements_to_avoid = Compatibility.flat_map(parent_unwinds, &:sub_dependencies_to_avoid) + + state.possibilities.reject! do |possibility_set| + !allowed_possibility_sets.include?(possibility_set) && + (requirements_to_avoid - possibility_set.dependencies).empty? + end + end + + # @param [Conflict] conflict + # @return [Array] minimal array of requirements that would cause the passed + # conflict to occur. + def binding_requirements_for_conflict(conflict) + return [conflict.requirement] if conflict.possibility.nil? + + possible_binding_requirements = conflict.requirements.values.flatten(1).uniq + + # When there’s a `CircularDependency` error the conflicting requirement + # (the one causing the circular) won’t be `conflict.requirement` + # (which won’t be for the right state, because we won’t have created it, + # because it’s circular). + # We need to make sure we have that requirement in the conflict’s list, + # otherwise we won’t be able to unwind properly, so we just return all + # the requirements for the conflict. + return possible_binding_requirements if conflict.underlying_error + + possibilities = search_for(conflict.requirement) + + # If all the requirements together don't filter out all possibilities, + # then the only two requirements we need to consider are the initial one + # (where the dependency's version was first chosen) and the last + if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) + return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact + end + + # Loop through the possible binding requirements, removing each one + # that doesn't bind. Use a `reverse_each` as we want the earliest set of + # binding requirements, and don't use `reject!` as we wish to refine the + # array *on each iteration*. + binding_requirements = possible_binding_requirements.dup + possible_binding_requirements.reverse_each do |req| + next if req == conflict.requirement + unless binding_requirement_in_set?(req, binding_requirements, possibilities) + binding_requirements -= [req] + end + end + + binding_requirements + end + + # @param [Object] requirement we wish to check + # @param [Array] array of requirements + # @param [Array] array of possibilities the requirements will be used to filter + # @return [Boolean] whether or not the given requirement is required to filter + # out all elements of the array of possibilities. + def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) + possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) + end + end + + # @return [Object] the requirement that led to `requirement` being added + # to the list of requirements. + def parent_of(requirement) + return unless requirement + return unless index = @parents_of[requirement].last + return unless parent_state = @states[index] + parent_state.requirement + end + + # @return [Object] the requirement that led to a version of a possibility + # with the given name being activated. + def requirement_for_existing_name(name) + return nil unless vertex = activated.vertex_named(name) + return nil unless vertex.payload + states.find { |s| s.name == name }.requirement + end + + # @return [ResolutionState] the state whose `requirement` is the given + # `requirement`. + def find_state_for(requirement) + return nil unless requirement + states.find { |i| requirement == i.requirement } + end + + # @return [Conflict] a {Conflict} that reflects the failure to activate + # the {#possibility} in conjunction with the current {#state} + def create_conflict(underlying_error = nil) + vertex = activated.vertex_named(name) + locked_requirement = locked_requirement_named(name) + + requirements = {} + unless vertex.explicit_requirements.empty? + requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements + end + requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement + vertex.incoming_edges.each do |edge| + (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement) + end + + activated_by_name = {} + activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload } + conflicts[name] = Conflict.new( + requirement, + requirements, + vertex.payload && vertex.payload.latest_version, + possibility, + locked_requirement, + requirement_trees, + activated_by_name, + underlying_error + ) + end + + # @return [Array>] The different requirement + # trees that led to every requirement for the current spec. + def requirement_trees + vertex = activated.vertex_named(name) + vertex.requirements.map { |r| requirement_tree_for(r) } + end + + # @return [Array] the list of requirements that led to + # `requirement` being required. + def requirement_tree_for(requirement) + tree = [] + while requirement + tree.unshift(requirement) + requirement = parent_of(requirement) + end + tree + end + + # Indicates progress roughly once every second + # @return [void] + def indicate_progress + @iteration_counter += 1 + @progress_rate ||= resolver_ui.progress_rate + if iteration_rate.nil? + if Time.now - started_at >= @progress_rate + self.iteration_rate = @iteration_counter + end + end + + if iteration_rate && (@iteration_counter % iteration_rate) == 0 + resolver_ui.indicate_progress + end + end + + # Calls the {#resolver_ui}'s {UI#debug} method + # @param [Integer] depth the depth of the {#states} stack + # @param [Proc] block a block that yields a {#to_s} + # @return [void] + def debug(depth = 0, &block) + resolver_ui.debug(depth, &block) + end + + # Attempts to activate the current {#possibility} + # @return [void] + def attempt_to_activate + debug(depth) { 'Attempting to activate ' + possibility.to_s } + existing_vertex = activated.vertex_named(name) + if existing_vertex.payload + debug(depth) { "Found existing spec (#{existing_vertex.payload})" } + attempt_to_filter_existing_spec(existing_vertex) + else + latest = possibility.latest_version + # use reject!(!satisfied) for 1.8.7 compatibility + possibility.possibilities.reject! do |possibility| + !requirement_satisfied_by?(requirement, activated, possibility) + end + if possibility.latest_version.nil? + # ensure there's a possibility for better error messages + possibility.possibilities << latest if latest + create_conflict + unwind_for_conflict + else + activate_new_spec + end + end + end + + # Attempts to update the existing vertex's `PossibilitySet` with a filtered version + # @return [void] + def attempt_to_filter_existing_spec(vertex) + filtered_set = filtered_possibility_set(vertex) + if !filtered_set.possibilities.empty? + activated.set_payload(name, filtered_set) + new_requirements = requirements.dup + push_state_for_requirements(new_requirements, false) + else + create_conflict + debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } + unwind_for_conflict + end + end + + # Generates a filtered version of the existing vertex's `PossibilitySet` using the + # current state's `requirement` + # @param [Object] existing vertex + # @return [PossibilitySet] filtered possibility set + def filtered_possibility_set(vertex) + PossibilitySet.new(vertex.payload.dependencies, vertex.payload.possibilities & possibility.possibilities) + end + + # @param [String] requirement_name the spec name to search for + # @return [Object] the locked spec named `requirement_name`, if one + # is found on {#base} + def locked_requirement_named(requirement_name) + vertex = base.vertex_named(requirement_name) + vertex && vertex.payload + end + + # Add the current {#possibility} to the dependency graph of the current + # {#state} + # @return [void] + def activate_new_spec + conflicts.delete(name) + debug(depth) { "Activated #{name} at #{possibility}" } + activated.set_payload(name, possibility) + require_nested_dependencies_for(possibility) + end + + # Requires the dependencies that the recently activated spec has + # @param [Object] activated_possibility the PossibilitySet that has just been + # activated + # @return [void] + def require_nested_dependencies_for(possibility_set) + nested_dependencies = dependencies_for(possibility_set.latest_version) + debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } + nested_dependencies.each do |d| + activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) + parent_index = states.size - 1 + parents = @parents_of[d] + parents << parent_index if parents.empty? + end + + push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?) + end + + # Pushes a new {DependencyState} that encapsulates both existing and new + # requirements + # @param [Array] new_requirements + # @return [void] + def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) + new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort + new_requirement = nil + loop do + new_requirement = new_requirements.shift + break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } + end + new_name = new_requirement ? name_for(new_requirement) : ''.freeze + possibilities = possibilities_for_requirement(new_requirement) + handle_missing_or_push_dependency_state DependencyState.new( + new_name, new_requirements, new_activated, + new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup + ) + end + + # Checks a proposed requirement with any existing locked requirement + # before generating an array of possibilities for it. + # @param [Object] the proposed requirement + # @return [Array] possibilities + def possibilities_for_requirement(requirement, activated = self.activated) + return [] unless requirement + if locked_requirement_named(name_for(requirement)) + return locked_requirement_possibility_set(requirement, activated) + end + + group_possibilities(search_for(requirement)) + end + + # @param [Object] the proposed requirement + # @return [Array] possibility set containing only the locked requirement, if any + def locked_requirement_possibility_set(requirement, activated = self.activated) + all_possibilities = search_for(requirement) + locked_requirement = locked_requirement_named(name_for(requirement)) + + # Longwinded way to build a possibilities array with either the locked + # requirement or nothing in it. Required, since the API for + # locked_requirement isn't guaranteed. + locked_possibilities = all_possibilities.select do |possibility| + requirement_satisfied_by?(locked_requirement, activated, possibility) + end + + group_possibilities(locked_possibilities) + end + + # Build an array of PossibilitySets, with each element representing a group of + # dependency versions that all have the same sub-dependency version constraints + # and are contiguous. + # @param [Array] an array of possibilities + # @return [Array] an array of possibility sets + def group_possibilities(possibilities) + possibility_sets = [] + current_possibility_set = nil + + possibilities.reverse_each do |possibility| + dependencies = dependencies_for(possibility) + if current_possibility_set && current_possibility_set.dependencies == dependencies + current_possibility_set.possibilities.unshift(possibility) + else + possibility_sets.unshift(PossibilitySet.new(dependencies, [possibility])) + current_possibility_set = possibility_sets.first + end + end + + possibility_sets + end + + # Pushes a new {DependencyState}. + # If the {#specification_provider} says to + # {SpecificationProvider#allow_missing?} that particular requirement, and + # there are no possibilities for that requirement, then `state` is not + # pushed, and the vertex in {#activated} is removed, and we continue + # resolving the remaining requirements. + # @param [DependencyState] state + # @return [void] + def handle_missing_or_push_dependency_state(state) + if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement) + state.activated.detach_vertex_named(state.name) + push_state_for_requirements(state.requirements.dup, false, state.activated) + else + states.push(state).tap { activated.tag(state) } + end + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb new file mode 100644 index 00000000000000..7d3685877800ef --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph' + +module Bundler::Molinillo + # This class encapsulates a dependency resolver. + # The resolver is responsible for determining which set of dependencies to + # activate, with feedback from the {#specification_provider} + # + # + class Resolver + require 'bundler/vendor/molinillo/lib/molinillo/resolution' + + # @return [SpecificationProvider] the specification provider used + # in the resolution process + attr_reader :specification_provider + + # @return [UI] the UI module used to communicate back to the user + # during the resolution process + attr_reader :resolver_ui + + # Initializes a new resolver. + # @param [SpecificationProvider] specification_provider + # see {#specification_provider} + # @param [UI] resolver_ui + # see {#resolver_ui} + def initialize(specification_provider, resolver_ui) + @specification_provider = specification_provider + @resolver_ui = resolver_ui + end + + # Resolves the requested dependencies into a {DependencyGraph}, + # locking to the base dependency graph (if specified) + # @param [Array] requested an array of 'requested' dependencies that the + # {#specification_provider} can understand + # @param [DependencyGraph,nil] base the base dependency graph to which + # dependencies should be 'locked' + def resolve(requested, base = DependencyGraph.new) + Resolution.new(specification_provider, + resolver_ui, + requested, + base). + resolve + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb new file mode 100644 index 00000000000000..68fa1f54e3e4cb --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # A state that a {Resolution} can be in + # @attr [String] name the name of the current requirement + # @attr [Array] requirements currently unsatisfied requirements + # @attr [DependencyGraph] activated the graph of activated dependencies + # @attr [Object] requirement the current requirement + # @attr [Object] possibilities the possibilities to satisfy the current requirement + # @attr [Integer] depth the depth of the resolution + # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name + # @attr [Array] unused_unwind_options unwinds for previous conflicts that weren't explored + ResolutionState = Struct.new( + :name, + :requirements, + :activated, + :requirement, + :possibilities, + :depth, + :conflicts, + :unused_unwind_options + ) + + class ResolutionState + # Returns an empty resolution state + # @return [ResolutionState] an empty state + def self.empty + new(nil, [], DependencyGraph.new, nil, nil, 0, {}, []) + end + end + + # A state that encapsulates a set of {#requirements} with an {Array} of + # possibilities + class DependencyState < ResolutionState + # Removes a possibility from `self` + # @return [PossibilityState] a state with a single possibility, + # the possibility that was removed from `self` + def pop_possibility_state + PossibilityState.new( + name, + requirements.dup, + activated, + requirement, + [possibilities.pop], + depth + 1, + conflicts.dup, + unused_unwind_options.dup + ).tap do |state| + state.activated.tag(state) + end + end + end + + # A state that encapsulates a single possibility to fulfill the given + # {#requirement} + class PossibilityState < ResolutionState + end +end diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb new file mode 100644 index 00000000000000..e5e09080c27a45 --- /dev/null +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb @@ -0,0 +1,27 @@ +require 'net/protocol' + +## +# Aaron Patterson's monkeypatch (accepted into 1.9.1) to fix Net::HTTP's speed +# problems. +# +# http://gist.github.com/251244 + +class Net::BufferedIO #:nodoc: + alias :old_rbuf_fill :rbuf_fill + + def rbuf_fill + if @io.respond_to? :read_nonblock then + begin + @rbuf << @io.read_nonblock(65536) + rescue Errno::EWOULDBLOCK, Errno::EAGAIN => e + retry if IO.select [@io], nil, nil, @read_timeout + raise Timeout::Error, e.message + end + else # SSL sockets do not have read_nonblock + timeout @read_timeout do + @rbuf << @io.sysread(65536) + end + end + end +end if RUBY_VERSION < '1.9' + diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb new file mode 100644 index 00000000000000..7cbca5bc068815 --- /dev/null +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb @@ -0,0 +1,1233 @@ +require 'net/http' +begin + require 'net/https' +rescue LoadError + # net/https or openssl +end if RUBY_VERSION < '1.9' # but only for 1.8 +require 'bundler/vendor/net-http-persistent/lib/net/http/faster' +require 'uri' +require 'cgi' # for escaping + +begin + require 'net/http/pipeline' +rescue LoadError +end + +autoload :OpenSSL, 'openssl' + +## +# Persistent connections for Net::HTTP +# +# Bundler::Persistent::Net::HTTP::Persistent maintains persistent connections across all the +# servers you wish to talk to. For each host:port you communicate with a +# single persistent connection is created. +# +# Multiple Bundler::Persistent::Net::HTTP::Persistent objects will share the same set of +# connections. +# +# For each thread you start a new connection will be created. A +# Bundler::Persistent::Net::HTTP::Persistent connection will not be shared across threads. +# +# You can shut down the HTTP connections when done by calling #shutdown. You +# should name your Bundler::Persistent::Net::HTTP::Persistent object if you intend to call this +# method. +# +# Example: +# +# require 'bundler/vendor/net-http-persistent/lib/net/http/persistent' +# +# uri = URI 'http://example.com/awesome/web/service' +# +# http = Bundler::Persistent::Net::HTTP::Persistent.new 'my_app_name' +# +# # perform a GET +# response = http.request uri +# +# # or +# +# get = Net::HTTP::Get.new uri.request_uri +# response = http.request get +# +# # create a POST +# post_uri = uri + 'create' +# post = Net::HTTP::Post.new post_uri.path +# post.set_form_data 'some' => 'cool data' +# +# # perform the POST, the URI is always required +# response http.request post_uri, post +# +# Note that for GET, HEAD and other requests that do not have a body you want +# to use URI#request_uri not URI#path. The request_uri contains the query +# params which are sent in the body for other requests. +# +# == SSL +# +# SSL connections are automatically created depending upon the scheme of the +# URI. SSL connections are automatically verified against the default +# certificate store for your computer. You can override this by changing +# verify_mode or by specifying an alternate cert_store. +# +# Here are the SSL settings, see the individual methods for documentation: +# +# #certificate :: This client's certificate +# #ca_file :: The certificate-authority +# #cert_store :: An SSL certificate store +# #private_key :: The client's SSL private key +# #reuse_ssl_sessions :: Reuse a previously opened SSL session for a new +# connection +# #ssl_version :: Which specific SSL version to use +# #verify_callback :: For server certificate verification +# #verify_mode :: How connections should be verified +# +# == Proxies +# +# A proxy can be set through #proxy= or at initialization time by providing a +# second argument to ::new. The proxy may be the URI of the proxy server or +# :ENV which will consult environment variables. +# +# See #proxy= and #proxy_from_env for details. +# +# == Headers +# +# Headers may be specified for use in every request. #headers are appended to +# any headers on the request. #override_headers replace existing headers on +# the request. +# +# The difference between the two can be seen in setting the User-Agent. Using +# http.headers['User-Agent'] = 'MyUserAgent' will send "Ruby, +# MyUserAgent" while http.override_headers['User-Agent'] = +# 'MyUserAgent' will send "MyUserAgent". +# +# == Tuning +# +# === Segregation +# +# By providing an application name to ::new you can separate your connections +# from the connections of other applications. +# +# === Idle Timeout +# +# If a connection hasn't been used for this number of seconds it will automatically be +# reset upon the next use to avoid attempting to send to a closed connection. +# The default value is 5 seconds. nil means no timeout. Set through #idle_timeout. +# +# Reducing this value may help avoid the "too many connection resets" error +# when sending non-idempotent requests while increasing this value will cause +# fewer round-trips. +# +# === Read Timeout +# +# The amount of time allowed between reading two chunks from the socket. Set +# through #read_timeout +# +# === Max Requests +# +# The number of requests that should be made before opening a new connection. +# Typically many keep-alive capable servers tune this to 100 or less, so the +# 101st request will fail with ECONNRESET. If unset (default), this value has no +# effect, if set, connections will be reset on the request after max_requests. +# +# === Open Timeout +# +# The amount of time to wait for a connection to be opened. Set through +# #open_timeout. +# +# === Socket Options +# +# Socket options may be set on newly-created connections. See #socket_options +# for details. +# +# === Non-Idempotent Requests +# +# By default non-idempotent requests will not be retried per RFC 2616. By +# setting retry_change_requests to true requests will automatically be retried +# once. +# +# Only do this when you know that retrying a POST or other non-idempotent +# request is safe for your application and will not create duplicate +# resources. +# +# The recommended way to handle non-idempotent requests is the following: +# +# require 'bundler/vendor/net-http-persistent/lib/net/http/persistent' +# +# uri = URI 'http://example.com/awesome/web/service' +# post_uri = uri + 'create' +# +# http = Bundler::Persistent::Net::HTTP::Persistent.new 'my_app_name' +# +# post = Net::HTTP::Post.new post_uri.path +# # ... fill in POST request +# +# begin +# response = http.request post_uri, post +# rescue Bundler::Persistent::Net::HTTP::Persistent::Error +# +# # POST failed, make a new request to verify the server did not process +# # the request +# exists_uri = uri + '...' +# response = http.get exists_uri +# +# # Retry if it failed +# retry if response.code == '404' +# end +# +# The method of determining if the resource was created or not is unique to +# the particular service you are using. Of course, you will want to add +# protection from infinite looping. +# +# === Connection Termination +# +# If you are done using the Bundler::Persistent::Net::HTTP::Persistent instance you may shut down +# all the connections in the current thread with #shutdown. This is not +# recommended for normal use, it should only be used when it will be several +# minutes before you make another HTTP request. +# +# If you are using multiple threads, call #shutdown in each thread when the +# thread is done making requests. If you don't call shutdown, that's OK. +# Ruby will automatically garbage collect and shutdown your HTTP connections +# when the thread terminates. + +class Bundler::Persistent::Net::HTTP::Persistent + + ## + # The beginning of Time + + EPOCH = Time.at 0 # :nodoc: + + ## + # Is OpenSSL available? This test works with autoload + + HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc: + + ## + # The version of Bundler::Persistent::Net::HTTP::Persistent you are using + + VERSION = '2.9.4' + + ## + # Exceptions rescued for automatic retry on ruby 2.0.0. This overlaps with + # the exception list for ruby 1.x. + + RETRIED_EXCEPTIONS = [ # :nodoc: + (Net::ReadTimeout if Net.const_defined? :ReadTimeout), + IOError, + EOFError, + Errno::ECONNRESET, + Errno::ECONNABORTED, + Errno::EPIPE, + (OpenSSL::SSL::SSLError if HAVE_OPENSSL), + Timeout::Error, + ].compact + + ## + # Error class for errors raised by Bundler::Persistent::Net::HTTP::Persistent. Various + # SystemCallErrors are re-raised with a human-readable message under this + # class. + + class Error < StandardError; end + + ## + # Use this method to detect the idle timeout of the host at +uri+. The + # value returned can be used to configure #idle_timeout. +max+ controls the + # maximum idle timeout to detect. + # + # After + # + # Idle timeout detection is performed by creating a connection then + # performing a HEAD request in a loop until the connection terminates + # waiting one additional second per loop. + # + # NOTE: This may not work on ruby > 1.9. + + def self.detect_idle_timeout uri, max = 10 + uri = URI uri unless URI::Generic === uri + uri += '/' + + req = Net::HTTP::Head.new uri.request_uri + + http = new 'net-http-persistent detect_idle_timeout' + + connection = http.connection_for uri + + sleep_time = 0 + + loop do + response = connection.request req + + $stderr.puts "HEAD #{uri} => #{response.code}" if $DEBUG + + unless Net::HTTPOK === response then + raise Error, "bad response code #{response.code} detecting idle timeout" + end + + break if sleep_time >= max + + sleep_time += 1 + + $stderr.puts "sleeping #{sleep_time}" if $DEBUG + sleep sleep_time + end + rescue + # ignore StandardErrors, we've probably found the idle timeout. + ensure + http.shutdown + + return sleep_time unless $! + end + + ## + # This client's OpenSSL::X509::Certificate + + attr_reader :certificate + + # For Net::HTTP parity + alias cert certificate + + ## + # An SSL certificate authority. Setting this will set verify_mode to + # VERIFY_PEER. + + attr_reader :ca_file + + ## + # An SSL certificate store. Setting this will override the default + # certificate store. See verify_mode for more information. + + attr_reader :cert_store + + ## + # Sends debug_output to this IO via Net::HTTP#set_debug_output. + # + # Never use this method in production code, it causes a serious security + # hole. + + attr_accessor :debug_output + + ## + # Current connection generation + + attr_reader :generation # :nodoc: + + ## + # Where this instance's connections live in the thread local variables + + attr_reader :generation_key # :nodoc: + + ## + # Headers that are added to every request using Net::HTTP#add_field + + attr_reader :headers + + ## + # Maps host:port to an HTTP version. This allows us to enable version + # specific features. + + attr_reader :http_versions + + ## + # Maximum time an unused connection can remain idle before being + # automatically closed. + + attr_accessor :idle_timeout + + ## + # Maximum number of requests on a connection before it is considered expired + # and automatically closed. + + attr_accessor :max_requests + + ## + # The value sent in the Keep-Alive header. Defaults to 30. Not needed for + # HTTP/1.1 servers. + # + # This may not work correctly for HTTP/1.0 servers + # + # This method may be removed in a future version as RFC 2616 does not + # require this header. + + attr_accessor :keep_alive + + ## + # A name for this connection. Allows you to keep your connections apart + # from everybody else's. + + attr_reader :name + + ## + # Seconds to wait until a connection is opened. See Net::HTTP#open_timeout + + attr_accessor :open_timeout + + ## + # Headers that are added to every request using Net::HTTP#[]= + + attr_reader :override_headers + + ## + # This client's SSL private key + + attr_reader :private_key + + # For Net::HTTP parity + alias key private_key + + ## + # The URL through which requests will be proxied + + attr_reader :proxy_uri + + ## + # List of host suffixes which will not be proxied + + attr_reader :no_proxy + + ## + # Seconds to wait until reading one block. See Net::HTTP#read_timeout + + attr_accessor :read_timeout + + ## + # Where this instance's request counts live in the thread local variables + + attr_reader :request_key # :nodoc: + + ## + # By default SSL sessions are reused to avoid extra SSL handshakes. Set + # this to false if you have problems communicating with an HTTPS server + # like: + # + # SSL_connect [...] read finished A: unexpected message (OpenSSL::SSL::SSLError) + + attr_accessor :reuse_ssl_sessions + + ## + # An array of options for Socket#setsockopt. + # + # By default the TCP_NODELAY option is set on sockets. + # + # To set additional options append them to this array: + # + # http.socket_options << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1] + + attr_reader :socket_options + + ## + # Current SSL connection generation + + attr_reader :ssl_generation # :nodoc: + + ## + # Where this instance's SSL connections live in the thread local variables + + attr_reader :ssl_generation_key # :nodoc: + + ## + # SSL version to use. + # + # By default, the version will be negotiated automatically between client + # and server. Ruby 1.9 and newer only. + + attr_reader :ssl_version if RUBY_VERSION > '1.9' + + ## + # Where this instance's last-use times live in the thread local variables + + attr_reader :timeout_key # :nodoc: + + ## + # SSL verification callback. Used when ca_file is set. + + attr_reader :verify_callback + + ## + # HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER which verifies + # the server certificate. + # + # If no ca_file or cert_store is set the default system certificate store is + # used. + # + # You can use +verify_mode+ to override any default values. + + attr_reader :verify_mode + + ## + # Enable retries of non-idempotent requests that change data (e.g. POST + # requests) when the server has disconnected. + # + # This will in the worst case lead to multiple requests with the same data, + # but it may be useful for some applications. Take care when enabling + # this option to ensure it is safe to POST or perform other non-idempotent + # requests to the server. + + attr_accessor :retry_change_requests + + ## + # Creates a new Bundler::Persistent::Net::HTTP::Persistent. + # + # Set +name+ to keep your connections apart from everybody else's. Not + # required currently, but highly recommended. Your library name should be + # good enough. This parameter will be required in a future version. + # + # +proxy+ may be set to a URI::HTTP or :ENV to pick up proxy options from + # the environment. See proxy_from_env for details. + # + # In order to use a URI for the proxy you may need to do some extra work + # beyond URI parsing if the proxy requires a password: + # + # proxy = URI 'http://proxy.example' + # proxy.user = 'AzureDiamond' + # proxy.password = 'hunter2' + + def initialize name = nil, proxy = nil + @name = name + + @debug_output = nil + @proxy_uri = nil + @no_proxy = [] + @headers = {} + @override_headers = {} + @http_versions = {} + @keep_alive = 30 + @open_timeout = nil + @read_timeout = nil + @idle_timeout = 5 + @max_requests = nil + @socket_options = [] + + @socket_options << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if + Socket.const_defined? :TCP_NODELAY + + key = ['net_http_persistent', name].compact + @generation_key = [key, 'generations' ].join('_').intern + @ssl_generation_key = [key, 'ssl_generations'].join('_').intern + @request_key = [key, 'requests' ].join('_').intern + @timeout_key = [key, 'timeouts' ].join('_').intern + + @certificate = nil + @ca_file = nil + @private_key = nil + @ssl_version = nil + @verify_callback = nil + @verify_mode = nil + @cert_store = nil + + @generation = 0 # incremented when proxy URI changes + @ssl_generation = 0 # incremented when SSL session variables change + + if HAVE_OPENSSL then + @verify_mode = OpenSSL::SSL::VERIFY_PEER + @reuse_ssl_sessions = OpenSSL::SSL.const_defined? :Session + end + + @retry_change_requests = false + + @ruby_1 = RUBY_VERSION < '2' + @retried_on_ruby_2 = !@ruby_1 + + self.proxy = proxy if proxy + end + + ## + # Sets this client's OpenSSL::X509::Certificate + + def certificate= certificate + @certificate = certificate + + reconnect_ssl + end + + # For Net::HTTP parity + alias cert= certificate= + + ## + # Sets the SSL certificate authority file. + + def ca_file= file + @ca_file = file + + reconnect_ssl + end + + ## + # Overrides the default SSL certificate store used for verifying + # connections. + + def cert_store= store + @cert_store = store + + reconnect_ssl + end + + ## + # Finishes all connections on the given +thread+ that were created before + # the given +generation+ in the threads +generation_key+ list. + # + # See #shutdown for a bunch of scary warning about misusing this method. + + def cleanup(generation, thread = Thread.current, + generation_key = @generation_key) # :nodoc: + timeouts = thread[@timeout_key] + + (0...generation).each do |old_generation| + next unless thread[generation_key] + + conns = thread[generation_key].delete old_generation + + conns.each_value do |conn| + finish conn, thread + + timeouts.delete conn.object_id if timeouts + end if conns + end + end + + ## + # Creates a new connection for +uri+ + + def connection_for uri + Thread.current[@generation_key] ||= Hash.new { |h,k| h[k] = {} } + Thread.current[@ssl_generation_key] ||= Hash.new { |h,k| h[k] = {} } + Thread.current[@request_key] ||= Hash.new 0 + Thread.current[@timeout_key] ||= Hash.new EPOCH + + use_ssl = uri.scheme.downcase == 'https' + + if use_ssl then + raise Bundler::Persistent::Net::HTTP::Persistent::Error, 'OpenSSL is not available' unless + HAVE_OPENSSL + + ssl_generation = @ssl_generation + + ssl_cleanup ssl_generation + + connections = Thread.current[@ssl_generation_key][ssl_generation] + else + generation = @generation + + cleanup generation + + connections = Thread.current[@generation_key][generation] + end + + net_http_args = [uri.host, uri.port] + connection_id = net_http_args.join ':' + + if @proxy_uri and not proxy_bypass? uri.host, uri.port then + connection_id << @proxy_connection_id + net_http_args.concat @proxy_args + else + net_http_args.concat [nil, nil, nil, nil] + end + + connection = connections[connection_id] + + unless connection = connections[connection_id] then + connections[connection_id] = http_class.new(*net_http_args) + connection = connections[connection_id] + ssl connection if use_ssl + else + reset connection if expired? connection + end + + start connection unless connection.started? + + connection.read_timeout = @read_timeout if @read_timeout + connection.keep_alive_timeout = @idle_timeout if @idle_timeout && connection.respond_to?(:keep_alive_timeout=) + + connection + rescue Errno::ECONNREFUSED + address = connection.proxy_address || connection.address + port = connection.proxy_port || connection.port + + raise Error, "connection refused: #{address}:#{port}" + rescue Errno::EHOSTDOWN + address = connection.proxy_address || connection.address + port = connection.proxy_port || connection.port + + raise Error, "host down: #{address}:#{port}" + end + + ## + # Returns an error message containing the number of requests performed on + # this connection + + def error_message connection + requests = Thread.current[@request_key][connection.object_id] - 1 # fixup + last_use = Thread.current[@timeout_key][connection.object_id] + + age = Time.now - last_use + + "after #{requests} requests on #{connection.object_id}, " \ + "last used #{age} seconds ago" + end + + ## + # URI::escape wrapper + + def escape str + CGI.escape str if str + end + + ## + # URI::unescape wrapper + + def unescape str + CGI.unescape str if str + end + + + ## + # Returns true if the connection should be reset due to an idle timeout, or + # maximum request count, false otherwise. + + def expired? connection + requests = Thread.current[@request_key][connection.object_id] + return true if @max_requests && requests >= @max_requests + return false unless @idle_timeout + return true if @idle_timeout.zero? + + last_used = Thread.current[@timeout_key][connection.object_id] + + Time.now - last_used > @idle_timeout + end + + ## + # Starts the Net::HTTP +connection+ + + def start connection + connection.set_debug_output @debug_output if @debug_output + connection.open_timeout = @open_timeout if @open_timeout + + connection.start + + socket = connection.instance_variable_get :@socket + + if socket then # for fakeweb + @socket_options.each do |option| + socket.io.setsockopt(*option) + end + end + end + + ## + # Finishes the Net::HTTP +connection+ + + def finish connection, thread = Thread.current + if requests = thread[@request_key] then + requests.delete connection.object_id + end + + connection.finish + rescue IOError + end + + def http_class # :nodoc: + if RUBY_VERSION > '2.0' then + Net::HTTP + elsif [:Artifice, :FakeWeb, :WebMock].any? { |klass| + Object.const_defined?(klass) + } or not @reuse_ssl_sessions then + Net::HTTP + else + Bundler::Persistent::Net::HTTP::Persistent::SSLReuse + end + end + + ## + # Returns the HTTP protocol version for +uri+ + + def http_version uri + @http_versions["#{uri.host}:#{uri.port}"] + end + + ## + # Is +req+ idempotent according to RFC 2616? + + def idempotent? req + case req + when Net::HTTP::Delete, Net::HTTP::Get, Net::HTTP::Head, + Net::HTTP::Options, Net::HTTP::Put, Net::HTTP::Trace then + true + end + end + + ## + # Is the request +req+ idempotent or is retry_change_requests allowed. + # + # If +retried_on_ruby_2+ is true, true will be returned if we are on ruby, + # retry_change_requests is allowed and the request is not idempotent. + + def can_retry? req, retried_on_ruby_2 = false + return @retry_change_requests && !idempotent?(req) if retried_on_ruby_2 + + @retry_change_requests || idempotent?(req) + end + + if RUBY_VERSION > '1.9' then + ## + # Workaround for missing Net::HTTPHeader#connection_close? on Ruby 1.8 + + def connection_close? header + header.connection_close? + end + + ## + # Workaround for missing Net::HTTPHeader#connection_keep_alive? on Ruby 1.8 + + def connection_keep_alive? header + header.connection_keep_alive? + end + else + ## + # Workaround for missing Net::HTTPRequest#connection_close? on Ruby 1.8 + + def connection_close? header + header['connection'] =~ /close/ or header['proxy-connection'] =~ /close/ + end + + ## + # Workaround for missing Net::HTTPRequest#connection_keep_alive? on Ruby + # 1.8 + + def connection_keep_alive? header + header['connection'] =~ /keep-alive/ or + header['proxy-connection'] =~ /keep-alive/ + end + end + + ## + # Deprecated in favor of #expired? + + def max_age # :nodoc: + return Time.now + 1 unless @idle_timeout + + Time.now - @idle_timeout + end + + ## + # Adds "http://" to the String +uri+ if it is missing. + + def normalize_uri uri + (uri =~ /^https?:/) ? uri : "http://#{uri}" + end + + ## + # Pipelines +requests+ to the HTTP server at +uri+ yielding responses if a + # block is given. Returns all responses received. + # + # See + # Net::HTTP::Pipeline[http://docs.seattlerb.org/net-http-pipeline/Net/HTTP/Pipeline.html] + # for further details. + # + # Only if net-http-pipeline was required before + # net-http-persistent #pipeline will be present. + + def pipeline uri, requests, &block # :yields: responses + connection = connection_for uri + + connection.pipeline requests, &block + end + + ## + # Sets this client's SSL private key + + def private_key= key + @private_key = key + + reconnect_ssl + end + + # For Net::HTTP parity + alias key= private_key= + + ## + # Sets the proxy server. The +proxy+ may be the URI of the proxy server, + # the symbol +:ENV+ which will read the proxy from the environment or nil to + # disable use of a proxy. See #proxy_from_env for details on setting the + # proxy from the environment. + # + # If the proxy URI is set after requests have been made, the next request + # will shut-down and re-open all connections. + # + # The +no_proxy+ query parameter can be used to specify hosts which shouldn't + # be reached via proxy; if set it should be a comma separated list of + # hostname suffixes, optionally with +:port+ appended, for example + # example.com,some.host:8080. + + def proxy= proxy + @proxy_uri = case proxy + when :ENV then proxy_from_env + when URI::HTTP then proxy + when nil then # ignore + else raise ArgumentError, 'proxy must be :ENV or a URI::HTTP' + end + + @no_proxy.clear + + if @proxy_uri then + @proxy_args = [ + @proxy_uri.host, + @proxy_uri.port, + unescape(@proxy_uri.user), + unescape(@proxy_uri.password), + ] + + @proxy_connection_id = [nil, *@proxy_args].join ':' + + if @proxy_uri.query then + @no_proxy = CGI.parse(@proxy_uri.query)['no_proxy'].join(',').downcase.split(',').map { |x| x.strip }.reject { |x| x.empty? } + end + end + + reconnect + reconnect_ssl + end + + ## + # Creates a URI for an HTTP proxy server from ENV variables. + # + # If +HTTP_PROXY+ is set a proxy will be returned. + # + # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the URI is given the + # indicated user and password unless HTTP_PROXY contains either of these in + # the URI. + # + # The +NO_PROXY+ ENV variable can be used to specify hosts which shouldn't + # be reached via proxy; if set it should be a comma separated list of + # hostname suffixes, optionally with +:port+ appended, for example + # example.com,some.host:8080. When set to * no proxy will + # be returned. + # + # For Windows users, lowercase ENV variables are preferred over uppercase ENV + # variables. + + def proxy_from_env + env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] + + return nil if env_proxy.nil? or env_proxy.empty? + + uri = URI normalize_uri env_proxy + + env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY'] + + # '*' is special case for always bypass + return nil if env_no_proxy == '*' + + if env_no_proxy then + uri.query = "no_proxy=#{escape(env_no_proxy)}" + end + + unless uri.user or uri.password then + uri.user = escape ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER'] + uri.password = escape ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS'] + end + + uri + end + + ## + # Returns true when proxy should by bypassed for host. + + def proxy_bypass? host, port + host = host.downcase + host_port = [host, port].join ':' + + @no_proxy.each do |name| + return true if host[-name.length, name.length] == name or + host_port[-name.length, name.length] == name + end + + false + end + + ## + # Forces reconnection of HTTP connections. + + def reconnect + @generation += 1 + end + + ## + # Forces reconnection of SSL connections. + + def reconnect_ssl + @ssl_generation += 1 + end + + ## + # Finishes then restarts the Net::HTTP +connection+ + + def reset connection + Thread.current[@request_key].delete connection.object_id + Thread.current[@timeout_key].delete connection.object_id + + finish connection + + start connection + rescue Errno::ECONNREFUSED + e = Error.new "connection refused: #{connection.address}:#{connection.port}" + e.set_backtrace $@ + raise e + rescue Errno::EHOSTDOWN + e = Error.new "host down: #{connection.address}:#{connection.port}" + e.set_backtrace $@ + raise e + end + + ## + # Makes a request on +uri+. If +req+ is nil a Net::HTTP::Get is performed + # against +uri+. + # + # If a block is passed #request behaves like Net::HTTP#request (the body of + # the response will not have been read). + # + # +req+ must be a Net::HTTPRequest subclass (see Net::HTTP for a list). + # + # If there is an error and the request is idempotent according to RFC 2616 + # it will be retried automatically. + + def request uri, req = nil, &block + retried = false + bad_response = false + + req = request_setup req || uri + + connection = connection_for uri + connection_id = connection.object_id + + begin + Thread.current[@request_key][connection_id] += 1 + response = connection.request req, &block + + if connection_close?(req) or + (response.http_version <= '1.0' and + not connection_keep_alive?(response)) or + connection_close?(response) then + connection.finish + end + rescue Net::HTTPBadResponse => e + message = error_message connection + + finish connection + + raise Error, "too many bad responses #{message}" if + bad_response or not can_retry? req + + bad_response = true + retry + rescue *RETRIED_EXCEPTIONS => e # retried on ruby 2 + request_failed e, req, connection if + retried or not can_retry? req, @retried_on_ruby_2 + + reset connection + + retried = true + retry + rescue Errno::EINVAL, Errno::ETIMEDOUT => e # not retried on ruby 2 + request_failed e, req, connection if retried or not can_retry? req + + reset connection + + retried = true + retry + rescue Exception => e + finish connection + + raise + ensure + Thread.current[@timeout_key][connection_id] = Time.now + end + + @http_versions["#{uri.host}:#{uri.port}"] ||= response.http_version + + response + end + + ## + # Raises an Error for +exception+ which resulted from attempting the request + # +req+ on the +connection+. + # + # Finishes the +connection+. + + def request_failed exception, req, connection # :nodoc: + due_to = "(due to #{exception.message} - #{exception.class})" + message = "too many connection resets #{due_to} #{error_message connection}" + + finish connection + + + raise Error, message, exception.backtrace + end + + ## + # Creates a GET request if +req_or_uri+ is a URI and adds headers to the + # request. + # + # Returns the request. + + def request_setup req_or_uri # :nodoc: + req = if URI === req_or_uri then + Net::HTTP::Get.new req_or_uri.request_uri + else + req_or_uri + end + + @headers.each do |pair| + req.add_field(*pair) + end + + @override_headers.each do |name, value| + req[name] = value + end + + unless req['Connection'] then + req.add_field 'Connection', 'keep-alive' + req.add_field 'Keep-Alive', @keep_alive + end + + req + end + + ## + # Shuts down all connections for +thread+. + # + # Uses the current thread by default. + # + # If you've used Bundler::Persistent::Net::HTTP::Persistent across multiple threads you should + # call this in each thread when you're done making HTTP requests. + # + # *NOTE*: Calling shutdown for another thread can be dangerous! + # + # If the thread is still using the connection it may cause an error! It is + # best to call #shutdown in the thread at the appropriate time instead! + + def shutdown thread = Thread.current + generation = reconnect + cleanup generation, thread, @generation_key + + ssl_generation = reconnect_ssl + cleanup ssl_generation, thread, @ssl_generation_key + + thread[@request_key] = nil + thread[@timeout_key] = nil + end + + ## + # Shuts down all connections in all threads + # + # *NOTE*: THIS METHOD IS VERY DANGEROUS! + # + # Do not call this method if other threads are still using their + # connections! Call #shutdown at the appropriate time instead! + # + # Use this method only as a last resort! + + def shutdown_in_all_threads + Thread.list.each do |thread| + shutdown thread + end + + nil + end + + ## + # Enables SSL on +connection+ + + def ssl connection + connection.use_ssl = true + + connection.ssl_version = @ssl_version if @ssl_version + + connection.verify_mode = @verify_mode + + if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and + not Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then + warn <<-WARNING + !!!SECURITY WARNING!!! + +The SSL HTTP connection to: + + #{connection.address}:#{connection.port} + + !!!MAY NOT BE VERIFIED!!! + +On your platform your OpenSSL implementation is broken. + +There is no difference between the values of VERIFY_NONE and VERIFY_PEER. + +This means that attempting to verify the security of SSL connections may not +work. This exposes you to man-in-the-middle exploits, snooping on the +contents of your connection and other dangers to the security of your data. + +To disable this warning define the following constant at top-level in your +application: + + I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG = nil + + WARNING + end + + if @ca_file then + connection.ca_file = @ca_file + connection.verify_mode = OpenSSL::SSL::VERIFY_PEER + connection.verify_callback = @verify_callback if @verify_callback + end + + if @certificate and @private_key then + connection.cert = @certificate + connection.key = @private_key + end + + connection.cert_store = if @cert_store then + @cert_store + else + store = OpenSSL::X509::Store.new + store.set_default_paths + store + end + end + + ## + # Finishes all connections that existed before the given SSL parameter + # +generation+. + + def ssl_cleanup generation # :nodoc: + cleanup generation, Thread.current, @ssl_generation_key + end + + ## + # SSL version to use + + def ssl_version= ssl_version + @ssl_version = ssl_version + + reconnect_ssl + end if RUBY_VERSION > '1.9' + + ## + # Sets the HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER. + # + # Setting this to VERIFY_NONE is a VERY BAD IDEA and should NEVER be used. + # Securely transfer the correct certificate and update the default + # certificate store or set the ca file instead. + + def verify_mode= verify_mode + @verify_mode = verify_mode + + reconnect_ssl + end + + ## + # SSL verification callback. + + def verify_callback= callback + @verify_callback = callback + + reconnect_ssl + end + +end + +require 'bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse' + diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb new file mode 100644 index 00000000000000..1b6b789f6d77bb --- /dev/null +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb @@ -0,0 +1,129 @@ +## +# This Net::HTTP subclass adds SSL session reuse and Server Name Indication +# (SNI) RFC 3546. +# +# DO NOT DEPEND UPON THIS CLASS +# +# This class is an implementation detail and is subject to change or removal +# at any time. + +class Bundler::Persistent::Net::HTTP::Persistent::SSLReuse < Net::HTTP + + @is_proxy_class = false + @proxy_addr = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + + def initialize address, port = nil # :nodoc: + super + + @ssl_session = nil + end + + ## + # From ruby trunk r33086 including http://redmine.ruby-lang.org/issues/5341 + + def connect # :nodoc: + D "opening connection to #{conn_address()}..." + s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) } + D "opened" + if use_ssl? + ssl_parameters = Hash.new + iv_list = instance_variables + SSL_ATTRIBUTES.each do |name| + ivname = "@#{name}".intern + if iv_list.include?(ivname) and + value = instance_variable_get(ivname) + ssl_parameters[name] = value + end + end + unless @ssl_context then + @ssl_context = OpenSSL::SSL::SSLContext.new + @ssl_context.set_params(ssl_parameters) + end + s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) + s.sync_close = true + end + @socket = Net::BufferedIO.new(s) + @socket.read_timeout = @read_timeout + @socket.continue_timeout = @continue_timeout if + @socket.respond_to? :continue_timeout + @socket.debug_output = @debug_output + if use_ssl? + begin + if proxy? + @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', + @address, @port, HTTPVersion) + @socket.writeline "Host: #{@address}:#{@port}" + if proxy_user + credential = ["#{proxy_user}:#{proxy_pass}"].pack('m') + credential.delete!("\r\n") + @socket.writeline "Proxy-Authorization: Basic #{credential}" + end + @socket.writeline '' + Net::HTTPResponse.read_new(@socket).value + end + s.session = @ssl_session if @ssl_session + # Server Name Indication (SNI) RFC 3546 + s.hostname = @address if s.respond_to? :hostname= + timeout(@open_timeout) { s.connect } + if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE + s.post_connection_check(@address) + end + @ssl_session = s.session + rescue => exception + D "Conn close because of connect error #{exception}" + @socket.close if @socket and not @socket.closed? + raise exception + end + end + on_connect + end if RUBY_VERSION > '1.9' + + ## + # From ruby_1_8_7 branch r29865 including a modified + # http://redmine.ruby-lang.org/issues/5341 + + def connect # :nodoc: + D "opening connection to #{conn_address()}..." + s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) } + D "opened" + if use_ssl? + unless @ssl_context.verify_mode + warn "warning: peer certificate won't be verified in this SSL session" + @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) + s.sync_close = true + end + @socket = Net::BufferedIO.new(s) + @socket.read_timeout = @read_timeout + @socket.debug_output = @debug_output + if use_ssl? + if proxy? + @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', + @address, @port, HTTPVersion) + @socket.writeline "Host: #{@address}:#{@port}" + if proxy_user + credential = ["#{proxy_user}:#{proxy_pass}"].pack('m') + credential.delete!("\r\n") + @socket.writeline "Proxy-Authorization: Basic #{credential}" + end + @socket.writeline '' + Net::HTTPResponse.read_new(@socket).value + end + s.session = @ssl_session if @ssl_session + s.connect + if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE + s.post_connection_check(@address) + end + @ssl_session = s.session + end + on_connect + end if RUBY_VERSION < '1.9' + + private :connect + +end + diff --git a/lib/bundler/vendor/thor/lib/thor.rb b/lib/bundler/vendor/thor/lib/thor.rb new file mode 100644 index 00000000000000..999e8b7e6141e4 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor.rb @@ -0,0 +1,509 @@ +require "set" +require "bundler/vendor/thor/lib/thor/base" + +class Bundler::Thor + class << self + # Allows for custom "Command" package naming. + # + # === Parameters + # name + # options + # + def package_name(name, _ = {}) + @package_name = name.nil? || name == "" ? nil : name + end + + # Sets the default command when thor is executed without an explicit command to be called. + # + # ==== Parameters + # meth:: name of the default command + # + def default_command(meth = nil) + if meth + @default_command = meth == :none ? "help" : meth.to_s + else + @default_command ||= from_superclass(:default_command, "help") + end + end + alias_method :default_task, :default_command + + # Registers another Bundler::Thor subclass as a command. + # + # ==== Parameters + # klass:: Bundler::Thor subclass to register + # command:: Subcommand name to use + # usage:: Short usage for the subcommand + # description:: Description for the subcommand + def register(klass, subcommand_name, usage, description, options = {}) + if klass <= Bundler::Thor::Group + desc usage, description, options + define_method(subcommand_name) { |*args| invoke(klass, args) } + else + desc usage, description, options + subcommand subcommand_name, klass + end + end + + # Defines the usage and the description of the next command. + # + # ==== Parameters + # usage + # description + # options + # + def desc(usage, description, options = {}) + if options[:for] + command = find_and_refresh_command(options[:for]) + command.usage = usage if usage + command.description = description if description + else + @usage = usage + @desc = description + @hide = options[:hide] || false + end + end + + # Defines the long description of the next command. + # + # ==== Parameters + # long description + # + def long_desc(long_description, options = {}) + if options[:for] + command = find_and_refresh_command(options[:for]) + command.long_description = long_description if long_description + else + @long_desc = long_description + end + end + + # Maps an input to a command. If you define: + # + # map "-T" => "list" + # + # Running: + # + # thor -T + # + # Will invoke the list command. + # + # ==== Parameters + # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given command. + # + def map(mappings = nil) + @map ||= from_superclass(:map, {}) + + if mappings + mappings.each do |key, value| + if key.respond_to?(:each) + key.each { |subkey| @map[subkey] = value } + else + @map[key] = value + end + end + end + + @map + end + + # Declares the options for the next command to be declared. + # + # ==== Parameters + # Hash[Symbol => Object]:: The hash key is the name of the option and the value + # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric + # or :required (string). If you give a value, the type of the value is used. + # + def method_options(options = nil) + @method_options ||= {} + build_options(options, @method_options) if options + @method_options + end + + alias_method :options, :method_options + + # Adds an option to the set of method options. If :for is given as option, + # it allows you to change the options from a previous defined command. + # + # def previous_command + # # magic + # end + # + # method_option :foo => :bar, :for => :previous_command + # + # def next_command + # # magic + # end + # + # ==== Parameters + # name:: The name of the argument. + # options:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :default - Default value for this argument. It cannot be required and have default values. + # :aliases - Aliases for this option. + # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean. + # :banner - String to show on usage notes. + # :hide - If you want to hide this option from the help. + # + def method_option(name, options = {}) + scope = if options[:for] + find_and_refresh_command(options[:for]).options + else + method_options + end + + build_option(name, options, scope) + end + alias_method :option, :method_option + + # Prints help information for the given command. + # + # ==== Parameters + # shell + # command_name + # + def command_help(shell, command_name) + meth = normalize_command_name(command_name) + command = all_commands[meth] + handle_no_command_error(meth) unless command + + shell.say "Usage:" + shell.say " #{banner(command)}" + shell.say + class_options_help(shell, nil => command.options.values) + if command.long_description + shell.say "Description:" + shell.print_wrapped(command.long_description, :indent => 2) + else + shell.say command.description + end + end + alias_method :task_help, :command_help + + # Prints help information for this class. + # + # ==== Parameters + # shell + # + def help(shell, subcommand = false) + list = printable_commands(true, subcommand) + Bundler::Thor::Util.thor_classes_in(self).each do |klass| + list += klass.printable_commands(false) + end + list.sort! { |a, b| a[0] <=> b[0] } + + if defined?(@package_name) && @package_name + shell.say "#{@package_name} commands:" + else + shell.say "Commands:" + end + + shell.print_table(list, :indent => 2, :truncate => true) + shell.say + class_options_help(shell) + end + + # Returns commands ready to be printed. + def printable_commands(all = true, subcommand = false) + (all ? all_commands : commands).map do |_, command| + next if command.hidden? + item = [] + item << banner(command, false, subcommand) + item << (command.description ? "# #{command.description.gsub(/\s+/m, ' ')}" : "") + item + end.compact + end + alias_method :printable_tasks, :printable_commands + + def subcommands + @subcommands ||= from_superclass(:subcommands, []) + end + alias_method :subtasks, :subcommands + + def subcommand_classes + @subcommand_classes ||= {} + end + + def subcommand(subcommand, subcommand_class) + subcommands << subcommand.to_s + subcommand_class.subcommand_help subcommand + subcommand_classes[subcommand.to_s] = subcommand_class + + define_method(subcommand) do |*args| + args, opts = Bundler::Thor::Arguments.split(args) + invoke_args = [args, opts, {:invoked_via_subcommand => true, :class_options => options}] + invoke_args.unshift "help" if opts.delete("--help") || opts.delete("-h") + invoke subcommand_class, *invoke_args + end + subcommand_class.commands.each do |_meth, command| + command.ancestor_name = subcommand + end + end + alias_method :subtask, :subcommand + + # Extend check unknown options to accept a hash of conditions. + # + # === Parameters + # options: A hash containing :only and/or :except keys + def check_unknown_options!(options = {}) + @check_unknown_options ||= {} + options.each do |key, value| + if value + @check_unknown_options[key] = Array(value) + else + @check_unknown_options.delete(key) + end + end + @check_unknown_options + end + + # Overwrite check_unknown_options? to take subcommands and options into account. + def check_unknown_options?(config) #:nodoc: + options = check_unknown_options + return false unless options + + command = config[:current_command] + return true unless command + + name = command.name + + if subcommands.include?(name) + false + elsif options[:except] + !options[:except].include?(name.to_sym) + elsif options[:only] + options[:only].include?(name.to_sym) + else + true + end + end + + # Stop parsing of options as soon as an unknown option or a regular + # argument is encountered. All remaining arguments are passed to the command. + # This is useful if you have a command that can receive arbitrary additional + # options, and where those additional options should not be handled by + # Bundler::Thor. + # + # ==== Example + # + # To better understand how this is useful, let's consider a command that calls + # an external command. A user may want to pass arbitrary options and + # arguments to that command. The command itself also accepts some options, + # which should be handled by Bundler::Thor. + # + # class_option "verbose", :type => :boolean + # stop_on_unknown_option! :exec + # check_unknown_options! :except => :exec + # + # desc "exec", "Run a shell command" + # def exec(*args) + # puts "diagnostic output" if options[:verbose] + # Kernel.exec(*args) + # end + # + # Here +exec+ can be called with +--verbose+ to get diagnostic output, + # e.g.: + # + # $ thor exec --verbose echo foo + # diagnostic output + # foo + # + # But if +--verbose+ is given after +echo+, it is passed to +echo+ instead: + # + # $ thor exec echo --verbose foo + # --verbose foo + # + # ==== Parameters + # Symbol ...:: A list of commands that should be affected. + def stop_on_unknown_option!(*command_names) + stop_on_unknown_option.merge(command_names) + end + + def stop_on_unknown_option?(command) #:nodoc: + command && stop_on_unknown_option.include?(command.name.to_sym) + end + + # Disable the check for required options for the given commands. + # This is useful if you have a command that does not need the required options + # to work, like help. + # + # ==== Parameters + # Symbol ...:: A list of commands that should be affected. + def disable_required_check!(*command_names) + disable_required_check.merge(command_names) + end + + def disable_required_check?(command) #:nodoc: + command && disable_required_check.include?(command.name.to_sym) + end + + protected + + def stop_on_unknown_option #:nodoc: + @stop_on_unknown_option ||= Set.new + end + + # help command has the required check disabled by default. + def disable_required_check #:nodoc: + @disable_required_check ||= Set.new([:help]) + end + + # The method responsible for dispatching given the args. + def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable MethodLength + meth ||= retrieve_command_name(given_args) + command = all_commands[normalize_command_name(meth)] + + if !command && config[:invoked_via_subcommand] + # We're a subcommand and our first argument didn't match any of our + # commands. So we put it back and call our default command. + given_args.unshift(meth) + command = all_commands[normalize_command_name(default_command)] + end + + if command + args, opts = Bundler::Thor::Options.split(given_args) + if stop_on_unknown_option?(command) && !args.empty? + # given_args starts with a non-option, so we treat everything as + # ordinary arguments + args.concat opts + opts.clear + end + else + args = given_args + opts = nil + command = dynamic_command_class.new(meth) + end + + opts = given_opts || opts || [] + config[:current_command] = command + config[:command_options] = command.options + + instance = new(args, opts, config) + yield instance if block_given? + args = instance.args + trailing = args[Range.new(arguments.size, -1)] + instance.invoke_command(command, trailing || []) + end + + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Bundler::Thor::Runner. It receives + # the command that is going to be invoked and a boolean which indicates if + # the namespace should be displayed as arguments. + # + def banner(command, namespace = nil, subcommand = false) + "#{basename} #{command.formatted_usage(self, $thor_runner, subcommand)}" + end + + def baseclass #:nodoc: + Bundler::Thor + end + + def dynamic_command_class #:nodoc: + Bundler::Thor::DynamicCommand + end + + def create_command(meth) #:nodoc: + @usage ||= nil + @desc ||= nil + @long_desc ||= nil + @hide ||= nil + + if @usage && @desc + base_class = @hide ? Bundler::Thor::HiddenCommand : Bundler::Thor::Command + commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options) + @usage, @desc, @long_desc, @method_options, @hide = nil + true + elsif all_commands[meth] || meth == "method_missing" + true + else + puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " \ + "Call desc if you want this method to be available as command or declare it inside a " \ + "no_commands{} block. Invoked from #{caller[1].inspect}." + false + end + end + alias_method :create_task, :create_command + + def initialize_added #:nodoc: + class_options.merge!(method_options) + @method_options = nil + end + + # Retrieve the command name from given args. + def retrieve_command_name(args) #:nodoc: + meth = args.first.to_s unless args.empty? + args.shift if meth && (map[meth] || meth !~ /^\-/) + end + alias_method :retrieve_task_name, :retrieve_command_name + + # receives a (possibly nil) command name and returns a name that is in + # the commands hash. In addition to normalizing aliases, this logic + # will determine if a shortened command is an unambiguous substring of + # a command or alias. + # + # +normalize_command_name+ also converts names like +animal-prison+ + # into +animal_prison+. + def normalize_command_name(meth) #:nodoc: + return default_command.to_s.tr("-", "_") unless meth + + possibilities = find_command_possibilities(meth) + raise AmbiguousTaskError, "Ambiguous command #{meth} matches [#{possibilities.join(', ')}]" if possibilities.size > 1 + + if possibilities.empty? + meth ||= default_command + elsif map[meth] + meth = map[meth] + else + meth = possibilities.first + end + + meth.to_s.tr("-", "_") # treat foo-bar as foo_bar + end + alias_method :normalize_task_name, :normalize_command_name + + # this is the logic that takes the command name passed in by the user + # and determines whether it is an unambiguous substrings of a command or + # alias name. + def find_command_possibilities(meth) + len = meth.to_s.length + possibilities = all_commands.merge(map).keys.select { |n| meth == n[0, len] }.sort + unique_possibilities = possibilities.map { |k| map[k] || k }.uniq + + if possibilities.include?(meth) + [meth] + elsif unique_possibilities.size == 1 + unique_possibilities + else + possibilities + end + end + alias_method :find_task_possibilities, :find_command_possibilities + + def subcommand_help(cmd) + desc "help [COMMAND]", "Describe subcommands or one specific subcommand" + class_eval " + def help(command = nil, subcommand = true); super; end +" + end + alias_method :subtask_help, :subcommand_help + end + + include Bundler::Thor::Base + + map HELP_MAPPINGS => :help + + desc "help [COMMAND]", "Describe available commands or one specific command" + def help(command = nil, subcommand = false) + if command + if self.class.subcommands.include? command + self.class.subcommand_classes[command].help(shell, true) + else + self.class.command_help(shell, command) + end + else + self.class.help(shell, subcommand) + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions.rb b/lib/bundler/vendor/thor/lib/thor/actions.rb new file mode 100644 index 00000000000000..e6698572a9609c --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions.rb @@ -0,0 +1,321 @@ +require "uri" +require "bundler/vendor/thor/lib/thor/core_ext/io_binary_read" +require "bundler/vendor/thor/lib/thor/actions/create_file" +require "bundler/vendor/thor/lib/thor/actions/create_link" +require "bundler/vendor/thor/lib/thor/actions/directory" +require "bundler/vendor/thor/lib/thor/actions/empty_directory" +require "bundler/vendor/thor/lib/thor/actions/file_manipulation" +require "bundler/vendor/thor/lib/thor/actions/inject_into_file" + +class Bundler::Thor + module Actions + attr_accessor :behavior + + def self.included(base) #:nodoc: + base.extend ClassMethods + end + + module ClassMethods + # Hold source paths for one Bundler::Thor instance. source_paths_for_search is the + # method responsible to gather source_paths from this current class, + # inherited paths and the source root. + # + def source_paths + @_source_paths ||= [] + end + + # Stores and return the source root for this class + def source_root(path = nil) + @_source_root = path if path + @_source_root ||= nil + end + + # Returns the source paths in the following order: + # + # 1) This class source paths + # 2) Source root + # 3) Parents source paths + # + def source_paths_for_search + paths = [] + paths += source_paths + paths << source_root if source_root + paths += from_superclass(:source_paths, []) + paths + end + + # Add runtime options that help actions execution. + # + def add_runtime_options! + class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime, + :desc => "Overwrite files that already exist" + + class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime, + :desc => "Run but do not make any changes" + + class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime, + :desc => "Suppress status output" + + class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime, + :desc => "Skip files that already exist" + end + end + + # Extends initializer to add more configuration options. + # + # ==== Configuration + # behavior:: The actions default behavior. Can be :invoke or :revoke. + # It also accepts :force, :skip and :pretend to set the behavior + # and the respective option. + # + # destination_root:: The root directory needed for some actions. + # + def initialize(args = [], options = {}, config = {}) + self.behavior = case config[:behavior].to_s + when "force", "skip" + _cleanup_options_and_set(options, config[:behavior]) + :invoke + when "revoke" + :revoke + else + :invoke + end + + super + self.destination_root = config[:destination_root] + end + + # Wraps an action object and call it accordingly to the thor class behavior. + # + def action(instance) #:nodoc: + if behavior == :revoke + instance.revoke! + else + instance.invoke! + end + end + + # Returns the root for this thor class (also aliased as destination root). + # + def destination_root + @destination_stack.last + end + + # Sets the root for this thor class. Relatives path are added to the + # directory where the script was invoked and expanded. + # + def destination_root=(root) + @destination_stack ||= [] + @destination_stack[0] = File.expand_path(root || "") + end + + # Returns the given path relative to the absolute root (ie, root where + # the script started). + # + def relative_to_original_destination_root(path, remove_dot = true) + path = path.dup + if path.gsub!(@destination_stack[0], ".") + remove_dot ? (path[2..-1] || "") : path + else + path + end + end + + # Holds source paths in instance so they can be manipulated. + # + def source_paths + @source_paths ||= self.class.source_paths_for_search + end + + # Receives a file or directory and search for it in the source paths. + # + def find_in_source_paths(file) + possible_files = [file, file + TEMPLATE_EXTNAME] + relative_root = relative_to_original_destination_root(destination_root, false) + + source_paths.each do |source| + possible_files.each do |f| + source_file = File.expand_path(f, File.join(source, relative_root)) + return source_file if File.exist?(source_file) + end + end + + message = "Could not find #{file.inspect} in any of your source paths. ".dup + + unless self.class.source_root + message << "Please invoke #{self.class.name}.source_root(PATH) with the PATH containing your templates. " + end + + message << if source_paths.empty? + "Currently you have no source paths." + else + "Your current source paths are: \n#{source_paths.join("\n")}" + end + + raise Error, message + end + + # Do something in the root or on a provided subfolder. If a relative path + # is given it's referenced from the current root. The full path is yielded + # to the block you provide. The path is set back to the previous path when + # the method exits. + # + # ==== Parameters + # dir:: the directory to move to. + # config:: give :verbose => true to log and use padding. + # + def inside(dir = "", config = {}, &block) + verbose = config.fetch(:verbose, false) + pretend = options[:pretend] + + say_status :inside, dir, verbose + shell.padding += 1 if verbose + @destination_stack.push File.expand_path(dir, destination_root) + + # If the directory doesnt exist and we're not pretending + if !File.exist?(destination_root) && !pretend + require "fileutils" + FileUtils.mkdir_p(destination_root) + end + + if pretend + # In pretend mode, just yield down to the block + block.arity == 1 ? yield(destination_root) : yield + else + require "fileutils" + FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield } + end + + @destination_stack.pop + shell.padding -= 1 if verbose + end + + # Goes to the root and execute the given block. + # + def in_root + inside(@destination_stack.first) { yield } + end + + # Loads an external file and execute it in the instance binding. + # + # ==== Parameters + # path:: The path to the file to execute. Can be a web address or + # a relative path from the source root. + # + # ==== Examples + # + # apply "http://gist.github.com/103208" + # + # apply "recipes/jquery.rb" + # + def apply(path, config = {}) + verbose = config.fetch(:verbose, true) + is_uri = path =~ %r{^https?\://} + path = find_in_source_paths(path) unless is_uri + + say_status :apply, path, verbose + shell.padding += 1 if verbose + + contents = if is_uri + open(path, "Accept" => "application/x-thor-template", &:read) + else + open(path, &:read) + end + + instance_eval(contents, path) + shell.padding -= 1 if verbose + end + + # Executes a command returning the contents of the command. + # + # ==== Parameters + # command:: the command to be executed. + # config:: give :verbose => false to not log the status, :capture => true to hide to output. Specify :with + # to append an executable to command execution. + # + # ==== Example + # + # inside('vendor') do + # run('ln -s ~/edge rails') + # end + # + def run(command, config = {}) + return unless behavior == :invoke + + destination = relative_to_original_destination_root(destination_root, false) + desc = "#{command} from #{destination.inspect}" + + if config[:with] + desc = "#{File.basename(config[:with].to_s)} #{desc}" + command = "#{config[:with]} #{command}" + end + + say_status :run, desc, config.fetch(:verbose, true) + + unless options[:pretend] + config[:capture] ? `#{command}` : system(command.to_s) + end + end + + # Executes a ruby script (taking into account WIN32 platform quirks). + # + # ==== Parameters + # command:: the command to be executed. + # config:: give :verbose => false to not log the status. + # + def run_ruby_script(command, config = {}) + return unless behavior == :invoke + run command, config.merge(:with => Bundler::Thor::Util.ruby_command) + end + + # Run a thor command. A hash of options can be given and it's converted to + # switches. + # + # ==== Parameters + # command:: the command to be invoked + # args:: arguments to the command + # config:: give :verbose => false to not log the status, :capture => true to hide to output. + # Other options are given as parameter to Bundler::Thor. + # + # + # ==== Examples + # + # thor :install, "http://gist.github.com/103208" + # #=> thor install http://gist.github.com/103208 + # + # thor :list, :all => true, :substring => 'rails' + # #=> thor list --all --substring=rails + # + def thor(command, *args) + config = args.last.is_a?(Hash) ? args.pop : {} + verbose = config.key?(:verbose) ? config.delete(:verbose) : true + pretend = config.key?(:pretend) ? config.delete(:pretend) : false + capture = config.key?(:capture) ? config.delete(:capture) : false + + args.unshift(command) + args.push Bundler::Thor::Options.to_switches(config) + command = args.join(" ").strip + + run command, :with => :thor, :verbose => verbose, :pretend => pretend, :capture => capture + end + + protected + + # Allow current root to be shared between invocations. + # + def _shared_configuration #:nodoc: + super.merge!(:destination_root => destination_root) + end + + def _cleanup_options_and_set(options, key) #:nodoc: + case options + when Array + %w(--force -f --skip -s).each { |i| options.delete(i) } + options << "--#{key}" + when Hash + [:force, :skip, "force", "skip"].each { |i| options.delete(i) } + options.merge!(key => true) + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb new file mode 100644 index 00000000000000..97d22d9bbdd622 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb @@ -0,0 +1,104 @@ +require "bundler/vendor/thor/lib/thor/actions/empty_directory" + +class Bundler::Thor + module Actions + # Create a new file relative to the destination root with the given data, + # which is the return value of a block or a data string. + # + # ==== Parameters + # destination:: the relative path to the destination root. + # data:: the data to append to the file. + # config:: give :verbose => false to not log the status. + # + # ==== Examples + # + # create_file "lib/fun_party.rb" do + # hostname = ask("What is the virtual hostname I should use?") + # "vhost.name = #{hostname}" + # end + # + # create_file "config/apache.conf", "your apache config" + # + def create_file(destination, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + data = args.first + action CreateFile.new(self, destination, block || data.to_s, config) + end + alias_method :add_file, :create_file + + # CreateFile is a subset of Template, which instead of rendering a file with + # ERB, it gets the content from the user. + # + class CreateFile < EmptyDirectory #:nodoc: + attr_reader :data + + def initialize(base, destination, data, config = {}) + @data = data + super(base, destination, config) + end + + # Checks if the content of the file at the destination is identical to the rendered result. + # + # ==== Returns + # Boolean:: true if it is identical, false otherwise. + # + def identical? + exists? && File.binread(destination) == render + end + + # Holds the content to be added to the file. + # + def render + @render ||= if data.is_a?(Proc) + data.call + else + data + end + end + + def invoke! + invoke_with_conflict_check do + require "fileutils" + FileUtils.mkdir_p(File.dirname(destination)) + File.open(destination, "wb") { |f| f.write render } + end + given_destination + end + + protected + + # Now on conflict we check if the file is identical or not. + # + def on_conflict_behavior(&block) + if identical? + say_status :identical, :blue + else + options = base.options.merge(config) + force_or_skip_or_conflict(options[:force], options[:skip], &block) + end + end + + # If force is true, run the action, otherwise check if it's not being + # skipped. If both are false, show the file_collision menu, if the menu + # returns true, force it, otherwise skip. + # + def force_or_skip_or_conflict(force, skip, &block) + if force + say_status :force, :yellow + yield unless pretend? + elsif skip + say_status :skip, :yellow + else + say_status :conflict, :red + force_or_skip_or_conflict(force_on_collision?, true, &block) + end + end + + # Shows the file collision menu to the user and gets the result. + # + def force_on_collision? + base.shell.file_collision(destination) { render } + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb new file mode 100644 index 00000000000000..3a664401b419b0 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb @@ -0,0 +1,60 @@ +require "bundler/vendor/thor/lib/thor/actions/create_file" + +class Bundler::Thor + module Actions + # Create a new file relative to the destination root from the given source. + # + # ==== Parameters + # destination:: the relative path to the destination root. + # source:: the relative path to the source root. + # config:: give :verbose => false to not log the status. + # :: give :symbolic => false for hard link. + # + # ==== Examples + # + # create_link "config/apache.conf", "/etc/apache.conf" + # + def create_link(destination, *args) + config = args.last.is_a?(Hash) ? args.pop : {} + source = args.first + action CreateLink.new(self, destination, source, config) + end + alias_method :add_link, :create_link + + # CreateLink is a subset of CreateFile, which instead of taking a block of + # data, just takes a source string from the user. + # + class CreateLink < CreateFile #:nodoc: + attr_reader :data + + # Checks if the content of the file at the destination is identical to the rendered result. + # + # ==== Returns + # Boolean:: true if it is identical, false otherwise. + # + def identical? + exists? && File.identical?(render, destination) + end + + def invoke! + invoke_with_conflict_check do + require "fileutils" + FileUtils.mkdir_p(File.dirname(destination)) + # Create a symlink by default + config[:symbolic] = true if config[:symbolic].nil? + File.unlink(destination) if exists? + if config[:symbolic] + File.symlink(render, destination) + else + File.link(render, destination) + end + end + given_destination + end + + def exists? + super || File.symlink?(destination) + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb new file mode 100644 index 00000000000000..f555f7b7e0fec9 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb @@ -0,0 +1,118 @@ +require "bundler/vendor/thor/lib/thor/actions/empty_directory" + +class Bundler::Thor + module Actions + # Copies recursively the files from source directory to root directory. + # If any of the files finishes with .tt, it's considered to be a template + # and is placed in the destination without the extension .tt. If any + # empty directory is found, it's copied and all .empty_directory files are + # ignored. If any file name is wrapped within % signs, the text within + # the % signs will be executed as a method and replaced with the returned + # value. Let's suppose a doc directory with the following files: + # + # doc/ + # components/.empty_directory + # README + # rdoc.rb.tt + # %app_name%.rb + # + # When invoked as: + # + # directory "doc" + # + # It will create a doc directory in the destination with the following + # files (assuming that the `app_name` method returns the value "blog"): + # + # doc/ + # components/ + # README + # rdoc.rb + # blog.rb + # + # Encoded path note: Since Bundler::Thor internals use Object#respond_to? to check if it can + # expand %something%, this `something` should be a public method in the class calling + # #directory. If a method is private, Bundler::Thor stack raises PrivateMethodEncodedError. + # + # ==== Parameters + # source:: the relative path to the source root. + # destination:: the relative path to the destination root. + # config:: give :verbose => false to not log the status. + # If :recursive => false, does not look for paths recursively. + # If :mode => :preserve, preserve the file mode from the source. + # If :exclude_pattern => /regexp/, prevents copying files that match that regexp. + # + # ==== Examples + # + # directory "doc" + # directory "doc", "docs", :recursive => false + # + def directory(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source + action Directory.new(self, source, destination || source, config, &block) + end + + class Directory < EmptyDirectory #:nodoc: + attr_reader :source + + def initialize(base, source, destination = nil, config = {}, &block) + @source = File.expand_path(base.find_in_source_paths(source.to_s)) + @block = block + super(base, destination, {:recursive => true}.merge(config)) + end + + def invoke! + base.empty_directory given_destination, config + execute! + end + + def revoke! + execute! + end + + protected + + def execute! + lookup = Util.escape_globs(source) + lookup = config[:recursive] ? File.join(lookup, "**") : lookup + lookup = file_level_lookup(lookup) + + files(lookup).sort.each do |file_source| + next if File.directory?(file_source) + next if config[:exclude_pattern] && file_source.match(config[:exclude_pattern]) + file_destination = File.join(given_destination, file_source.gsub(source, ".")) + file_destination.gsub!("/./", "/") + + case file_source + when /\.empty_directory$/ + dirname = File.dirname(file_destination).gsub(%r{/\.$}, "") + next if dirname == given_destination + base.empty_directory(dirname, config) + when /#{TEMPLATE_EXTNAME}$/ + base.template(file_source, file_destination[0..-4], config, &@block) + else + base.copy_file(file_source, file_destination, config, &@block) + end + end + end + + if RUBY_VERSION < "2.0" + def file_level_lookup(previous_lookup) + File.join(previous_lookup, "{*,.[a-z]*}") + end + + def files(lookup) + Dir[lookup] + end + else + def file_level_lookup(previous_lookup) + File.join(previous_lookup, "*") + end + + def files(lookup) + Dir.glob(lookup, File::FNM_DOTMATCH) + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb new file mode 100644 index 00000000000000..284d92c19aeb87 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb @@ -0,0 +1,143 @@ +class Bundler::Thor + module Actions + # Creates an empty directory. + # + # ==== Parameters + # destination:: the relative path to the destination root. + # config:: give :verbose => false to not log the status. + # + # ==== Examples + # + # empty_directory "doc" + # + def empty_directory(destination, config = {}) + action EmptyDirectory.new(self, destination, config) + end + + # Class which holds create directory logic. This is the base class for + # other actions like create_file and directory. + # + # This implementation is based in Templater actions, created by Jonas Nicklas + # and Michael S. Klishin under MIT LICENSE. + # + class EmptyDirectory #:nodoc: + attr_reader :base, :destination, :given_destination, :relative_destination, :config + + # Initializes given the source and destination. + # + # ==== Parameters + # base:: A Bundler::Thor::Base instance + # source:: Relative path to the source of this file + # destination:: Relative path to the destination of this file + # config:: give :verbose => false to not log the status. + # + def initialize(base, destination, config = {}) + @base = base + @config = {:verbose => true}.merge(config) + self.destination = destination + end + + # Checks if the destination file already exists. + # + # ==== Returns + # Boolean:: true if the file exists, false otherwise. + # + def exists? + ::File.exist?(destination) + end + + def invoke! + invoke_with_conflict_check do + require "fileutils" + ::FileUtils.mkdir_p(destination) + end + end + + def revoke! + say_status :remove, :red + require "fileutils" + ::FileUtils.rm_rf(destination) if !pretend? && exists? + given_destination + end + + protected + + # Shortcut for pretend. + # + def pretend? + base.options[:pretend] + end + + # Sets the absolute destination value from a relative destination value. + # It also stores the given and relative destination. Let's suppose our + # script is being executed on "dest", it sets the destination root to + # "dest". The destination, given_destination and relative_destination + # are related in the following way: + # + # inside "bar" do + # empty_directory "baz" + # end + # + # destination #=> dest/bar/baz + # relative_destination #=> bar/baz + # given_destination #=> baz + # + def destination=(destination) + return unless destination + @given_destination = convert_encoded_instructions(destination.to_s) + @destination = ::File.expand_path(@given_destination, base.destination_root) + @relative_destination = base.relative_to_original_destination_root(@destination) + end + + # Filenames in the encoded form are converted. If you have a file: + # + # %file_name%.rb + # + # It calls #file_name from the base and replaces %-string with the + # return value (should be String) of #file_name: + # + # user.rb + # + # The method referenced can be either public or private. + # + def convert_encoded_instructions(filename) + filename.gsub(/%(.*?)%/) do |initial_string| + method = $1.strip + base.respond_to?(method, true) ? base.send(method) : initial_string + end + end + + # Receives a hash of options and just execute the block if some + # conditions are met. + # + def invoke_with_conflict_check(&block) + if exists? + on_conflict_behavior(&block) + else + yield unless pretend? + say_status :create, :green + end + + destination + rescue Errno::EISDIR, Errno::EEXIST + on_file_clash_behavior + end + + def on_file_clash_behavior + say_status :file_clash, :red + end + + # What to do when the destination file already exists. + # + def on_conflict_behavior + say_status :exist, :blue + end + + # Shortcut to say_status shell method. + # + def say_status(status, color) + base.shell.say_status status, relative_destination, color if config[:verbose] + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb new file mode 100644 index 00000000000000..4c83bebc8615b1 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb @@ -0,0 +1,364 @@ +require "erb" + +class Bundler::Thor + module Actions + # Copies the file from the relative source to the relative destination. If + # the destination is not given it's assumed to be equal to the source. + # + # ==== Parameters + # source:: the relative path to the source root. + # destination:: the relative path to the destination root. + # config:: give :verbose => false to not log the status, and + # :mode => :preserve, to preserve the file mode from the source. + + # + # ==== Examples + # + # copy_file "README", "doc/README" + # + # copy_file "doc/README" + # + def copy_file(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source + source = File.expand_path(find_in_source_paths(source.to_s)) + + create_file destination, nil, config do + content = File.binread(source) + content = yield(content) if block + content + end + if config[:mode] == :preserve + mode = File.stat(source).mode + chmod(destination, mode, config) + end + end + + # Links the file from the relative source to the relative destination. If + # the destination is not given it's assumed to be equal to the source. + # + # ==== Parameters + # source:: the relative path to the source root. + # destination:: the relative path to the destination root. + # config:: give :verbose => false to not log the status. + # + # ==== Examples + # + # link_file "README", "doc/README" + # + # link_file "doc/README" + # + def link_file(source, *args) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source + source = File.expand_path(find_in_source_paths(source.to_s)) + + create_link destination, source, config + end + + # Gets the content at the given address and places it at the given relative + # destination. If a block is given instead of destination, the content of + # the url is yielded and used as location. + # + # ==== Parameters + # source:: the address of the given content. + # destination:: the relative path to the destination root. + # config:: give :verbose => false to not log the status. + # + # ==== Examples + # + # get "http://gist.github.com/103208", "doc/README" + # + # get "http://gist.github.com/103208" do |content| + # content.split("\n").first + # end + # + def get(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first + + if source =~ %r{^https?\://} + require "open-uri" + else + source = File.expand_path(find_in_source_paths(source.to_s)) + end + + render = open(source) { |input| input.binmode.read } + + destination ||= if block_given? + block.arity == 1 ? yield(render) : yield + else + File.basename(source) + end + + create_file destination, render, config + end + + # Gets an ERB template at the relative source, executes it and makes a copy + # at the relative destination. If the destination is not given it's assumed + # to be equal to the source removing .tt from the filename. + # + # ==== Parameters + # source:: the relative path to the source root. + # destination:: the relative path to the destination root. + # config:: give :verbose => false to not log the status. + # + # ==== Examples + # + # template "README", "doc/README" + # + # template "doc/README" + # + def template(source, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + destination = args.first || source.sub(/#{TEMPLATE_EXTNAME}$/, "") + + source = File.expand_path(find_in_source_paths(source.to_s)) + context = config.delete(:context) || instance_eval("binding") + + create_file destination, nil, config do + content = CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer").tap do |erb| + erb.filename = source + end.result(context) + content = yield(content) if block + content + end + end + + # Changes the mode of the given file or directory. + # + # ==== Parameters + # mode:: the file mode + # path:: the name of the file to change mode + # config:: give :verbose => false to not log the status. + # + # ==== Example + # + # chmod "script/server", 0755 + # + def chmod(path, mode, config = {}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true) + unless options[:pretend] + require "fileutils" + FileUtils.chmod_R(mode, path) + end + end + + # Prepend text to a file. Since it depends on insert_into_file, it's reversible. + # + # ==== Parameters + # path:: path of the file to be changed + # data:: the data to prepend to the file, can be also given as a block. + # config:: give :verbose => false to not log the status. + # + # ==== Example + # + # prepend_to_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # prepend_to_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def prepend_to_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:after] = /\A/ + insert_into_file(path, *(args << config), &block) + end + alias_method :prepend_file, :prepend_to_file + + # Append text to a file. Since it depends on insert_into_file, it's reversible. + # + # ==== Parameters + # path:: path of the file to be changed + # data:: the data to append to the file, can be also given as a block. + # config:: give :verbose => false to not log the status. + # + # ==== Example + # + # append_to_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # append_to_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def append_to_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:before] = /\z/ + insert_into_file(path, *(args << config), &block) + end + alias_method :append_file, :append_to_file + + # Injects text right after the class definition. Since it depends on + # insert_into_file, it's reversible. + # + # ==== Parameters + # path:: path of the file to be changed + # klass:: the class to be manipulated + # data:: the data to append to the class, can be also given as a block. + # config:: give :verbose => false to not log the status. + # + # ==== Examples + # + # inject_into_class "app/controllers/application_controller.rb", ApplicationController, " filter_parameter :password\n" + # + # inject_into_class "app/controllers/application_controller.rb", ApplicationController do + # " filter_parameter :password\n" + # end + # + def inject_into_class(path, klass, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:after] = /class #{klass}\n|class #{klass} .*\n/ + insert_into_file(path, *(args << config), &block) + end + + # Injects text right after the module definition. Since it depends on + # insert_into_file, it's reversible. + # + # ==== Parameters + # path:: path of the file to be changed + # module_name:: the module to be manipulated + # data:: the data to append to the class, can be also given as a block. + # config:: give :verbose => false to not log the status. + # + # ==== Examples + # + # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper, " def help; 'help'; end\n" + # + # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper do + # " def help; 'help'; end\n" + # end + # + def inject_into_module(path, module_name, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config[:after] = /module #{module_name}\n|module #{module_name} .*\n/ + insert_into_file(path, *(args << config), &block) + end + + # Run a regular expression replacement on a file. + # + # ==== Parameters + # path:: path of the file to be changed + # flag:: the regexp or string to be replaced + # replacement:: the replacement, can be also given as a block + # config:: give :verbose => false to not log the status. + # + # ==== Example + # + # gsub_file 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1' + # + # gsub_file 'README', /rake/, :green do |match| + # match << " no more. Use thor!" + # end + # + def gsub_file(path, flag, *args, &block) + return unless behavior == :invoke + config = args.last.is_a?(Hash) ? args.pop : {} + + path = File.expand_path(path, destination_root) + say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) + + unless options[:pretend] + content = File.binread(path) + content.gsub!(flag, *args, &block) + File.open(path, "wb") { |file| file.write(content) } + end + end + + # Uncomment all lines matching a given regex. It will leave the space + # which existed before the comment hash in tact but will remove any spacing + # between the comment hash and the beginning of the line. + # + # ==== Parameters + # path:: path of the file to be changed + # flag:: the regexp or string used to decide which lines to uncomment + # config:: give :verbose => false to not log the status. + # + # ==== Example + # + # uncomment_lines 'config/initializers/session_store.rb', /active_record/ + # + def uncomment_lines(path, flag, *args) + flag = flag.respond_to?(:source) ? flag.source : flag + + gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{flag})/, '\1\2', *args) + end + + # Comment all lines matching a given regex. It will leave the space + # which existed before the beginning of the line in tact and will insert + # a single space after the comment hash. + # + # ==== Parameters + # path:: path of the file to be changed + # flag:: the regexp or string used to decide which lines to comment + # config:: give :verbose => false to not log the status. + # + # ==== Example + # + # comment_lines 'config/initializers/session_store.rb', /cookie_store/ + # + def comment_lines(path, flag, *args) + flag = flag.respond_to?(:source) ? flag.source : flag + + gsub_file(path, /^(\s*)([^#|\n]*#{flag})/, '\1# \2', *args) + end + + # Removes a file at the given location. + # + # ==== Parameters + # path:: path of the file to be changed + # config:: give :verbose => false to not log the status. + # + # ==== Example + # + # remove_file 'README' + # remove_file 'app/controllers/application_controller.rb' + # + def remove_file(path, config = {}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + + say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true) + if !options[:pretend] && File.exist?(path) + require "fileutils" + ::FileUtils.rm_rf(path) + end + end + alias_method :remove_dir, :remove_file + + attr_accessor :output_buffer + private :output_buffer, :output_buffer= + + private + + def concat(string) + @output_buffer.concat(string) + end + + def capture(*args) + with_output_buffer { yield(*args) } + end + + def with_output_buffer(buf = "".dup) #:nodoc: + raise ArgumentError, "Buffer can not be a frozen object" if buf.frozen? + old_buffer = output_buffer + self.output_buffer = buf + yield + output_buffer + ensure + self.output_buffer = old_buffer + end + + # Bundler::Thor::Actions#capture depends on what kind of buffer is used in ERB. + # Thus CapturableERB fixes ERB to use String buffer. + class CapturableERB < ERB + def set_eoutvar(compiler, eoutvar = "_erbout") + compiler.put_cmd = "#{eoutvar}.concat" + compiler.insert_cmd = "#{eoutvar}.concat" + compiler.pre_cmd = ["#{eoutvar} = ''.dup"] + compiler.post_cmd = [eoutvar] + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb new file mode 100644 index 00000000000000..349b26ff65c706 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb @@ -0,0 +1,109 @@ +require "bundler/vendor/thor/lib/thor/actions/empty_directory" + +class Bundler::Thor + module Actions + # Injects the given content into a file. Different from gsub_file, this + # method is reversible. + # + # ==== Parameters + # destination:: Relative path to the destination root + # data:: Data to add to the file. Can be given as a block. + # config:: give :verbose => false to not log the status and the flag + # for injection (:after or :before) or :force => true for + # insert two or more times the same content. + # + # ==== Examples + # + # insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n" + # + # insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do + # gems = ask "Which gems would you like to add?" + # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") + # end + # + def insert_into_file(destination, *args, &block) + data = block_given? ? block : args.shift + config = args.shift + action InjectIntoFile.new(self, destination, data, config) + end + alias_method :inject_into_file, :insert_into_file + + class InjectIntoFile < EmptyDirectory #:nodoc: + attr_reader :replacement, :flag, :behavior + + def initialize(base, destination, data, config) + super(base, destination, {:verbose => true}.merge(config)) + + @behavior, @flag = if @config.key?(:after) + [:after, @config.delete(:after)] + else + [:before, @config.delete(:before)] + end + + @replacement = data.is_a?(Proc) ? data.call : data + @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp) + end + + def invoke! + say_status :invoke + + content = if @behavior == :after + '\0' + replacement + else + replacement + '\0' + end + + if exists? + replace!(/#{flag}/, content, config[:force]) + else + unless pretend? + raise Bundler::Thor::Error, "The file #{ destination } does not appear to exist" + end + end + end + + def revoke! + say_status :revoke + + regexp = if @behavior == :after + content = '\1\2' + /(#{flag})(.*)(#{Regexp.escape(replacement)})/m + else + content = '\2\3' + /(#{Regexp.escape(replacement)})(.*)(#{flag})/m + end + + replace!(regexp, content, true) + end + + protected + + def say_status(behavior) + status = if behavior == :invoke + if flag == /\A/ + :prepend + elsif flag == /\z/ + :append + else + :insert + end + else + :subtract + end + + super(status, config[:verbose]) + end + + # Adds the content to the file. + # + def replace!(regexp, string, force) + return if pretend? + content = File.read(destination) + if force || !content.include?(replacement) + content.gsub!(regexp, string) + File.open(destination, "wb") { |file| file.write(content) } + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/base.rb b/lib/bundler/vendor/thor/lib/thor/base.rb new file mode 100644 index 00000000000000..9bd10771704914 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/base.rb @@ -0,0 +1,679 @@ +require "bundler/vendor/thor/lib/thor/command" +require "bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access" +require "bundler/vendor/thor/lib/thor/core_ext/ordered_hash" +require "bundler/vendor/thor/lib/thor/error" +require "bundler/vendor/thor/lib/thor/invocation" +require "bundler/vendor/thor/lib/thor/parser" +require "bundler/vendor/thor/lib/thor/shell" +require "bundler/vendor/thor/lib/thor/line_editor" +require "bundler/vendor/thor/lib/thor/util" + +class Bundler::Thor + autoload :Actions, "bundler/vendor/thor/lib/thor/actions" + autoload :RakeCompat, "bundler/vendor/thor/lib/thor/rake_compat" + autoload :Group, "bundler/vendor/thor/lib/thor/group" + + # Shortcuts for help. + HELP_MAPPINGS = %w(-h -? --help -D) + + # Bundler::Thor methods that should not be overwritten by the user. + THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root + action add_file create_file in_root inside run run_ruby_script) + + TEMPLATE_EXTNAME = ".tt" + + module Base + attr_accessor :options, :parent_options, :args + + # It receives arguments in an Array and two hashes, one for options and + # other for configuration. + # + # Notice that it does not check if all required arguments were supplied. + # It should be done by the parser. + # + # ==== Parameters + # args:: An array of objects. The objects are applied to their + # respective accessors declared with argument. + # + # options:: An options hash that will be available as self.options. + # The hash given is converted to a hash with indifferent + # access, magic predicates (options.skip?) and then frozen. + # + # config:: Configuration for this Bundler::Thor class. + # + def initialize(args = [], local_options = {}, config = {}) + parse_options = self.class.class_options + + # The start method splits inbound arguments at the first argument + # that looks like an option (starts with - or --). It then calls + # new, passing in the two halves of the arguments Array as the + # first two parameters. + + command_options = config.delete(:command_options) # hook for start + parse_options = parse_options.merge(command_options) if command_options + if local_options.is_a?(Array) + array_options = local_options + hash_options = {} + else + # Handle the case where the class was explicitly instantiated + # with pre-parsed options. + array_options = [] + hash_options = local_options + end + + # Let Bundler::Thor::Options parse the options first, so it can remove + # declared options from the array. This will leave us with + # a list of arguments that weren't declared. + stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command] + disable_required_check = self.class.disable_required_check? config[:current_command] + opts = Bundler::Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check) + self.options = opts.parse(array_options) + self.options = config[:class_options].merge(options) if config[:class_options] + + # If unknown options are disallowed, make sure that none of the + # remaining arguments looks like an option. + opts.check_unknown! if self.class.check_unknown_options?(config) + + # Add the remaining arguments from the options parser to the + # arguments passed in to initialize. Then remove any positional + # arguments declared using #argument (this is primarily used + # by Bundler::Thor::Group). Tis will leave us with the remaining + # positional arguments. + to_parse = args + to_parse += opts.remaining unless self.class.strict_args_position?(config) + + thor_args = Bundler::Thor::Arguments.new(self.class.arguments) + thor_args.parse(to_parse).each { |k, v| __send__("#{k}=", v) } + @args = thor_args.remaining + end + + class << self + def included(base) #:nodoc: + base.extend ClassMethods + base.send :include, Invocation + base.send :include, Shell + end + + # Returns the classes that inherits from Bundler::Thor or Bundler::Thor::Group. + # + # ==== Returns + # Array[Class] + # + def subclasses + @subclasses ||= [] + end + + # Returns the files where the subclasses are kept. + # + # ==== Returns + # Hash[path => Class] + # + def subclass_files + @subclass_files ||= Hash.new { |h, k| h[k] = [] } + end + + # Whenever a class inherits from Bundler::Thor or Bundler::Thor::Group, we should track the + # class and the file on Bundler::Thor::Base. This is the method responsable for it. + # + def register_klass_file(klass) #:nodoc: + file = caller[1].match(/(.*):\d+/)[1] + Bundler::Thor::Base.subclasses << klass unless Bundler::Thor::Base.subclasses.include?(klass) + + file_subclasses = Bundler::Thor::Base.subclass_files[File.expand_path(file)] + file_subclasses << klass unless file_subclasses.include?(klass) + end + end + + module ClassMethods + def attr_reader(*) #:nodoc: + no_commands { super } + end + + def attr_writer(*) #:nodoc: + no_commands { super } + end + + def attr_accessor(*) #:nodoc: + no_commands { super } + end + + # If you want to raise an error for unknown options, call check_unknown_options! + # This is disabled by default to allow dynamic invocations. + def check_unknown_options! + @check_unknown_options = true + end + + def check_unknown_options #:nodoc: + @check_unknown_options ||= from_superclass(:check_unknown_options, false) + end + + def check_unknown_options?(config) #:nodoc: + !!check_unknown_options + end + + # If you want to raise an error when the default value of an option does not match + # the type call check_default_type! + # This is disabled by default for compatibility. + def check_default_type! + @check_default_type = true + end + + def check_default_type #:nodoc: + @check_default_type ||= from_superclass(:check_default_type, false) + end + + def check_default_type? #:nodoc: + !!check_default_type + end + + # If true, option parsing is suspended as soon as an unknown option or a + # regular argument is encountered. All remaining arguments are passed to + # the command as regular arguments. + def stop_on_unknown_option?(command_name) #:nodoc: + false + end + + # If true, option set will not suspend the execution of the command when + # a required option is not provided. + def disable_required_check?(command_name) #:nodoc: + false + end + + # If you want only strict string args (useful when cascading thor classes), + # call strict_args_position! This is disabled by default to allow dynamic + # invocations. + def strict_args_position! + @strict_args_position = true + end + + def strict_args_position #:nodoc: + @strict_args_position ||= from_superclass(:strict_args_position, false) + end + + def strict_args_position?(config) #:nodoc: + !!strict_args_position + end + + # Adds an argument to the class and creates an attr_accessor for it. + # + # Arguments are different from options in several aspects. The first one + # is how they are parsed from the command line, arguments are retrieved + # from position: + # + # thor command NAME + # + # Instead of: + # + # thor command --name=NAME + # + # Besides, arguments are used inside your code as an accessor (self.argument), + # while options are all kept in a hash (self.options). + # + # Finally, arguments cannot have type :default or :boolean but can be + # optional (supplying :optional => :true or :required => false), although + # you cannot have a required argument after a non-required argument. If you + # try it, an error is raised. + # + # ==== Parameters + # name:: The name of the argument. + # options:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :optional - If the argument is optional or not. + # :type - The type of the argument, can be :string, :hash, :array, :numeric. + # :default - Default value for this argument. It cannot be required and have default values. + # :banner - String to show on usage notes. + # + # ==== Errors + # ArgumentError:: Raised if you supply a required argument after a non required one. + # + def argument(name, options = {}) + is_thor_reserved_word?(name, :argument) + no_commands { attr_accessor name } + + required = if options.key?(:optional) + !options[:optional] + elsif options.key?(:required) + options[:required] + else + options[:default].nil? + end + + remove_argument name + + if required + arguments.each do |argument| + next if argument.required? + raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " \ + "the non-required argument #{argument.human_name.inspect}." + end + end + + options[:required] = required + + arguments << Bundler::Thor::Argument.new(name, options) + end + + # Returns this class arguments, looking up in the ancestors chain. + # + # ==== Returns + # Array[Bundler::Thor::Argument] + # + def arguments + @arguments ||= from_superclass(:arguments, []) + end + + # Adds a bunch of options to the set of class options. + # + # class_options :foo => false, :bar => :required, :baz => :string + # + # If you prefer more detailed declaration, check class_option. + # + # ==== Parameters + # Hash[Symbol => Object] + # + def class_options(options = nil) + @class_options ||= from_superclass(:class_options, {}) + build_options(options, @class_options) if options + @class_options + end + + # Adds an option to the set of class options + # + # ==== Parameters + # name:: The name of the argument. + # options:: Described below. + # + # ==== Options + # :desc:: -- Description for the argument. + # :required:: -- If the argument is required or not. + # :default:: -- Default value for this argument. + # :group:: -- The group for this options. Use by class options to output options in different levels. + # :aliases:: -- Aliases for this option. Note: Bundler::Thor follows a convention of one-dash-one-letter options. Thus aliases like "-something" wouldn't be parsed; use either "\--something" or "-s" instead. + # :type:: -- The type of the argument, can be :string, :hash, :array, :numeric or :boolean. + # :banner:: -- String to show on usage notes. + # :hide:: -- If you want to hide this option from the help. + # + def class_option(name, options = {}) + build_option(name, options, class_options) + end + + # Removes a previous defined argument. If :undefine is given, undefine + # accessors as well. + # + # ==== Parameters + # names:: Arguments to be removed + # + # ==== Examples + # + # remove_argument :foo + # remove_argument :foo, :bar, :baz, :undefine => true + # + def remove_argument(*names) + options = names.last.is_a?(Hash) ? names.pop : {} + + names.each do |name| + arguments.delete_if { |a| a.name == name.to_s } + undef_method name, "#{name}=" if options[:undefine] + end + end + + # Removes a previous defined class option. + # + # ==== Parameters + # names:: Class options to be removed + # + # ==== Examples + # + # remove_class_option :foo + # remove_class_option :foo, :bar, :baz + # + def remove_class_option(*names) + names.each do |name| + class_options.delete(name) + end + end + + # Defines the group. This is used when thor list is invoked so you can specify + # that only commands from a pre-defined group will be shown. Defaults to standard. + # + # ==== Parameters + # name + # + def group(name = nil) + if name + @group = name.to_s + else + @group ||= from_superclass(:group, "standard") + end + end + + # Returns the commands for this Bundler::Thor class. + # + # ==== Returns + # OrderedHash:: An ordered hash with commands names as keys and Bundler::Thor::Command + # objects as values. + # + def commands + @commands ||= Bundler::Thor::CoreExt::OrderedHash.new + end + alias_method :tasks, :commands + + # Returns the commands for this Bundler::Thor class and all subclasses. + # + # ==== Returns + # OrderedHash:: An ordered hash with commands names as keys and Bundler::Thor::Command + # objects as values. + # + def all_commands + @all_commands ||= from_superclass(:all_commands, Bundler::Thor::CoreExt::OrderedHash.new) + @all_commands.merge!(commands) + end + alias_method :all_tasks, :all_commands + + # Removes a given command from this Bundler::Thor class. This is usually done if you + # are inheriting from another class and don't want it to be available + # anymore. + # + # By default it only remove the mapping to the command. But you can supply + # :undefine => true to undefine the method from the class as well. + # + # ==== Parameters + # name:: The name of the command to be removed + # options:: You can give :undefine => true if you want commands the method + # to be undefined from the class as well. + # + def remove_command(*names) + options = names.last.is_a?(Hash) ? names.pop : {} + + names.each do |name| + commands.delete(name.to_s) + all_commands.delete(name.to_s) + undef_method name if options[:undefine] + end + end + alias_method :remove_task, :remove_command + + # All methods defined inside the given block are not added as commands. + # + # So you can do: + # + # class MyScript < Bundler::Thor + # no_commands do + # def this_is_not_a_command + # end + # end + # end + # + # You can also add the method and remove it from the command list: + # + # class MyScript < Bundler::Thor + # def this_is_not_a_command + # end + # remove_command :this_is_not_a_command + # end + # + def no_commands + @no_commands = true + yield + ensure + @no_commands = false + end + alias_method :no_tasks, :no_commands + + # Sets the namespace for the Bundler::Thor or Bundler::Thor::Group class. By default the + # namespace is retrieved from the class name. If your Bundler::Thor class is named + # Scripts::MyScript, the help method, for example, will be called as: + # + # thor scripts:my_script -h + # + # If you change the namespace: + # + # namespace :my_scripts + # + # You change how your commands are invoked: + # + # thor my_scripts -h + # + # Finally, if you change your namespace to default: + # + # namespace :default + # + # Your commands can be invoked with a shortcut. Instead of: + # + # thor :my_command + # + def namespace(name = nil) + if name + @namespace = name.to_s + else + @namespace ||= Bundler::Thor::Util.namespace_from_thor_class(self) + end + end + + # Parses the command and options from the given args, instantiate the class + # and invoke the command. This method is used when the arguments must be parsed + # from an array. If you are inside Ruby and want to use a Bundler::Thor class, you + # can simply initialize it: + # + # script = MyScript.new(args, options, config) + # script.invoke(:command, first_arg, second_arg, third_arg) + # + def start(given_args = ARGV, config = {}) + config[:shell] ||= Bundler::Thor::Base.shell.new + dispatch(nil, given_args.dup, nil, config) + rescue Bundler::Thor::Error => e + config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message) + exit(1) if exit_on_failure? + rescue Errno::EPIPE + # This happens if a thor command is piped to something like `head`, + # which closes the pipe when it's done reading. This will also + # mean that if the pipe is closed, further unnecessary + # computation will not occur. + exit(0) + end + + # Allows to use private methods from parent in child classes as commands. + # + # ==== Parameters + # names:: Method names to be used as commands + # + # ==== Examples + # + # public_command :foo + # public_command :foo, :bar, :baz + # + def public_command(*names) + names.each do |name| + class_eval "def #{name}(*); super end" + end + end + alias_method :public_task, :public_command + + def handle_no_command_error(command, has_namespace = $thor_runner) #:nodoc: + raise UndefinedCommandError, "Could not find command #{command.inspect} in #{namespace.inspect} namespace." if has_namespace + raise UndefinedCommandError, "Could not find command #{command.inspect}." + end + alias_method :handle_no_task_error, :handle_no_command_error + + def handle_argument_error(command, error, args, arity) #:nodoc: + name = [command.ancestor_name, command.name].compact.join(" ") + msg = "ERROR: \"#{basename} #{name}\" was called with ".dup + msg << "no arguments" if args.empty? + msg << "arguments " << args.inspect unless args.empty? + msg << "\nUsage: #{banner(command).inspect}" + raise InvocationError, msg + end + + protected + + # Prints the class options per group. If an option does not belong to + # any group, it's printed as Class option. + # + def class_options_help(shell, groups = {}) #:nodoc: + # Group options by group + class_options.each do |_, value| + groups[value.group] ||= [] + groups[value.group] << value + end + + # Deal with default group + global_options = groups.delete(nil) || [] + print_options(shell, global_options) + + # Print all others + groups.each do |group_name, options| + print_options(shell, options, group_name) + end + end + + # Receives a set of options and print them. + def print_options(shell, options, group_name = nil) + return if options.empty? + + list = [] + padding = options.map { |o| o.aliases.size }.max.to_i * 4 + + options.each do |option| + next if option.hide + item = [option.usage(padding)] + item.push(option.description ? "# #{option.description}" : "") + + list << item + list << ["", "# Default: #{option.default}"] if option.show_default? + list << ["", "# Possible values: #{option.enum.join(', ')}"] if option.enum + end + + shell.say(group_name ? "#{group_name} options:" : "Options:") + shell.print_table(list, :indent => 2) + shell.say "" + end + + # Raises an error if the word given is a Bundler::Thor reserved word. + def is_thor_reserved_word?(word, type) #:nodoc: + return false unless THOR_RESERVED_WORDS.include?(word.to_s) + raise "#{word.inspect} is a Bundler::Thor reserved word and cannot be defined as #{type}" + end + + # Build an option and adds it to the given scope. + # + # ==== Parameters + # name:: The name of the argument. + # options:: Described in both class_option and method_option. + # scope:: Options hash that is being built up + def build_option(name, options, scope) #:nodoc: + scope[name] = Bundler::Thor::Option.new(name, options.merge(:check_default_type => check_default_type?)) + end + + # Receives a hash of options, parse them and add to the scope. This is a + # fast way to set a bunch of options: + # + # build_options :foo => true, :bar => :required, :baz => :string + # + # ==== Parameters + # Hash[Symbol => Object] + def build_options(options, scope) #:nodoc: + options.each do |key, value| + scope[key] = Bundler::Thor::Option.parse(key, value) + end + end + + # Finds a command with the given name. If the command belongs to the current + # class, just return it, otherwise dup it and add the fresh copy to the + # current command hash. + def find_and_refresh_command(name) #:nodoc: + if commands[name.to_s] + commands[name.to_s] + elsif command = all_commands[name.to_s] # rubocop:disable AssignmentInCondition + commands[name.to_s] = command.clone + else + raise ArgumentError, "You supplied :for => #{name.inspect}, but the command #{name.inspect} could not be found." + end + end + alias_method :find_and_refresh_task, :find_and_refresh_command + + # Everytime someone inherits from a Bundler::Thor class, register the klass + # and file into baseclass. + def inherited(klass) + Bundler::Thor::Base.register_klass_file(klass) + klass.instance_variable_set(:@no_commands, false) + end + + # Fire this callback whenever a method is added. Added methods are + # tracked as commands by invoking the create_command method. + def method_added(meth) + meth = meth.to_s + + if meth == "initialize" + initialize_added + return + end + + # Return if it's not a public instance method + return unless public_method_defined?(meth.to_sym) + + @no_commands ||= false + return if @no_commands || !create_command(meth) + + is_thor_reserved_word?(meth, :command) + Bundler::Thor::Base.register_klass_file(self) + end + + # Retrieves a value from superclass. If it reaches the baseclass, + # returns default. + def from_superclass(method, default = nil) + if self == baseclass || !superclass.respond_to?(method, true) + default + else + value = superclass.send(method) + + # Ruby implements `dup` on Object, but raises a `TypeError` + # if the method is called on immediates. As a result, we + # don't have a good way to check whether dup will succeed + # without calling it and rescuing the TypeError. + begin + value.dup + rescue TypeError + value + end + + end + end + + # A flag that makes the process exit with status 1 if any error happens. + def exit_on_failure? + false + end + + # + # The basename of the program invoking the thor class. + # + def basename + File.basename($PROGRAM_NAME).split(" ").first + end + + # SIGNATURE: Sets the baseclass. This is where the superclass lookup + # finishes. + def baseclass #:nodoc: + end + + # SIGNATURE: Creates a new command if valid_command? is true. This method is + # called when a new method is added to the class. + def create_command(meth) #:nodoc: + end + alias_method :create_task, :create_command + + # SIGNATURE: Defines behavior when the initialize method is added to the + # class. + def initialize_added #:nodoc: + end + + # SIGNATURE: The hook invoked by start. + def dispatch(command, given_args, given_opts, config) #:nodoc: + raise NotImplementedError + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/command.rb b/lib/bundler/vendor/thor/lib/thor/command.rb new file mode 100644 index 00000000000000..c636948e5dc6dd --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/command.rb @@ -0,0 +1,135 @@ +class Bundler::Thor + class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name) + FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/ + + def initialize(name, description, long_description, usage, options = nil) + super(name.to_s, description, long_description, usage, options || {}) + end + + def initialize_copy(other) #:nodoc: + super(other) + self.options = other.options.dup if other.options + end + + def hidden? + false + end + + # By default, a command invokes a method in the thor class. You can change this + # implementation to create custom commands. + def run(instance, args = []) + arity = nil + + if private_method?(instance) + instance.class.handle_no_command_error(name) + elsif public_method?(instance) + arity = instance.method(name).arity + instance.__send__(name, *args) + elsif local_method?(instance, :method_missing) + instance.__send__(:method_missing, name.to_sym, *args) + else + instance.class.handle_no_command_error(name) + end + rescue ArgumentError => e + handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e) + rescue NoMethodError => e + handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (raise e) + end + + # Returns the formatted usage by injecting given required arguments + # and required options into the given usage. + def formatted_usage(klass, namespace = true, subcommand = false) + if ancestor_name + formatted = "#{ancestor_name} ".dup # add space + elsif namespace + namespace = klass.namespace + formatted = "#{namespace.gsub(/^(default)/, '')}:".dup + end + formatted ||= "#{klass.namespace.split(':').last} ".dup if subcommand + + formatted ||= "".dup + + # Add usage with required arguments + formatted << if klass && !klass.arguments.empty? + usage.to_s.gsub(/^#{name}/) do |match| + match << " " << klass.arguments.map(&:usage).compact.join(" ") + end + else + usage.to_s + end + + # Add required options + formatted << " #{required_options}" + + # Strip and go! + formatted.strip + end + + protected + + def not_debugging?(instance) + !(instance.class.respond_to?(:debugging) && instance.class.debugging) + end + + def required_options + @required_options ||= options.map { |_, o| o.usage if o.required? }.compact.sort.join(" ") + end + + # Given a target, checks if this class name is a public method. + def public_method?(instance) #:nodoc: + !(instance.public_methods & [name.to_s, name.to_sym]).empty? + end + + def private_method?(instance) + !(instance.private_methods & [name.to_s, name.to_sym]).empty? + end + + def local_method?(instance, name) + methods = instance.public_methods(false) + instance.private_methods(false) + instance.protected_methods(false) + !(methods & [name.to_s, name.to_sym]).empty? + end + + def sans_backtrace(backtrace, caller) #:nodoc: + saned = backtrace.reject { |frame| frame =~ FILE_REGEXP || (frame =~ /\.java:/ && RUBY_PLATFORM =~ /java/) || (frame =~ %r{^kernel/} && RUBY_ENGINE =~ /rbx/) } + saned - caller + end + + def handle_argument_error?(instance, error, caller) + not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin + saned = sans_backtrace(error.backtrace, caller) + # Ruby 1.9 always include the called method in the backtrace + saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9") + end + end + + def handle_no_method_error?(instance, error, caller) + not_debugging?(instance) && + error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/ + end + end + Task = Command + + # A command that is hidden in help messages but still invocable. + class HiddenCommand < Command + def hidden? + true + end + end + HiddenTask = HiddenCommand + + # A dynamic command that handles method missing scenarios. + class DynamicCommand < Command + def initialize(name, options = nil) + super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options) + end + + def run(instance, args = []) + if (instance.methods & [name.to_s, name.to_sym]).empty? + super + else + instance.class.handle_no_command_error(name) + end + end + end + DynamicTask = DynamicCommand +end diff --git a/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb b/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb new file mode 100644 index 00000000000000..c167aa33b86b84 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb @@ -0,0 +1,97 @@ +class Bundler::Thor + module CoreExt #:nodoc: + # A hash with indifferent access and magic predicates. + # + # hash = Bundler::Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true + # + # hash[:foo] #=> 'bar' + # hash['foo'] #=> 'bar' + # hash.foo? #=> true + # + class HashWithIndifferentAccess < ::Hash #:nodoc: + def initialize(hash = {}) + super() + hash.each do |key, value| + self[convert_key(key)] = value + end + end + + def [](key) + super(convert_key(key)) + end + + def []=(key, value) + super(convert_key(key), value) + end + + def delete(key) + super(convert_key(key)) + end + + def fetch(key, *args) + super(convert_key(key), *args) + end + + def key?(key) + super(convert_key(key)) + end + + def values_at(*indices) + indices.map { |key| self[convert_key(key)] } + end + + def merge(other) + dup.merge!(other) + end + + def merge!(other) + other.each do |key, value| + self[convert_key(key)] = value + end + self + end + + def reverse_merge(other) + self.class.new(other).merge(self) + end + + def reverse_merge!(other_hash) + replace(reverse_merge(other_hash)) + end + + def replace(other_hash) + super(other_hash) + end + + # Convert to a Hash with String keys. + def to_hash + Hash.new(default).merge!(self) + end + + protected + + def convert_key(key) + key.is_a?(Symbol) ? key.to_s : key + end + + # Magic predicates. For instance: + # + # options.force? # => !!options['force'] + # options.shebang # => "/usr/lib/local/ruby" + # options.test_framework?(:rspec) # => options[:test_framework] == :rspec + # + def method_missing(method, *args) + method = method.to_s + if method =~ /^(\w+)\?$/ + if args.empty? + !!self[$1] + else + self[$1] == args.first + end + else + self[method] + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb b/lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb new file mode 100644 index 00000000000000..0f6e2e0af2f657 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb @@ -0,0 +1,12 @@ +class IO #:nodoc: + class << self + unless method_defined? :binread + def binread(file, *args) + raise ArgumentError, "wrong number of arguments (#{1 + args.size} for 1..3)" unless args.size < 3 + File.open(file, "rb") do |f| + f.read(*args) + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb b/lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb new file mode 100644 index 00000000000000..76f1e43c65728a --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb @@ -0,0 +1,129 @@ +class Bundler::Thor + module CoreExt + class OrderedHash < ::Hash + if RUBY_VERSION < "1.9" + def initialize(*args, &block) + super + @keys = [] + end + + def initialize_copy(other) + super + # make a deep copy of keys + @keys = other.keys + end + + def []=(key, value) + @keys << key unless key?(key) + super + end + + def delete(key) + if key? key + index = @keys.index(key) + @keys.delete_at index + end + super + end + + def delete_if + super + sync_keys! + self + end + + alias_method :reject!, :delete_if + + def reject(&block) + dup.reject!(&block) + end + + def keys + @keys.dup + end + + def values + @keys.map { |key| self[key] } + end + + def to_hash + self + end + + def to_a + @keys.map { |key| [key, self[key]] } + end + + def each_key + return to_enum(:each_key) unless block_given? + @keys.each { |key| yield(key) } + self + end + + def each_value + return to_enum(:each_value) unless block_given? + @keys.each { |key| yield(self[key]) } + self + end + + def each + return to_enum(:each) unless block_given? + @keys.each { |key| yield([key, self[key]]) } + self + end + + def each_pair + return to_enum(:each_pair) unless block_given? + @keys.each { |key| yield(key, self[key]) } + self + end + + alias_method :select, :find_all + + def clear + super + @keys.clear + self + end + + def shift + k = @keys.first + v = delete(k) + [k, v] + end + + def merge!(other_hash) + if block_given? + other_hash.each { |k, v| self[k] = key?(k) ? yield(k, self[k], v) : v } + else + other_hash.each { |k, v| self[k] = v } + end + self + end + + alias_method :update, :merge! + + def merge(other_hash, &block) + dup.merge!(other_hash, &block) + end + + # When replacing with another hash, the initial order of our keys must come from the other hash -ordered or not. + def replace(other) + super + @keys = other.keys + self + end + + def inspect + "#<#{self.class} #{super}>" + end + + private + + def sync_keys! + @keys.delete_if { |k| !key?(k) } + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/error.rb b/lib/bundler/vendor/thor/lib/thor/error.rb new file mode 100644 index 00000000000000..2f816081f35c98 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/error.rb @@ -0,0 +1,32 @@ +class Bundler::Thor + # Bundler::Thor::Error is raised when it's caused by wrong usage of thor classes. Those + # errors have their backtrace suppressed and are nicely shown to the user. + # + # Errors that are caused by the developer, like declaring a method which + # overwrites a thor keyword, SHOULD NOT raise a Bundler::Thor::Error. This way, we + # ensure that developer errors are shown with full backtrace. + class Error < StandardError + end + + # Raised when a command was not found. + class UndefinedCommandError < Error + end + UndefinedTaskError = UndefinedCommandError + + class AmbiguousCommandError < Error + end + AmbiguousTaskError = AmbiguousCommandError + + # Raised when a command was found, but not invoked properly. + class InvocationError < Error + end + + class UnknownArgumentError < Error + end + + class RequiredArgumentMissingError < InvocationError + end + + class MalformattedArgumentError < InvocationError + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/group.rb b/lib/bundler/vendor/thor/lib/thor/group.rb new file mode 100644 index 00000000000000..05ddc10cd3072c --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/group.rb @@ -0,0 +1,281 @@ +require "bundler/vendor/thor/lib/thor/base" + +# Bundler::Thor has a special class called Bundler::Thor::Group. The main difference to Bundler::Thor class +# is that it invokes all commands at once. It also include some methods that allows +# invocations to be done at the class method, which are not available to Bundler::Thor +# commands. +class Bundler::Thor::Group + class << self + # The description for this Bundler::Thor::Group. If none is provided, but a source root + # exists, tries to find the USAGE one folder above it, otherwise searches + # in the superclass. + # + # ==== Parameters + # description:: The description for this Bundler::Thor::Group. + # + def desc(description = nil) + if description + @desc = description + else + @desc ||= from_superclass(:desc, nil) + end + end + + # Prints help information. + # + # ==== Options + # short:: When true, shows only usage. + # + def help(shell) + shell.say "Usage:" + shell.say " #{banner}\n" + shell.say + class_options_help(shell) + shell.say desc if desc + end + + # Stores invocations for this class merging with superclass values. + # + def invocations #:nodoc: + @invocations ||= from_superclass(:invocations, {}) + end + + # Stores invocation blocks used on invoke_from_option. + # + def invocation_blocks #:nodoc: + @invocation_blocks ||= from_superclass(:invocation_blocks, {}) + end + + # Invoke the given namespace or class given. It adds an instance + # method that will invoke the klass and command. You can give a block to + # configure how it will be invoked. + # + # The namespace/class given will have its options showed on the help + # usage. Check invoke_from_option for more information. + # + def invoke(*names, &block) + options = names.last.is_a?(Hash) ? names.pop : {} + verbose = options.fetch(:verbose, true) + + names.each do |name| + invocations[name] = false + invocation_blocks[name] = block if block_given? + + class_eval <<-METHOD, __FILE__, __LINE__ + def _invoke_#{name.to_s.gsub(/\W/, '_')} + klass, command = self.class.prepare_for_invocation(nil, #{name.inspect}) + + if klass + say_status :invoke, #{name.inspect}, #{verbose.inspect} + block = self.class.invocation_blocks[#{name.inspect}] + _invoke_for_class_method klass, command, &block + else + say_status :error, %(#{name.inspect} [not found]), :red + end + end + METHOD + end + end + + # Invoke a thor class based on the value supplied by the user to the + # given option named "name". A class option must be created before this + # method is invoked for each name given. + # + # ==== Examples + # + # class GemGenerator < Bundler::Thor::Group + # class_option :test_framework, :type => :string + # invoke_from_option :test_framework + # end + # + # ==== Boolean options + # + # In some cases, you want to invoke a thor class if some option is true or + # false. This is automatically handled by invoke_from_option. Then the + # option name is used to invoke the generator. + # + # ==== Preparing for invocation + # + # In some cases you want to customize how a specified hook is going to be + # invoked. You can do that by overwriting the class method + # prepare_for_invocation. The class method must necessarily return a klass + # and an optional command. + # + # ==== Custom invocations + # + # You can also supply a block to customize how the option is going to be + # invoked. The block receives two parameters, an instance of the current + # class and the klass to be invoked. + # + def invoke_from_option(*names, &block) + options = names.last.is_a?(Hash) ? names.pop : {} + verbose = options.fetch(:verbose, :white) + + names.each do |name| + unless class_options.key?(name) + raise ArgumentError, "You have to define the option #{name.inspect} " \ + "before setting invoke_from_option." + end + + invocations[name] = true + invocation_blocks[name] = block if block_given? + + class_eval <<-METHOD, __FILE__, __LINE__ + def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')} + return unless options[#{name.inspect}] + + value = options[#{name.inspect}] + value = #{name.inspect} if TrueClass === value + klass, command = self.class.prepare_for_invocation(#{name.inspect}, value) + + if klass + say_status :invoke, value, #{verbose.inspect} + block = self.class.invocation_blocks[#{name.inspect}] + _invoke_for_class_method klass, command, &block + else + say_status :error, %(\#{value} [not found]), :red + end + end + METHOD + end + end + + # Remove a previously added invocation. + # + # ==== Examples + # + # remove_invocation :test_framework + # + def remove_invocation(*names) + names.each do |name| + remove_command(name) + remove_class_option(name) + invocations.delete(name) + invocation_blocks.delete(name) + end + end + + # Overwrite class options help to allow invoked generators options to be + # shown recursively when invoking a generator. + # + def class_options_help(shell, groups = {}) #:nodoc: + get_options_from_invocations(groups, class_options) do |klass| + klass.send(:get_options_from_invocations, groups, class_options) + end + super(shell, groups) + end + + # Get invocations array and merge options from invocations. Those + # options are added to group_options hash. Options that already exists + # in base_options are not added twice. + # + def get_options_from_invocations(group_options, base_options) #:nodoc: # rubocop:disable MethodLength + invocations.each do |name, from_option| + value = if from_option + option = class_options[name] + option.type == :boolean ? name : option.default + else + name + end + next unless value + + klass, _ = prepare_for_invocation(name, value) + next unless klass && klass.respond_to?(:class_options) + + value = value.to_s + human_name = value.respond_to?(:classify) ? value.classify : value + + group_options[human_name] ||= [] + group_options[human_name] += klass.class_options.values.select do |class_option| + base_options[class_option.name.to_sym].nil? && class_option.group.nil? && + !group_options.values.flatten.any? { |i| i.name == class_option.name } + end + + yield klass if block_given? + end + end + + # Returns commands ready to be printed. + def printable_commands(*) + item = [] + item << banner + item << (desc ? "# #{desc.gsub(/\s+/m, ' ')}" : "") + [item] + end + alias_method :printable_tasks, :printable_commands + + def handle_argument_error(command, error, _args, arity) #:nodoc: + msg = "#{basename} #{command.name} takes #{arity} argument".dup + msg << "s" if arity > 1 + msg << ", but it should not." + raise error, msg + end + + protected + + # The method responsible for dispatching given the args. + def dispatch(command, given_args, given_opts, config) #:nodoc: + if Bundler::Thor::HELP_MAPPINGS.include?(given_args.first) + help(config[:shell]) + return + end + + args, opts = Bundler::Thor::Options.split(given_args) + opts = given_opts || opts + + instance = new(args, opts, config) + yield instance if block_given? + + if command + instance.invoke_command(all_commands[command]) + else + instance.invoke_all + end + end + + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Bundler::Thor::Runner. + def banner + "#{basename} #{self_command.formatted_usage(self, false)}" + end + + # Represents the whole class as a command. + def self_command #:nodoc: + Bundler::Thor::DynamicCommand.new(namespace, class_options) + end + alias_method :self_task, :self_command + + def baseclass #:nodoc: + Bundler::Thor::Group + end + + def create_command(meth) #:nodoc: + commands[meth.to_s] = Bundler::Thor::Command.new(meth, nil, nil, nil, nil) + true + end + alias_method :create_task, :create_command + end + + include Bundler::Thor::Base + +protected + + # Shortcut to invoke with padding and block handling. Use internally by + # invoke and invoke_from_option class methods. + def _invoke_for_class_method(klass, command = nil, *args, &block) #:nodoc: + with_padding do + if block + case block.arity + when 3 + yield(self, klass, command) + when 2 + yield(self, klass) + when 1 + instance_exec(klass, &block) + end + else + invoke klass, command, *args + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/invocation.rb b/lib/bundler/vendor/thor/lib/thor/invocation.rb new file mode 100644 index 00000000000000..866d2212a7f8cb --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/invocation.rb @@ -0,0 +1,177 @@ +class Bundler::Thor + module Invocation + def self.included(base) #:nodoc: + base.extend ClassMethods + end + + module ClassMethods + # This method is responsible for receiving a name and find the proper + # class and command for it. The key is an optional parameter which is + # available only in class methods invocations (i.e. in Bundler::Thor::Group). + def prepare_for_invocation(key, name) #:nodoc: + case name + when Symbol, String + Bundler::Thor::Util.find_class_and_command_by_namespace(name.to_s, !key) + else + name + end + end + end + + # Make initializer aware of invocations and the initialization args. + def initialize(args = [], options = {}, config = {}, &block) #:nodoc: + @_invocations = config[:invocations] || Hash.new { |h, k| h[k] = [] } + @_initializer = [args, options, config] + super + end + + # Make the current command chain accessible with in a Bundler::Thor-(sub)command + def current_command_chain + @_invocations.values.flatten.map(&:to_sym) + end + + # Receives a name and invokes it. The name can be a string (either "command" or + # "namespace:command"), a Bundler::Thor::Command, a Class or a Bundler::Thor instance. If the + # command cannot be guessed by name, it can also be supplied as second argument. + # + # You can also supply the arguments, options and configuration values for + # the command to be invoked, if none is given, the same values used to + # initialize the invoker are used to initialize the invoked. + # + # When no name is given, it will invoke the default command of the current class. + # + # ==== Examples + # + # class A < Bundler::Thor + # def foo + # invoke :bar + # invoke "b:hello", ["Erik"] + # end + # + # def bar + # invoke "b:hello", ["Erik"] + # end + # end + # + # class B < Bundler::Thor + # def hello(name) + # puts "hello #{name}" + # end + # end + # + # You can notice that the method "foo" above invokes two commands: "bar", + # which belongs to the same class and "hello" which belongs to the class B. + # + # By using an invocation system you ensure that a command is invoked only once. + # In the example above, invoking "foo" will invoke "b:hello" just once, even + # if it's invoked later by "bar" method. + # + # When class A invokes class B, all arguments used on A initialization are + # supplied to B. This allows lazy parse of options. Let's suppose you have + # some rspec commands: + # + # class Rspec < Bundler::Thor::Group + # class_option :mock_framework, :type => :string, :default => :rr + # + # def invoke_mock_framework + # invoke "rspec:#{options[:mock_framework]}" + # end + # end + # + # As you noticed, it invokes the given mock framework, which might have its + # own options: + # + # class Rspec::RR < Bundler::Thor::Group + # class_option :style, :type => :string, :default => :mock + # end + # + # Since it's not rspec concern to parse mock framework options, when RR + # is invoked all options are parsed again, so RR can extract only the options + # that it's going to use. + # + # If you want Rspec::RR to be initialized with its own set of options, you + # have to do that explicitly: + # + # invoke "rspec:rr", [], :style => :foo + # + # Besides giving an instance, you can also give a class to invoke: + # + # invoke Rspec::RR, [], :style => :foo + # + def invoke(name = nil, *args) + if name.nil? + warn "[Bundler::Thor] Calling invoke() without argument is deprecated. Please use invoke_all instead.\n#{caller.join("\n")}" + return invoke_all + end + + args.unshift(nil) if args.first.is_a?(Array) || args.first.nil? + command, args, opts, config = args + + klass, command = _retrieve_class_and_command(name, command) + raise "Missing Bundler::Thor class for invoke #{name}" unless klass + raise "Expected Bundler::Thor class, got #{klass}" unless klass <= Bundler::Thor::Base + + args, opts, config = _parse_initialization_options(args, opts, config) + klass.send(:dispatch, command, args, opts, config) do |instance| + instance.parent_options = options + end + end + + # Invoke the given command if the given args. + def invoke_command(command, *args) #:nodoc: + current = @_invocations[self.class] + + unless current.include?(command.name) + current << command.name + command.run(self, *args) + end + end + alias_method :invoke_task, :invoke_command + + # Invoke all commands for the current instance. + def invoke_all #:nodoc: + self.class.all_commands.map { |_, command| invoke_command(command) } + end + + # Invokes using shell padding. + def invoke_with_padding(*args) + with_padding { invoke(*args) } + end + + protected + + # Configuration values that are shared between invocations. + def _shared_configuration #:nodoc: + {:invocations => @_invocations} + end + + # This method simply retrieves the class and command to be invoked. + # If the name is nil or the given name is a command in the current class, + # use the given name and return self as class. Otherwise, call + # prepare_for_invocation in the current class. + def _retrieve_class_and_command(name, sent_command = nil) #:nodoc: + if name.nil? + [self.class, nil] + elsif self.class.all_commands[name.to_s] + [self.class, name.to_s] + else + klass, command = self.class.prepare_for_invocation(nil, name) + [klass, command || sent_command] + end + end + alias_method :_retrieve_class_and_task, :_retrieve_class_and_command + + # Initialize klass using values stored in the @_initializer. + def _parse_initialization_options(args, opts, config) #:nodoc: + stored_args, stored_opts, stored_config = @_initializer + + args ||= stored_args.dup + opts ||= stored_opts.dup + + config ||= {} + config = stored_config.merge(_shared_configuration).merge!(config) + + [args, opts, config] + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor.rb b/lib/bundler/vendor/thor/lib/thor/line_editor.rb new file mode 100644 index 00000000000000..ce81a17484d054 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/line_editor.rb @@ -0,0 +1,17 @@ +require "bundler/vendor/thor/lib/thor/line_editor/basic" +require "bundler/vendor/thor/lib/thor/line_editor/readline" + +class Bundler::Thor + module LineEditor + def self.readline(prompt, options = {}) + best_available.new(prompt, options).readline + end + + def self.best_available + [ + Bundler::Thor::LineEditor::Readline, + Bundler::Thor::LineEditor::Basic + ].detect(&:available?) + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb b/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb new file mode 100644 index 00000000000000..0adb2b3137f817 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb @@ -0,0 +1,37 @@ +class Bundler::Thor + module LineEditor + class Basic + attr_reader :prompt, :options + + def self.available? + true + end + + def initialize(prompt, options) + @prompt = prompt + @options = options + end + + def readline + $stdout.print(prompt) + get_input + end + + private + + def get_input + if echo? + $stdin.gets + else + # Lazy-load io/console since it is gem-ified as of 2.3 + require "io/console" if RUBY_VERSION > "1.9.2" + $stdin.noecho(&:gets) + end + end + + def echo? + options.fetch(:echo, true) + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb b/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb new file mode 100644 index 00000000000000..dd39cff35dce2d --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb @@ -0,0 +1,88 @@ +begin + require "readline" +rescue LoadError +end + +class Bundler::Thor + module LineEditor + class Readline < Basic + def self.available? + Object.const_defined?(:Readline) + end + + def readline + if echo? + ::Readline.completion_append_character = nil + # Ruby 1.8.7 does not allow Readline.completion_proc= to receive nil. + if complete = completion_proc + ::Readline.completion_proc = complete + end + ::Readline.readline(prompt, add_to_history?) + else + super + end + end + + private + + def add_to_history? + options.fetch(:add_to_history, true) + end + + def completion_proc + if use_path_completion? + proc { |text| PathCompletion.new(text).matches } + elsif completion_options.any? + proc do |text| + completion_options.select { |option| option.start_with?(text) } + end + end + end + + def completion_options + options.fetch(:limited_to, []) + end + + def use_path_completion? + options.fetch(:path, false) + end + + class PathCompletion + attr_reader :text + private :text + + def initialize(text) + @text = text + end + + def matches + relative_matches + end + + private + + def relative_matches + absolute_matches.map { |path| path.sub(base_path, "") } + end + + def absolute_matches + Dir[glob_pattern].map do |path| + if File.directory?(path) + "#{path}/" + else + path + end + end + end + + def glob_pattern + "#{base_path}#{text}*" + end + + def base_path + "#{Dir.pwd}/" + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/parser.rb b/lib/bundler/vendor/thor/lib/thor/parser.rb new file mode 100644 index 00000000000000..08f80e565def83 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/parser.rb @@ -0,0 +1,4 @@ +require "bundler/vendor/thor/lib/thor/parser/argument" +require "bundler/vendor/thor/lib/thor/parser/arguments" +require "bundler/vendor/thor/lib/thor/parser/option" +require "bundler/vendor/thor/lib/thor/parser/options" diff --git a/lib/bundler/vendor/thor/lib/thor/parser/argument.rb b/lib/bundler/vendor/thor/lib/thor/parser/argument.rb new file mode 100644 index 00000000000000..dfe7398583556a --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/parser/argument.rb @@ -0,0 +1,70 @@ +class Bundler::Thor + class Argument #:nodoc: + VALID_TYPES = [:numeric, :hash, :array, :string] + + attr_reader :name, :description, :enum, :required, :type, :default, :banner + alias_method :human_name, :name + + def initialize(name, options = {}) + class_name = self.class.name.split("::").last + + type = options[:type] + + raise ArgumentError, "#{class_name} name can't be nil." if name.nil? + raise ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type) + + @name = name.to_s + @description = options[:desc] + @required = options.key?(:required) ? options[:required] : true + @type = (type || :string).to_sym + @default = options[:default] + @banner = options[:banner] || default_banner + @enum = options[:enum] + + validate! # Trigger specific validations + end + + def usage + required? ? banner : "[#{banner}]" + end + + def required? + required + end + + def show_default? + case default + when Array, String, Hash + !default.empty? + else + default + end + end + + protected + + def validate! + raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil? + raise ArgumentError, "An argument cannot have an enum other than an array." if @enum && !@enum.is_a?(Array) + end + + def valid_type?(type) + self.class::VALID_TYPES.include?(type.to_sym) + end + + def default_banner + case type + when :boolean + nil + when :string, :default + human_name.upcase + when :numeric + "N" + when :hash + "key:value" + when :array + "one two three" + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb b/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb new file mode 100644 index 00000000000000..1fd790f4b73112 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb @@ -0,0 +1,175 @@ +class Bundler::Thor + class Arguments #:nodoc: # rubocop:disable ClassLength + NUMERIC = /[-+]?(\d*\.\d+|\d+)/ + + # Receives an array of args and returns two arrays, one with arguments + # and one with switches. + # + def self.split(args) + arguments = [] + + args.each do |item| + break if item =~ /^-/ + arguments << item + end + + [arguments, args[Range.new(arguments.size, -1)]] + end + + def self.parse(*args) + to_parse = args.pop + new(*args).parse(to_parse) + end + + # Takes an array of Bundler::Thor::Argument objects. + # + def initialize(arguments = []) + @assigns = {} + @non_assigned_required = [] + @switches = arguments + + arguments.each do |argument| + if !argument.default.nil? + @assigns[argument.human_name] = argument.default + elsif argument.required? + @non_assigned_required << argument + end + end + end + + def parse(args) + @pile = args.dup + + @switches.each do |argument| + break unless peek + @non_assigned_required.delete(argument) + @assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name) + end + + check_requirement! + @assigns + end + + def remaining + @pile + end + + private + + def no_or_skip?(arg) + arg =~ /^--(no|skip)-([-\w]+)$/ + $2 + end + + def last? + @pile.empty? + end + + def peek + @pile.first + end + + def shift + @pile.shift + end + + def unshift(arg) + if arg.is_a?(Array) + @pile = arg + @pile + else + @pile.unshift(arg) + end + end + + def current_is_value? + peek && peek.to_s !~ /^-/ + end + + # Runs through the argument array getting strings that contains ":" and + # mark it as a hash: + # + # [ "name:string", "age:integer" ] + # + # Becomes: + # + # { "name" => "string", "age" => "integer" } + # + def parse_hash(name) + return shift if peek.is_a?(Hash) + hash = {} + + while current_is_value? && peek.include?(":") + key, value = shift.split(":", 2) + raise MalformattedArgumentError, "You can't specify '#{key}' more than once in option '#{name}'; got #{key}:#{hash[key]} and #{key}:#{value}" if hash.include? key + hash[key] = value + end + hash + end + + # Runs through the argument array getting all strings until no string is + # found or a switch is found. + # + # ["a", "b", "c"] + # + # And returns it as an array: + # + # ["a", "b", "c"] + # + def parse_array(name) + return shift if peek.is_a?(Array) + array = [] + array << shift while current_is_value? + array + end + + # Check if the peek is numeric format and return a Float or Integer. + # Check if the peek is included in enum if enum is provided. + # Otherwise raises an error. + # + def parse_numeric(name) + return shift if peek.is_a?(Numeric) + + unless peek =~ NUMERIC && $& == peek + raise MalformattedArgumentError, "Expected numeric value for '#{name}'; got #{peek.inspect}" + end + + value = $&.index(".") ? shift.to_f : shift.to_i + if @switches.is_a?(Hash) && switch = @switches[name] + if switch.enum && !switch.enum.include?(value) + raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}" + end + end + value + end + + # Parse string: + # for --string-arg, just return the current value in the pile + # for --no-string-arg, nil + # Check if the peek is included in enum if enum is provided. Otherwise raises an error. + # + def parse_string(name) + if no_or_skip?(name) + nil + else + value = shift + if @switches.is_a?(Hash) && switch = @switches[name] + if switch.enum && !switch.enum.include?(value) + raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}" + end + end + value + end + end + + # Raises an error if @non_assigned_required array is not empty. + # + def check_requirement! + return if @non_assigned_required.empty? + names = @non_assigned_required.map do |o| + o.respond_to?(:switch_name) ? o.switch_name : o.human_name + end.join("', '") + class_name = self.class.name.split("::").last.downcase + raise RequiredArgumentMissingError, "No value provided for required #{class_name} '#{names}'" + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/parser/option.rb b/lib/bundler/vendor/thor/lib/thor/parser/option.rb new file mode 100644 index 00000000000000..85169b56c8baff --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/parser/option.rb @@ -0,0 +1,146 @@ +class Bundler::Thor + class Option < Argument #:nodoc: + attr_reader :aliases, :group, :lazy_default, :hide + + VALID_TYPES = [:boolean, :numeric, :hash, :array, :string] + + def initialize(name, options = {}) + @check_default_type = options[:check_default_type] + options[:required] = false unless options.key?(:required) + super + @lazy_default = options[:lazy_default] + @group = options[:group].to_s.capitalize if options[:group] + @aliases = Array(options[:aliases]) + @hide = options[:hide] + end + + # This parse quick options given as method_options. It makes several + # assumptions, but you can be more specific using the option method. + # + # parse :foo => "bar" + # #=> Option foo with default value bar + # + # parse [:foo, :baz] => "bar" + # #=> Option foo with default value bar and alias :baz + # + # parse :foo => :required + # #=> Required option foo without default value + # + # parse :foo => 2 + # #=> Option foo with default value 2 and type numeric + # + # parse :foo => :numeric + # #=> Option foo without default value and type numeric + # + # parse :foo => true + # #=> Option foo with default value true and type boolean + # + # The valid types are :boolean, :numeric, :hash, :array and :string. If none + # is given a default type is assumed. This default type accepts arguments as + # string (--foo=value) or booleans (just --foo). + # + # By default all options are optional, unless :required is given. + # + def self.parse(key, value) + if key.is_a?(Array) + name, *aliases = key + else + name = key + aliases = [] + end + + name = name.to_s + default = value + + type = case value + when Symbol + default = nil + if VALID_TYPES.include?(value) + value + elsif required = (value == :required) # rubocop:disable AssignmentInCondition + :string + end + when TrueClass, FalseClass + :boolean + when Numeric + :numeric + when Hash, Array, String + value.class.name.downcase.to_sym + end + + new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases) + end + + def switch_name + @switch_name ||= dasherized? ? name : dasherize(name) + end + + def human_name + @human_name ||= dasherized? ? undasherize(name) : name + end + + def usage(padding = 0) + sample = if banner && !banner.to_s.empty? + "#{switch_name}=#{banner}".dup + else + switch_name + end + + sample = "[#{sample}]".dup unless required? + + if boolean? + sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-") + end + + if aliases.empty? + (" " * padding) << sample + else + "#{aliases.join(', ')}, #{sample}" + end + end + + VALID_TYPES.each do |type| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{type}? + self.type == #{type.inspect} + end + RUBY + end + + protected + + def validate! + raise ArgumentError, "An option cannot be boolean and required." if boolean? && required? + validate_default_type! if @check_default_type + end + + def validate_default_type! + default_type = case @default + when nil + return + when TrueClass, FalseClass + required? ? :string : :boolean + when Numeric + :numeric + when Symbol + :string + when Hash, Array, String + @default.class.name.downcase.to_sym + end + + raise ArgumentError, "Expected #{@type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" unless default_type == @type + end + + def dasherized? + name.index("-") == 0 + end + + def undasherize(str) + str.sub(/^-{1,2}/, "") + end + + def dasherize(str) + (str.length > 1 ? "--" : "-") + str.tr("_", "-") + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/parser/options.rb b/lib/bundler/vendor/thor/lib/thor/parser/options.rb new file mode 100644 index 00000000000000..70f6366842c691 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/parser/options.rb @@ -0,0 +1,221 @@ +class Bundler::Thor + class Options < Arguments #:nodoc: # rubocop:disable ClassLength + LONG_RE = /^(--\w+(?:-\w+)*)$/ + SHORT_RE = /^(-[a-z])$/i + EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i + SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args + SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i + OPTS_END = "--".freeze + + # Receives a hash and makes it switches. + def self.to_switches(options) + options.map do |key, value| + case value + when true + "--#{key}" + when Array + "--#{key} #{value.map(&:inspect).join(' ')}" + when Hash + "--#{key} #{value.map { |k, v| "#{k}:#{v}" }.join(' ')}" + when nil, false + nil + else + "--#{key} #{value.inspect}" + end + end.compact.join(" ") + end + + # Takes a hash of Bundler::Thor::Option and a hash with defaults. + # + # If +stop_on_unknown+ is true, #parse will stop as soon as it encounters + # an unknown option or a regular argument. + def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false) + @stop_on_unknown = stop_on_unknown + @disable_required_check = disable_required_check + options = hash_options.values + super(options) + + # Add defaults + defaults.each do |key, value| + @assigns[key.to_s] = value + @non_assigned_required.delete(hash_options[key]) + end + + @shorts = {} + @switches = {} + @extra = [] + + options.each do |option| + @switches[option.switch_name] = option + + option.aliases.each do |short| + name = short.to_s.sub(/^(?!\-)/, "-") + @shorts[name] ||= option.switch_name + end + end + end + + def remaining + @extra + end + + def peek + return super unless @parsing_options + + result = super + if result == OPTS_END + shift + @parsing_options = false + super + else + result + end + end + + def parse(args) # rubocop:disable MethodLength + @pile = args.dup + @parsing_options = true + + while peek + if parsing_options? + match, is_switch = current_is_switch? + shifted = shift + + if is_switch + case shifted + when SHORT_SQ_RE + unshift($1.split("").map { |f| "-#{f}" }) + next + when EQ_RE, SHORT_NUM + unshift($2) + switch = $1 + when LONG_RE, SHORT_RE + switch = $1 + end + + switch = normalize_switch(switch) + option = switch_option(switch) + @assigns[option.human_name] = parse_peek(switch, option) + elsif @stop_on_unknown + @parsing_options = false + @extra << shifted + @extra << shift while peek + break + elsif match + @extra << shifted + @extra << shift while peek && peek !~ /^-/ + else + @extra << shifted + end + else + @extra << shift + end + end + + check_requirement! unless @disable_required_check + + assigns = Bundler::Thor::CoreExt::HashWithIndifferentAccess.new(@assigns) + assigns.freeze + assigns + end + + def check_unknown! + # an unknown option starts with - or -- and has no more --'s afterward. + unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ } + raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty? + end + + protected + + # Check if the current value in peek is a registered switch. + # + # Two booleans are returned. The first is true if the current value + # starts with a hyphen; the second is true if it is a registered switch. + def current_is_switch? + case peek + when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM + [true, switch?($1)] + when SHORT_SQ_RE + [true, $1.split("").any? { |f| switch?("-#{f}") }] + else + [false, false] + end + end + + def current_is_switch_formatted? + case peek + when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE + true + else + false + end + end + + def current_is_value? + peek && (!parsing_options? || super) + end + + def switch?(arg) + switch_option(normalize_switch(arg)) + end + + def switch_option(arg) + if match = no_or_skip?(arg) # rubocop:disable AssignmentInCondition + @switches[arg] || @switches["--#{match}"] + else + @switches[arg] + end + end + + # Check if the given argument is actually a shortcut. + # + def normalize_switch(arg) + (@shorts[arg] || arg).tr("_", "-") + end + + def parsing_options? + peek + @parsing_options + end + + # Parse boolean values which can be given as --foo=true, --foo or --no-foo. + # + def parse_boolean(switch) + if current_is_value? + if ["true", "TRUE", "t", "T", true].include?(peek) + shift + true + elsif ["false", "FALSE", "f", "F", false].include?(peek) + shift + false + else + !no_or_skip?(switch) + end + else + @switches.key?(switch) || !no_or_skip?(switch) + end + end + + # Parse the value at the peek analyzing if it requires an input or not. + # + def parse_peek(switch, option) + if parsing_options? && (current_is_switch_formatted? || last?) + if option.boolean? + # No problem for boolean types + elsif no_or_skip?(switch) + return nil # User set value to nil + elsif option.string? && !option.required? + # Return the default if there is one, else the human name + return option.lazy_default || option.default || option.human_name + elsif option.lazy_default + return option.lazy_default + else + raise MalformattedArgumentError, "No value provided for option '#{switch}'" + end + end + + @non_assigned_required.delete(option) + send(:"parse_#{option.type}", switch) + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/rake_compat.rb b/lib/bundler/vendor/thor/lib/thor/rake_compat.rb new file mode 100644 index 00000000000000..60282e29914a67 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/rake_compat.rb @@ -0,0 +1,71 @@ +require "rake" +require "rake/dsl_definition" + +class Bundler::Thor + # Adds a compatibility layer to your Bundler::Thor classes which allows you to use + # rake package tasks. For example, to use rspec rake tasks, one can do: + # + # require 'bundler/vendor/thor/lib/thor/rake_compat' + # require 'rspec/core/rake_task' + # + # class Default < Bundler::Thor + # include Bundler::Thor::RakeCompat + # + # RSpec::Core::RakeTask.new(:spec) do |t| + # t.spec_opts = ['--options', './.rspec'] + # t.spec_files = FileList['spec/**/*_spec.rb'] + # end + # end + # + module RakeCompat + include Rake::DSL if defined?(Rake::DSL) + + def self.rake_classes + @rake_classes ||= [] + end + + def self.included(base) + # Hack. Make rakefile point to invoker, so rdoc task is generated properly. + rakefile = File.basename(caller[0].match(/(.*):\d+/)[1]) + Rake.application.instance_variable_set(:@rakefile, rakefile) + rake_classes << base + end + end +end + +# override task on (main), for compatibility with Rake 0.9 +instance_eval do + alias rake_namespace namespace + + def task(*) + task = super + + if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition + non_namespaced_name = task.name.split(":").last + + description = non_namespaced_name + description << task.arg_names.map { |n| n.to_s.upcase }.join(" ") + description.strip! + + klass.desc description, Rake.application.last_description || non_namespaced_name + Rake.application.last_description = nil + klass.send :define_method, non_namespaced_name do |*args| + Rake::Task[task.name.to_sym].invoke(*args) + end + end + + task + end + + def namespace(name) + if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition + const_name = Bundler::Thor::Util.camel_case(name.to_s).to_sym + klass.const_set(const_name, Class.new(Bundler::Thor)) + new_klass = klass.const_get(const_name) + Bundler::Thor::RakeCompat.rake_classes << new_klass + end + + super + Bundler::Thor::RakeCompat.rake_classes.pop + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/runner.rb b/lib/bundler/vendor/thor/lib/thor/runner.rb new file mode 100644 index 00000000000000..b110b8d4781db1 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/runner.rb @@ -0,0 +1,324 @@ +require "bundler/vendor/thor/lib/thor" +require "bundler/vendor/thor/lib/thor/group" +require "bundler/vendor/thor/lib/thor/core_ext/io_binary_read" + +require "yaml" +require "digest" +require "pathname" + +class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLength + map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version + + def self.banner(command, all = false, subcommand = false) + "thor " + command.formatted_usage(self, all, subcommand) + end + + def self.exit_on_failure? + true + end + + # Override Bundler::Thor#help so it can give information about any class and any method. + # + def help(meth = nil) + if meth && !respond_to?(meth) + initialize_thorfiles(meth) + klass, command = Bundler::Thor::Util.find_class_and_command_by_namespace(meth) + self.class.handle_no_command_error(command, false) if klass.nil? + klass.start(["-h", command].compact, :shell => shell) + else + super + end + end + + # If a command is not found on Bundler::Thor::Runner, method missing is invoked and + # Bundler::Thor::Runner is then responsible for finding the command in all classes. + # + def method_missing(meth, *args) + meth = meth.to_s + initialize_thorfiles(meth) + klass, command = Bundler::Thor::Util.find_class_and_command_by_namespace(meth) + self.class.handle_no_command_error(command, false) if klass.nil? + args.unshift(command) if command + klass.start(args, :shell => shell) + end + + desc "install NAME", "Install an optionally named Bundler::Thor file into your system commands" + method_options :as => :string, :relative => :boolean, :force => :boolean + def install(name) # rubocop:disable MethodLength + initialize_thorfiles + + # If a directory name is provided as the argument, look for a 'main.thor' + # command in said directory. + begin + if File.directory?(File.expand_path(name)) + base = File.join(name, "main.thor") + package = :directory + contents = open(base, &:read) + else + base = name + package = :file + contents = open(name, &:read) + end + rescue OpenURI::HTTPError + raise Error, "Error opening URI '#{name}'" + rescue Errno::ENOENT + raise Error, "Error opening file '#{name}'" + end + + say "Your Bundler::Thorfile contains:" + say contents + + unless options["force"] + return false if no?("Do you wish to continue [y/N]?") + end + + as = options["as"] || begin + first_line = contents.split("\n")[0] + (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil + end + + unless as + basename = File.basename(name) + as = ask("Please specify a name for #{name} in the system repository [#{basename}]:") + as = basename if as.empty? + end + + location = if options[:relative] || name =~ %r{^https?://} + name + else + File.expand_path(name) + end + + thor_yaml[as] = { + :filename => Digest(:MD5).hexdigest(name + as), + :location => location, + :namespaces => Bundler::Thor::Util.namespaces_in_content(contents, base) + } + + save_yaml(thor_yaml) + say "Storing thor file in your system repository" + destination = File.join(thor_root, thor_yaml[as][:filename]) + + if package == :file + File.open(destination, "w") { |f| f.puts contents } + else + require "fileutils" + FileUtils.cp_r(name, destination) + end + + thor_yaml[as][:filename] # Indicate success + end + + desc "version", "Show Bundler::Thor version" + def version + require "bundler/vendor/thor/lib/thor/version" + say "Bundler::Thor #{Bundler::Thor::VERSION}" + end + + desc "uninstall NAME", "Uninstall a named Bundler::Thor module" + def uninstall(name) + raise Error, "Can't find module '#{name}'" unless thor_yaml[name] + say "Uninstalling #{name}." + require "fileutils" + FileUtils.rm_rf(File.join(thor_root, (thor_yaml[name][:filename]).to_s)) + + thor_yaml.delete(name) + save_yaml(thor_yaml) + + puts "Done." + end + + desc "update NAME", "Update a Bundler::Thor file from its original location" + def update(name) + raise Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location] + + say "Updating '#{name}' from #{thor_yaml[name][:location]}" + + old_filename = thor_yaml[name][:filename] + self.options = options.merge("as" => name) + + if File.directory? File.expand_path(name) + require "fileutils" + FileUtils.rm_rf(File.join(thor_root, old_filename)) + + thor_yaml.delete(old_filename) + save_yaml(thor_yaml) + + filename = install(name) + else + filename = install(thor_yaml[name][:location]) + end + + File.delete(File.join(thor_root, old_filename)) unless filename == old_filename + end + + desc "installed", "List the installed Bundler::Thor modules and commands" + method_options :internal => :boolean + def installed + initialize_thorfiles(nil, true) + display_klasses(true, options["internal"]) + end + + desc "list [SEARCH]", "List the available thor commands (--substring means .*SEARCH)" + method_options :substring => :boolean, :group => :string, :all => :boolean, :debug => :boolean + def list(search = "") + initialize_thorfiles + + search = ".*#{search}" if options["substring"] + search = /^#{search}.*/i + group = options[:group] || "standard" + + klasses = Bundler::Thor::Base.subclasses.select do |k| + (options[:all] || k.group == group) && k.namespace =~ search + end + + display_klasses(false, false, klasses) + end + +private + + def thor_root + Bundler::Thor::Util.thor_root + end + + def thor_yaml + @thor_yaml ||= begin + yaml_file = File.join(thor_root, "thor.yml") + yaml = YAML.load_file(yaml_file) if File.exist?(yaml_file) + yaml || {} + end + end + + # Save the yaml file. If none exists in thor root, creates one. + # + def save_yaml(yaml) + yaml_file = File.join(thor_root, "thor.yml") + + unless File.exist?(yaml_file) + require "fileutils" + FileUtils.mkdir_p(thor_root) + yaml_file = File.join(thor_root, "thor.yml") + FileUtils.touch(yaml_file) + end + + File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml } + end + + # Load the Bundler::Thorfiles. If relevant_to is supplied, looks for specific files + # in the thor_root instead of loading them all. + # + # By default, it also traverses the current path until find Bundler::Thor files, as + # described in thorfiles. This look up can be skipped by supplying + # skip_lookup true. + # + def initialize_thorfiles(relevant_to = nil, skip_lookup = false) + thorfiles(relevant_to, skip_lookup).each do |f| + Bundler::Thor::Util.load_thorfile(f, nil, options[:debug]) unless Bundler::Thor::Base.subclass_files.keys.include?(File.expand_path(f)) + end + end + + # Finds Bundler::Thorfiles by traversing from your current directory down to the root + # directory of your system. If at any time we find a Bundler::Thor file, we stop. + # + # We also ensure that system-wide Bundler::Thorfiles are loaded first, so local + # Bundler::Thorfiles can override them. + # + # ==== Example + # + # If we start at /Users/wycats/dev/thor ... + # + # 1. /Users/wycats/dev/thor + # 2. /Users/wycats/dev + # 3. /Users/wycats <-- we find a Bundler::Thorfile here, so we stop + # + # Suppose we start at c:\Documents and Settings\james\dev\thor ... + # + # 1. c:\Documents and Settings\james\dev\thor + # 2. c:\Documents and Settings\james\dev + # 3. c:\Documents and Settings\james + # 4. c:\Documents and Settings + # 5. c:\ <-- no Bundler::Thorfiles found! + # + def thorfiles(relevant_to = nil, skip_lookup = false) + thorfiles = [] + + unless skip_lookup + Pathname.pwd.ascend do |path| + thorfiles = Bundler::Thor::Util.globs_for(path).map { |g| Dir[g] }.flatten + break unless thorfiles.empty? + end + end + + files = (relevant_to ? thorfiles_relevant_to(relevant_to) : Bundler::Thor::Util.thor_root_glob) + files += thorfiles + files -= ["#{thor_root}/thor.yml"] + + files.map! do |file| + File.directory?(file) ? File.join(file, "main.thor") : file + end + end + + # Load Bundler::Thorfiles relevant to the given method. If you provide "foo:bar" it + # will load all thor files in the thor.yaml that has "foo" e "foo:bar" + # namespaces registered. + # + def thorfiles_relevant_to(meth) + lookup = [meth, meth.split(":")[0...-1].join(":")] + + files = thor_yaml.select do |_, v| + v[:namespaces] && !(v[:namespaces] & lookup).empty? + end + + files.map { |_, v| File.join(thor_root, (v[:filename]).to_s) } + end + + # Display information about the given klasses. If with_module is given, + # it shows a table with information extracted from the yaml file. + # + def display_klasses(with_modules = false, show_internal = false, klasses = Bundler::Thor::Base.subclasses) + klasses -= [Bundler::Thor, Bundler::Thor::Runner, Bundler::Thor::Group] unless show_internal + + raise Error, "No Bundler::Thor commands available" if klasses.empty? + show_modules if with_modules && !thor_yaml.empty? + + list = Hash.new { |h, k| h[k] = [] } + groups = klasses.select { |k| k.ancestors.include?(Bundler::Thor::Group) } + + # Get classes which inherit from Bundler::Thor + (klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_commands(false) } + + # Get classes which inherit from Bundler::Thor::Base + groups.map! { |k| k.printable_commands(false).first } + list["root"] = groups + + # Order namespaces with default coming first + list = list.sort { |a, b| a[0].sub(/^default/, "") <=> b[0].sub(/^default/, "") } + list.each { |n, commands| display_commands(n, commands) unless commands.empty? } + end + + def display_commands(namespace, list) #:nodoc: + list.sort! { |a, b| a[0] <=> b[0] } + + say shell.set_color(namespace, :blue, true) + say "-" * namespace.size + + print_table(list, :truncate => true) + say + end + alias_method :display_tasks, :display_commands + + def show_modules #:nodoc: + info = [] + labels = %w(Modules Namespaces) + + info << labels + info << ["-" * labels[0].size, "-" * labels[1].size] + + thor_yaml.each do |name, hash| + info << [name, hash[:namespaces].join(", ")] + end + + print_table info + say "" + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/shell.rb b/lib/bundler/vendor/thor/lib/thor/shell.rb new file mode 100644 index 00000000000000..e9455493242690 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell.rb @@ -0,0 +1,81 @@ +require "rbconfig" + +class Bundler::Thor + module Base + class << self + attr_writer :shell + + # Returns the shell used in all Bundler::Thor classes. If you are in a Unix platform + # it will use a colored log, otherwise it will use a basic one without color. + # + def shell + @shell ||= if ENV["THOR_SHELL"] && !ENV["THOR_SHELL"].empty? + Bundler::Thor::Shell.const_get(ENV["THOR_SHELL"]) + elsif RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ && !ENV["ANSICON"] + Bundler::Thor::Shell::Basic + else + Bundler::Thor::Shell::Color + end + end + end + end + + module Shell + SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_status, :print_in_columns, :print_table, :print_wrapped, :file_collision, :terminal_width] + attr_writer :shell + + autoload :Basic, "bundler/vendor/thor/lib/thor/shell/basic" + autoload :Color, "bundler/vendor/thor/lib/thor/shell/color" + autoload :HTML, "bundler/vendor/thor/lib/thor/shell/html" + + # Add shell to initialize config values. + # + # ==== Configuration + # shell:: An instance of the shell to be used. + # + # ==== Examples + # + # class MyScript < Bundler::Thor + # argument :first, :type => :numeric + # end + # + # MyScript.new [1.0], { :foo => :bar }, :shell => Bundler::Thor::Shell::Basic.new + # + def initialize(args = [], options = {}, config = {}) + super + self.shell = config[:shell] + shell.base ||= self if shell.respond_to?(:base) + end + + # Holds the shell for the given Bundler::Thor instance. If no shell is given, + # it gets a default shell from Bundler::Thor::Base.shell. + def shell + @shell ||= Bundler::Thor::Base.shell.new + end + + # Common methods that are delegated to the shell. + SHELL_DELEGATED_METHODS.each do |method| + module_eval <<-METHOD, __FILE__, __LINE__ + def #{method}(*args,&block) + shell.#{method}(*args,&block) + end + METHOD + end + + # Yields the given block with padding. + def with_padding + shell.padding += 1 + yield + ensure + shell.padding -= 1 + end + + protected + + # Allow shell to be shared between invocations. + # + def _shared_configuration #:nodoc: + super.merge!(:shell => shell) + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb new file mode 100644 index 00000000000000..5162390efd1460 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb @@ -0,0 +1,437 @@ +class Bundler::Thor + module Shell + class Basic + attr_accessor :base + attr_reader :padding + + # Initialize base, mute and padding to nil. + # + def initialize #:nodoc: + @base = nil + @mute = false + @padding = 0 + @always_force = false + end + + # Mute everything that's inside given block + # + def mute + @mute = true + yield + ensure + @mute = false + end + + # Check if base is muted + # + def mute? + @mute + end + + # Sets the output padding, not allowing less than zero values. + # + def padding=(value) + @padding = [0, value].max + end + + # Sets the output padding while executing a block and resets it. + # + def indent(count = 1) + orig_padding = padding + self.padding = padding + count + yield + self.padding = orig_padding + end + + # Asks something to the user and receives a response. + # + # If asked to limit the correct responses, you can pass in an + # array of acceptable answers. If one of those is not supplied, + # they will be shown a message stating that one of those answers + # must be given and re-asked the question. + # + # If asking for sensitive information, the :echo option can be set + # to false to mask user input from $stdin. + # + # If the required input is a path, then set the path option to + # true. This will enable tab completion for file paths relative + # to the current working directory on systems that support + # Readline. + # + # ==== Example + # ask("What is your name?") + # + # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"]) + # + # ask("What is your password?", :echo => false) + # + # ask("Where should the file be saved?", :path => true) + # + def ask(statement, *args) + options = args.last.is_a?(Hash) ? args.pop : {} + color = args.first + + if options[:limited_to] + ask_filtered(statement, color, options) + else + ask_simply(statement, color, options) + end + end + + # Say (print) something to the user. If the sentence ends with a whitespace + # or tab character, a new line is not appended (print + flush). Otherwise + # are passed straight to puts (behavior got from Highline). + # + # ==== Example + # say("I know you knew that.") + # + def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/)) + buffer = prepare_message(message, *color) + buffer << "\n" if force_new_line && !message.to_s.end_with?("\n") + + stdout.print(buffer) + stdout.flush + end + + # Say a status with the given color and appends the message. Since this + # method is used frequently by actions, it allows nil or false to be given + # in log_status, avoiding the message from being shown. If a Symbol is + # given in log_status, it's used as the color. + # + def say_status(status, message, log_status = true) + return if quiet? || log_status == false + spaces = " " * (padding + 1) + color = log_status.is_a?(Symbol) ? log_status : :green + + status = status.to_s.rjust(12) + status = set_color status, color, true if color + + buffer = "#{status}#{spaces}#{message}" + buffer = "#{buffer}\n" unless buffer.end_with?("\n") + + stdout.print(buffer) + stdout.flush + end + + # Make a question the to user and returns true if the user replies "y" or + # "yes". + # + def yes?(statement, color = nil) + !!(ask(statement, color, :add_to_history => false) =~ is?(:yes)) + end + + # Make a question the to user and returns true if the user replies "n" or + # "no". + # + def no?(statement, color = nil) + !!(ask(statement, color, :add_to_history => false) =~ is?(:no)) + end + + # Prints values in columns + # + # ==== Parameters + # Array[String, String, ...] + # + def print_in_columns(array) + return if array.empty? + colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2 + array.each_with_index do |value, index| + # Don't output trailing spaces when printing the last column + if ((((index + 1) % (terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length + stdout.puts value + else + stdout.printf("%-#{colwidth}s", value) + end + end + end + + # Prints a table. + # + # ==== Parameters + # Array[Array[String, String, ...]] + # + # ==== Options + # indent:: Indent the first column by indent value. + # colwidth:: Force the first column to colwidth spaces wide. + # + def print_table(array, options = {}) # rubocop:disable MethodLength + return if array.empty? + + formats = [] + indent = options[:indent].to_i + colwidth = options[:colwidth] + options[:truncate] = terminal_width if options[:truncate] == true + + formats << "%-#{colwidth + 2}s".dup if colwidth + start = colwidth ? 1 : 0 + + colcount = array.max { |a, b| a.size <=> b.size }.size + + maximas = [] + + start.upto(colcount - 1) do |index| + maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max + maximas << maxima + formats << if index == colcount - 1 + # Don't output 2 trailing spaces when printing the last column + "%-s".dup + else + "%-#{maxima + 2}s".dup + end + end + + formats[0] = formats[0].insert(0, " " * indent) + formats << "%s" + + array.each do |row| + sentence = "".dup + + row.each_with_index do |column, index| + maxima = maximas[index] + + f = if column.is_a?(Numeric) + if index == row.size - 1 + # Don't output 2 trailing spaces when printing the last column + "%#{maxima}s" + else + "%#{maxima}s " + end + else + formats[index] + end + sentence << f % column.to_s + end + + sentence = truncate(sentence, options[:truncate]) if options[:truncate] + stdout.puts sentence + end + end + + # Prints a long string, word-wrapping the text to the current width of the + # terminal display. Ideal for printing heredocs. + # + # ==== Parameters + # String + # + # ==== Options + # indent:: Indent each line of the printed paragraph by indent value. + # + def print_wrapped(message, options = {}) + indent = options[:indent] || 0 + width = terminal_width - indent + paras = message.split("\n\n") + + paras.map! do |unwrapped| + unwrapped.strip.tr("\n", " ").squeeze(" ").gsub(/.{1,#{width}}(?:\s|\Z)/) { ($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n") } + end + + paras.each do |para| + para.split("\n").each do |line| + stdout.puts line.insert(0, " " * indent) + end + stdout.puts unless para == paras.last + end + end + + # Deals with file collision and returns true if the file should be + # overwritten and false otherwise. If a block is given, it uses the block + # response as the content for the diff. + # + # ==== Parameters + # destination:: the destination file to solve conflicts + # block:: an optional block that returns the value to be used in diff + # + def file_collision(destination) + return true if @always_force + options = block_given? ? "[Ynaqdh]" : "[Ynaqh]" + + loop do + answer = ask( + %[Overwrite #{destination}? (enter "h" for help) #{options}], + :add_to_history => false + ) + + case answer + when nil + say "" + return true + when is?(:yes), is?(:force), "" + return true + when is?(:no), is?(:skip) + return false + when is?(:always) + return @always_force = true + when is?(:quit) + say "Aborting..." + raise SystemExit + when is?(:diff) + show_diff(destination, yield) if block_given? + say "Retrying..." + else + say file_collision_help + end + end + end + + # This code was copied from Rake, available under MIT-LICENSE + # Copyright (c) 2003, 2004 Jim Weirich + def terminal_width + result = if ENV["THOR_COLUMNS"] + ENV["THOR_COLUMNS"].to_i + else + unix? ? dynamic_width : 80 + end + result < 10 ? 80 : result + rescue + 80 + end + + # Called if something goes wrong during the execution. This is used by Bundler::Thor + # internally and should not be used inside your scripts. If something went + # wrong, you can always raise an exception. If you raise a Bundler::Thor::Error, it + # will be rescued and wrapped in the method below. + # + def error(statement) + stderr.puts statement + end + + # Apply color to the given string with optional bold. Disabled in the + # Bundler::Thor::Shell::Basic class. + # + def set_color(string, *) #:nodoc: + string + end + + protected + + def prepare_message(message, *color) + spaces = " " * padding + spaces + set_color(message.to_s, *color) + end + + def can_display_colors? + false + end + + def lookup_color(color) + return color unless color.is_a?(Symbol) + self.class.const_get(color.to_s.upcase) + end + + def stdout + $stdout + end + + def stderr + $stderr + end + + def is?(value) #:nodoc: + value = value.to_s + + if value.size == 1 + /\A#{value}\z/i + else + /\A(#{value}|#{value[0, 1]})\z/i + end + end + + def file_collision_help #:nodoc: + <<-HELP + Y - yes, overwrite + n - no, do not overwrite + a - all, overwrite this and all others + q - quit, abort + d - diff, show the differences between the old and the new + h - help, show this help + HELP + end + + def show_diff(destination, content) #:nodoc: + diff_cmd = ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u" + + require "tempfile" + Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp| + temp.write content + temp.rewind + system %(#{diff_cmd} "#{destination}" "#{temp.path}") + end + end + + def quiet? #:nodoc: + mute? || (base && base.options[:quiet]) + end + + # Calculate the dynamic width of the terminal + def dynamic_width + @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) + end + + def dynamic_width_stty + `stty size 2>/dev/null`.split[1].to_i + end + + def dynamic_width_tput + `tput cols 2>/dev/null`.to_i + end + + def unix? + RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i + end + + def truncate(string, width) + as_unicode do + chars = string.chars.to_a + if chars.length <= width + chars.join + else + chars[0, width - 3].join + "..." + end + end + end + + if "".respond_to?(:encode) + def as_unicode + yield + end + else + def as_unicode + old = $KCODE + $KCODE = "U" + yield + ensure + $KCODE = old + end + end + + def ask_simply(statement, color, options) + default = options[:default] + message = [statement, ("(#{default})" if default), nil].uniq.join(" ") + message = prepare_message(message, *color) + result = Bundler::Thor::LineEditor.readline(message, options) + + return unless result + + result = result.strip + + if default && result == "" + default + else + result + end + end + + def ask_filtered(statement, color, options) + answer_set = options[:limited_to] + correct_answer = nil + until correct_answer + answers = answer_set.join(", ") + answer = ask_simply("#{statement} [#{answers}]", color, options) + correct_answer = answer_set.include?(answer) ? answer : nil + say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer + end + correct_answer + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/color.rb b/lib/bundler/vendor/thor/lib/thor/shell/color.rb new file mode 100644 index 00000000000000..da289cb50cfb2e --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell/color.rb @@ -0,0 +1,149 @@ +require "bundler/vendor/thor/lib/thor/shell/basic" + +class Bundler::Thor + module Shell + # Inherit from Bundler::Thor::Shell::Basic and add set_color behavior. Check + # Bundler::Thor::Shell::Basic to see all available methods. + # + class Color < Basic + # Embed in a String to clear all previous ANSI sequences. + CLEAR = "\e[0m" + # The start of an ANSI bold sequence. + BOLD = "\e[1m" + + # Set the terminal's foreground ANSI color to black. + BLACK = "\e[30m" + # Set the terminal's foreground ANSI color to red. + RED = "\e[31m" + # Set the terminal's foreground ANSI color to green. + GREEN = "\e[32m" + # Set the terminal's foreground ANSI color to yellow. + YELLOW = "\e[33m" + # Set the terminal's foreground ANSI color to blue. + BLUE = "\e[34m" + # Set the terminal's foreground ANSI color to magenta. + MAGENTA = "\e[35m" + # Set the terminal's foreground ANSI color to cyan. + CYAN = "\e[36m" + # Set the terminal's foreground ANSI color to white. + WHITE = "\e[37m" + + # Set the terminal's background ANSI color to black. + ON_BLACK = "\e[40m" + # Set the terminal's background ANSI color to red. + ON_RED = "\e[41m" + # Set the terminal's background ANSI color to green. + ON_GREEN = "\e[42m" + # Set the terminal's background ANSI color to yellow. + ON_YELLOW = "\e[43m" + # Set the terminal's background ANSI color to blue. + ON_BLUE = "\e[44m" + # Set the terminal's background ANSI color to magenta. + ON_MAGENTA = "\e[45m" + # Set the terminal's background ANSI color to cyan. + ON_CYAN = "\e[46m" + # Set the terminal's background ANSI color to white. + ON_WHITE = "\e[47m" + + # Set color by using a string or one of the defined constants. If a third + # option is set to true, it also adds bold to the string. This is based + # on Highline implementation and it automatically appends CLEAR to the end + # of the returned String. + # + # Pass foreground, background and bold options to this method as + # symbols. + # + # Example: + # + # set_color "Hi!", :red, :on_white, :bold + # + # The available colors are: + # + # :bold + # :black + # :red + # :green + # :yellow + # :blue + # :magenta + # :cyan + # :white + # :on_black + # :on_red + # :on_green + # :on_yellow + # :on_blue + # :on_magenta + # :on_cyan + # :on_white + def set_color(string, *colors) + if colors.compact.empty? || !can_display_colors? + string + elsif colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) } + ansi_colors = colors.map { |color| lookup_color(color) } + "#{ansi_colors.join}#{string}#{CLEAR}" + else + # The old API was `set_color(color, bold=boolean)`. We + # continue to support the old API because you should never + # break old APIs unnecessarily :P + foreground, bold = colors + foreground = self.class.const_get(foreground.to_s.upcase) if foreground.is_a?(Symbol) + + bold = bold ? BOLD : "" + "#{bold}#{foreground}#{string}#{CLEAR}" + end + end + + protected + + def can_display_colors? + stdout.tty? + end + + # Overwrite show_diff to show diff with colors if Diff::LCS is + # available. + # + def show_diff(destination, content) #:nodoc: + if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil? + actual = File.binread(destination).to_s.split("\n") + content = content.to_s.split("\n") + + Diff::LCS.sdiff(actual, content).each do |diff| + output_diff_line(diff) + end + else + super + end + end + + def output_diff_line(diff) #:nodoc: + case diff.action + when "-" + say "- #{diff.old_element.chomp}", :red, true + when "+" + say "+ #{diff.new_element.chomp}", :green, true + when "!" + say "- #{diff.old_element.chomp}", :red, true + say "+ #{diff.new_element.chomp}", :green, true + else + say " #{diff.old_element.chomp}", nil, true + end + end + + # Check if Diff::LCS is loaded. If it is, use it to create pretty output + # for diff. + # + def diff_lcs_loaded? #:nodoc: + return true if defined?(Diff::LCS) + return @diff_lcs_loaded unless @diff_lcs_loaded.nil? + + @diff_lcs_loaded = begin + require "diff/lcs" + true + rescue LoadError + false + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/html.rb b/lib/bundler/vendor/thor/lib/thor/shell/html.rb new file mode 100644 index 00000000000000..83d20549882591 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell/html.rb @@ -0,0 +1,126 @@ +require "bundler/vendor/thor/lib/thor/shell/basic" + +class Bundler::Thor + module Shell + # Inherit from Bundler::Thor::Shell::Basic and add set_color behavior. Check + # Bundler::Thor::Shell::Basic to see all available methods. + # + class HTML < Basic + # The start of an HTML bold sequence. + BOLD = "font-weight: bold" + + # Set the terminal's foreground HTML color to black. + BLACK = "color: black" + # Set the terminal's foreground HTML color to red. + RED = "color: red" + # Set the terminal's foreground HTML color to green. + GREEN = "color: green" + # Set the terminal's foreground HTML color to yellow. + YELLOW = "color: yellow" + # Set the terminal's foreground HTML color to blue. + BLUE = "color: blue" + # Set the terminal's foreground HTML color to magenta. + MAGENTA = "color: magenta" + # Set the terminal's foreground HTML color to cyan. + CYAN = "color: cyan" + # Set the terminal's foreground HTML color to white. + WHITE = "color: white" + + # Set the terminal's background HTML color to black. + ON_BLACK = "background-color: black" + # Set the terminal's background HTML color to red. + ON_RED = "background-color: red" + # Set the terminal's background HTML color to green. + ON_GREEN = "background-color: green" + # Set the terminal's background HTML color to yellow. + ON_YELLOW = "background-color: yellow" + # Set the terminal's background HTML color to blue. + ON_BLUE = "background-color: blue" + # Set the terminal's background HTML color to magenta. + ON_MAGENTA = "background-color: magenta" + # Set the terminal's background HTML color to cyan. + ON_CYAN = "background-color: cyan" + # Set the terminal's background HTML color to white. + ON_WHITE = "background-color: white" + + # Set color by using a string or one of the defined constants. If a third + # option is set to true, it also adds bold to the string. This is based + # on Highline implementation and it automatically appends CLEAR to the end + # of the returned String. + # + def set_color(string, *colors) + if colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) } + html_colors = colors.map { |color| lookup_color(color) } + "#{string}" + else + color, bold = colors + html_color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol) + styles = [html_color] + styles << BOLD if bold + "#{string}" + end + end + + # Ask something to the user and receives a response. + # + # ==== Example + # ask("What is your name?") + # + # TODO: Implement #ask for Bundler::Thor::Shell::HTML + def ask(statement, color = nil) + raise NotImplementedError, "Implement #ask for Bundler::Thor::Shell::HTML" + end + + protected + + def can_display_colors? + true + end + + # Overwrite show_diff to show diff with colors if Diff::LCS is + # available. + # + def show_diff(destination, content) #:nodoc: + if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil? + actual = File.binread(destination).to_s.split("\n") + content = content.to_s.split("\n") + + Diff::LCS.sdiff(actual, content).each do |diff| + output_diff_line(diff) + end + else + super + end + end + + def output_diff_line(diff) #:nodoc: + case diff.action + when "-" + say "- #{diff.old_element.chomp}", :red, true + when "+" + say "+ #{diff.new_element.chomp}", :green, true + when "!" + say "- #{diff.old_element.chomp}", :red, true + say "+ #{diff.new_element.chomp}", :green, true + else + say " #{diff.old_element.chomp}", nil, true + end + end + + # Check if Diff::LCS is loaded. If it is, use it to create pretty output + # for diff. + # + def diff_lcs_loaded? #:nodoc: + return true if defined?(Diff::LCS) + return @diff_lcs_loaded unless @diff_lcs_loaded.nil? + + @diff_lcs_loaded = begin + require "diff/lcs" + true + rescue LoadError + false + end + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/util.rb b/lib/bundler/vendor/thor/lib/thor/util.rb new file mode 100644 index 00000000000000..5d03177a28d6d5 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/util.rb @@ -0,0 +1,268 @@ +require "rbconfig" + +class Bundler::Thor + module Sandbox #:nodoc: + end + + # This module holds several utilities: + # + # 1) Methods to convert thor namespaces to constants and vice-versa. + # + # Bundler::Thor::Util.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz" + # + # 2) Loading thor files and sandboxing: + # + # Bundler::Thor::Util.load_thorfile("~/.thor/foo") + # + module Util + class << self + # Receives a namespace and search for it in the Bundler::Thor::Base subclasses. + # + # ==== Parameters + # namespace:: The namespace to search for. + # + def find_by_namespace(namespace) + namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/ + Bundler::Thor::Base.subclasses.detect { |klass| klass.namespace == namespace } + end + + # Receives a constant and converts it to a Bundler::Thor namespace. Since Bundler::Thor + # commands can be added to a sandbox, this method is also responsable for + # removing the sandbox namespace. + # + # This method should not be used in general because it's used to deal with + # older versions of Bundler::Thor. On current versions, if you need to get the + # namespace from a class, just call namespace on it. + # + # ==== Parameters + # constant:: The constant to be converted to the thor path. + # + # ==== Returns + # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz" + # + def namespace_from_thor_class(constant) + constant = constant.to_s.gsub(/^Bundler::Thor::Sandbox::/, "") + constant = snake_case(constant).squeeze(":") + constant + end + + # Given the contents, evaluate it inside the sandbox and returns the + # namespaces defined in the sandbox. + # + # ==== Parameters + # contents + # + # ==== Returns + # Array[Object] + # + def namespaces_in_content(contents, file = __FILE__) + old_constants = Bundler::Thor::Base.subclasses.dup + Bundler::Thor::Base.subclasses.clear + + load_thorfile(file, contents) + + new_constants = Bundler::Thor::Base.subclasses.dup + Bundler::Thor::Base.subclasses.replace(old_constants) + + new_constants.map!(&:namespace) + new_constants.compact! + new_constants + end + + # Returns the thor classes declared inside the given class. + # + def thor_classes_in(klass) + stringfied_constants = klass.constants.map(&:to_s) + Bundler::Thor::Base.subclasses.select do |subclass| + next unless subclass.name + stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", "")) + end + end + + # Receives a string and convert it to snake case. SnakeCase returns snake_case. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def snake_case(str) + return str.downcase if str =~ /^[A-Z_]+$/ + str.gsub(/\B[A-Z]/, '_\&').squeeze("_") =~ /_*(.*)/ + $+.downcase + end + + # Receives a string and convert it to camel case. camel_case returns CamelCase. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def camel_case(str) + return str if str !~ /_/ && str =~ /[A-Z]+.*/ + str.split("_").map(&:capitalize).join + end + + # Receives a namespace and tries to retrieve a Bundler::Thor or Bundler::Thor::Group class + # from it. It first searches for a class using the all the given namespace, + # if it's not found, removes the highest entry and searches for the class + # again. If found, returns the highest entry as the class name. + # + # ==== Examples + # + # class Foo::Bar < Bundler::Thor + # def baz + # end + # end + # + # class Baz::Foo < Bundler::Thor::Group + # end + # + # Bundler::Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default command + # Bundler::Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil + # Bundler::Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz" + # + # ==== Parameters + # namespace + # + def find_class_and_command_by_namespace(namespace, fallback = true) + if namespace.include?(":") # look for a namespaced command + pieces = namespace.split(":") + command = pieces.pop + klass = Bundler::Thor::Util.find_by_namespace(pieces.join(":")) + end + unless klass # look for a Bundler::Thor::Group with the right name + klass = Bundler::Thor::Util.find_by_namespace(namespace) + command = nil + end + if !klass && fallback # try a command in the default namespace + command = namespace + klass = Bundler::Thor::Util.find_by_namespace("") + end + [klass, command] + end + alias_method :find_class_and_task_by_namespace, :find_class_and_command_by_namespace + + # Receives a path and load the thor file in the path. The file is evaluated + # inside the sandbox to avoid namespacing conflicts. + # + def load_thorfile(path, content = nil, debug = false) + content ||= File.binread(path) + + begin + Bundler::Thor::Sandbox.class_eval(content, path) + rescue StandardError => e + $stderr.puts("WARNING: unable to load thorfile #{path.inspect}: #{e.message}") + if debug + $stderr.puts(*e.backtrace) + else + $stderr.puts(e.backtrace.first) + end + end + end + + def user_home + @@user_home ||= if ENV["HOME"] + ENV["HOME"] + elsif ENV["USERPROFILE"] + ENV["USERPROFILE"] + elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"] + File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"]) + elsif ENV["APPDATA"] + ENV["APPDATA"] + else + begin + File.expand_path("~") + rescue + if File::ALT_SEPARATOR + "C:/" + else + "/" + end + end + end + end + + # Returns the root where thor files are located, depending on the OS. + # + def thor_root + File.join(user_home, ".thor").tr('\\', "/") + end + + # Returns the files in the thor root. On Windows thor_root will be something + # like this: + # + # C:\Documents and Settings\james\.thor + # + # If we don't #gsub the \ character, Dir.glob will fail. + # + def thor_root_glob + files = Dir["#{escape_globs(thor_root)}/*"] + + files.map! do |file| + File.directory?(file) ? File.join(file, "main.thor") : file + end + end + + # Where to look for Bundler::Thor files. + # + def globs_for(path) + path = escape_globs(path) + ["#{path}/Bundler::Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] + end + + # Return the path to the ruby interpreter taking into account multiple + # installations and windows extensions. + # + def ruby_command + @ruby_command ||= begin + ruby_name = RbConfig::CONFIG["ruby_install_name"] + ruby = File.join(RbConfig::CONFIG["bindir"], ruby_name) + ruby << RbConfig::CONFIG["EXEEXT"] + + # avoid using different name than ruby (on platforms supporting links) + if ruby_name != "ruby" && File.respond_to?(:readlink) + begin + alternate_ruby = File.join(RbConfig::CONFIG["bindir"], "ruby") + alternate_ruby << RbConfig::CONFIG["EXEEXT"] + + # ruby is a symlink + if File.symlink? alternate_ruby + linked_ruby = File.readlink alternate_ruby + + # symlink points to 'ruby_install_name' + ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby + end + rescue NotImplementedError # rubocop:disable HandleExceptions + # just ignore on windows + end + end + + # escape string in case path to ruby executable contain spaces. + ruby.sub!(/.*\s.*/m, '"\&"') + ruby + end + end + + # Returns a string that has had any glob characters escaped. + # The glob characters are `* ? { } [ ]`. + # + # ==== Examples + # + # Bundler::Thor::Util.escape_globs('[apps]') # => '\[apps\]' + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def escape_globs(path) + path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&') + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/version.rb b/lib/bundler/vendor/thor/lib/thor/version.rb new file mode 100644 index 00000000000000..df8f18821aa2ec --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/version.rb @@ -0,0 +1,3 @@ +class Bundler::Thor + VERSION = "0.20.0" +end diff --git a/lib/bundler/vendored_fileutils.rb b/lib/bundler/vendored_fileutils.rb new file mode 100644 index 00000000000000..d14e98baf72fa5 --- /dev/null +++ b/lib/bundler/vendored_fileutils.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Bundler; end +if RUBY_VERSION >= "2.4" + require "bundler/vendor/fileutils/lib/fileutils" +else + # the version we vendor is 2.4+ + require "fileutils" +end diff --git a/lib/bundler/vendored_molinillo.rb b/lib/bundler/vendored_molinillo.rb new file mode 100644 index 00000000000000..061b634f72825b --- /dev/null +++ b/lib/bundler/vendored_molinillo.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module Bundler; end +require "bundler/vendor/molinillo/lib/molinillo" diff --git a/lib/bundler/vendored_persistent.rb b/lib/bundler/vendored_persistent.rb new file mode 100644 index 00000000000000..de9c42fcc1412e --- /dev/null +++ b/lib/bundler/vendored_persistent.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# We forcibly require OpenSSL, because net/http/persistent will only autoload +# it. On some Rubies, autoload fails but explicit require succeeds. +begin + require "openssl" +rescue LoadError + # some Ruby builds don't have OpenSSL +end +module Bundler + module Persistent + module Net + module HTTP + end + end + end +end +require "bundler/vendor/net-http-persistent/lib/net/http/persistent" + +module Bundler + class PersistentHTTP < Persistent::Net::HTTP::Persistent + def connection_for(uri) + connection = super + warn_old_tls_version_rubygems_connection(uri, connection) + connection + end + + def warn_old_tls_version_rubygems_connection(uri, connection) + return unless connection.use_ssl? + return unless (uri.host || "").end_with?("rubygems.org") + + socket = connection.instance_variable_get(:@socket) + return unless socket + socket_io = socket.io + return unless socket_io.respond_to?(:ssl_version) + ssl_version = socket_io.ssl_version + + case ssl_version + when /TLSv([\d\.]+)/ + version = Gem::Version.new($1) + if version < Gem::Version.new("1.2") + Bundler.ui.warn \ + "Warning: Your Ruby version is compiled against a copy of OpenSSL that is very old. " \ + "Starting in January 2018, RubyGems.org will refuse connection requests from these " \ + "very old versions of OpenSSL. If you will need to continue installing gems after " \ + "January 2018, please follow this guide to upgrade: http://ruby.to/tls-outdated.", + :wrap => true + end + end + end + end +end diff --git a/lib/bundler/vendored_thor.rb b/lib/bundler/vendored_thor.rb new file mode 100644 index 00000000000000..8cca090f551ada --- /dev/null +++ b/lib/bundler/vendored_thor.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Bundler + def self.require_thor_actions + Kernel.send(:require, "bundler/vendor/thor/lib/thor/actions") + end +end +require "bundler/vendor/thor/lib/thor" diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb new file mode 100644 index 00000000000000..bf6c477c9210d5 --- /dev/null +++ b/lib/bundler/version.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: false + +# Ruby 1.9.3 and old RubyGems don't play nice with frozen version strings +# rubocop:disable MutableConstant + +module Bundler + # We're doing this because we might write tests that deal + # with other versions of bundler and we are unsure how to + # handle this better. + VERSION = "2.0.0.pre.2" unless defined?(::Bundler::VERSION) + + def self.overwrite_loaded_gem_version + begin + require "rubygems" + rescue LoadError + return + end + return unless bundler_spec = Gem.loaded_specs["bundler"] + return if bundler_spec.version == VERSION + bundler_spec.version = Bundler::VERSION + end + private_class_method :overwrite_loaded_gem_version + overwrite_loaded_gem_version + + def self.bundler_major_version + @bundler_major_version ||= VERSION.split(".").first.to_i + end +end diff --git a/lib/bundler/version_ranges.rb b/lib/bundler/version_ranges.rb new file mode 100644 index 00000000000000..ec25716cde7576 --- /dev/null +++ b/lib/bundler/version_ranges.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Bundler + module VersionRanges + NEq = Struct.new(:version) + ReqR = Struct.new(:left, :right) + class ReqR + Endpoint = Struct.new(:version, :inclusive) + def to_s + "#{left.inclusive ? "[" : "("}#{left.version}, #{right.version}#{right.inclusive ? "]" : ")"}" + end + INFINITY = Object.new.freeze + ZERO = Gem::Version.new("0.a") + + def cover?(v) + return false if left.inclusive && left.version > v + return false if !left.inclusive && left.version >= v + + if right.version != INFINITY + return false if right.inclusive && right.version < v + return false if !right.inclusive && right.version <= v + end + + true + end + + def empty? + left.version == right.version && !(left.inclusive && right.inclusive) + end + + def single? + left.version == right.version + end + + UNIVERSAL = ReqR.new(ReqR::Endpoint.new(Gem::Version.new("0.a"), true), ReqR::Endpoint.new(ReqR::INFINITY, false)).freeze + end + + def self.for_many(requirements) + requirements = requirements.map(&:requirements).flatten(1).map {|r| r.join(" ") } + requirements << ">= 0.a" if requirements.empty? + requirement = Gem::Requirement.new(requirements) + self.for(requirement) + end + + def self.for(requirement) + ranges = requirement.requirements.map do |op, v| + case op + when "=" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(v, true)) + when "!=" then NEq.new(v) + when ">=" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(ReqR::INFINITY, false)) + when ">" then ReqR.new(ReqR::Endpoint.new(v, false), ReqR::Endpoint.new(ReqR::INFINITY, false)) + when "<" then ReqR.new(ReqR::Endpoint.new(ReqR::ZERO, true), ReqR::Endpoint.new(v, false)) + when "<=" then ReqR.new(ReqR::Endpoint.new(ReqR::ZERO, true), ReqR::Endpoint.new(v, true)) + when "~>" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(v.bump, false)) + else raise "unknown version op #{op} in requirement #{requirement}" + end + end.uniq + ranges, neqs = ranges.partition {|r| !r.is_a?(NEq) } + + [ranges.sort_by {|range| [range.left.version, range.left.inclusive ? 0 : 1] }, neqs.map(&:version)] + end + + def self.empty?(ranges, neqs) + !ranges.reduce(ReqR::UNIVERSAL) do |last_range, curr_range| + next false unless last_range + next false if curr_range.single? && neqs.include?(curr_range.left.version) + next curr_range if last_range.right.version == ReqR::INFINITY + case last_range.right.version <=> curr_range.left.version + when 1 then next curr_range + when 0 then next(last_range.right.inclusive && curr_range.left.inclusive && !neqs.include?(curr_range.left.version) && curr_range) + when -1 then next false + end + end + end + end +end diff --git a/lib/bundler/vlad.rb b/lib/bundler/vlad.rb new file mode 100644 index 00000000000000..a6b13435c9f80a --- /dev/null +++ b/lib/bundler/vlad.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "bundler/shared_helpers" +Bundler::SharedHelpers.major_deprecation 3, + "The Bundler task for Vlad" + +# Vlad task for Bundler. +# +# Add "require 'bundler/vlad'" in your Vlad deploy.rb, and +# include the vlad:bundle:install task in your vlad:deploy task. +require "bundler/deployment" + +include Rake::DSL if defined? Rake::DSL + +namespace :vlad do + Bundler::Deployment.define_task(Rake::RemoteTask, :remote_task, :roles => :app) +end diff --git a/lib/bundler/worker.rb b/lib/bundler/worker.rb new file mode 100644 index 00000000000000..e91cfa78057274 --- /dev/null +++ b/lib/bundler/worker.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "thread" + +module Bundler + class Worker + POISON = Object.new + + class WrappedException < StandardError + attr_reader :exception + def initialize(exn) + @exception = exn + end + end + + # @return [String] the name of the worker + attr_reader :name + + # Creates a worker pool of specified size + # + # @param size [Integer] Size of pool + # @param name [String] name the name of the worker + # @param func [Proc] job to run in inside the worker pool + def initialize(size, name, func) + @name = name + @request_queue = Queue.new + @response_queue = Queue.new + @func = func + @size = size + @threads = nil + SharedHelpers.trap("INT") { abort_threads } + end + + # Enqueue a request to be executed in the worker pool + # + # @param obj [String] mostly it is name of spec that should be downloaded + def enq(obj) + create_threads unless @threads + @request_queue.enq obj + end + + # Retrieves results of job function being executed in worker pool + def deq + result = @response_queue.deq + raise result.exception if result.is_a?(WrappedException) + result + end + + def stop + stop_threads + end + + private + + def process_queue(i) + loop do + obj = @request_queue.deq + break if obj.equal? POISON + @response_queue.enq apply_func(obj, i) + end + end + + def apply_func(obj, i) + @func.call(obj, i) + rescue Exception => e + WrappedException.new(e) + end + + # Stop the worker threads by sending a poison object down the request queue + # so as worker threads after retrieving it, shut themselves down + def stop_threads + return unless @threads + @threads.each { @request_queue.enq POISON } + @threads.each(&:join) + @threads = nil + end + + def abort_threads + return unless @threads + Bundler.ui.debug("\n#{caller.join("\n")}") + @threads.each(&:exit) + exit 1 + end + + def create_threads + creation_errors = [] + + @threads = Array.new(@size) do |i| + begin + Thread.start { process_queue(i) }.tap do |thread| + thread.name = "#{name} Worker ##{i}" if thread.respond_to?(:name=) + end + rescue ThreadError => e + creation_errors << e + nil + end + end.compact + + return if creation_errors.empty? + + message = "Failed to create threads for the #{name} worker: #{creation_errors.map(&:to_s).uniq.join(", ")}" + raise ThreadCreationError, message if @threads.empty? + Bundler.ui.info message + end + end +end diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb new file mode 100644 index 00000000000000..0fd81c40eff46a --- /dev/null +++ b/lib/bundler/yaml_serializer.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Bundler + # A stub yaml serializer that can handle only hashes and strings (as of now). + module YAMLSerializer + module_function + + def dump(hash) + yaml = String.new("---") + yaml << dump_hash(hash) + end + + def dump_hash(hash) + yaml = String.new("\n") + hash.each do |k, v| + yaml << k << ":" + if v.is_a?(Hash) + yaml << dump_hash(v).gsub(/^(?!$)/, " ") # indent all non-empty lines + elsif v.is_a?(Array) # Expected to be array of strings + yaml << "\n- " << v.map {|s| s.to_s.gsub(/\s+/, " ").inspect }.join("\n- ") << "\n" + else + yaml << " " << v.to_s.gsub(/\s+/, " ").inspect << "\n" + end + end + yaml + end + + ARRAY_REGEX = / + ^ + (?:[ ]*-[ ]) # '- ' before array items + (['"]?) # optional opening quote + (.*) # value + \1 # matching closing quote + $ + /xo + + HASH_REGEX = / + ^ + ([ ]*) # indentations + (.+) # key + (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value) + [ ]? + (?: !\s)? # optional exclamation mark found with ruby 1.9.3 + (['"]?) # optional opening quote + (.*) # value + \3 # matching closing quote + $ + /xo + + def load(str) + res = {} + stack = [res] + last_hash = nil + last_empty_key = nil + str.split(/\r?\n/).each do |line| + if match = HASH_REGEX.match(line) + indent, key, quote, val = match.captures + key = convert_to_backward_compatible_key(key) + depth = indent.scan(/ /).length + if quote.empty? && val.empty? + new_hash = {} + stack[depth][key] = new_hash + stack[depth + 1] = new_hash + last_empty_key = key + last_hash = stack[depth] + else + stack[depth][key] = val + end + elsif match = ARRAY_REGEX.match(line) + _, val = match.captures + last_hash[last_empty_key] = [] unless last_hash[last_empty_key].is_a?(Array) + + last_hash[last_empty_key].push(val) + end + end + res + end + + # for settings' keys + def convert_to_backward_compatible_key(key) + key = "#{key}/" if key =~ /https?:/i && key !~ %r{/\Z} + key = key.gsub(".", "__") if key.include?(".") + key + end + + class << self + private :dump_hash, :convert_to_backward_compatible_key + end + end +end diff --git a/lib/cgi/cookie.rb b/lib/cgi/cookie.rb index a2155edb774421..9a0d41e2b8570b 100644 --- a/lib/cgi/cookie.rb +++ b/lib/cgi/cookie.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'cgi/util' +require_relative 'util' class CGI # Class representing an HTTP cookie. # diff --git a/lib/cgi/core.rb b/lib/cgi/core.rb index 73daae3e17c440..3fb8dbab542333 100644 --- a/lib/cgi/core.rb +++ b/lib/cgi/core.rb @@ -862,24 +862,24 @@ def initialize(options = {}, &block) # :yields: name, value case @options[:tag_maker] when "html3" - require 'cgi/html' + require_relative 'html' extend Html3 extend HtmlExtension when "html4" - require 'cgi/html' + require_relative 'html' extend Html4 extend HtmlExtension when "html4Tr" - require 'cgi/html' + require_relative 'html' extend Html4Tr extend HtmlExtension when "html4Fr" - require 'cgi/html' + require_relative 'html' extend Html4Tr extend Html4Fr extend HtmlExtension when "html5" - require 'cgi/html' + require_relative 'html' extend Html5 extend HtmlExtension end diff --git a/lib/cgi/session/pstore.rb b/lib/cgi/session/pstore.rb index cb0370b619e244..5a6e25d1374b23 100644 --- a/lib/cgi/session/pstore.rb +++ b/lib/cgi/session/pstore.rb @@ -10,7 +10,7 @@ # persistent of session data on top of the pstore library. See # cgi/session.rb for more details on session storage managers. -require 'cgi/session' +require_relative '../session' require 'pstore' class CGI diff --git a/lib/drb/drb.rb b/lib/drb/drb.rb index 6da8c5f6a739d2..de57362f24211a 100644 --- a/lib/drb/drb.rb +++ b/lib/drb/drb.rb @@ -48,7 +48,7 @@ require 'socket' require 'io/wait' -require 'drb/eq' +require_relative 'eq' # # == Overview @@ -1638,7 +1638,7 @@ def perform_without_block end - require 'drb/invokemethod' + require_relative 'invokemethod' class InvokeMethod include InvokeMethod18Mixin end diff --git a/lib/drb/extserv.rb b/lib/drb/extserv.rb index 1cb1be47099b85..a93d5d1576e37c 100644 --- a/lib/drb/extserv.rb +++ b/lib/drb/extserv.rb @@ -4,7 +4,7 @@ Copyright (c) 2000,2002 Masatoshi SEKI =end -require 'drb/drb' +require_relative 'drb' require 'monitor' module DRb diff --git a/lib/drb/extservm.rb b/lib/drb/extservm.rb index 9cce962062a8f0..040e4e3e08ee3d 100644 --- a/lib/drb/extservm.rb +++ b/lib/drb/extservm.rb @@ -4,7 +4,7 @@ Copyright (c) 2000 Masatoshi SEKI =end -require 'drb/drb' +require_relative 'drb' require 'monitor' module DRb diff --git a/lib/drb/gw.rb b/lib/drb/gw.rb index d00050764456d3..65a525476ec549 100644 --- a/lib/drb/gw.rb +++ b/lib/drb/gw.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'drb/drb' +require_relative 'drb' require 'monitor' module DRb diff --git a/lib/drb/ssl.rb b/lib/drb/ssl.rb index e2bc247b1731a3..adc85dbc9c168a 100644 --- a/lib/drb/ssl.rb +++ b/lib/drb/ssl.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false require 'socket' require 'openssl' -require 'drb/drb' +require_relative 'drb' require 'singleton' module DRb diff --git a/lib/drb/timeridconv.rb b/lib/drb/timeridconv.rb index 9ac7e1e69c9093..3ead98a7f24527 100644 --- a/lib/drb/timeridconv.rb +++ b/lib/drb/timeridconv.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'drb/drb' +require_relative 'drb' require 'monitor' module DRb diff --git a/lib/drb/unix.rb b/lib/drb/unix.rb index 7dcf2daaf83c9a..89957c9e7bcc33 100644 --- a/lib/drb/unix.rb +++ b/lib/drb/unix.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false require 'socket' -require 'drb/drb' +require_relative 'drb' require 'tmpdir' raise(LoadError, "UNIXServer is required") unless defined?(UNIXServer) diff --git a/lib/e2mmap.gemspec b/lib/e2mmap.gemspec index e9518a5fee08d1..b9808d89ffdb5a 100644 --- a/lib/e2mmap.gemspec +++ b/lib/e2mmap.gemspec @@ -5,7 +5,6 @@ rescue LoadError require_relative "e2mmap/version" end - Gem::Specification.new do |spec| spec.name = "e2mmap" spec.version = Exception2MessageMapper::VERSION diff --git a/lib/erb.rb b/lib/erb.rb index c9b987dfb73aac..5b725d7820f7d8 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -665,9 +665,13 @@ def prepare_trim_mode(mode) # :nodoc: return [false, '>'] when 2 return [false, '<>'] - when 0 + when 0, nil return [false, nil] when String + unless mode.match?(/\A(%|-|>|<>){1,2}\z/) + warn_invalid_trim_mode(mode, uplevel: 5) + end + perc = mode.include?('%') if mode.include?('-') return [perc, '-'] @@ -679,6 +683,7 @@ def prepare_trim_mode(mode) # :nodoc: [perc, nil] end else + warn_invalid_trim_mode(mode, uplevel: 5) return [false, nil] end end @@ -730,6 +735,10 @@ def detect_magic_comment(s, enc = nil) end return enc, frozen end + + def warn_invalid_trim_mode(mode, uplevel:) + warn "Invalid ERB trim mode: #{mode.inspect} (trim_mode: nil, 0, 1, 2, or String composed of '%' and/or '-', '>', '<>')", uplevel: uplevel + 1 + end end end diff --git a/lib/fileutils.rb b/lib/fileutils.rb index 74b21975e3d427..dc7261857bb329 100644 --- a/lib/fileutils.rb +++ b/lib/fileutils.rb @@ -1274,8 +1274,7 @@ def door? def entries opts = {} opts[:encoding] = ::Encoding::UTF_8 if fu_windows? - Dir.entries(path(), opts)\ - .reject {|n| n == '.' or n == '..' }\ + Dir.children(path, opts)\ .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) } end diff --git a/lib/irb.rb b/lib/irb.rb index 85953346a79540..f2c7959cae611e 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -733,7 +733,63 @@ def @CONF.inspect end class Binding - # :nodoc: + # Opens an IRB session where +binding.irb+ is called which allows for + # interactive debugging. You can call any methods or variables available in + # the current scope, and mutate state if you need to. + # + # + # Given a Ruby file called +potato.rb+ containing the following code: + # + # class Potato + # def initialize + # @cooked = false + # binding.irb + # puts "Cooked potato: #{@cooked}" + # end + # end + # + # Potato.new + # + # Running +ruby potato.rb+ will open an IRB session where +binding.irb+ is + # called, and you will see the following: + # + # $ ruby potato.rb + # + # From: potato.rb @ line 4 : + # + # 1: class Potato + # 2: def initialize + # 3: @cooked = false + # => 4: binding.irb + # 5: puts "Cooked potato: #{@cooked}" + # 6: end + # 7: end + # 8: + # 9: Potato.new + # + # irb(#):001:0> + # + # You can type any valid Ruby code and it will be evaluated in the current + # context. This allows you to debug without having to run your code repeatedly: + # + # irb(#):001:0> @cooked + # => false + # irb(#):002:0> self.class + # => Potato + # irb(#):003:0> caller.first + # => ".../2.5.1/lib/ruby/2.5.0/irb/workspace.rb:85:in `eval'" + # irb(#):004:0> @cooked = true + # => true + # + # You can exit the IRB session with the `exit` command. Note that exiting will + # resume execution where +binding.irb+ had paused it, as you can see from the + # output printed to standard output in this example: + # + # irb(#):005:0> exit + # Cooked potato: true + # + # + # See IRB@IRB+Usage for more information. def irb IRB.setup(eval("__FILE__"), argv: []) workspace = IRB::WorkSpace.new(self) diff --git a/lib/irb/cmd/chws.rb b/lib/irb/cmd/chws.rb index e93c976f82d3e7..e9f257791ced75 100644 --- a/lib/irb/cmd/chws.rb +++ b/lib/irb/cmd/chws.rb @@ -10,8 +10,8 @@ # # -require "irb/cmd/nop.rb" -require "irb/ext/change-ws.rb" +require_relative "nop" +require_relative "../ext/change-ws" # :stopdoc: module IRB diff --git a/lib/irb/cmd/help.rb b/lib/irb/cmd/help.rb index db2bd567e5687c..71590ee844d414 100644 --- a/lib/irb/cmd/help.rb +++ b/lib/irb/cmd/help.rb @@ -11,7 +11,7 @@ require 'rdoc/ri/driver' -require "irb/cmd/nop.rb" +require_relative "nop" # :stopdoc: module IRB diff --git a/lib/irb/cmd/load.rb b/lib/irb/cmd/load.rb index f800b741eb5504..b6769a4124f6f4 100644 --- a/lib/irb/cmd/load.rb +++ b/lib/irb/cmd/load.rb @@ -10,8 +10,8 @@ # # -require "irb/cmd/nop.rb" -require "irb/ext/loader" +require_relative "nop" +require_relative "../ext/loader" # :stopdoc: module IRB diff --git a/lib/irb/cmd/pushws.rb b/lib/irb/cmd/pushws.rb index ffe55abed6e651..187b276e482092 100644 --- a/lib/irb/cmd/pushws.rb +++ b/lib/irb/cmd/pushws.rb @@ -10,8 +10,8 @@ # # -require "irb/cmd/nop.rb" -require "irb/ext/workspaces.rb" +require_relative "nop" +require_relative "../ext/workspaces" # :stopdoc: module IRB diff --git a/lib/irb/cmd/subirb.rb b/lib/irb/cmd/subirb.rb index c1602f6e45e100..1e18607d1af61b 100644 --- a/lib/irb/cmd/subirb.rb +++ b/lib/irb/cmd/subirb.rb @@ -9,8 +9,8 @@ # # -require "irb/cmd/nop.rb" -require "irb/ext/multi-irb" +require_relative "nop" +require_relative "../ext/multi-irb" # :stopdoc: module IRB diff --git a/lib/irb/context.rb b/lib/irb/context.rb index b82aaea6a60181..e8e6a118e6a46b 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -9,10 +9,10 @@ # # # -require "irb/workspace" -require "irb/inspector" -require "irb/input-method" -require "irb/output-method" +require_relative "workspace" +require_relative "inspector" +require_relative "input-method" +require_relative "output-method" module IRB # A class that wraps the current state of the irb session, including the diff --git a/lib/irb/ext/use-loader.rb b/lib/irb/ext/use-loader.rb index 571dd25d17aed7..cc7170667175d8 100644 --- a/lib/irb/ext/use-loader.rb +++ b/lib/irb/ext/use-loader.rb @@ -10,8 +10,8 @@ # # -require "irb/cmd/load" -require "irb/ext/loader" +require_relative "../cmd/load" +require_relative "loader" class Object alias __original__load__IRB_use_loader__ load diff --git a/lib/irb/help.rb b/lib/irb/help.rb index a4264ab4abb3ea..7868a70a6c9c3b 100644 --- a/lib/irb/help.rb +++ b/lib/irb/help.rb @@ -10,7 +10,7 @@ # # -require 'irb/magic-file' +require_relative 'magic-file' module IRB # Outputs the irb help message, see IRB@Command+line+options. diff --git a/lib/irb/init.rb b/lib/irb/init.rb index a971b75ac3e166..2066d8cb64581f 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -197,7 +197,7 @@ def IRB.parse_opts(argv: ::ARGV) print IRB.version, "\n" exit 0 when "-h", "--help" - require "irb/help" + require_relative "help" IRB.print_usage exit 0 when "--" diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index f7b1aac3bf3c27..f491d5a760d29c 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -9,8 +9,8 @@ # # # -require 'irb/src_encoding' -require 'irb/magic-file' +require_relative 'src_encoding' +require_relative 'magic-file' module IRB STDIN_FILE_NAME = "(line)" # :nodoc: diff --git a/lib/irb/notifier.rb b/lib/irb/notifier.rb index a21e865f2ef2b9..5a68b0e8c84c23 100644 --- a/lib/irb/notifier.rb +++ b/lib/irb/notifier.rb @@ -11,7 +11,7 @@ # require "e2mmap" -require "irb/output-method" +require_relative "output-method" module IRB # An output formatter used internally by the lexer. diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index fb7e08099fd6d4..555d1f024ff973 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -11,8 +11,8 @@ # require "e2mmap" -require "irb/slex" -require "irb/ruby-token" +require_relative "slex" +require_relative "ruby-token" # :stopdoc: class RubyLex diff --git a/lib/irb/slex.rb b/lib/irb/slex.rb index 039b214a8d815c..e584b312bdb92d 100644 --- a/lib/irb/slex.rb +++ b/lib/irb/slex.rb @@ -11,7 +11,7 @@ # require "e2mmap" -require "irb/notifier" +require_relative "notifier" # :stopdoc: module IRB diff --git a/lib/irb/xmp.rb b/lib/irb/xmp.rb index 3234cff7f3f030..60cf3b4e4dfe40 100644 --- a/lib/irb/xmp.rb +++ b/lib/irb/xmp.rb @@ -11,7 +11,7 @@ # require "irb" -require "irb/frame" +require_relative "frame" # An example printer for irb. # diff --git a/lib/matrix.rb b/lib/matrix.rb index 62852bdad04f22..7f338bb07e8c55 100644 --- a/lib/matrix.rb +++ b/lib/matrix.rb @@ -145,8 +145,8 @@ def Matrix.identity(n) scalar(n, 1) end class << Matrix - alias unit identity - alias I identity + alias_method :unit, :identity + alias_method :I, :identity end # @@ -289,10 +289,9 @@ def initialize(rows, column_count = rows[0].size) @column_count = column_count end - def new_matrix(rows, column_count = rows[0].size) # :nodoc: + private def new_matrix(rows, column_count = rows[0].size) # :nodoc: self.class.send(:new, rows, column_count) # bypass privacy of Matrix.new end - private :new_matrix # # Returns element (+i+,+j+) of the matrix. That is: row +i+, column +j+. @@ -303,12 +302,107 @@ def [](i, j) alias element [] alias component [] + # + # :call-seq: + # matrix[range, range] = matrix/element + # matrix[range, integer] = vector/column_matrix/element + # matrix[integer, range] = vector/row_matrix/element + # matrix[integer, integer] = element + # + # Set element or elements of matrix. def []=(i, j, v) - @rows[i][j] = v + raise FrozenError, "can't modify frozen Matrix" if frozen? + rows = check_range(i, :row) or row = check_int(i, :row) + columns = check_range(j, :column) or column = check_int(j, :column) + if rows && columns + set_row_and_col_range(rows, columns, v) + elsif rows + set_row_range(rows, column, v) + elsif columns + set_col_range(row, columns, v) + else + set_value(row, column, v) + end end alias set_element []= alias set_component []= - private :[]=, :set_element, :set_component + private :set_element, :set_component + + # Returns range or nil + private def check_range(val, direction) + return unless val.is_a?(Range) + count = direction == :row ? row_count : column_count + CoercionHelper.check_range(val, count, direction) + end + + private def check_int(val, direction) + count = direction == :row ? row_count : column_count + CoercionHelper.check_int(val, count, direction) + end + + private def set_value(row, col, value) + raise ErrDimensionMismatch, "Expected a a value, got a #{value.class}" if value.respond_to?(:to_matrix) + + @rows[row][col] = value + end + + private def set_row_and_col_range(row_range, col_range, value) + if value.is_a?(Matrix) + if row_range.size != value.row_count || col_range.size != value.column_count + raise ErrDimensionMismatch, [ + 'Expected a Matrix of dimensions', + "#{row_range.size}x#{col_range.size}", + 'got', + "#{value.row_count}x#{value.column_count}", + ].join(' ') + end + source = value.instance_variable_get :@rows + row_range.each_with_index do |row, i| + @rows[row][col_range] = source[i] + end + elsif value.is_a?(Vector) + raise ErrDimensionMismatch, 'Expected a Matrix or a value, got a Vector' + else + value_to_set = Array.new(col_range.size, value) + row_range.each do |i| + @rows[i][col_range] = value_to_set + end + end + end + + private def set_row_range(row_range, col, value) + if value.is_a?(Vector) + Matrix.Raise ErrDimensionMismatch unless row_range.size == value.size + set_column_vector(row_range, col, value) + elsif value.is_a?(Matrix) + Matrix.Raise ErrDimensionMismatch unless value.column_count == 1 + value = value.column(0) + Matrix.Raise ErrDimensionMismatch unless row_range.size == value.size + set_column_vector(row_range, col, value) + else + @rows[row_range].each{|e| e[col] = value } + end + end + + private def set_column_vector(row_range, col, value) + value.each_with_index do |e, index| + r = row_range.begin + index + @rows[r][col] = e + end + end + + private def set_col_range(row, col_range, value) + value = if value.is_a?(Vector) + value.to_a + elsif value.is_a?(Matrix) + Matrix.Raise ErrDimensionMismatch unless value.row_count == 1 + value.row(0).to_a + else + Array.new(col_range.size, value) + end + Matrix.Raise ErrDimensionMismatch unless col_range.size == value.size + @rows[row][col_range] = value + end # # Returns the number of rows. @@ -361,16 +455,48 @@ def column(j) # :yield: e # # Returns a matrix that is the result of iteration of the given block over all # elements of the matrix. + # Elements can be restricted by passing an argument: + # * :all (default): yields all elements + # * :diagonal: yields only elements on the diagonal + # * :off_diagonal: yields all elements except on the diagonal + # * :lower: yields only elements on or below the diagonal + # * :strict_lower: yields only elements below the diagonal + # * :strict_upper: yields only elements above the diagonal + # * :upper: yields only elements on or above the diagonal # Matrix[ [1,2], [3,4] ].collect { |e| e**2 } # => 1 4 # 9 16 # - def collect(&block) # :yield: e - return to_enum(:collect) unless block_given? - rows = @rows.collect{|row| row.collect(&block)} - new_matrix rows, column_count + def collect(which = :all, &block) # :yield: e + return to_enum(:collect, which) unless block_given? + dup.collect!(which, &block) + end + alias_method :map, :collect + + # + # Invokes the given block for each element of matrix, replacing the element with the value + # returned by the block. + # Elements can be restricted by passing an argument: + # * :all (default): yields all elements + # * :diagonal: yields only elements on the diagonal + # * :off_diagonal: yields all elements except on the diagonal + # * :lower: yields only elements on or below the diagonal + # * :strict_lower: yields only elements below the diagonal + # * :strict_upper: yields only elements above the diagonal + # * :upper: yields only elements on or above the diagonal + # + def collect!(which = :all) + return to_enum(:collect!, which) unless block_given? + raise FrozenError, "can't modify frozen Matrix" if frozen? + each_with_index(which){ |e, row_index, col_index| @rows[row_index][col_index] = yield e } + end + + alias map! collect! + + def freeze + @rows.freeze + super end - alias map collect # # Yields all elements of the matrix, starting with those of the first row, @@ -812,15 +938,7 @@ def antisymmetric? end true end - - # - # Returns +true+ if this is a reflexive matrix. - # Raises an error if matrix is not square. - # - def reflexive? - Matrix.Raise ErrDimensionMismatch unless square? - each(:diagonal).all? { |e| e == 1 } - end + alias_method :skew_symmetric?, :antisymmetric? # # Returns +true+ if this is a unitary matrix @@ -874,12 +992,11 @@ def eql?(other) end # - # Returns a clone of the matrix, so that the contents of each do not reference - # identical objects. - # There should be no good reason to do this since Matrices are immutable. + # Called for dup & clone. # - def clone - new_matrix @rows.map(&:dup), column_count + private def initialize_copy(m) + super + @rows = @rows.map(&:dup) unless frozen? end # @@ -1021,9 +1138,9 @@ def inverse Matrix.Raise ErrDimensionMismatch unless square? self.class.I(row_count).send(:inverse_from, self) end - alias inv inverse + alias_method :inv, :inverse - def inverse_from(src) # :nodoc: + private def inverse_from(src) # :nodoc: last = row_count - 1 a = src.to_a @@ -1066,7 +1183,6 @@ def inverse_from(src) # :nodoc: end self end - private :inverse_from # # Matrix exponentiation. @@ -1173,7 +1289,7 @@ def determinant # with smaller bignums (if any), while a matrix of Float will usually have # intermediate results with better precision. # - def determinant_bareiss + private def determinant_bareiss size = row_count last = size - 1 a = to_a @@ -1199,7 +1315,6 @@ def determinant_bareiss end sign * pivot end - private :determinant_bareiss # # deprecated; use Matrix#determinant @@ -1208,7 +1323,7 @@ def determinant_e warn "Matrix#determinant_e is deprecated; use #determinant", uplevel: 1 determinant end - alias det_e determinant_e + alias_method :det_e, :determinant_e # # Returns a new matrix resulting by stacking horizontally @@ -1285,7 +1400,7 @@ def trace tr + @rows[i][i] end end - alias tr trace + alias_method :tr, :trace # # Returns the transpose of the matrix. @@ -1301,7 +1416,7 @@ def transpose return self.class.empty(column_count, 0) if row_count.zero? new_matrix @rows.transpose, row_count end - alias t transpose + alias_method :t, :transpose # # Returns a new matrix resulting by stacking vertically @@ -1330,7 +1445,7 @@ def vstack(*matrices) def eigensystem EigenvalueDecomposition.new(self) end - alias eigen eigensystem + alias_method :eigen, :eigensystem # # Returns the LUP decomposition of the matrix; see +LUPDecomposition+. @@ -1345,7 +1460,7 @@ def eigensystem def lup LUPDecomposition.new(self) end - alias lup_decomposition lup + alias_method :lup_decomposition, :lup #-- # COMPLEX ARITHMETIC -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1363,7 +1478,7 @@ def lup def conjugate collect(&:conjugate) end - alias conj conjugate + alias_method :conj, :conjugate # # Returns the imaginary part of the matrix. @@ -1377,7 +1492,7 @@ def conjugate def imaginary collect(&:imaginary) end - alias imag imaginary + alias_method :imag, :imaginary # # Returns the real part of the matrix. @@ -1401,7 +1516,7 @@ def real def rect [real, imag] end - alias rectangular rect + alias_method :rectangular, :rect #-- # CONVERTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -1514,7 +1629,7 @@ module ConversionHelper # :nodoc: # Converts the obj to an Array. If copy is set to true # a copy of obj will be made if necessary. # - def convert_to_array(obj, copy = false) # :nodoc: + private def convert_to_array(obj, copy = false) # :nodoc: case obj when Array copy ? obj.dup : obj @@ -1530,7 +1645,6 @@ def convert_to_array(obj, copy = false) # :nodoc: converted end end - private :convert_to_array end extend ConversionHelper @@ -1540,14 +1654,13 @@ module CoercionHelper # :nodoc: # Applies the operator +oper+ with argument +obj+ # through coercion of +obj+ # - def apply_through_coercion(obj, oper) + private def apply_through_coercion(obj, oper) coercion = obj.coerce(self) raise TypeError unless coercion.is_a?(Array) && coercion.length == 2 coercion[0].public_send(oper, coercion[1]) rescue raise TypeError, "#{obj.inspect} can't be coerced into #{self.class}" end - private :apply_through_coercion # # Helper method to coerce a value into a specific class. @@ -1575,6 +1688,26 @@ def self.coerce_to_int(obj) def self.coerce_to_matrix(obj) coerce_to(obj, Matrix, :to_matrix) end + + # Returns `nil` for non Ranges + # Checks range validity, return canonical range with 0 <= begin <= end < count + def self.check_range(val, count, kind) + canonical = (val.begin + (val.begin < 0 ? count : 0)).. + (val.end ? val.end + (val.end < 0 ? count : 0) - (val.exclude_end? ? 1 : 0) + : count - 1) + unless 0 <= canonical.begin && canonical.begin <= canonical.end && canonical.end < count + raise IndexError, "given range #{val} is outside of #{kind} dimensions: 0...#{count}" + end + canonical + end + + def self.check_int(val, count, kind) + val = CoercionHelper.coerce_to_int(val) + if val >= count || val < -count + raise IndexError, "given #{kind} #{val} is outside of #{-count}...#{count}" + end + val + end end include CoercionHelper @@ -1669,6 +1802,9 @@ def **(other) # To access elements: # * #[](i) # +# To set elements: +# * #[]=(i, v) +# # To enumerate the elements: # * #each2(v) # * #collect2(v) @@ -1691,8 +1827,10 @@ def **(other) # * #inner_product(v), dot(v) # * #cross_product(v), cross(v) # * #collect +# * #collect! # * #magnitude # * #map +# * #map! # * #map2(v) # * #norm # * #normalize @@ -1771,7 +1909,11 @@ def initialize(array) # ACCESSING # - # Returns element number +i+ (starting at zero) of the vector. + # :call-seq: + # vector[range] + # vector[integer] + # + # Returns element or elements of the vector. # def [](i) @elements[i] @@ -1779,12 +1921,44 @@ def [](i) alias element [] alias component [] + # + # :call-seq: + # vector[range] = new_vector + # vector[range] = row_matrix + # vector[range] = new_element + # vector[integer] = new_element + # + # Set element or elements of vector. + # def []=(i, v) - @elements[i]= v + raise FrozenError, "can't modify frozen Vector" if frozen? + if i.is_a?(Range) + range = Matrix::CoercionHelper.check_range(i, size, :vector) + set_range(range, v) + else + index = Matrix::CoercionHelper.check_int(i, size, :index) + set_value(index, v) + end end alias set_element []= alias set_component []= - private :[]=, :set_element, :set_component + private :set_element, :set_component + + private def set_value(index, value) + @elements[index] = value + end + + private def set_range(range, value) + if value.is_a?(Vector) + raise ArgumentError, "vector to be set has wrong size" unless range.size == value.size + @elements[range] = value.elements + elsif value.is_a?(Matrix) + Matrix.Raise ErrDimensionMismatch unless value.row_count == 1 + @elements[range] = value.row(0).elements + else + @elements[range] = Array.new(range.size, value) + end + end # Returns a vector with entries rounded to the given precision # (see Float#round) @@ -1881,6 +2055,20 @@ def zero? all?(&:zero?) end + def freeze + @elements.freeze + super + end + + # + # Called for dup & clone. + # + private def initialize_copy(v) + super + @elements = @elements.dup unless frozen? + end + + #-- # COMPARING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #++ @@ -1898,13 +2086,6 @@ def eql?(other) @elements.eql? other.elements end - # - # Returns a copy of the vector. - # - def clone - self.class.elements(@elements) - end - # # Returns a hash-code for the vector. # @@ -2053,7 +2234,18 @@ def collect(&block) # :yield: e els = @elements.collect(&block) self.class.elements(els, false) end - alias map collect + alias_method :map, :collect + + # + # Like Array#collect! + # + def collect!(&block) + return to_enum(:collect!) unless block_given? + raise FrozenError, "can't modify frozen Vector" if frozen? + @elements.collect!(&block) + self + end + alias map! collect! # # Returns the modulus (Pythagorean distance) of the vector. @@ -2062,8 +2254,8 @@ def collect(&block) # :yield: e def magnitude Math.sqrt(@elements.inject(0) {|v, e| v + e.abs2}) end - alias r magnitude - alias norm magnitude + alias_method :r, :magnitude + alias_method :norm, :magnitude # # Like Vector#collect2, but returns a Vector instead of an Array. diff --git a/lib/matrix/eigenvalue_decomposition.rb b/lib/matrix/eigenvalue_decomposition.rb index 919db9e83d2976..bf6637635a2741 100644 --- a/lib/matrix/eigenvalue_decomposition.rb +++ b/lib/matrix/eigenvalue_decomposition.rb @@ -43,7 +43,7 @@ def initialize(a) def eigenvector_matrix Matrix.send(:new, build_eigenvectors.transpose) end - alias v eigenvector_matrix + alias_method :v, :eigenvector_matrix # Returns the inverse of the eigenvector matrix +V+ # @@ -52,7 +52,7 @@ def eigenvector_matrix_inv r = r.transpose.inverse unless @symmetric r end - alias v_inv eigenvector_matrix_inv + alias_method :v_inv, :eigenvector_matrix_inv # Returns the eigenvalues in an array # @@ -73,7 +73,7 @@ def eigenvectors def eigenvalue_matrix Matrix.diagonal(*eigenvalues) end - alias d eigenvalue_matrix + alias_method :d, :eigenvalue_matrix # Returns [eigenvector_matrix, eigenvalue_matrix, eigenvector_matrix_inv] # @@ -82,8 +82,8 @@ def to_ary end alias_method :to_a, :to_ary - private - def build_eigenvectors + + private def build_eigenvectors # JAMA stores complex eigenvectors in a strange way # See http://web.archive.org/web/20111016032731/http://cio.nist.gov/esd/emaildir/lists/jama/msg01021.html @e.each_with_index.map do |imag, i| @@ -96,9 +96,10 @@ def build_eigenvectors end end end + # Complex scalar division. - def cdiv(xr, xi, yr, yi) + private def cdiv(xr, xi, yr, yi) if (yr.abs > yi.abs) r = yi/yr d = yr + r*yi @@ -113,7 +114,7 @@ def cdiv(xr, xi, yr, yi) # Symmetric Householder reduction to tridiagonal form. - def tridiagonalize + private def tridiagonalize # This is derived from the Algol procedures tred2 by # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for @@ -231,7 +232,7 @@ def tridiagonalize # Symmetric tridiagonal QL algorithm. - def diagonalize + private def diagonalize # This is derived from the Algol procedures tql2, by # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for # Auto. Comp., Vol.ii-Linear Algebra, and the corresponding @@ -350,7 +351,7 @@ def diagonalize # Nonsymmetric reduction to Hessenberg form. - def reduce_to_hessenberg + private def reduce_to_hessenberg # This is derived from the Algol procedures orthes and ortran, # by Martin and Wilkinson, Handbook for Auto. Comp., # Vol.ii-Linear Algebra, and the corresponding @@ -440,11 +441,9 @@ def reduce_to_hessenberg end end - - # Nonsymmetric reduction from Hessenberg to real Schur form. - def hessenberg_to_real_schur + private def hessenberg_to_real_schur # This is derived from the Algol procedure hqr2, # by Martin and Wilkinson, Handbook for Auto. Comp., diff --git a/lib/mkmf.rb b/lib/mkmf.rb index a1cf99e3de92c7..355ea171ee8adc 100644 --- a/lib/mkmf.rb +++ b/lib/mkmf.rb @@ -1971,6 +1971,7 @@ def configuration(srcdir) headers << '$(RUBY_EXTCONF_H)' if $extconf_h mk << %{ +CC_WRAPPER = #{CONFIG['CC_WRAPPER']} CC = #{CONFIG['CC']} CXX = #{CONFIG['CXX']} LIBRUBY = #{CONFIG['LIBRUBY']} @@ -2519,6 +2520,9 @@ def init_mkmf(config = CONFIG, rbconfig = RbConfig::CONFIG) end $warnflags = config['warnflags'] unless $extmk end + if (w = rbconfig['CC_WRAPPER']) and !w.empty? and !File.executable?(w) + rbconfig['CC_WRAPPER'] = config['CC_WRAPPER'] = '' + end $CFLAGS = with_config("cflags", arg_config("CFLAGS", config["CFLAGS"])).dup $CXXFLAGS = (with_config("cxxflags", arg_config("CXXFLAGS", config["CXXFLAGS"]))||'').dup $ARCH_FLAG = with_config("arch_flag", arg_config("ARCH_FLAG", config["ARCH_FLAG"])).dup diff --git a/lib/monitor.rb b/lib/monitor.rb index 288ed755eabb35..2a59fa7cc0370c 100644 --- a/lib/monitor.rb +++ b/lib/monitor.rb @@ -103,13 +103,17 @@ class Timeout < Exception; end # even if no other thread doesn't signal. # def wait(timeout = nil) - @monitor.__send__(:mon_check_owner) - count = @monitor.__send__(:mon_exit_for_cond) - begin - @cond.wait(@monitor.instance_variable_get(:@mon_mutex), timeout) - return true - ensure - @monitor.__send__(:mon_enter_for_cond, count) + Thread.handle_interrupt(Exception => :never) do + @monitor.__send__(:mon_check_owner) + count = @monitor.__send__(:mon_exit_for_cond) + begin + Thread.handle_interrupt(Exception => :immediate) do + @cond.wait(@monitor.instance_variable_get(:@mon_mutex), timeout) + end + return true + ensure + @monitor.__send__(:mon_enter_for_cond, count) + end end end diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb index 9902f9dc657a08..e68d825dcf53f2 100644 --- a/lib/net/ftp.rb +++ b/lib/net/ftp.rb @@ -17,7 +17,7 @@ require "socket" require "monitor" -require "net/protocol" +require_relative "protocol" require "time" begin require "openssl" diff --git a/lib/net/http/status.rb b/lib/net/http/status.rb index cd4dcb095e1d20..b3995f763f1d34 100644 --- a/lib/net/http/status.rb +++ b/lib/net/http/status.rb @@ -1,7 +1,7 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'net/http' +require_relative '../http' if $0 == __FILE__ require 'open-uri' diff --git a/lib/net/https.rb b/lib/net/https.rb index 58cb6ddf192621..d46721c82a582a 100644 --- a/lib/net/https.rb +++ b/lib/net/https.rb @@ -19,5 +19,5 @@ =end -require 'net/http' +require_relative 'http' require 'openssl' diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 4debec8d5426de..fa9b19071a9712 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -18,7 +18,7 @@ require "monitor" require "digest/md5" require "strscan" -require 'net/protocol' +require_relative 'protocol' begin require "openssl" rescue LoadError diff --git a/lib/net/pop.rb b/lib/net/pop.rb index 92a4fe7303b30e..a6374cd78c824e 100644 --- a/lib/net/pop.rb +++ b/lib/net/pop.rb @@ -21,7 +21,7 @@ # See Net::POP3 for documentation. # -require 'net/protocol' +require_relative 'protocol' require 'digest/md5' require 'timeout' diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index 1777a7fa7e9b7b..86b55d278b726a 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -17,7 +17,7 @@ # See Net::SMTP for documentation. # -require 'net/protocol' +require_relative 'protocol' require 'digest/md5' require 'timeout' begin diff --git a/lib/optparse.rb b/lib/optparse.rb index d3e8bf398117bd..d3b504a4137114 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -534,8 +534,9 @@ def self.pattern def initialize(pattern = nil, conv = nil, short = nil, long = nil, arg = nil, - desc = ([] if short or long), block = Proc.new) + desc = ([] if short or long), block = nil, &_block) raise if Array === pattern + block ||= _block @pattern, @conv, @short, @long, @arg, @desc, @block = pattern, conv, short, long, arg, desc, block end @@ -654,7 +655,7 @@ def compsys(sdone, ldone) # :nodoc: return if sopts.empty? and lopts.empty? # completely hidden (sopts+lopts).each do |opt| - # "(-x -c -r)-l[left justify]" \ + # "(-x -c -r)-l[left justify]" if /^--\[no-\](.+)$/ =~ opt o = $1 yield("--#{o}", desc.join("")) diff --git a/lib/rdoc/markup/to_markdown.rb b/lib/rdoc/markup/to_markdown.rb index 79328154051c65..3ee48becb0d36f 100644 --- a/lib/rdoc/markup/to_markdown.rb +++ b/lib/rdoc/markup/to_markdown.rb @@ -131,7 +131,7 @@ def accept_verbatim verbatim @res << part end - @res << "\n" unless @res =~ /\n\z/ + @res << "\n" end ## diff --git a/lib/rdoc/markup/to_rdoc.rb b/lib/rdoc/markup/to_rdoc.rb index 3aee85afbe9f4c..81b16c49739f2b 100644 --- a/lib/rdoc/markup/to_rdoc.rb +++ b/lib/rdoc/markup/to_rdoc.rb @@ -234,7 +234,7 @@ def accept_verbatim verbatim @res << part end - @res << "\n" unless @res =~ /\n\z/ + @res << "\n" end ## diff --git a/lib/rdoc/rdoc.gemspec b/lib/rdoc/rdoc.gemspec index 99bf3a63f58f29..9c6fbe1970f242 100644 --- a/lib/rdoc/rdoc.gemspec +++ b/lib/rdoc/rdoc.gemspec @@ -32,7 +32,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat s.executables = ["rdoc", "ri"] s.require_paths = ["lib"] # for ruby core repository. It was generated by `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - s.files = [".document", ".gitignore", ".travis.yml", "CONTRIBUTING.rdoc", "CVE-2013-0256.rdoc", "ExampleMarkdown.md", "ExampleRDoc.rdoc", "Gemfile", "History.rdoc", "LEGAL.rdoc", "LICENSE.rdoc", "README.rdoc", "RI.rdoc", "Rakefile", "TODO.rdoc", "appveyor.yml", "bin/console", "bin/setup", "exe/rdoc", "exe/ri", "lib/rdoc.rb", "lib/rdoc/alias.rb", "lib/rdoc/anon_class.rb", "lib/rdoc/any_method.rb", "lib/rdoc/attr.rb", "lib/rdoc/class_module.rb", "lib/rdoc/code_object.rb", "lib/rdoc/code_objects.rb", "lib/rdoc/comment.rb", "lib/rdoc/constant.rb", "lib/rdoc/context.rb", "lib/rdoc/context/section.rb", "lib/rdoc/cross_reference.rb", "lib/rdoc/encoding.rb", "lib/rdoc/erb_partial.rb", "lib/rdoc/erbio.rb", "lib/rdoc/extend.rb", "lib/rdoc/generator.rb", "lib/rdoc/generator/darkfish.rb", "lib/rdoc/generator/json_index.rb", "lib/rdoc/generator/markup.rb", "lib/rdoc/generator/pot.rb", "lib/rdoc/generator/pot/message_extractor.rb", "lib/rdoc/generator/pot/po.rb", "lib/rdoc/generator/pot/po_entry.rb", "lib/rdoc/generator/ri.rb", "lib/rdoc/generator/template/darkfish/.document", "lib/rdoc/generator/template/darkfish/_footer.rhtml", "lib/rdoc/generator/template/darkfish/_head.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_VCS_info.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_classes.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_extends.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_in_files.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_includes.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_installed.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_methods.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_navigation.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_parent.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_search.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_sections.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml", "lib/rdoc/generator/template/darkfish/class.rhtml", "lib/rdoc/generator/template/darkfish/css/fonts.css", "lib/rdoc/generator/template/darkfish/css/rdoc.css", "lib/rdoc/generator/template/darkfish/fonts/Lato-Light.ttf", "lib/rdoc/generator/template/darkfish/fonts/Lato-LightItalic.ttf", "lib/rdoc/generator/template/darkfish/fonts/Lato-Regular.ttf", "lib/rdoc/generator/template/darkfish/fonts/Lato-RegularItalic.ttf", "lib/rdoc/generator/template/darkfish/fonts/SourceCodePro-Bold.ttf", "lib/rdoc/generator/template/darkfish/fonts/SourceCodePro-Regular.ttf", "lib/rdoc/generator/template/darkfish/images/add.png", "lib/rdoc/generator/template/darkfish/images/arrow_up.png", "lib/rdoc/generator/template/darkfish/images/brick.png", "lib/rdoc/generator/template/darkfish/images/brick_link.png", "lib/rdoc/generator/template/darkfish/images/bug.png", "lib/rdoc/generator/template/darkfish/images/bullet_black.png", "lib/rdoc/generator/template/darkfish/images/bullet_toggle_minus.png", "lib/rdoc/generator/template/darkfish/images/bullet_toggle_plus.png", "lib/rdoc/generator/template/darkfish/images/date.png", "lib/rdoc/generator/template/darkfish/images/delete.png", "lib/rdoc/generator/template/darkfish/images/find.png", "lib/rdoc/generator/template/darkfish/images/loadingAnimation.gif", "lib/rdoc/generator/template/darkfish/images/macFFBgHack.png", "lib/rdoc/generator/template/darkfish/images/package.png", "lib/rdoc/generator/template/darkfish/images/page_green.png", "lib/rdoc/generator/template/darkfish/images/page_white_text.png", "lib/rdoc/generator/template/darkfish/images/page_white_width.png", "lib/rdoc/generator/template/darkfish/images/plugin.png", "lib/rdoc/generator/template/darkfish/images/ruby.png", "lib/rdoc/generator/template/darkfish/images/tag_blue.png", "lib/rdoc/generator/template/darkfish/images/tag_green.png", "lib/rdoc/generator/template/darkfish/images/transparent.png", "lib/rdoc/generator/template/darkfish/images/wrench.png", "lib/rdoc/generator/template/darkfish/images/wrench_orange.png", "lib/rdoc/generator/template/darkfish/images/zoom.png", "lib/rdoc/generator/template/darkfish/index.rhtml", "lib/rdoc/generator/template/darkfish/js/darkfish.js", "lib/rdoc/generator/template/darkfish/js/jquery.js", "lib/rdoc/generator/template/darkfish/js/search.js", "lib/rdoc/generator/template/darkfish/page.rhtml", "lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml", "lib/rdoc/generator/template/darkfish/servlet_root.rhtml", "lib/rdoc/generator/template/darkfish/table_of_contents.rhtml", "lib/rdoc/generator/template/json_index/.document", "lib/rdoc/generator/template/json_index/js/navigation.js", "lib/rdoc/generator/template/json_index/js/searcher.js", "lib/rdoc/ghost_method.rb", "lib/rdoc/i18n.rb", "lib/rdoc/i18n/locale.rb", "lib/rdoc/i18n/text.rb", "lib/rdoc/include.rb", "lib/rdoc/known_classes.rb", "lib/rdoc/markdown.kpeg", "lib/rdoc/markdown/entities.rb", "lib/rdoc/markdown/literals.kpeg", "lib/rdoc/markup.rb", "lib/rdoc/markup/attr_changer.rb", "lib/rdoc/markup/attr_span.rb", "lib/rdoc/markup/attribute_manager.rb", "lib/rdoc/markup/attributes.rb", "lib/rdoc/markup/blank_line.rb", "lib/rdoc/markup/block_quote.rb", "lib/rdoc/markup/document.rb", "lib/rdoc/markup/formatter.rb", "lib/rdoc/markup/formatter_test_case.rb", "lib/rdoc/markup/hard_break.rb", "lib/rdoc/markup/heading.rb", "lib/rdoc/markup/include.rb", "lib/rdoc/markup/indented_paragraph.rb", "lib/rdoc/markup/list.rb", "lib/rdoc/markup/list_item.rb", "lib/rdoc/markup/paragraph.rb", "lib/rdoc/markup/parser.rb", "lib/rdoc/markup/pre_process.rb", "lib/rdoc/markup/raw.rb", "lib/rdoc/markup/regexp_handling.rb", "lib/rdoc/markup/rule.rb", "lib/rdoc/markup/text_formatter_test_case.rb", "lib/rdoc/markup/to_ansi.rb", "lib/rdoc/markup/to_bs.rb", "lib/rdoc/markup/to_html.rb", "lib/rdoc/markup/to_html_crossref.rb", "lib/rdoc/markup/to_html_snippet.rb", "lib/rdoc/markup/to_joined_paragraph.rb", "lib/rdoc/markup/to_label.rb", "lib/rdoc/markup/to_markdown.rb", "lib/rdoc/markup/to_rdoc.rb", "lib/rdoc/markup/to_table_of_contents.rb", "lib/rdoc/markup/to_test.rb", "lib/rdoc/markup/to_tt_only.rb", "lib/rdoc/markup/verbatim.rb", "lib/rdoc/meta_method.rb", "lib/rdoc/method_attr.rb", "lib/rdoc/mixin.rb", "lib/rdoc/normal_class.rb", "lib/rdoc/normal_module.rb", "lib/rdoc/options.rb", "lib/rdoc/parser.rb", "lib/rdoc/parser/c.rb", "lib/rdoc/parser/changelog.rb", "lib/rdoc/parser/markdown.rb", "lib/rdoc/parser/rd.rb", "lib/rdoc/parser/ripper_state_lex.rb", "lib/rdoc/parser/ruby.rb", "lib/rdoc/parser/ruby_tools.rb", "lib/rdoc/parser/simple.rb", "lib/rdoc/parser/text.rb", "lib/rdoc/rd.rb", "lib/rdoc/rd/block_parser.ry", "lib/rdoc/rd/inline.rb", "lib/rdoc/rd/inline_parser.ry", "lib/rdoc/rdoc.rb", "lib/rdoc/require.rb", "lib/rdoc/ri.rb", "lib/rdoc/ri/driver.rb", "lib/rdoc/ri/formatter.rb", "lib/rdoc/ri/paths.rb", "lib/rdoc/ri/store.rb", "lib/rdoc/ri/task.rb", "lib/rdoc/rubygems_hook.rb", "lib/rdoc/servlet.rb", "lib/rdoc/single_class.rb", "lib/rdoc/stats.rb", "lib/rdoc/stats/normal.rb", "lib/rdoc/stats/quiet.rb", "lib/rdoc/stats/verbose.rb", "lib/rdoc/store.rb", "lib/rdoc/task.rb", "lib/rdoc/text.rb", "lib/rdoc/token_stream.rb", "lib/rdoc/tom_doc.rb", "lib/rdoc/top_level.rb", "lib/rdoc/version.rb", "rdoc.gemspec"] + s.files = [".document", ".gitignore", ".travis.yml", "CONTRIBUTING.rdoc", "CVE-2013-0256.rdoc", "ExampleMarkdown.md", "ExampleRDoc.rdoc", "Gemfile", "History.rdoc", "LEGAL.rdoc", "LICENSE.rdoc", "README.rdoc", "RI.rdoc", "Rakefile", "TODO.rdoc", "appveyor.yml", "bin/console", "bin/setup", "exe/rdoc", "exe/ri", "lib/rdoc.rb", "lib/rdoc/alias.rb", "lib/rdoc/anon_class.rb", "lib/rdoc/any_method.rb", "lib/rdoc/attr.rb", "lib/rdoc/class_module.rb", "lib/rdoc/code_object.rb", "lib/rdoc/code_objects.rb", "lib/rdoc/comment.rb", "lib/rdoc/constant.rb", "lib/rdoc/context.rb", "lib/rdoc/context/section.rb", "lib/rdoc/cross_reference.rb", "lib/rdoc/encoding.rb", "lib/rdoc/erb_partial.rb", "lib/rdoc/erbio.rb", "lib/rdoc/extend.rb", "lib/rdoc/generator.rb", "lib/rdoc/generator/darkfish.rb", "lib/rdoc/generator/json_index.rb", "lib/rdoc/generator/markup.rb", "lib/rdoc/generator/pot.rb", "lib/rdoc/generator/pot/message_extractor.rb", "lib/rdoc/generator/pot/po.rb", "lib/rdoc/generator/pot/po_entry.rb", "lib/rdoc/generator/ri.rb", "lib/rdoc/generator/template/darkfish/.document", "lib/rdoc/generator/template/darkfish/_footer.rhtml", "lib/rdoc/generator/template/darkfish/_head.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_VCS_info.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_classes.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_extends.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_in_files.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_includes.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_installed.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_methods.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_navigation.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_parent.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_search.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_sections.rhtml", "lib/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml", "lib/rdoc/generator/template/darkfish/class.rhtml", "lib/rdoc/generator/template/darkfish/css/fonts.css", "lib/rdoc/generator/template/darkfish/css/rdoc.css", "lib/rdoc/generator/template/darkfish/fonts/Lato-Light.ttf", "lib/rdoc/generator/template/darkfish/fonts/Lato-LightItalic.ttf", "lib/rdoc/generator/template/darkfish/fonts/Lato-Regular.ttf", "lib/rdoc/generator/template/darkfish/fonts/Lato-RegularItalic.ttf", "lib/rdoc/generator/template/darkfish/fonts/SourceCodePro-Bold.ttf", "lib/rdoc/generator/template/darkfish/fonts/SourceCodePro-Regular.ttf", "lib/rdoc/generator/template/darkfish/images/add.png", "lib/rdoc/generator/template/darkfish/images/arrow_up.png", "lib/rdoc/generator/template/darkfish/images/brick.png", "lib/rdoc/generator/template/darkfish/images/brick_link.png", "lib/rdoc/generator/template/darkfish/images/bug.png", "lib/rdoc/generator/template/darkfish/images/bullet_black.png", "lib/rdoc/generator/template/darkfish/images/bullet_toggle_minus.png", "lib/rdoc/generator/template/darkfish/images/bullet_toggle_plus.png", "lib/rdoc/generator/template/darkfish/images/date.png", "lib/rdoc/generator/template/darkfish/images/delete.png", "lib/rdoc/generator/template/darkfish/images/find.png", "lib/rdoc/generator/template/darkfish/images/loadingAnimation.gif", "lib/rdoc/generator/template/darkfish/images/macFFBgHack.png", "lib/rdoc/generator/template/darkfish/images/package.png", "lib/rdoc/generator/template/darkfish/images/page_green.png", "lib/rdoc/generator/template/darkfish/images/page_white_text.png", "lib/rdoc/generator/template/darkfish/images/page_white_width.png", "lib/rdoc/generator/template/darkfish/images/plugin.png", "lib/rdoc/generator/template/darkfish/images/ruby.png", "lib/rdoc/generator/template/darkfish/images/tag_blue.png", "lib/rdoc/generator/template/darkfish/images/tag_green.png", "lib/rdoc/generator/template/darkfish/images/transparent.png", "lib/rdoc/generator/template/darkfish/images/wrench.png", "lib/rdoc/generator/template/darkfish/images/wrench_orange.png", "lib/rdoc/generator/template/darkfish/images/zoom.png", "lib/rdoc/generator/template/darkfish/index.rhtml", "lib/rdoc/generator/template/darkfish/js/darkfish.js", "lib/rdoc/generator/template/darkfish/js/jquery.js", "lib/rdoc/generator/template/darkfish/js/search.js", "lib/rdoc/generator/template/darkfish/page.rhtml", "lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml", "lib/rdoc/generator/template/darkfish/servlet_root.rhtml", "lib/rdoc/generator/template/darkfish/table_of_contents.rhtml", "lib/rdoc/generator/template/json_index/.document", "lib/rdoc/generator/template/json_index/js/navigation.js", "lib/rdoc/generator/template/json_index/js/searcher.js", "lib/rdoc/ghost_method.rb", "lib/rdoc/i18n.rb", "lib/rdoc/i18n/locale.rb", "lib/rdoc/i18n/text.rb", "lib/rdoc/include.rb", "lib/rdoc/known_classes.rb", "lib/rdoc/markdown.kpeg", "lib/rdoc/markdown/entities.rb", "lib/rdoc/markdown/literals.kpeg", "lib/rdoc/markup.rb", "lib/rdoc/markup/attr_changer.rb", "lib/rdoc/markup/attr_span.rb", "lib/rdoc/markup/attribute_manager.rb", "lib/rdoc/markup/attributes.rb", "lib/rdoc/markup/blank_line.rb", "lib/rdoc/markup/block_quote.rb", "lib/rdoc/markup/document.rb", "lib/rdoc/markup/formatter.rb", "lib/rdoc/markup/formatter_test_case.rb", "lib/rdoc/markup/hard_break.rb", "lib/rdoc/markup/heading.rb", "lib/rdoc/markup/include.rb", "lib/rdoc/markup/indented_paragraph.rb", "lib/rdoc/markup/list.rb", "lib/rdoc/markup/list_item.rb", "lib/rdoc/markup/paragraph.rb", "lib/rdoc/markup/parser.rb", "lib/rdoc/markup/pre_process.rb", "lib/rdoc/markup/raw.rb", "lib/rdoc/markup/rule.rb", "lib/rdoc/markup/text_formatter_test_case.rb", "lib/rdoc/markup/to_ansi.rb", "lib/rdoc/markup/to_bs.rb", "lib/rdoc/markup/to_html.rb", "lib/rdoc/markup/to_html_crossref.rb", "lib/rdoc/markup/to_html_snippet.rb", "lib/rdoc/markup/to_joined_paragraph.rb", "lib/rdoc/markup/to_label.rb", "lib/rdoc/markup/to_markdown.rb", "lib/rdoc/markup/to_rdoc.rb", "lib/rdoc/markup/to_table_of_contents.rb", "lib/rdoc/markup/to_test.rb", "lib/rdoc/markup/to_tt_only.rb", "lib/rdoc/markup/verbatim.rb", "lib/rdoc/meta_method.rb", "lib/rdoc/method_attr.rb", "lib/rdoc/mixin.rb", "lib/rdoc/normal_class.rb", "lib/rdoc/normal_module.rb", "lib/rdoc/options.rb", "lib/rdoc/parser.rb", "lib/rdoc/parser/c.rb", "lib/rdoc/parser/changelog.rb", "lib/rdoc/parser/markdown.rb", "lib/rdoc/parser/rd.rb", "lib/rdoc/parser/ripper_state_lex.rb", "lib/rdoc/parser/ruby.rb", "lib/rdoc/parser/ruby_tools.rb", "lib/rdoc/parser/simple.rb", "lib/rdoc/parser/text.rb", "lib/rdoc/rd.rb", "lib/rdoc/rd/block_parser.ry", "lib/rdoc/rd/inline.rb", "lib/rdoc/rd/inline_parser.ry", "lib/rdoc/rdoc.rb", "lib/rdoc/require.rb", "lib/rdoc/ri.rb", "lib/rdoc/ri/driver.rb", "lib/rdoc/ri/formatter.rb", "lib/rdoc/ri/paths.rb", "lib/rdoc/ri/store.rb", "lib/rdoc/ri/task.rb", "lib/rdoc/rubygems_hook.rb", "lib/rdoc/servlet.rb", "lib/rdoc/single_class.rb", "lib/rdoc/stats.rb", "lib/rdoc/stats/normal.rb", "lib/rdoc/stats/quiet.rb", "lib/rdoc/stats/verbose.rb", "lib/rdoc/store.rb", "lib/rdoc/task.rb", "lib/rdoc/text.rb", "lib/rdoc/token_stream.rb", "lib/rdoc/tom_doc.rb", "lib/rdoc/top_level.rb", "lib/rdoc/version.rb", "rdoc.gemspec"] # files from .gitignore s.files << "lib/rdoc/rd/block_parser.rb" << "lib/rdoc/rd/inline_parser.rb" << "lib/rdoc/markdown.rb" << "lib/rdoc/markdown/literals.rb" diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb index c4d1dd03dfcc4a..46b98e99b5307e 100644 --- a/lib/rdoc/ri/driver.rb +++ b/lib/rdoc/ri/driver.rb @@ -1468,7 +1468,7 @@ def render_method_comment out, method, alias_for = nil# :nodoc: out << method.comment end out << RDoc::Markup::BlankLine.new - out << RDoc::Markup::Paragraph.new("(this method is alias for #{alias_for.full_name})") + out << RDoc::Markup::Paragraph.new("(This method is an alias for #{alias_for.full_name}.)") out << RDoc::Markup::BlankLine.new out << alias_for.comment out << RDoc::Markup::BlankLine.new diff --git a/lib/rexml/attlistdecl.rb b/lib/rexml/attlistdecl.rb index dc1d2add0b8995..44a91d66d63158 100644 --- a/lib/rexml/attlistdecl.rb +++ b/lib/rexml/attlistdecl.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false #vim:ts=2 sw=2 noexpandtab: -require 'rexml/child' -require 'rexml/source' +require_relative 'child' +require_relative 'source' module REXML # This class needs: diff --git a/lib/rexml/attribute.rb b/lib/rexml/attribute.rb index ca5984e178f2e5..4ae8b10062f676 100644 --- a/lib/rexml/attribute.rb +++ b/lib/rexml/attribute.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require "rexml/namespace" -require 'rexml/text' +require_relative "namespace" +require_relative 'text' module REXML # Defines an Element Attribute; IE, a attribute=value pair, as in: diff --git a/lib/rexml/cdata.rb b/lib/rexml/cdata.rb index 2238446dc4e522..997f5a08dbcea1 100644 --- a/lib/rexml/cdata.rb +++ b/lib/rexml/cdata.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/text" +require_relative "text" module REXML class CData < Text diff --git a/lib/rexml/child.rb b/lib/rexml/child.rb index d23451e71ec843..cc6e9a471991b0 100644 --- a/lib/rexml/child.rb +++ b/lib/rexml/child.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/node" +require_relative "node" module REXML ## diff --git a/lib/rexml/comment.rb b/lib/rexml/comment.rb index 822fe0d5864348..52c58b46f6fa09 100644 --- a/lib/rexml/comment.rb +++ b/lib/rexml/comment.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/child" +require_relative "child" module REXML ## diff --git a/lib/rexml/doctype.rb b/lib/rexml/doctype.rb index 1eb1f5b4e18f8e..ca44454decffaf 100644 --- a/lib/rexml/doctype.rb +++ b/lib/rexml/doctype.rb @@ -1,10 +1,10 @@ # frozen_string_literal: false -require "rexml/parent" -require "rexml/parseexception" -require "rexml/namespace" -require 'rexml/entity' -require 'rexml/attlistdecl' -require 'rexml/xmltokens' +require_relative "parent" +require_relative "parseexception" +require_relative "namespace" +require_relative 'entity' +require_relative 'attlistdecl' +require_relative 'xmltokens' module REXML # Represents an XML DOCTYPE declaration; that is, the contents of -1 if transitive - require "rexml/formatters/transitive" + require_relative "formatters/transitive" REXML::Formatters::Transitive.new( indent, ie_hack ) else REXML::Formatters::Pretty.new( indent, ie_hack ) diff --git a/lib/rexml/dtd/attlistdecl.rb b/lib/rexml/dtd/attlistdecl.rb index 32847daadb2975..1326cb21e4c0f9 100644 --- a/lib/rexml/dtd/attlistdecl.rb +++ b/lib/rexml/dtd/attlistdecl.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/child" +require_relative "../child" module REXML module DTD class AttlistDecl < Child diff --git a/lib/rexml/dtd/dtd.rb b/lib/rexml/dtd/dtd.rb index 927d5d847b1a36..8b0f2d753afe21 100644 --- a/lib/rexml/dtd/dtd.rb +++ b/lib/rexml/dtd/dtd.rb @@ -1,10 +1,10 @@ # frozen_string_literal: false -require "rexml/dtd/elementdecl" -require "rexml/dtd/entitydecl" -require "rexml/comment" -require "rexml/dtd/notationdecl" -require "rexml/dtd/attlistdecl" -require "rexml/parent" +require_relative "elementdecl" +require_relative "entitydecl" +require_relative "../comment" +require_relative "notationdecl" +require_relative "attlistdecl" +require_relative "../parent" module REXML module DTD diff --git a/lib/rexml/dtd/elementdecl.rb b/lib/rexml/dtd/elementdecl.rb index 119fd41a8f6140..20ed0232441c2f 100644 --- a/lib/rexml/dtd/elementdecl.rb +++ b/lib/rexml/dtd/elementdecl.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/child" +require_relative "../child" module REXML module DTD class ElementDecl < Child diff --git a/lib/rexml/dtd/entitydecl.rb b/lib/rexml/dtd/entitydecl.rb index 45707e2f42d009..312df655ff26a8 100644 --- a/lib/rexml/dtd/entitydecl.rb +++ b/lib/rexml/dtd/entitydecl.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/child" +require_relative "../child" module REXML module DTD class EntityDecl < Child diff --git a/lib/rexml/dtd/notationdecl.rb b/lib/rexml/dtd/notationdecl.rb index cfdf0b9b74a10e..04a9b08aa7ddb8 100644 --- a/lib/rexml/dtd/notationdecl.rb +++ b/lib/rexml/dtd/notationdecl.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/child" +require_relative "../child" module REXML module DTD class NotationDecl < Child diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb index 1ef22d29a2fbd3..7903d83453211b 100644 --- a/lib/rexml/element.rb +++ b/lib/rexml/element.rb @@ -1,10 +1,10 @@ # frozen_string_literal: false -require "rexml/parent" -require "rexml/namespace" -require "rexml/attribute" -require "rexml/cdata" -require "rexml/xpath" -require "rexml/parseexception" +require_relative "parent" +require_relative "namespace" +require_relative "attribute" +require_relative "cdata" +require_relative "xpath" +require_relative "parseexception" module REXML # An implementation note about namespaces: @@ -713,7 +713,7 @@ def write(output=$stdout, indent=-1, transitive=false, ie_hack=false) Kernel.warn("#{self.class.name}.write is deprecated. See REXML::Formatters", uplevel: 1) formatter = if indent > -1 if transitive - require "rexml/formatters/transitive" + require_relative "formatters/transitive" REXML::Formatters::Transitive.new( indent, ie_hack ) else REXML::Formatters::Pretty.new( indent, ie_hack ) diff --git a/lib/rexml/entity.rb b/lib/rexml/entity.rb index 97c7b6b42fa898..d6fd5edd0dee2f 100644 --- a/lib/rexml/entity.rb +++ b/lib/rexml/entity.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require 'rexml/child' -require 'rexml/source' -require 'rexml/xmltokens' +require_relative 'child' +require_relative 'source' +require_relative 'xmltokens' module REXML class Entity < Child diff --git a/lib/rexml/formatters/pretty.rb b/lib/rexml/formatters/pretty.rb index a80274bdad529a..562ef9462e1797 100644 --- a/lib/rexml/formatters/pretty.rb +++ b/lib/rexml/formatters/pretty.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rexml/formatters/default' +require_relative 'default' module REXML module Formatters diff --git a/lib/rexml/formatters/transitive.rb b/lib/rexml/formatters/transitive.rb index 81e67f3274ef3a..5ff51e10f3720f 100644 --- a/lib/rexml/formatters/transitive.rb +++ b/lib/rexml/formatters/transitive.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rexml/formatters/pretty' +require_relative 'pretty' module REXML module Formatters diff --git a/lib/rexml/instruction.rb b/lib/rexml/instruction.rb index c4f65eefc14df2..2552f3e4425714 100644 --- a/lib/rexml/instruction.rb +++ b/lib/rexml/instruction.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require "rexml/child" -require "rexml/source" +require_relative "child" +require_relative "source" module REXML # Represents an XML Instruction; IE, diff --git a/lib/rexml/light/node.rb b/lib/rexml/light/node.rb index d58119a3a44ebf..01177c64d2846a 100644 --- a/lib/rexml/light/node.rb +++ b/lib/rexml/light/node.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rexml/xmltokens' +require_relative '../xmltokens' # [ :element, parent, name, attributes, children* ] # a = Node.new diff --git a/lib/rexml/namespace.rb b/lib/rexml/namespace.rb index 90ba7cc635bc8e..4a7174eaebc24c 100644 --- a/lib/rexml/namespace.rb +++ b/lib/rexml/namespace.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rexml/xmltokens' +require_relative 'xmltokens' module REXML # Adds named attributes to an object. diff --git a/lib/rexml/node.rb b/lib/rexml/node.rb index 52337ade44ee8a..081caba6cb7c0c 100644 --- a/lib/rexml/node.rb +++ b/lib/rexml/node.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require "rexml/parseexception" -require "rexml/formatters/pretty" -require "rexml/formatters/default" +require_relative "parseexception" +require_relative "formatters/pretty" +require_relative "formatters/default" module REXML # Represents a node in the tree. Nodes are never encountered except as diff --git a/lib/rexml/output.rb b/lib/rexml/output.rb index 96dfea570ed874..88a5fb378d9d12 100644 --- a/lib/rexml/output.rb +++ b/lib/rexml/output.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rexml/encoding' +require_relative 'encoding' module REXML class Output diff --git a/lib/rexml/parent.rb b/lib/rexml/parent.rb index 3bd0a962555032..6a53b37a123b65 100644 --- a/lib/rexml/parent.rb +++ b/lib/rexml/parent.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/child" +require_relative "child" module REXML # A parent has children, and has methods for accessing them. The Parent diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index 80eeb0fa79c1a6..4df1f57a05a840 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require 'rexml/parseexception' -require 'rexml/undefinednamespaceexception' -require 'rexml/source' +require_relative '../parseexception' +require_relative '../undefinednamespaceexception' +require_relative '../source' require 'set' module REXML diff --git a/lib/rexml/parsers/lightparser.rb b/lib/rexml/parsers/lightparser.rb index f0601ae51b1372..bdc08276a9ac4d 100644 --- a/lib/rexml/parsers/lightparser.rb +++ b/lib/rexml/parsers/lightparser.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require 'rexml/parsers/streamparser' -require 'rexml/parsers/baseparser' -require 'rexml/light/node' +require_relative 'streamparser' +require_relative 'baseparser' +require_relative '../light/node' module REXML module Parsers diff --git a/lib/rexml/parsers/pullparser.rb b/lib/rexml/parsers/pullparser.rb index 8c4921755333c3..f8b232a2cd35a5 100644 --- a/lib/rexml/parsers/pullparser.rb +++ b/lib/rexml/parsers/pullparser.rb @@ -1,9 +1,9 @@ # frozen_string_literal: false require 'forwardable' -require 'rexml/parseexception' -require 'rexml/parsers/baseparser' -require 'rexml/xmltokens' +require_relative '../parseexception' +require_relative 'baseparser' +require_relative '../xmltokens' module REXML module Parsers diff --git a/lib/rexml/parsers/sax2parser.rb b/lib/rexml/parsers/sax2parser.rb index 1386f69c83fe56..6a24ce2227a522 100644 --- a/lib/rexml/parsers/sax2parser.rb +++ b/lib/rexml/parsers/sax2parser.rb @@ -1,8 +1,8 @@ # frozen_string_literal: false -require 'rexml/parsers/baseparser' -require 'rexml/parseexception' -require 'rexml/namespace' -require 'rexml/text' +require_relative 'baseparser' +require_relative '../parseexception' +require_relative '../namespace' +require_relative '../text' module REXML module Parsers diff --git a/lib/rexml/parsers/streamparser.rb b/lib/rexml/parsers/streamparser.rb index f6a8bfa802cd67..9e0eb0b363c4eb 100644 --- a/lib/rexml/parsers/streamparser.rb +++ b/lib/rexml/parsers/streamparser.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rexml/parsers/baseparser" +require_relative "baseparser" module REXML module Parsers diff --git a/lib/rexml/parsers/treeparser.rb b/lib/rexml/parsers/treeparser.rb index fc0993c72a5aee..bf9a42545b8084 100644 --- a/lib/rexml/parsers/treeparser.rb +++ b/lib/rexml/parsers/treeparser.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rexml/validation/validationexception' -require 'rexml/undefinednamespaceexception' +require_relative '../validation/validationexception' +require_relative '../undefinednamespaceexception' module REXML module Parsers diff --git a/lib/rexml/parsers/ultralightparser.rb b/lib/rexml/parsers/ultralightparser.rb index 6571d119bd6930..e0029f43da535d 100644 --- a/lib/rexml/parsers/ultralightparser.rb +++ b/lib/rexml/parsers/ultralightparser.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rexml/parsers/streamparser' -require 'rexml/parsers/baseparser' +require_relative 'streamparser' +require_relative 'baseparser' module REXML module Parsers diff --git a/lib/rexml/parsers/xpathparser.rb b/lib/rexml/parsers/xpathparser.rb index ac3c4d4e676117..d01d325e04d705 100644 --- a/lib/rexml/parsers/xpathparser.rb +++ b/lib/rexml/parsers/xpathparser.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rexml/namespace' -require 'rexml/xmltokens' +require_relative '../namespace' +require_relative '../xmltokens' module REXML module Parsers diff --git a/lib/rexml/quickpath.rb b/lib/rexml/quickpath.rb index 5d6c77ca38d772..a0466b25d9b635 100644 --- a/lib/rexml/quickpath.rb +++ b/lib/rexml/quickpath.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rexml/functions' -require 'rexml/xmltokens' +require_relative 'functions' +require_relative 'xmltokens' module REXML class QuickPath diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index af65cf4751a78d..8663e489a85ea5 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -1,6 +1,6 @@ # coding: US-ASCII # frozen_string_literal: false -require 'rexml/encoding' +require_relative 'encoding' module REXML # Generates Source-s. USE THIS CLASS. diff --git a/lib/rexml/text.rb b/lib/rexml/text.rb index 86269dea1e531c..208febf5ee78fc 100644 --- a/lib/rexml/text.rb +++ b/lib/rexml/text.rb @@ -1,10 +1,10 @@ # frozen_string_literal: false -require 'rexml/security' -require 'rexml/entity' -require 'rexml/doctype' -require 'rexml/child' -require 'rexml/doctype' -require 'rexml/parseexception' +require_relative 'security' +require_relative 'entity' +require_relative 'doctype' +require_relative 'child' +require_relative 'doctype' +require_relative 'parseexception' module REXML # Represents text nodes in an XML document diff --git a/lib/rexml/undefinednamespaceexception.rb b/lib/rexml/undefinednamespaceexception.rb index e522ed57eaa3ad..492a098183e3cc 100644 --- a/lib/rexml/undefinednamespaceexception.rb +++ b/lib/rexml/undefinednamespaceexception.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rexml/parseexception' +require_relative 'parseexception' module REXML class UndefinedNamespaceException < ParseException def initialize( prefix, source, parser ) diff --git a/lib/rexml/validation/relaxng.rb b/lib/rexml/validation/relaxng.rb index fb52438290d3f4..f29a2c05e5782f 100644 --- a/lib/rexml/validation/relaxng.rb +++ b/lib/rexml/validation/relaxng.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require "rexml/validation/validation" -require "rexml/parsers/baseparser" +require_relative "validation" +require_relative "../parsers/baseparser" module REXML module Validation diff --git a/lib/rexml/validation/validation.rb b/lib/rexml/validation/validation.rb index f0c76f976ca928..0ad6ada4277777 100644 --- a/lib/rexml/validation/validation.rb +++ b/lib/rexml/validation/validation.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rexml/validation/validationexception' +require_relative 'validationexception' module REXML module Validation diff --git a/lib/rexml/xmldecl.rb b/lib/rexml/xmldecl.rb index a37e9f3ddc6420..d02204931cb33a 100644 --- a/lib/rexml/xmldecl.rb +++ b/lib/rexml/xmldecl.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rexml/encoding' -require 'rexml/source' +require_relative 'encoding' +require_relative 'source' module REXML # NEEDS DOCUMENTATION diff --git a/lib/rexml/xpath.rb b/lib/rexml/xpath.rb index 300d063fc5fe45..a0921bd8e10d8c 100644 --- a/lib/rexml/xpath.rb +++ b/lib/rexml/xpath.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rexml/functions' -require 'rexml/xpath_parser' +require_relative 'functions' +require_relative 'xpath_parser' module REXML # Wrapper class. Use this class to access the XPath functions. diff --git a/lib/rexml/xpath_parser.rb b/lib/rexml/xpath_parser.rb index 983db282c550f8..e30581d3d01c58 100644 --- a/lib/rexml/xpath_parser.rb +++ b/lib/rexml/xpath_parser.rb @@ -1,9 +1,9 @@ # frozen_string_literal: false -require 'rexml/namespace' -require 'rexml/xmltokens' -require 'rexml/attribute' -require 'rexml/syncenumerator' -require 'rexml/parsers/xpathparser' +require_relative 'namespace' +require_relative 'xmltokens' +require_relative 'attribute' +require_relative 'syncenumerator' +require_relative 'parsers/xpathparser' class Object # provides a unified +clone+ operation, for REXML::XPathParser diff --git a/lib/rinda/ring.rb b/lib/rinda/ring.rb index 34dc24512269c4..948cfaf20831a1 100644 --- a/lib/rinda/ring.rb +++ b/lib/rinda/ring.rb @@ -3,7 +3,7 @@ # Note: Rinda::Ring API is unstable. # require 'drb/drb' -require 'rinda/rinda' +require_relative 'rinda' require 'ipaddr' module Rinda diff --git a/lib/rinda/tuplespace.rb b/lib/rinda/tuplespace.rb index 47860bc8aed240..6a41a7ba75d503 100644 --- a/lib/rinda/tuplespace.rb +++ b/lib/rinda/tuplespace.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false require 'monitor' require 'drb/drb' -require 'rinda/rinda' +require_relative 'rinda' require 'forwardable' module Rinda diff --git a/lib/rss/0.9.rb b/lib/rss/0.9.rb index d852a6a85e98ee..219ccefcdbc28b 100644 --- a/lib/rss/0.9.rb +++ b/lib/rss/0.9.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/parser" +require_relative "parser" module RSS diff --git a/lib/rss/1.0.rb b/lib/rss/1.0.rb index fb63937c5e1505..c8f92fb54ef476 100644 --- a/lib/rss/1.0.rb +++ b/lib/rss/1.0.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/parser" +require_relative "parser" module RSS diff --git a/lib/rss/atom.rb b/lib/rss/atom.rb index 38e927478c2b59..48c27330d0b8f2 100644 --- a/lib/rss/atom.rb +++ b/lib/rss/atom.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require 'rss/parser' +require_relative 'parser' module RSS ## diff --git a/lib/rss/content.rb b/lib/rss/content.rb index d35311075a2cfa..78c18d103c3f6e 100644 --- a/lib/rss/content.rb +++ b/lib/rss/content.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/rss" +require_relative "rss" module RSS # The prefix for the Content XML namespace. diff --git a/lib/rss/converter.rb b/lib/rss/converter.rb index b92e35a051d172..d372e067256c32 100644 --- a/lib/rss/converter.rb +++ b/lib/rss/converter.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/utils" +require_relative "utils" module RSS diff --git a/lib/rss/dublincore.rb b/lib/rss/dublincore.rb index 8d1a5519472182..85b836d3bf3486 100644 --- a/lib/rss/dublincore.rb +++ b/lib/rss/dublincore.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/rss" +require_relative "rss" module RSS # The prefix for the Dublin Core XML namespace. @@ -161,4 +161,4 @@ class DublinCore#{Utils.to_class_name(name)} < Element require 'rss/dublincore/1.0' require 'rss/dublincore/2.0' -require 'rss/dublincore/atom' +require_relative 'dublincore/atom' diff --git a/lib/rss/dublincore/atom.rb b/lib/rss/dublincore/atom.rb index 0b8b11e440b4e3..1cfcdec677c8b7 100644 --- a/lib/rss/dublincore/atom.rb +++ b/lib/rss/dublincore/atom.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/atom" +require_relative "../atom" module RSS module Atom diff --git a/lib/rss/image.rb b/lib/rss/image.rb index 6b86ec0e5b60ca..837f7d18f4dd60 100644 --- a/lib/rss/image.rb +++ b/lib/rss/image.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false require 'rss/1.0' -require 'rss/dublincore' +require_relative 'dublincore' module RSS diff --git a/lib/rss/maker.rb b/lib/rss/maker.rb index 33d285f6afb50d..e32de81806658a 100644 --- a/lib/rss/maker.rb +++ b/lib/rss/maker.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/rss" +require_relative "rss" module RSS ## @@ -65,15 +65,15 @@ def maker(version) end end -require "rss/maker/1.0" -require "rss/maker/2.0" -require "rss/maker/feed" -require "rss/maker/entry" -require "rss/maker/content" -require "rss/maker/dublincore" -require "rss/maker/slash" -require "rss/maker/syndication" -require "rss/maker/taxonomy" -require "rss/maker/trackback" -require "rss/maker/image" -require "rss/maker/itunes" +require_relative "maker/1.0" +require_relative "maker/2.0" +require_relative "maker/feed" +require_relative "maker/entry" +require_relative "maker/content" +require_relative "maker/dublincore" +require_relative "maker/slash" +require_relative "maker/syndication" +require_relative "maker/taxonomy" +require_relative "maker/trackback" +require_relative "maker/image" +require_relative "maker/itunes" diff --git a/lib/rss/maker/0.9.rb b/lib/rss/maker/0.9.rb index 622a4c30b4c3b5..7f961b392ea257 100644 --- a/lib/rss/maker/0.9.rb +++ b/lib/rss/maker/0.9.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require "rss/0.9" +require_relative "../0.9" -require "rss/maker/base" +require_relative "base" module RSS module Maker diff --git a/lib/rss/maker/1.0.rb b/lib/rss/maker/1.0.rb index 3aee77e913b1d2..3934f9536c9b5d 100644 --- a/lib/rss/maker/1.0.rb +++ b/lib/rss/maker/1.0.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require "rss/1.0" +require_relative "../1.0" -require "rss/maker/base" +require_relative "base" module RSS module Maker diff --git a/lib/rss/maker/2.0.rb b/lib/rss/maker/2.0.rb index 1f77a014d12c86..43d00226b7fea6 100644 --- a/lib/rss/maker/2.0.rb +++ b/lib/rss/maker/2.0.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require "rss/2.0" +require_relative "../2.0" -require "rss/maker/0.9" +require_relative "0.9" module RSS module Maker diff --git a/lib/rss/maker/atom.rb b/lib/rss/maker/atom.rb index e0cd7623c86ecb..cdd1d8753e2b7e 100644 --- a/lib/rss/maker/atom.rb +++ b/lib/rss/maker/atom.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require "rss/atom" +require_relative "../atom" -require "rss/maker/base" +require_relative "base" module RSS module Maker diff --git a/lib/rss/maker/base.rb b/lib/rss/maker/base.rb index bc4ca841419fb8..17537b70062504 100644 --- a/lib/rss/maker/base.rb +++ b/lib/rss/maker/base.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false require 'forwardable' -require 'rss/rss' +require_relative '../rss' module RSS module Maker diff --git a/lib/rss/maker/content.rb b/lib/rss/maker/content.rb index 3559a45ad02c75..b3f4e5036e8945 100644 --- a/lib/rss/maker/content.rb +++ b/lib/rss/maker/content.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require 'rss/content' -require 'rss/maker/1.0' -require 'rss/maker/2.0' +require_relative '../content' +require_relative '1.0' +require_relative '2.0' module RSS module Maker diff --git a/lib/rss/maker/dublincore.rb b/lib/rss/maker/dublincore.rb index 988209c045b53a..beea3134b4f549 100644 --- a/lib/rss/maker/dublincore.rb +++ b/lib/rss/maker/dublincore.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rss/dublincore' -require 'rss/maker/1.0' +require_relative '../dublincore' +require_relative '1.0' module RSS module Maker diff --git a/lib/rss/maker/entry.rb b/lib/rss/maker/entry.rb index f806cbcaae85bb..ccdf9608ae6786 100644 --- a/lib/rss/maker/entry.rb +++ b/lib/rss/maker/entry.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require "rss/maker/atom" -require "rss/maker/feed" +require_relative "atom" +require_relative "feed" module RSS module Maker diff --git a/lib/rss/maker/feed.rb b/lib/rss/maker/feed.rb index fdef7ad6430bfc..72ee704d6af61c 100644 --- a/lib/rss/maker/feed.rb +++ b/lib/rss/maker/feed.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/maker/atom" +require_relative "atom" module RSS module Maker diff --git a/lib/rss/maker/image.rb b/lib/rss/maker/image.rb index 1957ba86891511..e3e62d8b9e9de1 100644 --- a/lib/rss/maker/image.rb +++ b/lib/rss/maker/image.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require 'rss/image' -require 'rss/maker/1.0' -require 'rss/maker/dublincore' +require_relative '../image' +require_relative '1.0' +require_relative 'dublincore' module RSS module Maker diff --git a/lib/rss/maker/itunes.rb b/lib/rss/maker/itunes.rb index d964a4d9427b3d..28cca3202137eb 100644 --- a/lib/rss/maker/itunes.rb +++ b/lib/rss/maker/itunes.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rss/itunes' -require 'rss/maker/2.0' +require_relative '../itunes' +require_relative '2.0' module RSS module Maker diff --git a/lib/rss/maker/slash.rb b/lib/rss/maker/slash.rb index 3bd82d305702d9..473991903f0df5 100644 --- a/lib/rss/maker/slash.rb +++ b/lib/rss/maker/slash.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rss/slash' -require 'rss/maker/1.0' +require_relative '../slash' +require_relative '1.0' module RSS module Maker diff --git a/lib/rss/maker/syndication.rb b/lib/rss/maker/syndication.rb index 840b70229ab06d..9fd0efe99ea452 100644 --- a/lib/rss/maker/syndication.rb +++ b/lib/rss/maker/syndication.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false -require 'rss/syndication' -require 'rss/maker/1.0' +require_relative '../syndication' +require_relative '1.0' module RSS module Maker diff --git a/lib/rss/maker/taxonomy.rb b/lib/rss/maker/taxonomy.rb index 76a2d1600d488c..f9858922dadf27 100644 --- a/lib/rss/maker/taxonomy.rb +++ b/lib/rss/maker/taxonomy.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require 'rss/taxonomy' -require 'rss/maker/1.0' -require 'rss/maker/dublincore' +require_relative '../taxonomy' +require_relative '1.0' +require_relative 'dublincore' module RSS module Maker diff --git a/lib/rss/maker/trackback.rb b/lib/rss/maker/trackback.rb index f97691c608eaf9..f78b4058f93573 100644 --- a/lib/rss/maker/trackback.rb +++ b/lib/rss/maker/trackback.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false -require 'rss/trackback' -require 'rss/maker/1.0' -require 'rss/maker/2.0' +require_relative '../trackback' +require_relative '1.0' +require_relative '2.0' module RSS module Maker diff --git a/lib/rss/parser.rb b/lib/rss/parser.rb index e271dc381c18fb..3a34d0adb47e69 100644 --- a/lib/rss/parser.rb +++ b/lib/rss/parser.rb @@ -2,8 +2,8 @@ require "forwardable" require "open-uri" -require "rss/rss" -require "rss/xml" +require_relative "rss" +require_relative "xml" module RSS diff --git a/lib/rss/rss.rb b/lib/rss/rss.rb index db87e11ad576f6..daaf6793b9dd67 100644 --- a/lib/rss/rss.rb +++ b/lib/rss/rss.rb @@ -63,9 +63,9 @@ def w3cdtf require "English" -require "rss/utils" -require "rss/converter" -require "rss/xml-stylesheet" +require_relative "utils" +require_relative "converter" +require_relative "xml-stylesheet" module RSS diff --git a/lib/rss/taxonomy.rb b/lib/rss/taxonomy.rb index b7ea219e8cbbfb..50688ee6c1f0f3 100644 --- a/lib/rss/taxonomy.rb +++ b/lib/rss/taxonomy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: false require "rss/1.0" -require "rss/dublincore" +require_relative "dublincore" module RSS # The prefix for the Taxonomy XML namespace. diff --git a/lib/rss/xml-stylesheet.rb b/lib/rss/xml-stylesheet.rb index be9cfaaf64bc4f..175c95fbcd7b10 100644 --- a/lib/rss/xml-stylesheet.rb +++ b/lib/rss/xml-stylesheet.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/utils" +require_relative "utils" module RSS diff --git a/lib/rss/xml.rb b/lib/rss/xml.rb index cda8668044f8fe..b74630295fc700 100644 --- a/lib/rss/xml.rb +++ b/lib/rss/xml.rb @@ -1,5 +1,5 @@ # frozen_string_literal: false -require "rss/utils" +require_relative "utils" module RSS module XML diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 6557256469a6dc..b335dc321b69a5 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -9,7 +9,7 @@ require 'rbconfig' module Gem - VERSION = "3.0.0.beta1".freeze + VERSION = "3.0.0.beta2".freeze end # Must be first since it unloads the prelude from 1.9.2 @@ -175,7 +175,7 @@ module Gem write_binary_errors end.freeze - USE_BUNDLER_FOR_GEMDEPS = false # :nodoc: + USE_BUNDLER_FOR_GEMDEPS = !ENV['DONT_USE_BUNDLER_FOR_GEMDEPS'] # :nodoc: @@win_platform = nil @@ -202,7 +202,7 @@ module Gem # activation succeeded or wasn't needed because it was already # activated. Returns false if it can't find the path in a gem. - def self.try_activate path + def self.try_activate(path) # finds the _latest_ version... regardless of loaded specs and their deps # if another gem had a requirement that would mean we shouldn't # activate the latest version, then either it would already be activated @@ -262,19 +262,14 @@ def self.bin_path(name, exec_name = nil, *requirements) find_spec_for_exe(name, exec_name, requirements).bin_file exec_name end - def self.find_spec_for_exe name, exec_name, requirements + def self.find_spec_for_exe(name, exec_name, requirements) dep = Gem::Dependency.new name, requirements loaded = Gem.loaded_specs[name] return loaded if loaded && dep.matches_spec?(loaded) - find_specs = proc { dep.matching_specs(true) } - if dep.to_s == "bundler (>= 0.a)" - specs = Gem::BundlerVersionFinder.without_filtering(&find_specs) - else - specs = find_specs.call - end + specs = dep.matching_specs(true) specs = specs.find_all { |spec| spec.executables.include? exec_name @@ -303,7 +298,7 @@ def self.find_spec_for_exe name, exec_name, requirements # # This method should *only* be used in bin stub files. - def self.activate_bin_path name, exec_name, requirement # :nodoc: + def self.activate_bin_path(name, exec_name, requirement) # :nodoc: spec = find_spec_for_exe name, exec_name, [requirement] Gem::LOADED_SPECS_MUTEX.synchronize do spec.activate @@ -446,7 +441,7 @@ def self.spec_cache_dir # # World-writable directories will never be created. - def self.ensure_gem_subdirectories dir = Gem.dir, mode = nil + def self.ensure_gem_subdirectories(dir = Gem.dir, mode = nil) ensure_subdirectories(dir, mode, REPOSITORY_SUBDIRECTORIES) end @@ -459,11 +454,11 @@ def self.ensure_gem_subdirectories dir = Gem.dir, mode = nil # # World-writable directories will never be created. - def self.ensure_default_gem_subdirectories dir = Gem.dir, mode = nil + def self.ensure_default_gem_subdirectories(dir = Gem.dir, mode = nil) ensure_subdirectories(dir, mode, REPOSITORY_DEFAULT_GEM_SUBDIRECTORIES) end - def self.ensure_subdirectories dir, mode, subdirs # :nodoc: + def self.ensure_subdirectories(dir, mode, subdirs) # :nodoc: old_umask = File.umask File.umask old_umask | 002 @@ -487,7 +482,7 @@ def self.ensure_subdirectories dir, mode, subdirs # :nodoc: # distinction as extensions cannot be shared between the two. def self.extension_api_version # :nodoc: - if 'no' == RbConfig::CONFIG['ENABLE_SHARED'] then + if 'no' == RbConfig::CONFIG['ENABLE_SHARED'] "#{ruby_api_version}-static" else ruby_api_version @@ -524,7 +519,7 @@ def self.find_files(glob, check_load_path=true) return files end - def self.find_files_from_load_path glob # :nodoc: + def self.find_files_from_load_path(glob) # :nodoc: glob_with_suffixes = "#{glob}#{Gem.suffix_pattern}" $LOAD_PATH.map { |load_path| Gem::Util.glob_files_in_dir(glob_with_suffixes, load_path) @@ -579,7 +574,7 @@ def self.find_latest_files(glob, check_load_path=true) def self.find_home Dir.home rescue - if Gem.win_platform? then + if Gem.win_platform? File.expand_path File.join(ENV['HOMEDRIVE'] || ENV['SystemDrive'], '/') else File.expand_path "/" @@ -634,7 +629,7 @@ class << self # Fetching: minitest-3.0.1.gem (100%) # => [#] - def self.install name, version = Gem::Requirement.default, *options + def self.install(name, version = Gem::Requirement.default, *options) require "rubygems/dependency_installer" inst = Gem::DependencyInstaller.new(*options) inst.install name, version @@ -652,7 +647,7 @@ def self.host ## Set the default RubyGems API host. - def self.host= host + def self.host=(host) # TODO: move to utils @host = host end @@ -841,7 +836,7 @@ def self.prefix if prefix != File.expand_path(RbConfig::CONFIG['sitelibdir']) and prefix != File.expand_path(RbConfig::CONFIG['libdir']) and - 'lib' == File.basename(RUBYGEMS_DIR) then + 'lib' == File.basename(RUBYGEMS_DIR) prefix end end @@ -899,7 +894,7 @@ def self.write_binary(path, data) # The path to the running Ruby interpreter. def self.ruby - if @ruby.nil? then + if @ruby.nil? @ruby = File.join(RbConfig::CONFIG['bindir'], "#{RbConfig::CONFIG['ruby_install_name']}#{RbConfig::CONFIG['EXEEXT']}") @@ -928,7 +923,7 @@ def self.env_requirement(gem_name) ## # Returns the latest release-version specification for the gem +name+. - def self.latest_spec_for name + def self.latest_spec_for(name) dependency = Gem::Dependency.new name fetcher = Gem::SpecFetcher.fetcher spec_tuples, = fetcher.spec_for_dependency dependency @@ -949,7 +944,7 @@ def self.latest_rubygems_version ## # Returns the version of the latest release-version of gem +name+ - def self.latest_version_for name + def self.latest_version_for(name) spec = latest_spec_for name spec and spec.version end @@ -961,10 +956,15 @@ def self.ruby_version return @ruby_version if defined? @ruby_version version = RUBY_VERSION.dup - if defined?(RUBY_PATCHLEVEL) && RUBY_PATCHLEVEL != -1 then + if defined?(RUBY_PATCHLEVEL) && RUBY_PATCHLEVEL != -1 version << ".#{RUBY_PATCHLEVEL}" - elsif defined?(RUBY_REVISION) then - version << ".dev.#{RUBY_REVISION}" + elsif defined?(RUBY_DESCRIPTION) + if RUBY_ENGINE == "ruby" + desc = RUBY_DESCRIPTION[/\Aruby #{Regexp.quote(RUBY_VERSION)}([^ ]+) /, 1] + else + desc = RUBY_DESCRIPTION[/\A#{RUBY_ENGINE} #{Regexp.quote(RUBY_ENGINE_VERSION)} \(#{RUBY_VERSION}([^ ]+)\) /, 1] + end + version << ".#{desc}" if desc end @ruby_version = Gem::Version.new version @@ -994,7 +994,7 @@ def self.sources # DOC: This comment is not documentation about the method itself, it's # more of a code comment about the implementation. - def self.sources= new_sources + def self.sources=(new_sources) if !new_sources @sources = nil else @@ -1071,7 +1071,7 @@ def self.user_home # Is this a windows platform? def self.win_platform? - if @@win_platform.nil? then + if @@win_platform.nil? ruby_platform = RbConfig::CONFIG['host_os'] @@win_platform = !!WIN_PATTERNS.find { |r| ruby_platform =~ r } end @@ -1082,7 +1082,7 @@ def self.win_platform? ## # Load +plugins+ as Ruby files - def self.load_plugin_files plugins # :nodoc: + def self.load_plugin_files(plugins) # :nodoc: plugins.each do |plugin| # Skip older versions of the GemCutter plugin: Its commands are in @@ -1151,7 +1151,7 @@ def self.load_env_plugins # execution of arbitrary code when used from directories outside your # control. - def self.use_gemdeps path = nil + def self.use_gemdeps(path = nil) raise_exception = path path ||= ENV['RUBYGEMS_GEMDEPS'] @@ -1159,7 +1159,7 @@ def self.use_gemdeps path = nil path = path.dup - if path == "-" then + if path == "-" Gem::Util.traverse_parents Dir.pwd do |directory| dep_file = GEM_DEP_FILES.find { |f| File.file?(f) } @@ -1172,7 +1172,7 @@ def self.use_gemdeps path = nil path.untaint - unless File.file? path then + unless File.file? path return unless raise_exception raise ArgumentError, "Unable to find gem dependencies file at #{path}" @@ -1376,7 +1376,7 @@ def clear_default_specs rescue LoadError end -if defined?(RUBY_ENGINE) then +if defined?(RUBY_ENGINE) begin ## # Defaults the Ruby implementation wants to provide for RubyGems diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb index 49b5d5fd06d65e..8334a73ecb672f 100644 --- a/lib/rubygems/available_set.rb +++ b/lib/rubygems/available_set.rb @@ -103,7 +103,7 @@ def source_for(spec) # Other options are :shallow for only direct development dependencies of the # gems in this set or :all for all development dependencies. - def to_request_set development = :none + def to_request_set(development = :none) request_set = Gem::RequestSet.new request_set.development = :all == development diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index 54242983ff50e1..d1878021bafc0e 100644 --- a/lib/rubygems/basic_specification.rb +++ b/lib/rubygems/basic_specification.rb @@ -65,10 +65,10 @@ def base_dir ## # Return true if this spec can require +file+. - def contains_requirable_file? file - if @ignored then + def contains_requirable_file?(file) + if @ignored return false - elsif missing_extensions? then + elsif missing_extensions? @ignored = true warn "Ignoring #{full_name} because its extensions are not built. " + @@ -124,7 +124,7 @@ def full_gem_path # default Ruby platform. def full_name - if platform == Gem::Platform::RUBY or platform.nil? then + if platform == Gem::Platform::RUBY or platform.nil? "#{name}-#{version}".dup.untaint else "#{name}-#{version}-#{platform}".dup.untaint @@ -160,8 +160,8 @@ def datadir # Full path of the target library file. # If the file is not in this gem, return nil. - def to_fullpath path - if activated? then + def to_fullpath(path) + if activated? @paths_map ||= {} @paths_map[path] ||= begin @@ -249,7 +249,7 @@ def require_paths def source_paths paths = raw_require_paths.dup - if have_extensions? then + if have_extensions? ext_dirs = extensions.map do |extension| extension.split(File::SEPARATOR, 2).first end.uniq @@ -263,7 +263,7 @@ def source_paths ## # Return all files in this gem that match for +glob+. - def matches_for_glob glob # TODO: rename? + def matches_for_glob(glob) # TODO: rename? # TODO: do we need these?? Kill it glob = File.join(self.lib_dirs_glob, glob) @@ -276,7 +276,7 @@ def matches_for_glob glob # TODO: rename? def lib_dirs_glob dirs = if self.raw_require_paths - if self.raw_require_paths.size > 1 then + if self.raw_require_paths.size > 1 "{#{self.raw_require_paths.join(',')}}" else self.raw_require_paths.first @@ -316,7 +316,7 @@ def this; self; end def have_extensions?; !extensions.empty?; end - def have_file? file, suffixes + def have_file?(file, suffixes) return true if raw_require_paths.any? do |path| base = File.join(gems_dir, full_name, path.untaint, file).untaint suffixes.any? { |suf| File.file? base + suf } diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb index 7babeaa8482700..11fb81450a6227 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -3,15 +3,6 @@ require "rubygems/util" module Gem::BundlerVersionFinder - @without_filtering = false - - def self.without_filtering - without_filtering, @without_filtering = true, @without_filtering - yield - ensure - @without_filtering = without_filtering - end - def self.bundler_version version, _ = bundler_version_with_reason @@ -21,8 +12,6 @@ def self.bundler_version end def self.bundler_version_with_reason - return if @without_filtering - if v = ENV["BUNDLER_VERSION"] return [v, "`$BUNDLER_VERSION`"] end @@ -40,7 +29,7 @@ def self.missing_version_message return unless vr = bundler_version_with_reason <<-EOS Could not find 'bundler' (#{vr.first}) required by #{vr.last}. -To update to the lastest version installed on your system, run `bundle update --bundler`. +To update to the latest version installed on your system, run `bundle update --bundler`. To install the missing version, run `gem install bundler:#{vr.first}` EOS end diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb index 3fc8a70a3e316b..5b8868b0cdd280 100644 --- a/lib/rubygems/command.rb +++ b/lib/rubygems/command.rb @@ -160,7 +160,7 @@ def show_lookup_failure(gem_name, version, errors, domain, required_by = nil) msg << ", here is why:\n" errors.each { |x| msg << " #{x.wordy}\n" } else - if required_by and gem != required_by then + if required_by and gem != required_by msg << " (required by #{required_by}) in any repository" else msg << " in any repository" @@ -169,7 +169,7 @@ def show_lookup_failure(gem_name, version, errors, domain, required_by = nil) alert_error msg - unless domain == :local then # HACK + unless domain == :local # HACK suggestions = Gem::SpecFetcher.fetcher.suggest_gems_from_name gem_name unless suggestions.empty? @@ -184,7 +184,7 @@ def show_lookup_failure(gem_name, version, errors, domain, required_by = nil) def get_all_gem_names args = options[:args] - if args.nil? or args.empty? then + if args.nil? or args.empty? raise Gem::CommandLineError, "Please specify at least one gem name (e.g. gem build GEMNAME)" end @@ -214,12 +214,12 @@ def get_all_gem_names_and_versions def get_one_gem_name args = options[:args] - if args.nil? or args.empty? then + if args.nil? or args.empty? raise Gem::CommandLineError, "Please specify a gem name on the command line (e.g. gem build GEMNAME)" end - if args.size > 1 then + if args.size > 1 raise Gem::CommandLineError, "Too many gem names (#{args.join(', ')}); please specify only one" end @@ -313,9 +313,9 @@ def invoke_with_build_args(args, build_args) self.ui = ui = Gem::SilentUI.new end - if options[:help] then + if options[:help] show_help - elsif @when_invoked then + elsif @when_invoked @when_invoked.call options else execute @@ -361,7 +361,7 @@ def add_option(*opts, &handler) # :yields: value, options def remove_option(name) @option_groups.each do |_, option_list| - option_list.reject! { |args, _| args.any? { |x| x =~ /^#{name}/ } } + option_list.reject! { |args, _| args.any? { |x| x.is_a?(String) && x =~ /^#{name}/ } } end end @@ -451,7 +451,7 @@ def add_parser_options # :nodoc: # Adds a section with +title+ and +content+ to the parser help view. Used # for adding command arguments and default arguments. - def add_parser_run_info title, content + def add_parser_run_info(title, content) return if content.empty? @parser.separator nil @@ -504,7 +504,6 @@ def configure_options(header, option_list) @parser.separator " #{header}Options:" option_list.each do |args, handler| - args.select { |arg| arg =~ /^-/ } @parser.on(*args) do |value| handler.call(value, @options) end @@ -531,7 +530,7 @@ def wrap(text, width) # :doc: add_common_option('-V', '--[no-]verbose', 'Set the verbose level of output') do |value, options| # Set us to "really verbose" so the progress meter works - if Gem.configuration.verbose and value then + if Gem.configuration.verbose and value Gem.configuration.verbose = 1 else Gem.configuration.verbose = value diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 40ae6191c5ddd9..8e4e26fdea1881 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -155,7 +155,7 @@ def run(args, build_args=nil) end def process_args(args, build_args=nil) - if args.empty? then + if args.empty? say Gem::Command::HELP terminate_interaction 1 end @@ -182,10 +182,10 @@ def find_command(cmd_name) possibilities = find_command_possibilities cmd_name - if possibilities.size > 1 then + if possibilities.size > 1 raise Gem::CommandLineError, "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]" - elsif possibilities.empty? then + elsif possibilities.empty? raise Gem::CommandLineError, "Unknown command #{cmd_name}" end @@ -230,4 +230,3 @@ def load_and_instantiate(command_name) end end - diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 3c778cf7054239..78737154b60ea5 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -50,11 +50,11 @@ def execute gemspec += '.gemspec' if File.exist? gemspec + '.gemspec' end - if File.exist? gemspec then + if File.exist? gemspec Dir.chdir(File.dirname(gemspec)) do spec = Gem::Specification.load File.basename(gemspec) - if spec then + if spec Gem::Package.build( spec, options[:force], @@ -72,4 +72,3 @@ def execute end end - diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index e93c39747cb6ea..5695460d9470d1 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -98,7 +98,7 @@ def initialize end end - def add_certificate certificate # :nodoc: + def add_certificate(certificate) # :nodoc: Gem::Security.trust_dir.trust_cert certificate say "Added '#{certificate.subject}'" @@ -132,7 +132,7 @@ def execute sign_certificates unless options[:sign].empty? end - def build email + def build(email) if !valid_email?(email) raise Gem::CommandLineError, "Invalid email address #{email}" end @@ -148,7 +148,7 @@ def build email end end - def build_cert email, key # :nodoc: + def build_cert(email, key) # :nodoc: expiration_length_days = options[:expiration_length_days] || Gem.configuration.cert_expiration_length_days @@ -179,7 +179,7 @@ def build_key # :nodoc: return key, key_path end - def certificates_matching filter + def certificates_matching(filter) return enum_for __method__, filter unless block_given? Gem::Security.trusted_certificates.select do |certificate, _| @@ -231,7 +231,7 @@ def description # :nodoc: EOF end - def list_certificates_matching filter # :nodoc: + def list_certificates_matching(filter) # :nodoc: certificates_matching filter do |certificate, _| # this could probably be formatted more gracefully say certificate.subject.to_s @@ -276,14 +276,14 @@ def load_defaults # :nodoc: load_default_key unless options[:key] end - def remove_certificates_matching filter # :nodoc: + def remove_certificates_matching(filter) # :nodoc: certificates_matching filter do |certificate, path| FileUtils.rm path say "Removed '#{certificate.subject}'" end end - def sign cert_file + def sign(cert_file) cert = File.read cert_file cert = OpenSSL::X509::Certificate.new cert @@ -314,11 +314,10 @@ def re_sign_cert(cert, cert_path, private_key) private - def valid_email? email + def valid_email?(email) # It's simple, but is all we need email =~ /\A.+@.+\z/ end end if defined?(OpenSSL::SSL) - diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb index 818cb05f55836a..7905b8ab69a19f 100644 --- a/lib/rubygems/commands/check_command.rb +++ b/lib/rubygems/commands/check_command.rb @@ -44,7 +44,7 @@ def check_gems gems = get_all_gem_names rescue [] Gem::Validator.new.alien(gems).sort.each do |key, val| - unless val.empty? then + unless val.empty? say "#{key} has #{val.size} problems" val.each do |error_entry| say " #{error_entry.path}:" diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index fe85dedddad63f..aeb4d82fae2fae 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -62,7 +62,7 @@ def usage # :nodoc: def execute say "Cleaning up installed gems..." - if options[:args].empty? then + if options[:args].empty? done = false last_set = nil @@ -111,7 +111,7 @@ def clean_gems end def get_candidate_gems - @candidate_gems = unless options[:args].empty? then + @candidate_gems = unless options[:args].empty? options[:args].map do |gem_name| Gem::Specification.find_all_by_name gem_name end.flatten @@ -121,7 +121,6 @@ def get_candidate_gems end def get_gems_to_cleanup - gems_to_cleanup = @candidate_gems.select { |spec| @primary_gems[spec.name].version != spec.version } @@ -146,16 +145,16 @@ def get_primary_gems Gem::Specification.each do |spec| if @primary_gems[spec.name].nil? or - @primary_gems[spec.name].version < spec.version then + @primary_gems[spec.name].version < spec.version @primary_gems[spec.name] = spec end end end - def uninstall_dep spec + def uninstall_dep(spec) return unless @full.ok_to_remove?(spec.full_name, options[:check_dev]) - if options[:dryrun] then + if options[:dryrun] say "Dry Run Mode: Would uninstall #{spec.full_name}" return end diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index e0f2eedb5d21f2..6da89a136ec411 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -73,7 +73,7 @@ def execute names.each do |name| found = - if options[:show_install_dir] then + if options[:show_install_dir] gem_install_dir name else gem_contents name @@ -83,15 +83,15 @@ def execute end end - def files_in spec - if spec.default_gem? then + def files_in(spec) + if spec.default_gem? files_in_default_gem spec else files_in_gem spec end end - def files_in_gem spec + def files_in_gem(spec) gem_path = spec.full_gem_path extra = "/{#{spec.require_paths.join ','}}" if options[:lib_only] glob = "#{gem_path}#{extra}/**/*" @@ -102,7 +102,7 @@ def files_in_gem spec end end - def files_in_default_gem spec + def files_in_default_gem(spec) spec.files.map do |file| case file when /\A#{spec.bindir}\// @@ -115,7 +115,7 @@ def files_in_default_gem spec end end - def gem_contents name + def gem_contents(name) spec = spec_for name return false unless spec @@ -127,7 +127,7 @@ def gem_contents name true end - def gem_install_dir name + def gem_install_dir(name) spec = spec_for name return false unless spec @@ -138,27 +138,27 @@ def gem_install_dir name end def gem_names # :nodoc: - if options[:all] then + if options[:all] Gem::Specification.map(&:name) else get_all_gem_names end end - def path_description spec_dirs # :nodoc: - if spec_dirs.empty? then + def path_description(spec_dirs) # :nodoc: + if spec_dirs.empty? "default gem paths" else "specified path" end end - def show_files files + def show_files(files) files.sort.each do |prefix, basename| absolute_path = File.join(prefix, basename) next if File.directory? absolute_path - if options[:prefix] then + if options[:prefix] say absolute_path else say basename @@ -166,14 +166,14 @@ def show_files files end end - def spec_for name + def spec_for(name) spec = Gem::Specification.find_all_by_name(name, @version).last return spec if spec say "Unable to find gem '#{name}' in #{@path_kind}" - if Gem.configuration.verbose then + if Gem.configuration.verbose say "\nDirectories searched:" @spec_dirs.sort.each { |dir| say dir } end @@ -188,4 +188,3 @@ def specification_directories # :nodoc: end end - diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb index 97fd812ffa8b41..9e88b04ea57eb6 100644 --- a/lib/rubygems/commands/dependency_command.rb +++ b/lib/rubygems/commands/dependency_command.rb @@ -54,7 +54,7 @@ def usage # :nodoc: "#{program_name} REGEXP" end - def fetch_remote_specs dependency # :nodoc: + def fetch_remote_specs(dependency) # :nodoc: fetcher = Gem::SpecFetcher.fetcher ss, = fetcher.spec_for_dependency dependency @@ -62,7 +62,7 @@ def fetch_remote_specs dependency # :nodoc: ss.map { |spec, _| spec } end - def fetch_specs name_pattern, dependency # :nodoc: + def fetch_specs(name_pattern, dependency) # :nodoc: specs = [] if local? @@ -79,7 +79,7 @@ def fetch_specs name_pattern, dependency # :nodoc: specs.uniq.sort end - def gem_dependency pattern, version, prerelease # :nodoc: + def gem_dependency(pattern, version, prerelease) # :nodoc: dependency = Gem::Deprecate.skip_during { Gem::Dependency.new pattern, version } @@ -89,9 +89,9 @@ def gem_dependency pattern, version, prerelease # :nodoc: dependency end - def display_pipe specs # :nodoc: + def display_pipe(specs) # :nodoc: specs.each do |spec| - unless spec.dependencies.empty? then + unless spec.dependencies.empty? spec.dependencies.sort_by { |dep| dep.name }.each do |dep| say "#{dep.name} --version '#{dep.requirement}'" end @@ -99,12 +99,12 @@ def display_pipe specs # :nodoc: end end - def display_readable specs, reverse # :nodoc: + def display_readable(specs, reverse) # :nodoc: response = String.new specs.each do |spec| response << print_dependencies(spec) - unless reverse[spec.full_name].empty? then + unless reverse[spec.full_name].empty? response << " Used by\n" reverse[spec.full_name].each do |sp, dep| response << " #{sp} (#{dep})\n" @@ -128,7 +128,7 @@ def execute reverse = reverse_dependencies specs - if options[:pipe_format] then + if options[:pipe_format] display_pipe specs else display_readable specs, reverse @@ -136,13 +136,13 @@ def execute end def ensure_local_only_reverse_dependencies # :nodoc: - if options[:reverse_dependencies] and remote? and not local? then + if options[:reverse_dependencies] and remote? and not local? alert_error 'Only reverse dependencies for local gems are supported.' terminate_interaction 1 end end - def ensure_specs specs # :nodoc: + def ensure_specs(specs) # :nodoc: return unless specs.empty? patterns = options[:args].join ',' @@ -155,7 +155,7 @@ def ensure_specs specs # :nodoc: def print_dependencies(spec, level = 0) # :nodoc: response = String.new response << ' ' * level + "Gem #{spec.full_name}\n" - unless spec.dependencies.empty? then + unless spec.dependencies.empty? spec.dependencies.sort_by { |dep| dep.name }.each do |dep| response << ' ' * level + " #{dep}\n" end @@ -163,7 +163,7 @@ def print_dependencies(spec, level = 0) # :nodoc: response end - def remote_specs dependency # :nodoc: + def remote_specs(dependency) # :nodoc: fetcher = Gem::SpecFetcher.fetcher ss, _ = fetcher.spec_for_dependency dependency @@ -171,7 +171,7 @@ def remote_specs dependency # :nodoc: ss.map { |s,o| s } end - def reverse_dependencies specs # :nodoc: + def reverse_dependencies(specs) # :nodoc: reverse = Hash.new { |h, k| h[k] = [] } return reverse unless options[:reverse_dependencies] @@ -186,7 +186,7 @@ def reverse_dependencies specs # :nodoc: ## # Returns an Array of [specification, dep] that are satisfied by +spec+. - def find_reverse_dependencies spec # :nodoc: + def find_reverse_dependencies(spec) # :nodoc: result = [] Gem::Specification.each do |sp| @@ -194,7 +194,7 @@ def find_reverse_dependencies spec # :nodoc: dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep if spec.name == dep.name and - dep.requirement.satisfied_by?(spec.version) then + dep.requirement.satisfied_by?(spec.version) result << [sp.full_name, dep] end end @@ -205,10 +205,10 @@ def find_reverse_dependencies spec # :nodoc: private - def name_pattern args + def name_pattern(args) args << '' if args.empty? - if args.length == 1 and args.first =~ /\A\/(.*)\/(i)?\z/m then + if args.length == 1 and args.first =~ /\A\/(.*)\/(i)?\z/m flags = $2 ? Regexp::IGNORECASE : nil Regexp.new $1, flags else diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index 29c9d4d38f9586..11fb45f68d26c2 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -97,7 +97,7 @@ def execute true end - def add_path out, path + def add_path(out, path) path.each do |component| out << " - #{component}\n" end diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index 19559a7774102e..66562d7fb73a69 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -56,14 +56,14 @@ def execute specs_and_sources, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep - if platform then + if platform filtered = specs_and_sources.select { |s,| s.platform == platform } specs_and_sources = filtered unless filtered.empty? end spec, source = specs_and_sources.max_by { |s,| s.version } - if spec.nil? then + if spec.nil? show_lookup_failure gem_name, version, errors, options[:domain] next end @@ -75,4 +75,3 @@ def execute end end - diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb index 0b677b73a9567b..941637ea9c8fdc 100644 --- a/lib/rubygems/commands/generate_index_command.rb +++ b/lib/rubygems/commands/generate_index_command.rb @@ -67,13 +67,13 @@ def execute options[:build_modern] = true if not File.exist?(options[:directory]) or - not File.directory?(options[:directory]) then + not File.directory?(options[:directory]) alert_error "unknown directory name #{options[:directory]}." terminate_interaction 1 else indexer = Gem::Indexer.new options.delete(:directory), options - if options[:update] then + if options[:update] indexer.update_index else indexer.generate_index @@ -82,4 +82,3 @@ def execute end end - diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index 0c96963fac793c..9f14e22f901da1 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -297,8 +297,8 @@ def execute begins? command, arg end - if help then - if Symbol === help then + if help + if Symbol === help send help else say help @@ -306,10 +306,10 @@ def execute return end - if options[:help] then + if options[:help] show_help - elsif arg then + elsif arg show_command_help arg else @@ -334,7 +334,7 @@ def show_commands # :nodoc: command = @command_manager[cmd_name] summary = - if command then + if command command.summary else "[No command found for #{cmd_name}]" @@ -356,15 +356,15 @@ def show_commands # :nodoc: say out.join("\n") end - def show_command_help command_name # :nodoc: + def show_command_help(command_name) # :nodoc: command_name = command_name.downcase possibilities = @command_manager.find_command_possibilities command_name - if possibilities.size == 1 then + if possibilities.size == 1 command = @command_manager[possibilities.first] command.invoke("--help") - elsif possibilities.size > 1 then + elsif possibilities.size > 1 alert_warning "Ambiguous command #{command_name} (#{possibilities.join(', ')})" else alert_warning "Unknown command #{command_name}. Try: gem help commands" @@ -372,4 +372,3 @@ def show_command_help command_name # :nodoc: end end - diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 9fe131c290c2f1..776b58651f7961 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -132,7 +132,7 @@ def usage # :nodoc: end def check_install_dir # :nodoc: - if options[:install_dir] and options[:user_install] then + if options[:install_dir] and options[:user_install] alert_error "Use --install-dir or --user-install but not both" terminate_interaction 1 end @@ -140,16 +140,15 @@ def check_install_dir # :nodoc: def check_version # :nodoc: if options[:version] != Gem::Requirement.default and - get_all_gem_names.size > 1 then + get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirments using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" terminate_interaction 1 end end def execute - - if options.include? :gemdeps then + if options.include? :gemdeps install_from_gemdeps return # not reached end @@ -189,13 +188,13 @@ def install_from_gemdeps # :nodoc: terminate_interaction end - def install_gem name, version # :nodoc: + def install_gem(name, version) # :nodoc: return if options[:conservative] and not Gem::Dependency.new(name, version).matching_specs.empty? req = Gem::Requirement.create(version) - if options[:ignore_dependencies] then + if options[:ignore_dependencies] install_gem_without_dependencies name, req else inst = Gem::DependencyInstaller.new options @@ -217,11 +216,11 @@ def install_gem name, version # :nodoc: end end - def install_gem_without_dependencies name, req # :nodoc: + def install_gem_without_dependencies(name, req) # :nodoc: gem = nil - if local? then - if name =~ /\.gem$/ and File.file? name then + if local? + if name =~ /\.gem$/ and File.file? name source = Gem::Source::SpecificFile.new name spec = source.spec else @@ -231,7 +230,7 @@ def install_gem_without_dependencies name, req # :nodoc: gem = source.download spec if spec end - if remote? and not gem then + if remote? and not gem dependency = Gem::Dependency.new name, req dependency.prerelease = options[:prerelease] @@ -293,7 +292,7 @@ def load_hooks # :nodoc: require 'rubygems/rdoc' end - def show_install_errors errors # :nodoc: + def show_install_errors(errors) # :nodoc: return unless errors errors.each do |x| diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index 1acb49e5fbcf2c..cd21543537776a 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -38,4 +38,3 @@ def usage # :nodoc: end end - diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb index 3eebfadc055a82..7f02e9feda964a 100644 --- a/lib/rubygems/commands/lock_command.rb +++ b/lib/rubygems/commands/lock_command.rb @@ -59,7 +59,7 @@ def usage # :nodoc: end def complain(message) - if options[:strict] then + if options[:strict] raise Gem::Exception, message else say "# #{message}" @@ -78,7 +78,7 @@ def execute spec = Gem::Specification.load spec_path(full_name) - if spec.nil? then + if spec.nil? complain "Could not find gem #{full_name}, try using the full name" next end @@ -90,7 +90,7 @@ def execute next if locked[dep.name] candidates = dep.matching_specs - if candidates.empty? then + if candidates.empty? complain "Unable to satisfy '#{dep}' from currently installed gems" else pending << candidates.last.full_name @@ -108,4 +108,3 @@ def spec_path(gem_full_name) end end - diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb index fdac19dc1f2a60..2794fe05e65022 100644 --- a/lib/rubygems/commands/open_command.rb +++ b/lib/rubygems/commands/open_command.rb @@ -11,9 +11,9 @@ class Gem::Commands::OpenCommand < Gem::Command def initialize super 'open', 'Open gem sources in editor' - add_option('-e', '--editor EDITOR', String, - "Opens gem sources in EDITOR") do |editor, options| - options[:editor] = editor || get_env_editor + add_option('-e', '--editor COMMAND', String, + "Prepends COMMAND to gem path. Could be used to specify editor.") do |command, options| + options[:editor] = command || get_env_editor end add_option('-v', '--version VERSION', String, "Opens specific gem version") do |version| @@ -32,14 +32,14 @@ def defaults_str # :nodoc: def description # :nodoc: <<-EOF The open command opens gem in editor and changes current path - to gem's source directory. Editor can be specified with -e option, - otherwise rubygems will look for editor in $EDITOR, $VISUAL and - $GEM_EDITOR variables. + to gem's source directory. + Editor command can be specified with -e option, otherwise rubygems + will look for editor in $EDITOR, $VISUAL and $GEM_EDITOR variables. EOF end def usage # :nodoc: - "#{program_name} GEMNAME [-e EDITOR]" + "#{program_name} GEMNAME [-e COMMAND]" end def get_env_editor @@ -58,7 +58,7 @@ def execute terminate_interaction 1 unless found end - def open_gem name + def open_gem(name) spec = spec_for name return false unless spec @@ -71,13 +71,13 @@ def open_gem name open_editor(spec.full_gem_path) end - def open_editor path + def open_editor(path) Dir.chdir(path) do system(*@editor.split(/\s+/) + [path]) end end - def spec_for name + def spec_for(name) spec = Gem::Specification.find_all_by_name(name, @version).first return spec if spec diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 637b5bdc4dfe6a..9cfaa241b5bd61 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -58,7 +58,7 @@ def execute show_owners name end - def show_owners name + def show_owners(name) response = rubygems_api_request :get, "api/v1/gems/#{name}/owners.yaml" do |request| request.add_field "Authorization", api_key end @@ -73,15 +73,15 @@ def show_owners name end end - def add_owners name, owners + def add_owners(name, owners) manage_owners :post, name, owners end - def remove_owners name, owners + def remove_owners(name, owners) manage_owners :delete, name, owners end - def manage_owners method, name, owners + def manage_owners(method, name, owners) owners.each do |owner| begin response = rubygems_api_request method, "api/v1/gems/#{name}/owners" do |request| diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 575c344130469d..41decde856165f 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -88,13 +88,13 @@ def usage # :nodoc: end def execute - specs = if options[:all] then + specs = if options[:all] Gem::Specification.map # `--extensions` must be explicitly given to pristine only gems # with extensions. elsif options[:extensions_set] and - options[:extensions] and options[:args].empty? then + options[:extensions] and options[:args].empty? Gem::Specification.select do |spec| spec.extensions and not spec.extensions.empty? end @@ -104,7 +104,7 @@ def execute end.flatten end - if specs.to_a.empty? then + if specs.to_a.empty? raise Gem::Exception, "Failed to find gems #{options[:args]} #{options[:version]}" end @@ -129,19 +129,14 @@ def execute end end - if spec.bundled_gem_in_old_ruby? - say "Skipped #{spec.full_name}, it is bundled with old Ruby" - next - end - - unless spec.extensions.empty? or options[:extensions] or options[:only_executables] then + unless spec.extensions.empty? or options[:extensions] or options[:only_executables] say "Skipped #{spec.full_name}, it needs to compile an extension" next end gem = spec.cache_file - unless File.exist? gem or options[:only_executables] then + unless File.exist? gem or options[:only_executables] require 'rubygems/remote_fetcher' say "Cached gem for #{spec.full_name} not found, attempting to fetch..." @@ -159,7 +154,7 @@ def execute end env_shebang = - if options.include? :env_shebang then + if options.include? :env_shebang options[:env_shebang] else install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS['install'] @@ -177,7 +172,7 @@ def execute :bin_dir => bin_dir } - if options[:only_executables] then + if options[:only_executables] installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_bin else diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 83c7131afc939b..20812368c49bce 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -79,7 +79,7 @@ def send_gem(name) if latest_rubygems_version < Gem.rubygems_version and Gem.rubygems_version.prerelease? and - Gem::Version.new('2.0.0.rc.2') != Gem.rubygems_version then + Gem::Version.new('2.0.0.rc.2') != Gem.rubygems_version alert_error <<-ERROR You are using a beta release of RubyGems (#{Gem::VERSION}) which is not allowed to push gems. Please downgrade or upgrade to a release version. @@ -96,7 +96,7 @@ def send_gem(name) gem_data = Gem::Package.new(name) - unless @host then + unless @host @host = gem_data.spec.metadata['default_gem_server'] end @@ -134,4 +134,3 @@ def get_hosts_for(name) ] end end - diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb index 4624e5a1e917a0..d947d588ef0e59 100644 --- a/lib/rubygems/commands/query_command.rb +++ b/lib/rubygems/commands/query_command.rb @@ -91,8 +91,8 @@ def execute prerelease = options[:prerelease] - unless options[:installed].nil? then - if no_name then + unless options[:installed].nil? + if no_name alert_error "You must specify a gem name" exit_code |= 4 elsif name.count > 1 @@ -102,7 +102,7 @@ def execute installed = installed? name.first, options[:version] installed = !installed unless options[:installed] - if installed then + if installed say "true" else say "false" @@ -119,8 +119,8 @@ def execute private - def display_header type - if (ui.outs.tty? and Gem.configuration.verbose) or both? then + def display_header(type) + if (ui.outs.tty? and Gem.configuration.verbose) or both? say say "*** #{type} GEMS ***" say @@ -128,14 +128,14 @@ def display_header type end #Guts of original execute - def show_gems name, prerelease + def show_gems(name, prerelease) req = Gem::Requirement.default # TODO: deprecate for real dep = Gem::Deprecate.skip_during { Gem::Dependency.new name, req } dep.prerelease = prerelease - if local? then - if prerelease and not both? then + if local? + if prerelease and not both? alert_warning "prereleases are always shown locally" end @@ -152,7 +152,7 @@ def show_gems name, prerelease output_query_results spec_tuples end - if remote? then + if remote? display_header 'REMOTE' fetcher = Gem::SpecFetcher.fetcher @@ -205,7 +205,7 @@ def output_query_results(spec_tuples) say output.join(options[:details] ? "\n\n" : "\n") end - def output_versions output, versions + def output_versions(output, versions) versions.each do |gem_name, matching_tuples| matching_tuples = matching_tuples.sort_by { |n,_| n.version }.reverse @@ -218,7 +218,7 @@ def output_versions output, versions seen = {} matching_tuples.delete_if do |n,_| - if seen[n.version] then + if seen[n.version] true else seen[n.version] = true @@ -230,7 +230,7 @@ def output_versions output, versions end end - def entry_details entry, detail_tuple, specs, platforms + def entry_details(entry, detail_tuple, specs, platforms) return unless options[:details] name_tuple, spec = detail_tuple @@ -247,11 +247,11 @@ def entry_details entry, detail_tuple, specs, platforms spec_summary entry, spec end - def entry_versions entry, name_tuples, platforms, specs + def entry_versions(entry, name_tuples, platforms, specs) return unless options[:versions] list = - if platforms.empty? or options[:details] then + if platforms.empty? or options[:details] name_tuples.map { |n| n.version }.uniq else platforms.sort.reverse.map do |version, pls| @@ -264,7 +264,7 @@ def entry_versions entry, name_tuples, platforms, specs out = "default: #{out}" if default end - if pls != [Gem::Platform::RUBY] then + if pls != [Gem::Platform::RUBY] platform_list = [pls.delete(Gem::Platform::RUBY), *pls.sort].compact out = platform_list.unshift(out).join(' ') end @@ -276,7 +276,7 @@ def entry_versions entry, name_tuples, platforms, specs entry << " (#{list.join ', '})" end - def make_entry entry_tuples, platforms + def make_entry(entry_tuples, platforms) detail_tuple = entry_tuples.first name_tuples, specs = entry_tuples.flatten.partition do |item| @@ -291,19 +291,19 @@ def make_entry entry_tuples, platforms entry.join end - def spec_authors entry, spec + def spec_authors(entry, spec) authors = "Author#{spec.authors.length > 1 ? 's' : ''}: ".dup authors << spec.authors.join(', ') entry << format_text(authors, 68, 4) end - def spec_homepage entry, spec + def spec_homepage(entry, spec) return if spec.homepage.nil? or spec.homepage.empty? entry << "\n" << format_text("Homepage: #{spec.homepage}", 68, 4) end - def spec_license entry, spec + def spec_license(entry, spec) return if spec.license.nil? or spec.license.empty? licenses = "License#{spec.licenses.length > 1 ? 's' : ''}: ".dup @@ -311,10 +311,10 @@ def spec_license entry, spec entry << "\n" << format_text(licenses, 68, 4) end - def spec_loaded_from entry, spec, specs + def spec_loaded_from(entry, spec, specs) return unless spec.loaded_from - if specs.length == 1 then + if specs.length == 1 default = spec.default_gem? ? ' (default)' : nil entry << "\n" << " Installed at#{default}: #{spec.base_dir}" else @@ -328,14 +328,14 @@ def spec_loaded_from entry, spec, specs end end - def spec_platforms entry, platforms + def spec_platforms(entry, platforms) non_ruby = platforms.any? do |_, pls| pls.any? { |pl| pl != Gem::Platform::RUBY } end return unless non_ruby - if platforms.length == 1 then + if platforms.length == 1 title = platforms.values.length == 1 ? 'Platform' : 'Platforms' entry << " #{title}: #{platforms.values.sort.join ', '}\n" else @@ -351,7 +351,7 @@ def spec_platforms entry, platforms end end - def spec_summary entry, spec + def spec_summary(entry, spec) summary = truncate_text(spec.summary, "the summary for #{spec.full_name}") entry << "\n\n" << format_text(summary, 68, 4) end diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb index 6992040dcaee12..5f8b72eb7ac8e6 100644 --- a/lib/rubygems/commands/rdoc_command.rb +++ b/lib/rubygems/commands/rdoc_command.rb @@ -60,7 +60,7 @@ def usage # :nodoc: end def execute - specs = if options[:all] then + specs = if options[:all] Gem::Specification.to_a else get_all_gem_names.map do |name| @@ -68,7 +68,7 @@ def execute end.flatten.uniq end - if specs.empty? then + if specs.empty? alert_error 'No matching gems found' terminate_interaction 1 end @@ -78,7 +78,7 @@ def execute doc.force = options[:overwrite] - if options[:overwrite] then + if options[:overwrite] FileUtils.rm_rf File.join(spec.doc_dir, 'ri') FileUtils.rm_rf File.join(spec.doc_dir, 'rdoc') end @@ -94,4 +94,3 @@ def execute end end - diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb index 933436d84d8efa..c9cb1f2d43b023 100644 --- a/lib/rubygems/commands/search_command.rb +++ b/lib/rubygems/commands/search_command.rb @@ -38,4 +38,3 @@ def usage # :nodoc: end end - diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb index 245156d50dc46b..e91a8e5176c28d 100644 --- a/lib/rubygems/commands/server_command.rb +++ b/lib/rubygems/commands/server_command.rb @@ -9,7 +9,7 @@ def initialize :port => 8808, :gemdir => [], :daemon => false OptionParser.accept :Port do |port| - if port =~ /\A\d+\z/ then + if port =~ /\A\d+\z/ port = Integer port raise OptionParser::InvalidArgument, "#{port}: not a port number" if port > 65535 @@ -84,4 +84,3 @@ def execute end end - diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 37abe3b2ec0357..7992818863dd72 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -6,8 +6,8 @@ # RubyGems checkout or tarball. class Gem::Commands::SetupCommand < Gem::Command - HISTORY_HEADER = /^===\s*[\d.a-zA-Z]+\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/ - VERSION_MATCHER = /^===\s*([\d.a-zA-Z]+)\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/ + HISTORY_HEADER = /^===\s*[\d.a-zA-Z]+\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze + VERSION_MATCHER = /^===\s*([\d.a-zA-Z]+)\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze ENV_PATHS = %w[/usr/bin/env /bin/env].freeze @@ -62,7 +62,7 @@ def initialize add_option '--[no-]rdoc', 'Generate RDoc documentation for RubyGems' do |value, options| - if value then + if value options[:document] << 'rdoc' else options[:document].delete 'rdoc' @@ -73,7 +73,7 @@ def initialize add_option '--[no-]ri', 'Generate RI documentation for RubyGems' do |value, options| - if value then + if value options[:document] << 'ri' else options[:document].delete 'ri' @@ -99,7 +99,7 @@ def initialize def check_ruby_version required_version = Gem::Requirement.new '>= 1.8.7' - unless required_version.satisfied_by? Gem.ruby_version then + unless required_version.satisfied_by? Gem.ruby_version alert_error "Expected Ruby version #{required_version}, is #{Gem.ruby_version}" terminate_interaction 1 end @@ -139,7 +139,7 @@ def execute install_destdir = options[:destdir] - unless install_destdir.empty? then + unless install_destdir.empty? ENV['GEM_HOME'] ||= File.join(install_destdir, Gem.default_dir.gsub(/^[a-zA-Z]:/, '')) end @@ -147,7 +147,7 @@ def execute check_ruby_version require 'fileutils' - if Gem.configuration.really_verbose then + if Gem.configuration.really_verbose extend FileUtils::Verbose else extend FileUtils @@ -180,7 +180,7 @@ def execute documentation_success = install_rdoc say - if @verbose then + if @verbose say "-" * 78 say end @@ -201,14 +201,14 @@ def execute say @bin_file_names.map { |name| "\t#{name}\n" } say - unless @bin_file_names.grep(/#{File::SEPARATOR}gem$/) then + unless @bin_file_names.grep(/#{File::SEPARATOR}gem$/) say "If `gem` was installed by a previous RubyGems installation, you may need" say "to remove it by hand." say end if documentation_success - if options[:document].include? 'rdoc' then + if options[:document].include? 'rdoc' say "Rdoc documentation was installed. You may now invoke:" say " gem server" say "and then peruse beautifully formatted documentation for your gems" @@ -219,7 +219,7 @@ def execute say end - if options[:document].include? 'ri' then + if options[:document].include? 'ri' say "Ruby Interactive (ri) documentation was installed. ri is kind of like man " say "pages for Ruby libraries. You may access it like this:" say " ri Classname" @@ -250,7 +250,7 @@ def install_executables(bin_dir) bin_files -= %w[update_rubygems bundler bundle_ruby] bin_files.each do |bin_file| - bin_file_formatted = if options[:format_executable] then + bin_file_formatted = if options[:format_executable] Gem.default_exec_format % bin_file else bin_file @@ -308,7 +308,7 @@ def shebang end end - def install_file file, dest_dir + def install_file(file, dest_dir) dest_file = File.join dest_dir, file dest_dir = File.dirname dest_file unless File.directory? dest_dir @@ -354,7 +354,7 @@ def install_rdoc if File.writable? gem_doc_dir and (not File.exist? rubygems_doc_dir or - File.writable? rubygems_doc_dir) then + 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| rm_rf dir @@ -374,7 +374,7 @@ def fake_spec.full_gem_path rdoc.generate return true - elsif @verbose then + elsif @verbose say "Skipping RDoc generation, #{gem_doc_dir} not writable" say "Set the GEM_HOME environment variable if you want RDoc generated" end @@ -456,7 +456,7 @@ def generate_default_dirs(install_destdir) prefix = options[:prefix] site_or_vendor = options[:site_or_vendor] - if prefix.empty? then + if prefix.empty? lib_dir = RbConfig::CONFIG[site_or_vendor] bin_dir = RbConfig::CONFIG['bindir'] else @@ -467,7 +467,7 @@ def generate_default_dirs(install_destdir) # just in case Apple and RubyGems don't get this patched up proper. (prefix == RbConfig::CONFIG['libdir'] or # this one is important - prefix == File.join(RbConfig::CONFIG['libdir'], 'ruby')) then + prefix == File.join(RbConfig::CONFIG['libdir'], 'ruby')) lib_dir = RbConfig::CONFIG[site_or_vendor] bin_dir = RbConfig::CONFIG['bindir'] else @@ -476,7 +476,7 @@ def generate_default_dirs(install_destdir) end end - unless install_destdir.empty? then + unless install_destdir.empty? lib_dir = File.join install_destdir, lib_dir.gsub(/^[a-zA-Z]:/, '') bin_dir = File.join install_destdir, bin_dir.gsub(/^[a-zA-Z]:/, '') end @@ -484,13 +484,13 @@ def generate_default_dirs(install_destdir) [lib_dir, bin_dir] end - def pem_files_in dir + def pem_files_in(dir) Dir.chdir dir do Dir[File.join('**', '*pem')] end end - def rb_files_in dir + def rb_files_in(dir) Dir.chdir dir do Dir[File.join('**', '*rb')] end @@ -505,7 +505,7 @@ def template_files end # for cleanup old bundler files - def template_files_in dir + def template_files_in(dir) Dir.chdir dir do (Dir[File.join('templates', '**', '{*,.*}')]). select{|f| !File.directory?(f)} @@ -544,7 +544,7 @@ def remove_old_bin_files(bin_dir) end end - def remove_old_lib_files lib_dir + def remove_old_lib_files(lib_dir) lib_dirs = { File.join(lib_dir, 'rubygems') => 'lib/rubygems' } lib_dirs[File.join(lib_dir, 'bundler')] = 'bundler/lib/bundler' if Gem::USE_BUNDLER_FOR_GEMDEPS lib_dirs.each do |old_lib_dir, new_lib_dir| @@ -575,7 +575,7 @@ def show_release_notes release_notes = File.join Dir.pwd, 'History.txt' release_notes = - if File.exist? release_notes then + if File.exist? release_notes history = File.read release_notes history.force_encoding Encoding::UTF_8 diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb index a48c32b52e7f3a..64b187f26f237a 100644 --- a/lib/rubygems/commands/signin_command.rb +++ b/lib/rubygems/commands/signin_command.rb @@ -12,7 +12,6 @@ def initialize add_option('--host HOST', 'Push to another gemcutter-compatible host') do |value, options| options[:host] = value end - end def description # :nodoc: diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb index 2452e8cae1ca10..2d7329c5909066 100644 --- a/lib/rubygems/commands/signout_command.rb +++ b/lib/rubygems/commands/signout_command.rb @@ -19,9 +19,9 @@ def usage # :nodoc: def execute credentials_path = Gem.configuration.credentials_path - if !File.exist?(credentials_path) then + if !File.exist?(credentials_path) alert_error 'You are not currently signed in.' - elsif !File.writable?(credentials_path) then + elsif !File.writable?(credentials_path) alert_error "File '#{Gem.configuration.credentials_path}' is read-only."\ ' Please make sure it is writable.' else diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index 7e46963a4c336e..af145fd7b4b3a9 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -38,13 +38,13 @@ def initialize add_proxy_option end - def add_source source_uri # :nodoc: + def add_source(source_uri) # :nodoc: check_rubygems_https source_uri source = Gem::Source.new source_uri begin - if Gem.sources.include? source then + if Gem.sources.include? source say "source #{source_uri} already present in the cache" else source.load_specs :released @@ -62,11 +62,11 @@ def add_source source_uri # :nodoc: end end - def check_rubygems_https source_uri # :nodoc: + def check_rubygems_https(source_uri) # :nodoc: uri = URI source_uri if uri.scheme and uri.scheme.downcase == 'http' and - uri.host.downcase == 'rubygems.org' then + uri.host.downcase == 'rubygems.org' question = <<-QUESTION.chomp https://rubygems.org is recommended for security over #{uri} @@ -81,10 +81,10 @@ def clear_all # :nodoc: path = Gem.spec_cache_dir FileUtils.rm_rf path - unless File.exist? path then + unless File.exist? path say "*** Removed specs cache ***" else - unless File.writable? path then + unless File.writable? path say "*** Unable to remove source cache (write protected) ***" else say "*** Unable to remove source cache ***" @@ -175,8 +175,8 @@ def execute list if list? end - def remove_source source_uri # :nodoc: - unless Gem.sources.include? source_uri then + def remove_source(source_uri) # :nodoc: + unless Gem.sources.include? source_uri say "source #{source_uri} not present in cache" else Gem.sources.delete source_uri @@ -195,12 +195,12 @@ def update # :nodoc: say "source cache successfully updated" end - def remove_cache_file desc, path # :nodoc: + def remove_cache_file(desc, path) # :nodoc: FileUtils.rm_rf path - if not File.exist?(path) then + if not File.exist?(path) say "*** Removed #{desc} source cache ***" - elsif not File.writable?(path) then + elsif not File.writable?(path) say "*** Unable to remove #{desc} source cache (write protected) ***" else say "*** Unable to remove #{desc} source cache ***" @@ -208,4 +208,3 @@ def remove_cache_file desc, path # :nodoc: end end - diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index ad8840adc2bc40..56b9371686ba7f 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -75,7 +75,7 @@ def execute specs = [] gem = options[:args].shift - unless gem then + unless gem raise Gem::CommandLineError, "Please specify a gem name or file on the command line" end @@ -105,29 +105,29 @@ def execute raise Gem::CommandLineError, "--ruby and FIELD are mutually exclusive" if field and options[:format] == :ruby - if local? then - if File.exist? gem then + if local? + if File.exist? gem specs << Gem::Package.new(gem).spec rescue nil end - if specs.empty? then + if specs.empty? specs.push(*dep.matching_specs) end end - if remote? then + if remote? dep.prerelease = options[:prerelease] found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep specs.push(*found.map { |spec,| spec }) end - if specs.empty? then + if specs.empty? alert_error "No gem matching '#{dep}' found" terminate_interaction 1 end - unless options[:all] then + unless options[:all] specs = [specs.max_by { |s| s.version }] end diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index d1ffe675fe37f2..9de0ea722bc2b8 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -81,7 +81,7 @@ def initialize add_option('--vendor', 'Uninstall gem from the vendor directory.', 'Only for use by gem repackagers.') do |value, options| - unless Gem.vendor_dir then + unless Gem.vendor_dir raise OptionParser::InvalidOption.new 'your platform is not supported' end @@ -114,10 +114,21 @@ def usage # :nodoc: "#{program_name} GEMNAME [GEMNAME ...]" end + def check_version # :nodoc: + if options[:version] != Gem::Requirement.default and + get_all_gem_names.size > 1 + alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ + " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + terminate_interaction 1 + end + end + def execute - if options[:all] and not options[:args].empty? then + check_version + + if options[:all] and not options[:args].empty? uninstall_specific - elsif options[:all] then + elsif options[:all] uninstall_all else uninstall_specific @@ -138,8 +149,9 @@ def uninstall_all def uninstall_specific deplist = Gem::DependencyList.new - get_all_gem_names.uniq.each do |name| - gem_specs = Gem::Specification.find_all_by_name(name) + get_all_gem_names_and_versions.each do |name, version| + requirement = Array(version || options[:version]) + gem_specs = Gem::Specification.find_all_by_name(name, *requirement) say("Gem '#{name}' is not installed") if gem_specs.empty? gem_specs.each do |spec| deplist.add spec @@ -148,8 +160,9 @@ def uninstall_specific deps = deplist.strongly_connected_components.flatten.reverse - deps.map(&:name).uniq.each do |gem_name| - uninstall_gem(gem_name) + deps.each do |dep| + options[:version] = dep.version + uninstall_gem(dep.name) end end diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb index b2edf7d34979a9..4a1bd8a0d6d5ca 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -79,15 +79,15 @@ def execute dependency = Gem::Dependency.new name, options[:version] path = get_path dependency - unless path then + unless path alert_error "Gem '#{name}' not installed nor fetchable." next end - if @options[:spec] then + if @options[:spec] spec, metadata = get_metadata path, security_policy - if metadata.nil? then + if metadata.nil? alert_error "--spec is unsupported on '#{name}' (old format gem)" next end @@ -152,7 +152,7 @@ def find_in_cache(filename) # TODO: It just uses Gem.dir for now. What's an easy way to get the list of # source directories? - def get_path dependency + def get_path(dependency) return dependency.name if dependency.name =~ /\.gem$/i specs = dependency.matching_specs @@ -180,7 +180,7 @@ def get_path dependency #-- # TODO move to Gem::Package as #raw_spec or something - def get_metadata path, security_policy = nil + def get_metadata(path, security_policy = nil) format = Gem::Package.new path, security_policy spec = format.spec @@ -202,4 +202,3 @@ def get_metadata path, security_policy = nil end end - diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index dc924265b0d7c7..9ab3b80e96cd90 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -68,8 +68,8 @@ def usage # :nodoc: "#{program_name} GEMNAME [GEMNAME ...]" end - def check_latest_rubygems version # :nodoc: - if Gem.rubygems_version == version then + def check_latest_rubygems(version) # :nodoc: + if Gem.rubygems_version == version say "Latest version already installed. Done." terminate_interaction end @@ -78,14 +78,14 @@ def check_latest_rubygems version # :nodoc: end def check_update_arguments # :nodoc: - unless options[:args].empty? then + unless options[:args].empty? alert_error "Gem names are not allowed with the --system option" terminate_interaction 1 end end def execute - if options[:system] then + if options[:system] update_rubygems return end @@ -111,7 +111,7 @@ def execute updated_names = updated.map { |spec| spec.name } not_updated_names = options[:args].uniq - updated_names - if updated.empty? then + if updated.empty? say "Nothing to update" else say "Gems updated: #{updated_names.join(' ')}" @@ -119,7 +119,7 @@ def execute end end - def fetch_remote_gems spec # :nodoc: + def fetch_remote_gems(spec) # :nodoc: dependency = Gem::Dependency.new spec.name, "> #{spec.version}" dependency.prerelease = options[:prerelease] @@ -138,7 +138,7 @@ def highest_installed_gems # :nodoc: hig = {} # highest installed gems Gem::Specification.each do |spec| - if hig[spec.name].nil? or hig[spec.name].version < spec.version then + if hig[spec.name].nil? or hig[spec.name].version < spec.version hig[spec.name] = spec end end @@ -146,7 +146,7 @@ def highest_installed_gems # :nodoc: hig end - def highest_remote_version spec # :nodoc: + def highest_remote_version(spec) # :nodoc: spec_tuples = fetch_remote_gems spec matching_gems = spec_tuples.select do |g,_| @@ -160,7 +160,7 @@ def highest_remote_version spec # :nodoc: highest_remote_gem.first.version end - def install_rubygems version # :nodoc: + def install_rubygems(version) # :nodoc: args = update_rubygems_arguments update_dir = File.join Gem.dir, 'gems', "rubygems-update-#{version}" @@ -177,7 +177,7 @@ def rubygems_target_version version = options[:system] update_latest = version == true - if update_latest then + if update_latest version = Gem::Version.new Gem::VERSION requirement = Gem::Requirement.new ">= #{Gem::VERSION}" else @@ -196,7 +196,7 @@ def rubygems_target_version gems_to_update = which_to_update hig, options[:args], :system _, up_ver = gems_to_update.first - target = if update_latest then + target = if update_latest up_ver else version @@ -205,7 +205,7 @@ def rubygems_target_version return target, requirement end - def update_gem name, version = Gem::Requirement.default + def update_gem(name, version = Gem::Requirement.default) return if @updated.any? { |spec| spec.name == name } update_options = options.dup @@ -225,7 +225,7 @@ def update_gem name, version = Gem::Requirement.default end end - def update_gems gems_to_update + def update_gems(gems_to_update) gems_to_update.uniq.sort.each do |(name, version)| update_gem name, version end @@ -264,7 +264,7 @@ def update_rubygems_arguments # :nodoc: args end - def which_to_update highest_installed_gems, gem_names, system = false + def which_to_update(highest_installed_gems, gem_names, system = false) result = [] highest_installed_gems.each do |l_name, l_spec| @@ -273,7 +273,7 @@ def which_to_update highest_installed_gems, gem_names, system = false highest_remote_ver = highest_remote_version l_spec - if system or (l_spec.version < highest_remote_ver) then + if system or (l_spec.version < highest_remote_ver) result << [l_spec.name, [l_spec.version, highest_remote_ver].max] end end diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb index 704d79fc60caea..5c51ed72dd9d67 100644 --- a/lib/rubygems/commands/which_command.rb +++ b/lib/rubygems/commands/which_command.rb @@ -44,8 +44,8 @@ def execute spec = Gem::Specification.find_by_path arg - if spec then - if options[:search_gems_first] then + if spec + if options[:search_gems_first] dirs = spec.full_require_paths + $LOAD_PATH else dirs = $LOAD_PATH + spec.full_require_paths @@ -55,7 +55,7 @@ def execute # TODO: this is totally redundant and stupid paths = find_paths arg, dirs - if paths.empty? then + if paths.empty? alert_error "Can't find Ruby library file or shared library #{arg}" found &&= false @@ -73,7 +73,7 @@ def find_paths(package_name, dirs) dirs.each do |dir| Gem.suffixes.each do |ext| full_path = File.join dir, "#{package_name}#{ext}" - if File.exist? full_path and not File.directory? full_path then + if File.exist? full_path and not File.directory? full_path result << full_path return result unless options[:show_all] end @@ -88,4 +88,3 @@ def usage # :nodoc: end end - diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb index ebf24e5c771e9f..d13b674b488a0c 100644 --- a/lib/rubygems/commands/yank_command.rb +++ b/lib/rubygems/commands/yank_command.rb @@ -51,7 +51,7 @@ def execute version = get_version_from_requirements(options[:version]) platform = get_platform_from_requirements(options) - if version then + if version yank_gem(version, platform) else say "A version argument is required: #{usage}" @@ -93,4 +93,3 @@ def get_platform_from_requirements(requirements) end end - diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index f8782015a1ce71..e02655fc515612 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -174,12 +174,12 @@ def initialize(args) arg_list = [] args.each do |arg| - if need_config_file_name then + if need_config_file_name @config_file_name = arg need_config_file_name = false - elsif arg =~ /^--config-file=(.*)/ then + elsif arg =~ /^--config-file=(.*)/ @config_file_name = $1 - elsif arg =~ /^--config-file$/ then + elsif arg =~ /^--config-file$/ need_config_file_name = true else arg_list << arg @@ -281,13 +281,13 @@ def credentials_path def load_api_keys check_credentials_permissions - @api_keys = if File.exist? credentials_path then + @api_keys = if File.exist? credentials_path load_file(credentials_path) else @hash end - if @api_keys.key? :rubygems_api_key then + if @api_keys.key? :rubygems_api_key @rubygems_api_key = @api_keys[:rubygems_api_key] @api_keys[:rubygems] = @api_keys.delete :rubygems_api_key unless @api_keys.key? :rubygems @@ -306,7 +306,7 @@ def rubygems_api_key ## # Sets the RubyGems.org API key to +api_key+ - def rubygems_api_key= api_key + def rubygems_api_key=(api_key) set_api_key :rubygems_api_key, api_key @rubygems_api_key = api_key @@ -315,7 +315,7 @@ def rubygems_api_key= api_key ## # Set a specific host's API key to +api_key+ - def set_api_key host, api_key + def set_api_key(host, api_key) check_credentials_permissions config = load_file(credentials_path).merge(host => api_key) diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb index e93cd7c7282be0..3b780116197827 100755 --- a/lib/rubygems/core_ext/kernel_require.rb +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -31,7 +31,7 @@ module Kernel # The normal require functionality of returning false if # that file has already been loaded is preserved. - def require path + def require(path) RUBYGEMS_ACTIVATION_MONITOR.enter path = path.to_path if path.respond_to? :to_path @@ -49,7 +49,7 @@ def require path # If there are no unresolved deps, then we can use just try # normal require handle loading a gem from the rescue below. - if Gem::Specification.unresolved_deps.empty? then + if Gem::Specification.unresolved_deps.empty? RUBYGEMS_ACTIVATION_MONITOR.exit return gem_original_require(path) end @@ -79,7 +79,7 @@ def require path # requested, then find_in_unresolved_tree will find d.rb in d because # it's a dependency of c. # - if found_specs.empty? then + if found_specs.empty? found_specs = Gem::Specification.find_in_unresolved_tree path found_specs.each do |found_spec| @@ -94,7 +94,7 @@ def require path # versions of the same gem names = found_specs.map(&:name).uniq - if names.size > 1 then + if names.size > 1 RUBYGEMS_ACTIVATION_MONITOR.exit raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ', '}" end @@ -103,7 +103,7 @@ def require path # at the highest version. valid = found_specs.find { |s| !s.has_conflicts? } - unless valid then + unless valid le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate" le.name = names.first RUBYGEMS_ACTIVATION_MONITOR.exit @@ -120,7 +120,7 @@ def require path begin if load_error.message.start_with?("Could not find") or - (load_error.message.end_with?(path) and Gem.try_activate(path)) then + (load_error.message.end_with?(path) and Gem.try_activate(path)) require_again = true end ensure diff --git a/lib/rubygems/core_ext/kernel_warn.rb b/lib/rubygems/core_ext/kernel_warn.rb index 319fdfe606ce07..3e531441edc385 100755 --- a/lib/rubygems/core_ext/kernel_warn.rb +++ b/lib/rubygems/core_ext/kernel_warn.rb @@ -1,28 +1,45 @@ # frozen_string_literal: true +# `uplevel` keyword argument of Kernel#warn is available since ruby 2.5. if RUBY_VERSION >= "2.5" + module Kernel - path = "#{__dir__}/" + path = "#{__dir__}/" # Frames to be skipped start with this path. + + # Suppress "method redefined" warning original_warn = instance_method(:warn) Module.new {define_method(:warn, original_warn)} + original_warn = method(:warn) module_function define_method(:warn) {|*messages, uplevel: nil| - if uplevel - uplevel, = [uplevel].pack("l!").unpack("l!") - if uplevel >= 0 - start = 0 - begin - loc, = caller_locations(start, 1) - break start += uplevel unless loc - start += 1 - end while (loc.path.start_with?(path) or (uplevel -= 1) >= 0) - uplevel = start + unless uplevel + return original_warn.call(*messages) + end + + # Ensure `uplevel` fits a `long` + uplevel, = [uplevel].pack("l!").unpack("l!") + + if uplevel >= 0 + start = 0 + while uplevel >= 0 + loc, = caller_locations(start, 1) + unless loc + # No more backtrace + start += uplevel + break + end + + start += 1 + + unless loc.path.start_with?(path) + # Non-rubygems frames + uplevel -= 1 + end end - original_warn.call(*messages, uplevel: uplevel) - else - original_warn.call(*messages) + uplevel = start end + original_warn.call(*messages, uplevel: uplevel) } end end diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index 334ef4d4c14a8b..50f46b32a272bc 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -28,13 +28,13 @@ def self.default_spec_cache_dir # specified in the environment def self.default_dir - path = if defined? RUBY_FRAMEWORK_VERSION then + path = if defined? RUBY_FRAMEWORK_VERSION [ File.dirname(RbConfig::CONFIG['sitedir']), 'Gems', RbConfig::CONFIG['ruby_version'] ] - elsif RbConfig::CONFIG['rubylibprefix'] then + elsif RbConfig::CONFIG['rubylibprefix'] [ RbConfig::CONFIG['rubylibprefix'], 'gems', @@ -59,7 +59,7 @@ def self.default_dir # By default, the binary extensions are located side by side with their # Ruby counterparts, therefore nil is returned - def self.default_ext_dir_for base_dir + def self.default_ext_dir_for(base_dir) nil end @@ -103,7 +103,7 @@ def self.default_path def self.default_exec_format exec_format = RbConfig::CONFIG['ruby_install_name'].sub('ruby', '%s') rescue '%s' - unless exec_format =~ /%s/ then + unless exec_format =~ /%s/ raise Gem::Exception, "[BUG] invalid exec_format #{exec_format.inspect}, no %s" end @@ -115,7 +115,7 @@ def self.default_exec_format # The default directory for binaries def self.default_bindir - if defined? RUBY_FRAMEWORK_VERSION then # mac framework support + if defined? RUBY_FRAMEWORK_VERSION # mac framework support '/usr/bin' else # generic install RbConfig::CONFIG['bindir'] @@ -126,7 +126,7 @@ def self.default_bindir # A wrapper around RUBY_ENGINE const that may not be defined def self.ruby_engine - if defined? RUBY_ENGINE then + if defined? RUBY_ENGINE RUBY_ENGINE else 'ruby' @@ -165,7 +165,7 @@ def self.install_extension_in_lib # :nodoc: # Directory where vendor gems are installed. def self.vendor_dir # :nodoc: - if vendor_dir = ENV['GEM_VENDOR'] then + if vendor_dir = ENV['GEM_VENDOR'] return vendor_dir.dup end diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index c06df0fa35b073..33ba1968d10c44 100644 --- a/lib/rubygems/dependency.rb +++ b/lib/rubygems/dependency.rb @@ -36,7 +36,7 @@ class Gem::Dependency # argument can optionally be the dependency type, which defaults to # :runtime. - def initialize name, *requirements + def initialize(name, *requirements) case name when String then # ok when Regexp then @@ -76,7 +76,7 @@ def hash # :nodoc: end def inspect # :nodoc: - if prerelease? then + if prerelease? "<%s type=%p name=%p requirements=%p prerelease=ok>" % [self.class, self.type, self.name, requirement.to_s] else @@ -100,7 +100,7 @@ def latest_version? @requirement.none? end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 1, 'Gem::Dependency.new(', ')' do q.pp name q.text ',' @@ -152,7 +152,7 @@ def requirements_list end def to_s # :nodoc: - if type != :runtime then + if type != :runtime "#{name} (#{requirement}, #{type})" else "#{name} (#{requirement})" @@ -170,7 +170,7 @@ def runtime? @type == :runtime || !@type end - def == other # :nodoc: + def ==(other) # :nodoc: Gem::Dependency === other && self.name == other.name && self.type == other.type && @@ -180,7 +180,7 @@ def == other # :nodoc: ## # Dependencies are ordered by name. - def <=> other + def <=>(other) self.name <=> other.name end @@ -190,7 +190,7 @@ def <=> other # other has only an equal version requirement that satisfies this # dependency. - def =~ other + def =~(other) unless Gem::Dependency === other return unless other.respond_to?(:name) && other.respond_to?(:version) other = Gem::Dependency.new other.name, other.version @@ -222,7 +222,7 @@ def =~ other # NOTE: Unlike #matches_spec? this method does not return true when the # version is a prerelease version unless this is a prerelease dependency. - def match? obj, version=nil, allow_prerelease=false + def match?(obj, version=nil, allow_prerelease=false) if !version name = obj.name version = obj.version @@ -249,7 +249,7 @@ def match? obj, version=nil, allow_prerelease=false # returns true when +spec+ is a prerelease version even if this dependency # is not a prerelease dependency. - def matches_spec? spec + def matches_spec?(spec) return false unless name === spec.name return true if requirement.none? @@ -259,8 +259,8 @@ def matches_spec? spec ## # Merges the requirements of +other+ into this dependency - def merge other - unless name == other.name then + def merge(other) + unless name == other.name raise ArgumentError, "#{self} and #{other} have different names" end @@ -275,7 +275,7 @@ def merge other self.class.new name, self_req.as_list.concat(other_req.as_list) end - def matching_specs platform_only = false + def matching_specs(platform_only = false) env_req = Gem.env_requirement(name) matches = Gem::Specification.stubs_for(name).find_all { |spec| requirement.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version) @@ -304,7 +304,7 @@ def to_specs # TODO: check Gem.activated_spec[self.name] in case matches falls outside - if matches.empty? then + if matches.empty? specs = Gem::Specification.stubs_for name if specs.empty? diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 89ce9afe29438e..346208603ce78f 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -66,7 +66,7 @@ class Gem::DependencyInstaller # :wrappers:: See Gem::Installer::new # :build_args:: See Gem::Installer::new - def initialize options = {} + def initialize(options = {}) @only_install_dir = !!options[:install_dir] @install_dir = options[:install_dir] || Gem.dir @build_root = options[:build_root] @@ -110,7 +110,7 @@ def initialize options = {} #-- # TODO remove at RubyGems 4, no longer used - def add_found_dependencies to_do, dependency_list # :nodoc: + def add_found_dependencies(to_do, dependency_list) # :nodoc: seen = {} dependencies = Hash.new { |h, name| h[name] = Gem::Dependency.new name } @@ -164,8 +164,8 @@ def add_found_dependencies to_do, dependency_list # :nodoc: # Creates an AvailableSet to install from based on +dep_or_name+ and # +version+ - def available_set_for dep_or_name, version # :nodoc: - if String === dep_or_name then + def available_set_for(dep_or_name, version) # :nodoc: + if String === dep_or_name find_spec_by_name_and_version dep_or_name, version, @prerelease else dep = dep_or_name.dup @@ -198,7 +198,7 @@ def consider_remote? # sources. Gems are sorted with newer gems preferred over older gems, and # local gems preferred over remote gems. - def find_gems_with_sources dep, best_only=false # :nodoc: + def find_gems_with_sources(dep, best_only=false) # :nodoc: set = Gem::AvailableSet.new if consider_local? @@ -272,16 +272,16 @@ def find_gems_with_sources dep, best_only=false # :nodoc: # +version+. Returns an Array of specs and sources required for # installation of the gem. - def find_spec_by_name_and_version gem_name, + def find_spec_by_name_and_version(gem_name, version = Gem::Requirement.default, - prerelease = false + prerelease = false) set = Gem::AvailableSet.new if consider_local? - if gem_name =~ /\.gem$/ and File.file? gem_name then + if gem_name =~ /\.gem$/ and File.file? gem_name src = Gem::Source::SpecificFile.new(gem_name) set.add src.spec, src - elsif gem_name =~ /\.gem$/ then + elsif gem_name =~ /\.gem$/ Dir[gem_name].each do |name| begin src = Gem::Source::SpecificFile.new name @@ -341,7 +341,7 @@ def gather_dependencies # :nodoc: Gem::Specification.include?(spec) } - unless dependency_list.ok? or @ignore_dependencies or @force then + unless dependency_list.ok? or @ignore_dependencies or @force reason = dependency_list.why_not_ok?.map { |k,v| "#{k} requires #{v.join(", ")}" }.join("; ") @@ -352,7 +352,7 @@ def gather_dependencies # :nodoc: end deprecate :gather_dependencies, :none, 2018, 12 - def in_background what # :nodoc: + def in_background(what) # :nodoc: fork_happened = false if @build_docs_in_background and Process.respond_to?(:fork) begin @@ -381,7 +381,7 @@ def in_background what # :nodoc: # c-1.a, b-1 and a-1.a will be installed. b-1.a will need to be installed # separately. - def install dep_or_name, version = Gem::Requirement.default + def install(dep_or_name, version = Gem::Requirement.default) request_set = resolve_dependencies dep_or_name, version @installed_gems = [] @@ -425,16 +425,16 @@ def install dep_or_name, version = Gem::Requirement.default end def install_development_deps # :nodoc: - if @development and @dev_shallow then + if @development and @dev_shallow :shallow - elsif @development then + elsif @development :all else :none end end - def resolve_dependencies dep_or_name, version # :nodoc: + def resolve_dependencies(dep_or_name, version) # :nodoc: request_set = Gem::RequestSet.new request_set.development = @development request_set.development_shallow = @dev_shallow @@ -446,11 +446,11 @@ def resolve_dependencies dep_or_name, version # :nodoc: installer_set.ignore_installed = @only_install_dir if consider_local? - if dep_or_name =~ /\.gem$/ and File.file? dep_or_name then + if dep_or_name =~ /\.gem$/ and File.file? dep_or_name src = Gem::Source::SpecificFile.new dep_or_name installer_set.add_local dep_or_name, src.spec, src version = src.spec.version if version == Gem::Requirement.default - elsif dep_or_name =~ /\.gem$/ then + elsif dep_or_name =~ /\.gem$/ Dir[dep_or_name].each do |name| begin src = Gem::Source::SpecificFile.new name @@ -463,9 +463,9 @@ def resolve_dependencies dep_or_name, version # :nodoc: end dependency = - if spec = installer_set.local?(dep_or_name) then + if spec = installer_set.local?(dep_or_name) Gem::Dependency.new spec.name, version - elsif String === dep_or_name then + elsif String === dep_or_name Gem::Dependency.new dep_or_name, version else dep_or_name @@ -479,7 +479,7 @@ def resolve_dependencies dep_or_name, version # :nodoc: request_set.always_install = installer_set.always_install - if @ignore_dependencies then + if @ignore_dependencies installer_set.ignore_dependencies = true request_set.ignore_dependencies = true request_set.soft_missing = true diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb index d8314eaf60f5b1..ee2e4c7343bf31 100644 --- a/lib/rubygems/dependency_list.rb +++ b/lib/rubygems/dependency_list.rb @@ -40,7 +40,7 @@ def self.from_specs # Creates a new DependencyList. If +development+ is true, development # dependencies will be included. - def initialize development = false + def initialize(development = false) @specs = [] @development = development @@ -79,8 +79,8 @@ def dependency_order seen = {} sorted.each do |spec| - if index = seen[spec.name] then - if result[index].version < spec.version then + if index = seen[spec.name] + if result[index].version < spec.version result[index] = spec end else @@ -114,7 +114,7 @@ def ok? why_not_ok?(:quick).empty? end - def why_not_ok? quick = false + def why_not_ok?(quick = false) unsatisfied = Hash.new { |h,k| h[k] = [] } each do |spec| spec.runtime_dependencies.each do |dep| @@ -123,7 +123,7 @@ def why_not_ok? quick = false dep.requirement.satisfied_by? installed_spec.version } - unless inst or @specs.find { |s| s.satisfies_requirement? dep } then + unless inst or @specs.find { |s| s.satisfies_requirement? dep } unsatisfied[spec.name] << dep return unsatisfied if quick end @@ -172,7 +172,7 @@ def ok_to_remove?(full_name, check_dev=true) # satisfy items in +dependencies+ (a hash of gem names to arrays of # dependencies). - def remove_specs_unsatisfied_by dependencies + def remove_specs_unsatisfied_by(dependencies) specs.reject! { |spec| dep = dependencies[spec.name] dep and not dep.requirement.satisfied_by? spec.version @@ -200,7 +200,7 @@ def spec_predecessors next if spec == other other.dependencies.each do |dep| - if spec.satisfies_requirement? dep then + if spec.satisfies_requirement? dep result[spec] << other end end @@ -222,7 +222,7 @@ def tsort_each_child(node) dependencies.each do |dep| specs.each do |spec| - if spec.satisfies_requirement? dep then + if spec.satisfies_requirement? dep yield spec break end @@ -241,4 +241,3 @@ def active_count(specs, ignored) end end - diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 375194c1e83c75..815f42ae8c5c98 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -27,7 +27,7 @@ def self.skip # :nodoc: @skip ||= false end - def self.skip= v # :nodoc: + def self.skip=(v) # :nodoc: @skip = v end @@ -47,7 +47,7 @@ def skip_during # telling the user of +repl+ (unless +repl+ is :none) and the # year/month that it is planned to go away. - def deprecate name, repl, year, month + def deprecate(name, repl, year, month) class_eval { old = "_deprecated_#{name}" alias_method old, name @@ -68,4 +68,3 @@ def deprecate name, repl, year, month module_function :deprecate, :skip_during end - diff --git a/lib/rubygems/doctor.rb b/lib/rubygems/doctor.rb index e5d8c43de86ebb..661ae5a4e10d43 100644 --- a/lib/rubygems/doctor.rb +++ b/lib/rubygems/doctor.rb @@ -41,7 +41,7 @@ class Gem::Doctor # # If +dry_run+ is true no files or directories will be removed. - def initialize gem_repository, dry_run = false + def initialize(gem_repository, dry_run = false) @gem_repository = gem_repository @dry_run = dry_run @@ -73,7 +73,7 @@ def doctor Gem.use_paths @gem_repository.to_s - unless gem_repository? then + unless gem_repository? say 'This directory does not appear to be a RubyGems repository, ' + 'skipping' say @@ -99,7 +99,7 @@ def doctor_children # :nodoc: ## # Removes files in +sub_directory+ with +extension+ - def doctor_child sub_directory, extension # :nodoc: + def doctor_child(sub_directory, extension) # :nodoc: directory = File.join(@gem_repository, sub_directory) Dir.entries(directory).sort.each do |ent| @@ -115,7 +115,7 @@ def doctor_child sub_directory, extension # :nodoc: type = File.directory?(child) ? 'directory' : 'file' - action = if @dry_run then + action = if @dry_run 'Extra' else FileUtils.rm_r(child) @@ -129,4 +129,3 @@ def doctor_child sub_directory, extension # :nodoc: end end - diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb index 6f2847d548a3eb..6773bbcd269059 100644 --- a/lib/rubygems/errors.rb +++ b/lib/rubygems/errors.rb @@ -25,7 +25,7 @@ class LoadError < ::LoadError # system. Instead of rescuing from this class, make sure to rescue from the # superclass Gem::LoadError to catch all types of load errors. class MissingSpecError < Gem::LoadError - def initialize name, requirement + def initialize(name, requirement) @name = name @requirement = requirement end @@ -50,7 +50,7 @@ def build_message class MissingSpecVersionError < MissingSpecError attr_reader :specs - def initialize name, requirement, specs + def initialize(name, requirement, specs) super(name, requirement) @specs = specs end @@ -81,7 +81,7 @@ class ConflictError < LoadError attr_reader :target - def initialize target, conflicts + def initialize(target, conflicts) @target = target @conflicts = conflicts @name = target.name diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index 5999bc2a0d5bc8..a387cd390fb9fe 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -36,7 +36,7 @@ class Gem::DependencyResolutionError < Gem::DependencyError attr_reader :conflict - def initialize conflict + def initialize(conflict) @conflict = conflict a, b = conflicting_dependencies @@ -77,7 +77,7 @@ class Gem::FilePermissionError < Gem::Exception attr_reader :directory - def initialize directory + def initialize(directory) @directory = directory super "You don't have write permissions for the #{directory} directory." @@ -137,7 +137,7 @@ class Gem::ImpossibleDependenciesError < Gem::Exception attr_reader :conflicts attr_reader :request - def initialize request, conflicts + def initialize(request, conflicts) @request = request @conflicts = conflicts @@ -249,7 +249,7 @@ class Gem::UnsatisfiableDependencyError < Gem::DependencyError # Creates a new UnsatisfiableDependencyError for the unsatisfiable # Gem::Resolver::DependencyRequest +dep+ - def initialize dep, platform_mismatch=nil + def initialize(dep, platform_mismatch=nil) if platform_mismatch and !platform_mismatch.empty? plats = platform_mismatch.map { |x| x.platform.to_s }.sort.uniq super "Unable to resolve dependency: No match for '#{dep}' on this platform. Found: #{plats.join(', ')}" diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb index 18d2bc233ae438..35a486606abbca 100644 --- a/lib/rubygems/ext.rb +++ b/lib/rubygems/ext.rb @@ -16,4 +16,3 @@ module Gem::Ext; end require 'rubygems/ext/ext_conf_builder' require 'rubygems/ext/rake_builder' require 'rubygems/ext/cmake_builder' - diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb index 0b3c17a9a0d5cd..6dffddb5cc7b26 100644 --- a/lib/rubygems/ext/build_error.rb +++ b/lib/rubygems/ext/build_error.rb @@ -4,4 +4,3 @@ class Gem::Ext::BuildError < Gem::InstallError end - diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index b3b90339624a84..f578e7feee4389 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -27,14 +27,14 @@ def self.class_name end def self.make(dest_path, results) - unless File.exist? 'Makefile' then + unless File.exist? 'Makefile' raise Gem::InstallError, 'Makefile not found' end # try to find make program from Ruby configure arguments first RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/ make_program = ENV['MAKE'] || ENV['make'] || $1 - unless make_program then + unless make_program make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make' end @@ -56,6 +56,7 @@ def self.make(dest_path, results) end def self.redirector + warn "#{caller[0]}: Use IO.popen(..., err: [:child, :out])" '2>&1' end @@ -63,28 +64,35 @@ def self.run(command, results, command_name = nil) verbose = Gem.configuration.really_verbose begin - # TODO use Process.spawn when ruby 1.8 support is dropped. rubygems_gemdeps, ENV['RUBYGEMS_GEMDEPS'] = ENV['RUBYGEMS_GEMDEPS'], nil if verbose puts("current directory: #{Dir.pwd}") - puts(command) - system(command) - else - results << "current directory: #{Dir.pwd}" - results << command - results << `#{command} #{redirector}` + p(command) end + results << "current directory: #{Dir.pwd}" + results << (command.respond_to?(:shelljoin) ? command.shelljoin : command) + + redirections = verbose ? {} : {err: [:child, :out]} + IO.popen(command, "r", redirections) do |io| + if verbose + IO.copy_stream(io, $stdout) + else + results << io.read + end + end + rescue => error + raise Gem::InstallError, "#{command_name || class_name} failed#{error.message}" ensure ENV['RUBYGEMS_GEMDEPS'] = rubygems_gemdeps end - unless $?.success? then + unless $?.success? results << "Building has failed. See above output for more information on the failure." if verbose exit_reason = - if $?.exited? then + if $?.exited? ", exit code #{$?.exitstatus}" - elsif $?.signaled? then + elsif $?.signaled? ", uncaught signal #{$?.termsig}" end @@ -97,7 +105,7 @@ def self.run(command, results, command_name = nil) # have build arguments, saved, set +build_args+ which is an ARGV-style # array. - def initialize spec, build_args = spec.build_args + def initialize(spec, build_args = spec.build_args) @spec = spec @build_args = build_args @gem_dir = spec.full_gem_path @@ -108,7 +116,7 @@ def initialize spec, build_args = spec.build_args ## # Chooses the extension builder class for +extension+ - def builder_for extension # :nodoc: + def builder_for(extension) # :nodoc: case extension when /extconf/ then Gem::Ext::ExtConfBuilder @@ -130,7 +138,7 @@ def builder_for extension # :nodoc: ## # Logs the build +output+ in +build_dir+, then raises Gem::Ext::BuildError. - def build_error build_dir, output, backtrace = nil # :nodoc: + def build_error(build_dir, output, backtrace = nil) # :nodoc: gem_make_out = write_gem_make_out output message = <<-EOF @@ -145,7 +153,7 @@ def build_error build_dir, output, backtrace = nil # :nodoc: raise Gem::Ext::BuildError, message, backtrace end - def build_extension extension, dest_path # :nodoc: + def build_extension(extension, dest_path) # :nodoc: results = [] # FIXME: Determine if this line is necessary and, if so, why. @@ -227,7 +235,7 @@ def build_extensions ## # Writes +output+ to gem_make.out in the extension install directory. - def write_gem_make_out output # :nodoc: + def write_gem_make_out(output) # :nodoc: destination = File.join @spec.extension_dir, 'gem_make.out' FileUtils.mkdir_p @spec.extension_dir diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb index cfcc83c9a7f644..ab226733d974eb 100644 --- a/lib/rubygems/ext/cmake_builder.rb +++ b/lib/rubygems/ext/cmake_builder.rb @@ -3,7 +3,7 @@ class Gem::Ext::CmakeBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args=[], lib_dir=nil) - unless File.exist?('Makefile') then + unless File.exist?('Makefile') cmd = "cmake . -DCMAKE_INSTALL_PREFIX=#{dest_path}" cmd << " #{Gem::Command.build_args.join ' '}" unless Gem::Command.build_args.empty? diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index 1b9e3740754fe5..7d105c9bd3d489 100644 --- a/lib/rubygems/ext/configure_builder.rb +++ b/lib/rubygems/ext/configure_builder.rb @@ -8,7 +8,7 @@ class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args=[], lib_dir=nil) - unless File.exist?('Makefile') then + unless File.exist?('Makefile') cmd = "sh ./configure --prefix=#{dest_path}" cmd << " #{args.join ' '}" unless args.empty? diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index 18e300d8c2495c..5a2b3eb5330b27 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -7,6 +7,7 @@ require 'fileutils' require 'tempfile' +require 'shellwords' class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder FileEntry = FileUtils::Entry_ # :nodoc: @@ -38,13 +39,15 @@ def self.build(extension, dest_path, results, args=[], lib_dir=nil) destdir = ENV["DESTDIR"] begin - cmd = [Gem.ruby, "-I", File.expand_path("../../..", __FILE__), "-r", get_relative_path(siteconf.path), File.basename(extension), *args].join ' ' + cmd = Gem.ruby.shellsplit << "-I" << File.expand_path("../../..", __FILE__) << + "-r" << get_relative_path(siteconf.path) << File.basename(extension) + cmd.push(*args) begin run cmd, results ensure if File.exist? 'mkmf.log' - unless $?.success? then + unless $?.success? results << "To see why this extension failed to compile, please check" \ " the mkmf.log which can be found here:\n" results << " " + File.join(dest_path, 'mkmf.log') + "\n" @@ -60,7 +63,7 @@ def self.build(extension, dest_path, results, args=[], lib_dir=nil) if tmp_dest # TODO remove in RubyGems 3 - if Gem.install_extension_in_lib and lib_dir then + if Gem.install_extension_in_lib and lib_dir FileUtils.mkdir_p lib_dir entries = Dir.entries(tmp_dest) - %w[. ..] entries = entries.map { |entry| File.join tmp_dest, entry } diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index 890803aaef0bb9..52041a2713df95 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -10,23 +10,24 @@ class Gem::Ext::RakeBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args=[], lib_dir=nil) - if File.basename(extension) =~ /mkrf_conf/i then - cmd = "#{Gem.ruby} #{File.basename extension}".dup - cmd << " #{args.join " "}" unless args.empty? - run cmd, results + if File.basename(extension) =~ /mkrf_conf/i + run([Gem.ruby, File.basename(extension), *args], results) end rake = ENV['rake'] - rake ||= begin - "#{Gem.ruby} -rrubygems #{Gem.bin_path('rake', 'rake')}" - rescue Gem::Exception - end - - rake ||= Gem.default_exec_format % 'rake' + if rake + rake = rake.shellsplit + else + begin + rake = [Gem.ruby, "-rrubygems", Gem.bin_path('rake', 'rake')] + rescue Gem::Exception + rake = [Gem.default_exec_format % 'rake'] + end + end rake_args = ["RUBYARCHDIR=#{dest_path}", "RUBYLIBDIR=#{dest_path}", *args] - run "#{rake} #{rake_args.shelljoin}", results + run(rake + rake_args, results) results end diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb index 349d49d66e6902..4159d81389ce48 100644 --- a/lib/rubygems/gem_runner.rb +++ b/lib/rubygems/gem_runner.rb @@ -38,7 +38,7 @@ def initialize(options={}) ## # Run the gem command with the following arguments. - def run args + def run(args) build_args = extract_build_args args do_configuration args @@ -63,7 +63,7 @@ def run args # Separates the build arguments (those following --) from the # other arguments in the list. - def extract_build_args args # :nodoc: + def extract_build_args(args) # :nodoc: return [] unless offset = args.index('--') build_args = args.slice!(offset...args.length) diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 7c6d6bb36404ae..6fe84554980fd1 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -28,7 +28,7 @@ def add_key_option # The API key from the command options or from the user's configuration. def api_key - if options[:key] then + if options[:key] verify_api_key options[:key] elsif Gem.configuration.api_keys.key?(host) Gem.configuration.api_keys[host] @@ -90,11 +90,11 @@ def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, &blo # Signs in with the RubyGems API at +sign_in_host+ and sets the rubygems API # key. - def sign_in sign_in_host = nil + def sign_in(sign_in_host = nil) sign_in_host ||= self.host return if api_key - pretty_host = if Gem::DEFAULT_HOST == sign_in_host then + pretty_host = if Gem::DEFAULT_HOST == sign_in_host 'RubyGems.org' else sign_in_host @@ -124,7 +124,7 @@ def sign_in sign_in_host = nil # an error. def verify_api_key(key) - if Gem.configuration.api_keys.key? key then + if Gem.configuration.api_keys.key? key Gem.configuration.api_keys[key] else alert_error "No such API key. Please add it to your configuration (done automatically on initial `gem push`)." @@ -139,10 +139,10 @@ def verify_api_key(key) # If the response was not successful, shows an error to the user including # the +error_prefix+ and the response body. - def with_response response, error_prefix = nil + def with_response(response, error_prefix = nil) case response when Net::HTTPSuccess then - if block_given? then + if block_given? yield response else say response.body @@ -156,7 +156,7 @@ def with_response response, error_prefix = nil end end - def set_api_key host, key + def set_api_key(host, key) if host == Gem::DEFAULT_HOST Gem.configuration.rubygems_api_key = key else @@ -165,4 +165,3 @@ def set_api_key host, key end end - diff --git a/lib/rubygems/indexer.rb b/lib/rubygems/indexer.rb index 5607bf3c778847..b97347177db700 100644 --- a/lib/rubygems/indexer.rb +++ b/lib/rubygems/indexer.rb @@ -4,10 +4,17 @@ require 'time' require 'tmpdir' +rescue_exceptions = [LoadError] +begin + require 'bundler/errors' +rescue LoadError # this rubygems + old ruby +else # this rubygems + ruby trunk with bundler + rescue_exceptions << Bundler::GemfileNotFound +end begin gem 'builder' require 'builder/xchar' -rescue LoadError +rescue *rescue_exceptions end ## @@ -55,7 +62,7 @@ def initialize(directory, options = {}) require 'tmpdir' require 'zlib' - unless defined?(Builder::XChar) then + unless defined?(Builder::XChar) raise "Gem::Indexer requires that the XML Builder library be installed:" + "\n\tgem install builder" end @@ -109,7 +116,7 @@ def build_indices ## # Builds Marshal quick index gemspecs. - def build_marshal_gemspecs specs + def build_marshal_gemspecs(specs) count = specs.count progress = ui.progress_reporter count, "Generating Marshal quick index gemspecs for #{count} gems", @@ -154,7 +161,7 @@ def build_modern_index(index, file, name) platform = spec.original_platform # win32-api-1.0.4-x86-mswin32-60 - unless String === platform then + unless String === platform alert_warning "Skipping invalid platform in gem: #{spec.full_name}" next end @@ -172,7 +179,7 @@ def build_modern_index(index, file, name) ## # Builds indices for RubyGems 1.2 and newer. Handles full, latest, prerelease - def build_modern_indices specs + def build_modern_indices(specs) prerelease, released = specs.partition { |s| s.version.prerelease? } @@ -192,9 +199,9 @@ def build_modern_indices specs "#{@prerelease_specs_index}.gz"] end - def map_gems_to_specs gems + def map_gems_to_specs(gems) gems.map { |gemfile| - if File.size(gemfile) == 0 then + if File.size(gemfile) == 0 alert_warning "Skipping zero-length gem: #{gemfile}" next end @@ -228,7 +235,7 @@ def compress_indices say "Compressing indices" Gem.time 'Compressed indices' do - if @build_modern then + if @build_modern gzip @specs_index gzip @latest_specs_index gzip @prerelease_specs_index @@ -306,7 +313,7 @@ def install_indices files = @files files.delete @quick_marshal_dir if files.include? @quick_dir - if files.include? @quick_marshal_dir and not files.include? @quick_dir then + if files.include? @quick_marshal_dir and not files.include? @quick_dir files.delete @quick_marshal_dir dst_name = File.join(@dest_directory, @quick_marshal_dir_base) @@ -347,7 +354,7 @@ def paranoid(path, extension) data = Gem.read_binary path compressed_data = Gem.read_binary "#{path}.#{extension}" - unless data == Gem::Util.inflate(compressed_data) then + unless data == Gem::Util.inflate(compressed_data) raise "Compressed file #{compressed_path} does not match uncompressed file #{path}" end end @@ -367,7 +374,7 @@ def update_index gem_mtime >= specs_mtime end - if updated_gems.empty? then + if updated_gems.empty? say 'No new gems' terminate_interaction 0 end diff --git a/lib/rubygems/install_default_message.rb b/lib/rubygems/install_default_message.rb index dc73fd962b9cca..f68fd2fd04305b 100644 --- a/lib/rubygems/install_default_message.rb +++ b/lib/rubygems/install_default_message.rb @@ -10,4 +10,3 @@ ui = Gem::DefaultUserInteraction.ui ui.say "Successfully installed #{installer.spec.full_name} as a default gem" end - diff --git a/lib/rubygems/install_message.rb b/lib/rubygems/install_message.rb index 6880db583ed816..3c13888a84a3f9 100644 --- a/lib/rubygems/install_message.rb +++ b/lib/rubygems/install_message.rb @@ -10,4 +10,3 @@ ui = Gem::DefaultUserInteraction.ui ui.say "Successfully installed #{installer.spec.full_name}" end - diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 824682f9ad0e85..f05491a31cc458 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -50,7 +50,7 @@ def add_install_update_options add_option(:"Install/Update", '--vendor', 'Install gem into the vendor directory.', 'Only for use by gem repackagers.') do |value, options| - unless Gem.vendor_dir then + unless Gem.vendor_dir raise OptionParser::InvalidOption.new 'your platform is not supported' end @@ -140,7 +140,7 @@ def add_install_update_options File.exist? file end unless v - unless v then + unless v message = v ? v : "(tried #{Gem::GEM_DEP_FILES.join ', '})" raise OptionParser::InvalidArgument, @@ -178,7 +178,6 @@ def add_install_update_options 'Suggest alternates when gems are not found') do |v,o| options[:suggest_alternate] = v end - end ## @@ -189,4 +188,3 @@ def install_update_defaults_str end end - diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 298a7053c7e595..b1f83174bda4ff 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -101,7 +101,7 @@ def exec_format ## # Construct an installer object for the gem file located at +path+ - def self.at path, options = {} + def self.at(path, options = {}) security_policy = options[:security_policy] package = Gem::Package.new path, security_policy new package, options @@ -118,7 +118,7 @@ def initialize(spec) @spec = spec end - def extract_files destination_dir, pattern = '*' + def extract_files(destination_dir, pattern = '*') FileUtils.mkdir_p destination_dir spec.files.each do |file| @@ -129,7 +129,7 @@ def extract_files destination_dir, pattern = '*' end end - def copy_to path + def copy_to(path) end end @@ -137,7 +137,7 @@ def copy_to path # Construct an installer object for an ephemeral gem (one where we don't # actually have a .gem file, just a spec) - def self.for_spec spec, options = {} + def self.for_spec(spec, options = {}) # FIXME: we should have a real Package class for this new FakePackage.new(spec), options end @@ -189,7 +189,7 @@ def initialize(package, options={}) @bin_dir = options[:bin_dir] if options[:bin_dir] - if options[:user_install] and not options[:unpack] then + if options[:user_install] and not options[:unpack] @gem_home = Gem.user_dir @bin_dir = Gem.bindir gem_home unless options[:bin_dir] check_that_user_bin_dir_is_in_path @@ -209,7 +209,7 @@ def initialize(package, options={}) # # Otherwise +filename+ is overwritten. - def check_executable_overwrite filename # :nodoc: + def check_executable_overwrite(filename) # :nodoc: return if @force generated_bin = File.join @bin_dir, formatted_program_filename(filename) @@ -245,7 +245,7 @@ def check_executable_overwrite filename # :nodoc: question = "#{spec.name}'s executable \"#{filename}\" conflicts with ".dup - if ruby_executable then + if ruby_executable question << (existing || 'an unknown executable') return if ask_yes_no "#{question}\nOverwrite the executable?", false @@ -298,7 +298,7 @@ def install run_pre_install_hooks # Set loaded_from to ensure extension_dir is correct - if @options[:install_as_default] then + if @options[:install_as_default] spec.loaded_from = default_spec_file else spec.loaded_from = spec_file @@ -311,7 +311,7 @@ def install dir_mode = options[:dir_mode] FileUtils.mkdir_p gem_dir, :mode => dir_mode && 0700 - if @options[:install_as_default] then + if @options[:install_as_default] extract_bin write_default_spec else @@ -344,7 +344,7 @@ def install def run_pre_install_hooks # :nodoc: Gem.pre_install_hooks.each do |hook| - if hook.call(self) == false then + if hook.call(self) == false location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ message = "pre-install hook#{location} failed for #{spec.full_name}" @@ -355,7 +355,7 @@ def run_pre_install_hooks # :nodoc: def run_post_build_hooks # :nodoc: Gem.post_build_hooks.each do |hook| - if hook.call(self) == false then + if hook.call(self) == false FileUtils.rm_rf gem_dir location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ @@ -398,7 +398,7 @@ def installed_specs # dependency :: Gem::Dependency def ensure_dependency(spec, dependency) - unless installation_satisfies_dependency? dependency then + unless installation_satisfies_dependency? dependency raise Gem::InstallError, "#{spec.name} requires #{dependency}" end true @@ -466,7 +466,7 @@ def write_default_spec # Creates windows .bat files for easy running of commands def generate_windows_script(filename, bindir) - if Gem.win_platform? then + if Gem.win_platform? script_name = filename + ".bat" script_path = File.join bindir, File.basename(script_name) File.open script_path, 'w' do |file| @@ -492,7 +492,7 @@ def generate_bin # :nodoc: filename.untaint bin_path = File.join gem_dir, spec.bindir, filename - unless File.exist? bin_path then + unless File.exist? bin_path # TODO change this to a more useful warning warn "`#{bin_path}` does not exist, maybe `gem pristine #{spec.name}` will fix it?" next @@ -504,7 +504,7 @@ def generate_bin # :nodoc: check_executable_overwrite filename - if @wrappers then + if @wrappers generate_bin_script filename, @bin_dir else generate_bin_symlink filename, @bin_dir @@ -543,8 +543,8 @@ def generate_bin_symlink(filename, bindir) src = File.join gem_dir, spec.bindir, filename dst = File.join bindir, formatted_program_filename(filename) - if File.exist? dst then - if File.symlink? dst then + if File.exist? dst + if File.symlink? dst link = File.readlink(dst).split File::SEPARATOR cur_version = Gem::Version.create(link[-3].sub(/^.*-/, '')) return if spec.version < cur_version @@ -578,7 +578,7 @@ def shebang(bin_file_name) path = File.join gem_dir, spec.bindir, bin_file_name first_line = File.open(path, "rb") {|file| file.gets} - if /\A#!/ =~ first_line then + if /\A#!/ =~ first_line # Preserve extra words on shebang line, like "-w". Thanks RPA. shebang = first_line.sub(/\A\#!.*?ruby\S*((\s+\S+)+)/, "#!#{Gem.ruby}") opts = $1 @@ -603,9 +603,9 @@ def shebang(bin_file_name) end "#!#{which}" - elsif not ruby_name then + elsif not ruby_name "#!#{Gem.ruby}#{opts}" - elsif opts then + elsif opts "#!/bin/sh\n'exec' #{ruby_name.dump} '-x' \"$0\" \"$@\"\n#{shebang}" else # Create a plain shebang line. @@ -631,9 +631,9 @@ def ensure_loadable_spec end def ensure_required_ruby_version_met # :nodoc: - if rrv = spec.required_ruby_version then - unless rrv.satisfied_by? Gem.ruby_version then - ruby_version = Gem.ruby_api_version + if rrv = spec.required_ruby_version + ruby_version = Gem.ruby_version + unless rrv.satisfied_by? ruby_version raise Gem::RuntimeRequirementNotMetError, "#{spec.name} requires Ruby version #{rrv}. The current ruby version is #{ruby_version}." end @@ -641,8 +641,8 @@ def ensure_required_ruby_version_met # :nodoc: end def ensure_required_rubygems_version_met # :nodoc: - if rrgv = spec.required_rubygems_version then - unless rrgv.satisfied_by? Gem.rubygems_version then + if rrgv = spec.required_rubygems_version + unless rrgv.satisfied_by? Gem.rubygems_version rg_version = Gem::VERSION raise Gem::RuntimeRequirementNotMetError, "#{spec.name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " + @@ -702,16 +702,16 @@ def check_that_user_bin_dir_is_in_path # :nodoc: File::ALT_SEPARATOR path = ENV['PATH'] - if Gem.win_platform? then + if Gem.win_platform? path = path.downcase user_bin_dir = user_bin_dir.downcase end path = path.split(File::PATH_SEPARATOR) - unless path.include? user_bin_dir then + unless path.include? user_bin_dir unless !Gem.win_platform? && (path.include? user_bin_dir.sub(ENV['HOME'], '~')) - unless self.class.path_warning then + unless self.class.path_warning alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables will not run." self.class.path_warning = true end @@ -749,11 +749,11 @@ def app_script_text(bin_file_name) version = "#{Gem::Requirement.default}.a" -if ARGV.first - str = ARGV.first - str = str.dup.force_encoding("BINARY") - if str =~ /\\A_(.*)_\\z/ and Gem::Version.correct?($1) then - version = $1 +str = ARGV.first +if str + str = str.b[/\\A_(.*)_\\z/, 1] + if str and Gem::Version.correct?(str) + version = str ARGV.shift end end @@ -771,33 +771,38 @@ def app_script_text(bin_file_name) # return the stub script text used to launch the true Ruby script def windows_stub_script(bindir, bin_file_name) - rb_bindir = RbConfig::CONFIG["bindir"] - # All comparisons should be case insensitive - if bindir.downcase == rb_bindir.downcase + rb_config = RbConfig::CONFIG + rb_topdir = RbConfig::TOPDIR || File.dirname(rb_config["bindir"]) + + # get ruby executable file name from RbConfig + ruby_exe = "#{rb_config['RUBY_INSTALL_NAME']}#{rb_config['EXEEXT']}" + ruby_exe = "ruby.exe" if ruby_exe.empty? + + if File.exist?(File.join bindir, ruby_exe) # stub & ruby.exe withing same folder. Portable <<-TEXT @ECHO OFF @"%~dp0ruby.exe" "%~dpn0" %* TEXT - elsif bindir.downcase.start_with?((RbConfig::TOPDIR || File.dirname(rb_bindir)).downcase) - # stub within ruby folder, but not standard bin. Not portable + elsif bindir.downcase.start_with? rb_topdir.downcase + # stub within ruby folder, but not standard bin. Portable require 'pathname' from = Pathname.new bindir - to = Pathname.new rb_bindir + to = Pathname.new "#{rb_topdir}/bin" rel = to.relative_path_from from <<-TEXT @ECHO OFF @"%~dp0#{rel}/ruby.exe" "%~dpn0" %* TEXT else - # outside ruby folder, maybe -user-install or bundler. Portable + # outside ruby folder, maybe -user-install or bundler. Portable, but ruby + # is dependent on PATH <<-TEXT @ECHO OFF @ruby.exe "%~dpn0" %* TEXT end end - ## # Builds extensions. Valid types of extensions are extconf.rb files, # configure scripts and rakefiles or mkrf_conf files. @@ -842,7 +847,7 @@ def extract_bin # Prefix and suffix the program filename the same as ruby. def formatted_program_filename(filename) - if @format_executable then + if @format_executable self.class.exec_format % File.basename(filename) else filename diff --git a/lib/rubygems/installer_test_case.rb b/lib/rubygems/installer_test_case.rb index 943c6c1256bcd1..d6d16a9194d8d4 100644 --- a/lib/rubygems/installer_test_case.rb +++ b/lib/rubygems/installer_test_case.rb @@ -183,4 +183,3 @@ def util_installer(spec, gem_home, user=false) end end - diff --git a/lib/rubygems/local_remote_options.rb b/lib/rubygems/local_remote_options.rb index fe09e34e54b233..9fa256b08a0b1f 100644 --- a/lib/rubygems/local_remote_options.rb +++ b/lib/rubygems/local_remote_options.rb @@ -108,7 +108,7 @@ def add_source_option source << '/' if source !~ /\/\z/ - if options.delete :sources_cleared then + if options.delete :sources_cleared Gem.sources = [source] else Gem.sources << source unless Gem.sources.include?(source) @@ -148,4 +148,3 @@ def remote? end end - diff --git a/lib/rubygems/mock_gem_ui.rb b/lib/rubygems/mock_gem_ui.rb index 0223f8c35d5cac..92ec85625c4f4c 100644 --- a/lib/rubygems/mock_gem_ui.rb +++ b/lib/rubygems/mock_gem_ui.rb @@ -12,7 +12,7 @@ class Gem::MockGemUi < Gem::StreamUI class InputEOFError < RuntimeError - def initialize question + def initialize(question) super "Out of input for MockGemUi on #{question.inspect}" end @@ -21,7 +21,7 @@ def initialize question class TermError < RuntimeError attr_reader :exit_code - def initialize exit_code + def initialize(exit_code) super @exit_code = exit_code end @@ -56,7 +56,7 @@ def initialize(input = "") @terminated = false end - def ask question + def ask(question) raise InputEOFError, question if @ins.eof? super @@ -86,4 +86,3 @@ def terminate_interaction(status=0) end end - diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb index 316329a0bd351a..e948fb3d865a62 100644 --- a/lib/rubygems/name_tuple.rb +++ b/lib/rubygems/name_tuple.rb @@ -24,7 +24,7 @@ def initialize(name, version, platform="ruby") # Turn an array of [name, version, platform] into an array of # NameTuple objects. - def self.from_list list + def self.from_list(list) list.map { |t| new(*t) } end @@ -32,7 +32,7 @@ def self.from_list list # Turn an array of NameTuple objects back into an array of # [name, version, platform] tuples. - def self.to_basic list + def self.to_basic(list) list.map { |t| t.to_a } end @@ -90,7 +90,7 @@ def inspect # :nodoc: alias to_s inspect # :nodoc: - def <=> other + def <=>(other) [@name, @version, @platform == Gem::Platform::RUBY ? -1 : 1] <=> [other.name, other.version, other.platform == Gem::Platform::RUBY ? -1 : 1] @@ -102,7 +102,7 @@ def <=> other # Compare with +other+. Supports another NameTuple or an Array # in the [name, version, platform] format. - def == other + def ==(other) case other when self.class @name == other.name and diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index a71c41afc7eb77..9af84981a4c1d3 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -55,7 +55,7 @@ class Error < Gem::Exception; end class FormatError < Error attr_reader :path - def initialize message, source = nil + def initialize(message, source = nil) if source @path = source.path @@ -68,7 +68,7 @@ def initialize message, source = nil end class PathError < Error - def initialize destination, destination_dir + def initialize(destination, destination_dir) super "installing into parent path %s of %s is not allowed" % [destination, destination_dir] end @@ -119,7 +119,7 @@ class TarInvalidError < Error; end # Permission for other files attr_accessor :data_mode - def self.build spec, skip_validation = false, strict_validation = false + def self.build(spec, skip_validation = false, strict_validation = false) gem_file = spec.file_name package = new gem_file @@ -136,7 +136,7 @@ def self.build spec, skip_validation = false, strict_validation = false # If +gem+ is an existing file in the old format a Gem::Package::Old will be # returned. - def self.new gem, security_policy = nil + def self.new(gem, security_policy = nil) gem = if gem.is_a?(Gem::Package::Source) gem elsif gem.respond_to? :read @@ -157,7 +157,7 @@ def self.new gem, security_policy = nil ## # Creates a new package that will read or write to the file +gem+. - def initialize gem, security_policy # :notnew: + def initialize(gem, security_policy) # :notnew: @gem = gem @build_time = ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now @@ -174,14 +174,14 @@ def initialize gem, security_policy # :notnew: ## # Copies this package to +path+ (if possible) - def copy_to path + def copy_to(path) FileUtils.cp @gem.path, path unless File.exist? path end ## # Adds a checksum for each entry in the gem to checksums.yaml.gz. - def add_checksums tar + def add_checksums(tar) Gem.load_yaml checksums_by_algorithm = Hash.new { |h, algorithm| h[algorithm] = {} } @@ -203,7 +203,7 @@ def add_checksums tar # Adds the files listed in the packages's Gem::Specification to data.tar.gz # and adds this file to the +tar+. - def add_contents tar # :nodoc: + def add_contents(tar) # :nodoc: digests = tar.add_file_signed 'data.tar.gz', 0444, @signer do |io| gzip_to io do |gz_io| Gem::Package::TarWriter.new gz_io do |data_tar| @@ -218,7 +218,7 @@ def add_contents tar # :nodoc: ## # Adds files included the package's Gem::Specification to the +tar+ file - def add_files tar # :nodoc: + def add_files(tar) # :nodoc: @spec.files.each do |file| stat = File.lstat file @@ -241,7 +241,7 @@ def add_files tar # :nodoc: ## # Adds the package's Gem::Specification to the +tar+ file - def add_metadata tar # :nodoc: + def add_metadata(tar) # :nodoc: digests = tar.add_file_signed 'metadata.gz', 0444, @signer do |io| gzip_to io do |gz_io| gz_io.write @spec.to_yaml @@ -254,7 +254,7 @@ def add_metadata tar # :nodoc: ## # Builds this package based on the specification set by #spec= - def build skip_validation = false, strict_validation = false + def build(skip_validation = false, strict_validation = false) raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation Gem.load_yaml @@ -318,8 +318,8 @@ def contents # Creates a digest of the TarEntry +entry+ from the digest algorithm set by # the security policy. - def digest entry # :nodoc: - algorithms = if @checksums then + def digest(entry) # :nodoc: + algorithms = if @checksums @checksums.keys else [Gem::Security::DIGEST_NAME].compact @@ -327,7 +327,7 @@ def digest entry # :nodoc: algorithms.each do |algorithm| digester = - if defined?(OpenSSL::Digest) then + if defined?(OpenSSL::Digest) OpenSSL::Digest.new algorithm else Digest.const_get(algorithm).new @@ -349,7 +349,7 @@ def digest entry # :nodoc: # If +pattern+ is specified, only entries matching that glob will be # extracted. - def extract_files destination_dir, pattern = "*" + def extract_files(destination_dir, pattern = "*") verify unless @spec FileUtils.mkdir_p destination_dir, :mode => dir_mode && 0700 @@ -378,7 +378,7 @@ def extract_files destination_dir, pattern = "*" # If +pattern+ is specified, only entries matching that glob will be # extracted. - def extract_tar_gz io, destination_dir, pattern = "*" # :nodoc: + def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: directories = [] if dir_mode open_tar_gz io do |tar| tar.each do |entry| @@ -391,7 +391,7 @@ def extract_tar_gz io, destination_dir, pattern = "*" # :nodoc: mkdir_options = {} mkdir_options[:mode] = dir_mode ? 0700 : (entry.header.mode if entry.directory?) mkdir = - if entry.directory? then + if entry.directory? destination else File.dirname destination @@ -427,7 +427,7 @@ def file_mode(mode) # :nodoc: # Also sets the gzip modification time to the package build time to ease # testing. - def gzip_to io # :yields: gz_io + def gzip_to(io) # :yields: gz_io gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION gz_io.mtime = @build_time @@ -441,7 +441,7 @@ def gzip_to io # :yields: gz_io # # If +filename+ is not inside +destination_dir+ an exception is raised. - def install_location filename, destination_dir # :nodoc: + def install_location(filename, destination_dir) # :nodoc: raise Gem::Package::PathError.new(filename, destination_dir) if filename.start_with? '/' @@ -463,7 +463,7 @@ def normalize_path(pathname) end end - def mkdir_p_safe mkdir, mkdir_options, destination_dir, file_name + def mkdir_p_safe(mkdir, mkdir_options, destination_dir, file_name) destination_dir = File.realpath(File.expand_path(destination_dir)) parts = mkdir.split(File::SEPARATOR) parts.reduce do |path, basename| @@ -482,7 +482,7 @@ def mkdir_p_safe mkdir, mkdir_options, destination_dir, file_name ## # Loads a Gem::Specification from the TarEntry +entry+ - def load_spec entry # :nodoc: + def load_spec(entry) # :nodoc: case entry.full_name when 'metadata' then @spec = Gem::Specification.from_yaml entry.read @@ -500,7 +500,7 @@ def load_spec entry # :nodoc: ## # Opens +io+ as a gzipped tar archive - def open_tar_gz io # :nodoc: + def open_tar_gz(io) # :nodoc: Zlib::GzipReader.wrap io do |gzio| tar = Gem::Package::TarReader.new gzio @@ -511,7 +511,7 @@ def open_tar_gz io # :nodoc: ## # Reads and loads checksums.yaml.gz from the tar file +gem+ - def read_checksums gem + def read_checksums(gem) Gem.load_yaml @checksums = gem.seek 'checksums.yaml.gz' do |entry| @@ -527,7 +527,7 @@ def read_checksums gem def setup_signer(signer_options: {}) passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] - if @spec.signing_key then + if @spec.signing_key @signer = Gem::Security::Signer.new( @spec.signing_key, @@ -600,14 +600,14 @@ def verify # Verifies the +checksums+ against the +digests+. This check is not # cryptographically secure. Missing checksums are ignored. - def verify_checksums digests, checksums # :nodoc: + def verify_checksums(digests, checksums) # :nodoc: return unless checksums checksums.sort.each do |algorithm, gem_digests| gem_digests.sort.each do |file_name, gem_hexdigest| computed_digest = digests[algorithm][file_name] - unless computed_digest.hexdigest == gem_hexdigest then + unless computed_digest.hexdigest == gem_hexdigest raise Gem::Package::FormatError.new \ "#{algorithm} checksum mismatch for #{file_name}", @gem end @@ -618,7 +618,7 @@ def verify_checksums digests, checksums # :nodoc: ## # Verifies +entry+ in a .gem file. - def verify_entry entry + def verify_entry(entry) file_name = entry.full_name @files << file_name @@ -645,16 +645,16 @@ def verify_entry entry ## # Verifies the files of the +gem+ - def verify_files gem + def verify_files(gem) gem.each do |entry| verify_entry entry end - unless @spec then + unless @spec raise Gem::Package::FormatError.new 'package metadata is missing', @gem end - unless @files.include? 'data.tar.gz' then + unless @files.include? 'data.tar.gz' raise Gem::Package::FormatError.new \ 'package content (data.tar.gz) is missing', @gem end @@ -667,7 +667,7 @@ def verify_files gem ## # Verifies that +entry+ is a valid gzipped file. - def verify_gz entry # :nodoc: + def verify_gz(entry) # :nodoc: Zlib::GzipReader.wrap entry do |gzio| gzio.read 16384 until gzio.eof? # gzip checksum verification end diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb index 4930c9aa7d8412..d9e6c3c0219aeb 100644 --- a/lib/rubygems/package/digest_io.rb +++ b/lib/rubygems/package/digest_io.rb @@ -31,7 +31,7 @@ class Gem::Package::DigestIO # digests['SHA1'].hexdigest #=> "aaf4c61d[...]" # digests['SHA512'].hexdigest #=> "9b71d224[...]" - def self.wrap io, digests + def self.wrap(io, digests) digest_io = new io, digests yield digest_io @@ -43,7 +43,7 @@ def self.wrap io, digests # Creates a new DigestIO instance. Using ::wrap is recommended, see the # ::wrap documentation for documentation of +io+ and +digests+. - def initialize io, digests + def initialize(io, digests) @io = io @digests = digests end @@ -51,7 +51,7 @@ def initialize io, digests ## # Writes +data+ to the underlying IO and updates the digests - def write data + def write(data) result = @io.write data @digests.each do |_, digest| @@ -62,4 +62,3 @@ def write data end end - diff --git a/lib/rubygems/package/file_source.rb b/lib/rubygems/package/file_source.rb index ecc3a686774f8a..8a4f9da6f2e2ac 100644 --- a/lib/rubygems/package/file_source.rb +++ b/lib/rubygems/package/file_source.rb @@ -10,7 +10,7 @@ class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all attr_reader :path - def initialize path + def initialize(path) @path = path end @@ -22,13 +22,12 @@ def present? File.exist? path end - def with_write_io &block + def with_write_io(&block) File.open path, 'wb', &block end - def with_read_io &block + def with_read_io(&block) File.open path, 'rb', &block end end - diff --git a/lib/rubygems/package/io_source.rb b/lib/rubygems/package/io_source.rb index ee79a21083ff59..669a859d0aebdc 100644 --- a/lib/rubygems/package/io_source.rb +++ b/lib/rubygems/package/io_source.rb @@ -11,7 +11,7 @@ class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all attr_reader :io - def initialize io + def initialize(io) @io = io end @@ -43,4 +43,3 @@ def path end end - diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb index a8457948ff2446..d028be134b658a 100644 --- a/lib/rubygems/package/old.rb +++ b/lib/rubygems/package/old.rb @@ -19,7 +19,7 @@ class Gem::Package::Old < Gem::Package # Creates a new old-format package reader for +gem+. Old-format packages # cannot be written. - def initialize gem, security_policy + def initialize(gem, security_policy) require 'fileutils' require 'zlib' Gem.load_yaml @@ -49,7 +49,7 @@ def contents ## # Extracts the files in this package into +destination_dir+ - def extract_files destination_dir + def extract_files(destination_dir) verify errstr = "Error reading files from gem" @@ -94,7 +94,7 @@ def extract_files destination_dir ## # Reads the file list section from the old-format gem +io+ - def file_list io # :nodoc: + def file_list(io) # :nodoc: header = String.new read_until_dashes io do |line| @@ -107,7 +107,7 @@ def file_list io # :nodoc: ## # Reads lines until a "---" separator is found - def read_until_dashes io # :nodoc: + def read_until_dashes(io) # :nodoc: while (line = io.gets) && line.chomp.strip != "---" do yield line if block_given? end @@ -116,7 +116,7 @@ def read_until_dashes io # :nodoc: ## # Skips the Ruby self-install header in +io+. - def skip_ruby io # :nodoc: + def skip_ruby(io) # :nodoc: loop do line = io.gets diff --git a/lib/rubygems/package/source.rb b/lib/rubygems/package/source.rb index fe19776c385a2b..69701e55e9cf98 100644 --- a/lib/rubygems/package/source.rb +++ b/lib/rubygems/package/source.rb @@ -1,4 +1,3 @@ # frozen_string_literal: true class Gem::Package::Source # :nodoc: end - diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb index d3c242f8152913..c7b5f88dbdecd6 100644 --- a/lib/rubygems/package/tar_header.rb +++ b/lib/rubygems/package/tar_header.rb @@ -134,7 +134,7 @@ def self.strict_oct(str) # Creates a new TarHeader using +vals+ def initialize(vals) - unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] then + unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] raise ArgumentError, ":name, :size, :prefix and :mode required" end diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb index d00f89e7f6218f..f64915eaeabb7a 100644 --- a/lib/rubygems/package/tar_reader.rb +++ b/lib/rubygems/package/tar_reader.rb @@ -92,7 +92,7 @@ def each # NOTE: Do not call #rewind during #each def rewind - if @init_pos == 0 then + if @init_pos == 0 @io.rewind else @io.pos = @init_pos @@ -104,7 +104,7 @@ def rewind # yields it. Rewinds the tar file to the beginning when the block # terminates. - def seek name # :yields: entry + def seek(name) # :yields: entry found = find do |entry| entry.full_name == name end diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb index 77b06af2330e4a..19054c16356a2e 100644 --- a/lib/rubygems/package/tar_reader/entry.rb +++ b/lib/rubygems/package/tar_reader/entry.rb @@ -64,7 +64,7 @@ def eof? # Full name of the tar entry def full_name - if @header.prefix != "" then + if @header.prefix != "" File.join @header.prefix, @header.name else @header.name diff --git a/lib/rubygems/package/tar_test_case.rb b/lib/rubygems/package/tar_test_case.rb index 381a5d0f4367fc..75978c8ed00ad1 100644 --- a/lib/rubygems/package/tar_test_case.rb +++ b/lib/rubygems/package/tar_test_case.rb @@ -52,7 +52,7 @@ def assert_headers_equal(expected, actual) name = fields.shift length = fields.shift.to_i - if name == "checksum" then + if name == "checksum" chksum_off = offset offset += length next diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index aa00633cddf487..87ee39a9449888 100644 --- a/lib/rubygems/package/tar_writer.rb +++ b/lib/rubygems/package/tar_writer.rb @@ -139,11 +139,11 @@ def add_file(name, mode) # :yields: io # # The created digest object is returned. - def add_file_digest name, mode, digest_algorithms # :yields: io + def add_file_digest(name, mode, digest_algorithms) # :yields: io digests = digest_algorithms.map do |digest_algorithm| digest = digest_algorithm.new digest_name = - if digest.respond_to? :name then + if digest.respond_to? :name digest.name else /::([^:]+)$/ =~ digest_algorithm.name @@ -172,7 +172,7 @@ def add_file_digest name, mode, digest_algorithms # :yields: io # # Returns the digest. - def add_file_signed name, mode, signer + def add_file_signed(name, mode, signer) digest_algorithms = [ signer.digest_algorithm, Digest::SHA512, @@ -184,7 +184,7 @@ def add_file_signed name, mode, signer signature_digest = digests.values.compact.find do |digest| digest_name = - if digest.respond_to? :name then + if digest.respond_to? :name digest.name else digest.class.name[/::([^:]+)\z/, 1] @@ -195,7 +195,7 @@ def add_file_signed name, mode, signer raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest - if signer.key then + if signer.key signature = signer.sign signature_digest.digest add_file_simple "#{name}.sig", 0444, signature.length do |io| @@ -309,12 +309,12 @@ def mkdir(name, mode) # Splits +name+ into a name and prefix that can fit in the TarHeader def split_name(name) # :nodoc: - if name.bytesize > 256 then + if name.bytesize > 256 raise Gem::Package::TooLongFileName.new("File \"#{name}\" has a too long path (should be 256 or less)") end prefix = '' - if name.bytesize > 100 then + if name.bytesize > 100 parts = name.split('/', -1) # parts are never empty here name = parts.pop # initially empty for names with a trailing slash ("foo/.../bar/") prefix = parts.join('/') # if empty, then it's impossible to split (parts is empty too) @@ -323,11 +323,11 @@ def split_name(name) # :nodoc: prefix = parts.join('/') end - if name.bytesize > 100 or prefix.empty? then + if name.bytesize > 100 or prefix.empty? raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)") end - if prefix.bytesize > 155 then + if prefix.bytesize > 155 raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long base path (should be 155 or less)") end end diff --git a/lib/rubygems/package_task.rb b/lib/rubygems/package_task.rb index d554e3697b86ec..a11d09fb219078 100644 --- a/lib/rubygems/package_task.rb +++ b/lib/rubygems/package_task.rb @@ -126,4 +126,3 @@ def define end end - diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb index 02332cef80ccee..ed680d655371f1 100644 --- a/lib/rubygems/path_support.rb +++ b/lib/rubygems/path_support.rb @@ -25,7 +25,7 @@ class Gem::PathSupport def initialize(env) @home = env["GEM_HOME"] || Gem.default_dir - if File::ALT_SEPARATOR then + if File::ALT_SEPARATOR @home = @home.gsub(File::ALT_SEPARATOR, File::SEPARATOR) end @@ -43,7 +43,7 @@ def initialize(env) ## # Split the Gem search path (as reported by Gem.path). - def split_gem_path gpaths, home + def split_gem_path(gpaths, home) # FIX: it should be [home, *path], not [*path, home] gem_path = [] @@ -56,7 +56,7 @@ def split_gem_path gpaths, home gem_path += default_path end - if File::ALT_SEPARATOR then + if File::ALT_SEPARATOR gem_path.map! do |this_path| this_path.gsub File::ALT_SEPARATOR, File::SEPARATOR end diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index 1019fc22b9b860..f8fe4dad546a7a 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -56,7 +56,7 @@ def initialize(arch) when String then arch = arch.split '-' - if arch.length > 2 and arch.last !~ /\d/ then # reassemble x86-linux-gnu + if arch.length > 2 and arch.last !~ /\d/ # reassemble x86-linux-gnu extra = arch.pop arch.last << "-#{extra}" end @@ -68,7 +68,7 @@ def initialize(arch) else cpu end - if arch.length == 2 and arch.last =~ /^\d+(\.\d+)?$/ then # for command-line + if arch.length == 2 and arch.last =~ /^\d+(\.\d+)?$/ # for command-line @os, @version = arch return end @@ -203,4 +203,3 @@ def =~(other) CURRENT = 'current'.freeze end - diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb index 41a7314b5360df..6f399a289efc25 100644 --- a/lib/rubygems/psych_tree.rb +++ b/lib/rubygems/psych_tree.rb @@ -18,7 +18,7 @@ def register(target, obj) end # This is ported over from the yaml_tree in 1.9.3 - def format_time time + def format_time(time) if time.utc? time.strftime("%Y-%m-%d %H:%M:%S.%9N Z") else diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 7c4f3f9a6ff9b3..446bed3c729f1d 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -97,7 +97,7 @@ def initialize(proxy=nil, dns=nil, headers={}) # Should probably be integrated with #download below, but that will be a # larger, more encompassing effort. -erikh - def download_to_cache dependency + def download_to_cache(dependency) found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency return if found.empty? @@ -114,9 +114,9 @@ def download_to_cache dependency def download(spec, source_uri, install_dir = Gem.dir) cache_dir = - if Dir.pwd == install_dir then # see fetch_command + if Dir.pwd == install_dir # see fetch_command install_dir - elsif File.writable? install_dir then + elsif File.writable? install_dir File.join install_dir, "cache" else File.join Gem.user_dir, "cache" @@ -149,7 +149,7 @@ def download(spec, source_uri, install_dir = Gem.dir) # REFACTOR: be sure to clean up fake fetcher when you do this... cleaner case scheme when 'http', 'https', 's3' then - unless File.exist? local_gem_path then + unless File.exist? local_gem_path begin verbose "Downloading gem #{gem_file_name}" @@ -183,7 +183,7 @@ def download(spec, source_uri, install_dir = Gem.dir) verbose "Using local gem #{local_gem_path}" when nil then # TODO test for local overriding cache source_path = if Gem.win_platform? && source_uri.scheme && - !source_uri.path.include?(':') then + !source_uri.path.include?(':') "#{source_uri.scheme}:#{source_uri.path}" else source_uri.path @@ -209,14 +209,14 @@ def download(spec, source_uri, install_dir = Gem.dir) ## # File Fetcher. Dispatched by +fetch_path+. Use it instead. - def fetch_file uri, *_ + def fetch_file(uri, *_) Gem.read_binary correct_for_windows_path uri.path end ## # HTTP Fetcher. Dispatched by +fetch_path+. Use it instead. - def fetch_http uri, last_modified = nil, head = false, depth = 0 + def fetch_http(uri, last_modified = nil, head = false, depth = 0) fetch_type = head ? Net::HTTP::Head : Net::HTTP::Get response = request uri, fetch_type, last_modified do |req| headers.each { |k,v| req.add_field(k,v) } @@ -291,7 +291,7 @@ def fetch_s3(uri, mtime = nil, head = false) # Downloads +uri+ to +path+ if necessary. If no path is given, it just # passes the data. - def cache_update_path uri, path = nil, update = true + def cache_update_path(uri, path = nil, update = true) mtime = path && File.stat(path).mtime rescue nil data = fetch_path(uri, mtime) @@ -375,11 +375,11 @@ def s3_expiration private - def proxy_for proxy, uri + def proxy_for(proxy, uri) Gem::Request.proxy_uri(proxy || Gem::Request.get_proxy_from_env(uri.scheme)) end - def pools_for proxy + def pools_for(proxy) @pool_lock.synchronize do @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files end diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb index d8d5d1bc313b74..fb164d79cfe27c 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -10,7 +10,7 @@ class Gem::Request ### # Legacy. This is used in tests. - def self.create_with_proxy uri, request_class, last_modified, proxy # :nodoc: + def self.create_with_proxy(uri, request_class, last_modified, proxy) # :nodoc: cert_files = get_cert_files proxy ||= get_proxy_from_env(uri.scheme) pool = ConnectionPools.new proxy_uri(proxy), cert_files @@ -18,7 +18,7 @@ def self.create_with_proxy uri, request_class, last_modified, proxy # :nodoc: new(uri, request_class, last_modified, pool.pool_for(uri)) end - def self.proxy_uri proxy # :nodoc: + def self.proxy_uri(proxy) # :nodoc: case proxy when :no_proxy then nil when URI::HTTP then proxy @@ -51,7 +51,7 @@ def self.configure_connection_for_https(connection, cert_files) Gem.configuration.ssl_verify_mode || OpenSSL::SSL::VERIFY_PEER store = OpenSSL::X509::Store.new - if Gem.configuration.ssl_client_cert then + if Gem.configuration.ssl_client_cert pem = File.read Gem.configuration.ssl_client_cert connection.cert = OpenSSL::X509::Certificate.new pem connection.key = OpenSSL::PKey::RSA.new pem @@ -85,7 +85,7 @@ def self.configure_connection_for_https(connection, cert_files) 'Unable to require openssl, install OpenSSL and rebuild Ruby (preferred) or use non-HTTPS sources') end - def self.verify_certificate store_context + def self.verify_certificate(store_context) depth = store_context.error_depth error = store_context.error_string number = store_context.error @@ -98,7 +98,7 @@ def self.verify_certificate store_context ui.alert_error extra_message if extra_message end - def self.verify_certificate_message error_number, cert + def self.verify_certificate_message(error_number, cert) return unless cert case error_number when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED then @@ -117,9 +117,11 @@ def self.verify_certificate_message error_number, cert "Certificate #{cert.subject} has an invalid purpose" when OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN then "Root certificate is not trusted (#{cert.subject})" - when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, - OpenSSL::X509::V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE then + when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY then "You must add #{cert.issuer} to your local trusted store" + when + OpenSSL::X509::V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE then + "Cannot verify certificate issued by #{cert.issuer}" end end @@ -137,7 +139,7 @@ def connection_for(uri) def fetch request = @request_class.new @uri.request_uri - unless @uri.nil? || @uri.user.nil? || @uri.user.empty? then + unless @uri.nil? || @uri.user.nil? || @uri.user.empty? request.basic_auth Gem::UriFormatter.new(@uri.user).unescape, Gem::UriFormatter.new(@uri.password).unescape end @@ -146,7 +148,7 @@ def fetch request.add_field 'Connection', 'keep-alive' request.add_field 'Keep-Alive', '30' - if @last_modified then + if @last_modified request.add_field 'If-Modified-Since', @last_modified.httpdate end @@ -159,7 +161,7 @@ def fetch # Returns a proxy URI for the given +scheme+ if one is set in the # environment variables. - def self.get_proxy_from_env scheme = 'http' + def self.get_proxy_from_env(scheme = 'http') _scheme = scheme.downcase _SCHEME = scheme.upcase env_proxy = ENV["#{_scheme}_proxy"] || ENV["#{_SCHEME}_PROXY"] @@ -171,7 +173,7 @@ def self.get_proxy_from_env scheme = 'http' uri = URI(Gem::UriFormatter.new(env_proxy).normalize) - if uri and uri.user.nil? and uri.password.nil? then + if uri and uri.user.nil? and uri.password.nil? user = ENV["#{_scheme}_proxy_user"] || ENV["#{_SCHEME}_PROXY_USER"] password = ENV["#{_scheme}_proxy_pass"] || ENV["#{_SCHEME}_PROXY_PASS"] @@ -182,7 +184,7 @@ def self.get_proxy_from_env scheme = 'http' uri end - def perform_request request # :nodoc: + def perform_request(request) # :nodoc: connection = connection_for @uri retried = false @@ -274,9 +276,9 @@ def user_agent ruby_version += 'dev' if RUBY_PATCHLEVEL == -1 ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}" - if RUBY_PATCHLEVEL >= 0 then + if RUBY_PATCHLEVEL >= 0 ua << " patchlevel #{RUBY_PATCHLEVEL}" - elsif defined?(RUBY_REVISION) then + elsif defined?(RUBY_REVISION) ua << " revision #{RUBY_REVISION}" end ua << ")" diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb index f95dd8aabfeb45..c02f083b638b69 100644 --- a/lib/rubygems/request/connection_pools.rb +++ b/lib/rubygems/request/connection_pools.rb @@ -8,19 +8,19 @@ class << self attr_accessor :client end - def initialize proxy_uri, cert_files + def initialize(proxy_uri, cert_files) @proxy_uri = proxy_uri @cert_files = cert_files @pools = {} @pool_mutex = Mutex.new end - def pool_for uri + def pool_for(uri) http_args = net_http_args(uri, @proxy_uri) key = http_args + [https?(uri)] @pool_mutex.synchronize do @pools[key] ||= - if https? uri then + if https? uri Gem::Request::HTTPSPool.new(http_args, @cert_files, @proxy_uri) else Gem::Request::HTTPPool.new(http_args, @cert_files, @proxy_uri) @@ -45,11 +45,11 @@ def get_no_proxy_from_env env_no_proxy.split(/\s*,\s*/) end - def https? uri + def https?(uri) uri.scheme.downcase == 'https' end - def no_proxy? host, env_no_proxy + def no_proxy?(host, env_no_proxy) host = host.downcase env_no_proxy.any? do |pattern| @@ -73,7 +73,7 @@ def no_proxy? host, env_no_proxy end end - def net_http_args uri, proxy_uri + def net_http_args(uri, proxy_uri) # URI::Generic#hostname was added in ruby 1.9.3, use it if exists, otherwise # don't support IPv6 literals and use host. hostname = uri.respond_to?(:hostname) ? uri.hostname : uri.host @@ -81,7 +81,7 @@ def net_http_args uri, proxy_uri no_proxy = get_no_proxy_from_env - if proxy_uri and not no_proxy?(hostname, no_proxy) then + if proxy_uri and not no_proxy?(hostname, no_proxy) proxy_hostname = proxy_uri.respond_to?(:hostname) ? proxy_uri.hostname : proxy_uri.host net_http_args + [ proxy_hostname, @@ -89,7 +89,7 @@ def net_http_args uri, proxy_uri Gem::UriFormatter.new(proxy_uri.user).unescape, Gem::UriFormatter.new(proxy_uri.password).unescape, ] - elsif no_proxy? hostname, no_proxy then + elsif no_proxy? hostname, no_proxy net_http_args + [nil, nil] else net_http_args @@ -97,4 +97,3 @@ def net_http_args uri, proxy_uri end end - diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb index bfcd15399d5b7a..a85fc2bdf65156 100644 --- a/lib/rubygems/request/http_pool.rb +++ b/lib/rubygems/request/http_pool.rb @@ -8,7 +8,7 @@ class Gem::Request::HTTPPool # :nodoc: attr_reader :cert_files, :proxy_uri - def initialize http_args, cert_files, proxy_uri + def initialize(http_args, cert_files, proxy_uri) @http_args = http_args @cert_files = cert_files @proxy_uri = proxy_uri @@ -20,7 +20,7 @@ def checkout @queue.pop || make_connection end - def checkin connection + def checkin(connection) @queue.push connection end @@ -39,10 +39,9 @@ def make_connection setup_connection Gem::Request::ConnectionPools.client.new(*@http_args) end - def setup_connection connection + def setup_connection(connection) connection.start connection end end - diff --git a/lib/rubygems/request/https_pool.rb b/lib/rubygems/request/https_pool.rb index e82c2440e1c270..50f42d9e0d16f0 100644 --- a/lib/rubygems/request/https_pool.rb +++ b/lib/rubygems/request/https_pool.rb @@ -2,10 +2,8 @@ class Gem::Request::HTTPSPool < Gem::Request::HTTPPool # :nodoc: private - def setup_connection connection + def setup_connection(connection) Gem::Request.configure_connection_for_https(connection, @cert_files) super end end - - diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index c68e40ee9e6fa4..6017d15d13ebba 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -91,7 +91,7 @@ class Gem::RequestSet # # set = Gem::RequestSet.new nokogiri, pg - def initialize *deps + def initialize(*deps) @dependencies = deps @always_install = [] @@ -119,8 +119,8 @@ def initialize *deps ## # Declare that a gem of name +name+ with +reqs+ requirements is needed. - def gem name, *reqs - if dep = @dependency_names[name] then + def gem(name, *reqs) + if dep = @dependency_names[name] dep.requirement.concat reqs else dep = Gem::Dependency.new name, *reqs @@ -132,7 +132,7 @@ def gem name, *reqs ## # Add +deps+ Gem::Dependency objects to the set. - def import deps + def import(deps) @dependencies.concat deps end @@ -143,7 +143,7 @@ def import deps # The +installer+ will be +nil+ if a gem matching the request was already # installed. - def install options, &block # :yields: request, installer + def install(options, &block) # :yields: request, installer if dir = options[:install_dir] requests = install_into dir, false, options, &block return requests @@ -181,10 +181,10 @@ def install options, &block # :yields: request, installer # Install requested gems after they have been downloaded sorted_requests.each do |req| - if req.installed? then + if req.installed? req.spec.spec.build_extensions - if @always_install.none? { |spec| spec == req.spec.spec } then + if @always_install.none? { |spec| spec == req.spec.spec } yield req, nil if block_given? next end @@ -230,7 +230,7 @@ def install options, &block # :yields: request, installer # If +:without_groups+ is given in the +options+, those groups in the gem # dependencies file are not used. See Gem::Installer for other +options+. - def install_from_gemdeps options, &block + def install_from_gemdeps(options, &block) gemdeps = options[:gemdeps] @install_dir = options[:install_dir] || Gem.dir @@ -255,7 +255,7 @@ def install_from_gemdeps options, &block else installed = install options, &block - if options.fetch :lock, true then + if options.fetch :lock, true lockfile = Gem::RequestSet::Lockfile.build self, gemdeps, gem_deps_api.dependencies lockfile.write @@ -265,7 +265,7 @@ def install_from_gemdeps options, &block end end - def install_into dir, force = true, options = {} + def install_into(dir, force = true, options = {}) gem_home, ENV['GEM_HOME'] = ENV['GEM_HOME'], dir existing = force ? [] : specs_in(dir) @@ -283,7 +283,7 @@ def install_into dir, force = true, options = {} sorted_requests.each do |request| spec = request.spec - if existing.find { |s| s.full_name == spec.full_name } then + if existing.find { |s| s.full_name == spec.full_name } yield request, nil if block_given? next end @@ -305,7 +305,7 @@ def install_into dir, force = true, options = {} ## # Call hooks on installed gems - def install_hooks requests, options + def install_hooks(requests, options) specs = requests.map do |request| case request when Gem::Resolver::ActivationRequest then @@ -327,7 +327,7 @@ def install_hooks requests, options ## # Load a dependency management file. - def load_gemdeps path, without_groups = [], installing = false + def load_gemdeps(path, without_groups = [], installing = false) @git_set = Gem::Resolver::GitSet.new @vendor_set = Gem::Resolver::VendorSet.new @source_set = Gem::Resolver::SourceSet.new @@ -348,29 +348,29 @@ def load_gemdeps path, without_groups = [], installing = false gf.load end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[RequestSet:', ']' do q.breakable - if @remote then + if @remote q.text 'remote' q.breakable end - if @prerelease then + if @prerelease q.text 'prerelease' q.breakable end - if @development_shallow then + if @development_shallow q.text 'shallow development' q.breakable - elsif @development then + elsif @development q.text 'development' q.breakable end - if @soft_missing then + if @soft_missing q.text 'soft missing' end @@ -394,7 +394,7 @@ def pretty_print q # :nodoc: # Resolve the requested dependencies and return an Array of Specification # objects to be activated. - def resolve set = Gem::Resolver::BestSet.new + def resolve(set = Gem::Resolver::BestSet.new) @sets << set @sets << @git_set @sets << @vendor_set @@ -443,17 +443,17 @@ def specs @specs ||= @requests.map { |r| r.full_spec } end - def specs_in dir + def specs_in(dir) Gem::Util.glob_files_in_dir("*.gemspec", File.join(dir, "specifications")).map do |g| Gem::Specification.load g end end - def tsort_each_node &block # :nodoc: + def tsort_each_node(&block) # :nodoc: @requests.each(&block) end - def tsort_each_child node # :nodoc: + def tsort_each_child(node) # :nodoc: node.spec.dependencies.each do |dep| next if dep.type == :development and not @development @@ -461,7 +461,7 @@ def tsort_each_child node # :nodoc: dep.match? r.spec.name, r.spec.version, @prerelease } - unless match then + unless match next if dep.type == :development and @development_shallow next if @soft_missing raise Gem::DependencyError, diff --git a/lib/rubygems/request_set/gem_dependency_api.rb b/lib/rubygems/request_set/gem_dependency_api.rb index 177079da034914..3892e7aa5f2fba 100644 --- a/lib/rubygems/request_set/gem_dependency_api.rb +++ b/lib/rubygems/request_set/gem_dependency_api.rb @@ -191,7 +191,7 @@ class Gem::RequestSet::GemDependencyAPI # Creates a new GemDependencyAPI that will add dependencies to the # Gem::RequestSet +set+ based on the dependency API description in +path+. - def initialize set, path + def initialize(set, path) @set = set @path = path @@ -228,7 +228,7 @@ def initialize set, path # Adds +dependencies+ to the request set if any of the +groups+ are allowed. # This is used for gemspec dependencies. - def add_dependencies groups, dependencies # :nodoc: + def add_dependencies(groups, dependencies) # :nodoc: return unless (groups & @without_groups).empty? dependencies.each do |dep| @@ -241,7 +241,7 @@ def add_dependencies groups, dependencies # :nodoc: ## # Finds a gemspec with the given +name+ that lives at +path+. - def find_gemspec name, path # :nodoc: + def find_gemspec(name, path) # :nodoc: glob = File.join path, "#{name}.gemspec" spec_files = Dir[glob] @@ -269,7 +269,7 @@ def find_gemspec name, path # :nodoc: # In installing mode certain restrictions are ignored such as ruby version # mismatch checks. - def installing= installing # :nodoc: + def installing=(installing) # :nodoc: @installing = installing end @@ -353,7 +353,7 @@ def load # tag: :: # Use the given tag for git:, gist: and github: dependencies. - def gem name, *requirements + def gem(name, *requirements) options = requirements.pop if requirements.last.kind_of?(Hash) options ||= {} @@ -369,9 +369,9 @@ def gem name, *requirements duplicate = @dependencies.include? name @dependencies[name] = - if requirements.empty? and not source_set then + if requirements.empty? and not source_set Gem::Requirement.default - elsif source_set then + elsif source_set Gem::Requirement.source_set else Gem::Requirement.create requirements @@ -387,7 +387,7 @@ def gem name, *requirements gem_requires name, options - if duplicate then + if duplicate warn <<-WARNING Gem dependencies file #{@path} requires #{name} more than once. WARNING @@ -401,8 +401,8 @@ def gem name, *requirements # # Returns +true+ if the gist or git option was handled. - def gem_git name, options # :nodoc: - if gist = options.delete(:gist) then + def gem_git(name, options) # :nodoc: + if gist = options.delete(:gist) options[:git] = "https://gist.github.com/#{gist}.git" end @@ -424,7 +424,7 @@ def gem_git name, options # :nodoc: # # Returns reference for the git gem. - def gem_git_reference options # :nodoc: + def gem_git_reference(options) # :nodoc: ref = options.delete :ref branch = options.delete :branch tag = options.delete :tag @@ -457,7 +457,7 @@ def gem_git_reference options # :nodoc: # # Returns +true+ if the custom source option was handled. - def gem_git_source name, options # :nodoc: + def gem_git_source(name, options) # :nodoc: return unless git_source = (@git_sources.keys & options.keys).last source_callback = @git_sources[git_source] @@ -478,7 +478,7 @@ def gem_git_source name, options # :nodoc: # Handles the :group and :groups +options+ for the gem with the given # +name+. - def gem_group name, options # :nodoc: + def gem_group(name, options) # :nodoc: g = options.delete :group all_groups = g ? Array(g) : [] @@ -497,7 +497,7 @@ def gem_group name, options # :nodoc: # # Returns +true+ if the path option was handled. - def gem_path name, options # :nodoc: + def gem_path(name, options) # :nodoc: return unless directory = options.delete(:path) pin_gem_source name, :path, directory @@ -514,7 +514,7 @@ def gem_path name, options # :nodoc: # # Returns +true+ if the source option was handled. - def gem_source name, options # :nodoc: + def gem_source(name, options) # :nodoc: return unless source = options.delete(:source) pin_gem_source name, :source, source @@ -530,7 +530,7 @@ def gem_source name, options # :nodoc: # Handles the platforms: option from +options+. Returns true if the # platform matches the current platform. - def gem_platforms options # :nodoc: + def gem_platforms(options) # :nodoc: platform_names = Array(options.delete :platform) platform_names.concat Array(options.delete :platforms) platform_names.concat @current_platforms if @current_platforms @@ -543,7 +543,7 @@ def gem_platforms options # :nodoc: next false unless Gem::Platform.match platform - if engines = ENGINE_MAP[platform_name] then + if engines = ENGINE_MAP[platform_name] next false unless engines.include? Gem.ruby_engine end @@ -564,9 +564,9 @@ def gem_platforms options # :nodoc: # Records the require: option from +options+ and adds those files, or the # default file to the require list for +name+. - def gem_requires name, options # :nodoc: - if options.include? :require then - if requires = options.delete(:require) then + def gem_requires(name, options) # :nodoc: + if options.include? :require + if requires = options.delete(:require) @requires[name].concat Array requires end else @@ -587,7 +587,7 @@ def gem_requires name, options # :nodoc: # gem 'activerecord' # end - def git repository + def git(repository) @current_repository = repository yield @@ -601,7 +601,7 @@ def git repository # for use in gems built from git repositories. You must provide a block # that accepts a git repository name for expansion. - def git_source name, &callback + def git_source(name, &callback) @git_sources[name] = callback end @@ -634,7 +634,7 @@ def gem_deps_file # :nodoc: # The group to add development dependencies to. By default this is # :development. Only one group may be specified. - def gemspec options = {} + def gemspec(options = {}) name = options.delete(:name) || '{,*}' path = options.delete(:path) || '.' development_group = options.delete(:development_group) || :development @@ -679,7 +679,7 @@ def gemspec options = {} # development`. See `gem help install` and `gem help gem_dependencies` for # further details. - def group *groups + def group(*groups) @current_groups = groups yield @@ -692,7 +692,7 @@ def group *groups # Pins the gem +name+ to the given +source+. Adding a gem with the same # name from a different +source+ will raise an exception. - def pin_gem_source name, type = :default, source = nil + def pin_gem_source(name, type = :default, source = nil) source_description = case type when :default then '(default)' @@ -754,7 +754,7 @@ def pin_gem_source name, type = :default, source = nil # NOTE: There is inconsistency in what environment a platform matches. You # may need to read the source to know the exact details. - def platform *platforms + def platform(*platforms) @current_platforms = platforms yield @@ -781,7 +781,7 @@ def platform *platforms # version. This matching is performed by using the RUBY_ENGINE and # engine_specific VERSION constants. (For JRuby, JRUBY_VERSION). - def ruby version, options = {} + def ruby(version, options = {}) engine = options[:engine] engine_version = options[:engine_version] @@ -791,24 +791,24 @@ def ruby version, options = {} return true if @installing - unless RUBY_VERSION == version then + unless RUBY_VERSION == version message = "Your Ruby version is #{RUBY_VERSION}, " + "but your #{gem_deps_file} requires #{version}" raise Gem::RubyVersionMismatch, message end - if engine and engine != Gem.ruby_engine then + if engine and engine != Gem.ruby_engine message = "Your Ruby engine is #{Gem.ruby_engine}, " + "but your #{gem_deps_file} requires #{engine}" raise Gem::RubyVersionMismatch, message end - if engine_version then + if engine_version my_engine_version = Object.const_get "#{Gem.ruby_engine.upcase}_VERSION" - if engine_version != my_engine_version then + if engine_version != my_engine_version message = "Your Ruby engine version is #{Gem.ruby_engine} #{my_engine_version}, " + "but your #{gem_deps_file} requires #{engine} #{engine_version}" @@ -834,7 +834,7 @@ def ruby version, options = {} # * The +prepend:+ option is not supported. If you wish to order sources # then list them in your preferred order. - def source url + def source(url) Gem.sources.clear if @default_sources @default_sources = false diff --git a/lib/rubygems/request_set/lockfile.rb b/lib/rubygems/request_set/lockfile.rb index 76ad17d4862b7b..1b374660f0f892 100644 --- a/lib/rubygems/request_set/lockfile.rb +++ b/lib/rubygems/request_set/lockfile.rb @@ -29,7 +29,7 @@ class ParseError < Gem::Exception # Raises a ParseError with the given +message+ which was encountered at a # +line+ and +column+ while parsing. - def initialize message, column, line, path + def initialize(message, column, line, path) @line = line @column = column @path = path @@ -41,13 +41,13 @@ def initialize message, column, line, path # Creates a new Lockfile for the given +request_set+ and +gem_deps_file+ # location. - def self.build request_set, gem_deps_file, dependencies = nil + def self.build(request_set, gem_deps_file, dependencies = nil) request_set.resolve dependencies ||= requests_to_deps request_set.sorted_requests new request_set, gem_deps_file, dependencies end - def self.requests_to_deps requests # :nodoc: + def self.requests_to_deps(requests) # :nodoc: deps = {} requests.each do |request| @@ -56,7 +56,7 @@ def self.requests_to_deps requests # :nodoc: requirement = request.request.dependency.requirement deps[name] = if [Gem::Resolver::VendorSpecification, - Gem::Resolver::GitSpecification].include? spec.class then + Gem::Resolver::GitSpecification].include? spec.class Gem::Requirement.source_set else requirement @@ -71,7 +71,7 @@ def self.requests_to_deps requests # :nodoc: attr_reader :platforms - def initialize request_set, gem_deps_file, dependencies + def initialize(request_set, gem_deps_file, dependencies) @set = request_set @dependencies = dependencies @gem_deps_file = File.expand_path(gem_deps_file) @@ -82,7 +82,7 @@ def initialize request_set, gem_deps_file, dependencies @platforms = [] end - def add_DEPENDENCIES out # :nodoc: + def add_DEPENDENCIES(out) # :nodoc: out << "DEPENDENCIES" out.concat @dependencies.sort_by { |name,| name }.map { |name, requirement| @@ -92,7 +92,7 @@ def add_DEPENDENCIES out # :nodoc: out << nil end - def add_GEM out, spec_groups # :nodoc: + def add_GEM(out, spec_groups) # :nodoc: return if spec_groups.empty? source_groups = spec_groups.values.flatten.group_by do |request| @@ -122,7 +122,7 @@ def add_GEM out, spec_groups # :nodoc: end end - def add_GIT out, git_requests + def add_GIT(out, git_requests) return if git_requests.empty? by_repository_revision = git_requests.group_by do |request| @@ -148,11 +148,11 @@ def add_GIT out, git_requests end end - def relative_path_from dest, base # :nodoc: + def relative_path_from(dest, base) # :nodoc: dest = File.expand_path(dest) base = File.expand_path(base) - if dest.index(base) == 0 then + if dest.index(base) == 0 offset = dest[base.size+1..-1] return '.' unless offset @@ -163,7 +163,7 @@ def relative_path_from dest, base # :nodoc: end end - def add_PATH out, path_requests # :nodoc: + def add_PATH(out, path_requests) # :nodoc: return if path_requests.empty? out << "PATH" @@ -178,7 +178,7 @@ def add_PATH out, path_requests # :nodoc: out << nil end - def add_PLATFORMS out # :nodoc: + def add_PLATFORMS(out) # :nodoc: out << "PLATFORMS" platforms = requests.map { |request| request.spec.platform }.uniq diff --git a/lib/rubygems/request_set/lockfile/parser.rb b/lib/rubygems/request_set/lockfile/parser.rb index 368b56e6e60ebd..0fe0405e3217ab 100644 --- a/lib/rubygems/request_set/lockfile/parser.rb +++ b/lib/rubygems/request_set/lockfile/parser.rb @@ -3,7 +3,7 @@ class Gem::RequestSet::Lockfile::Parser ### # Parses lockfiles - def initialize tokenizer, set, platforms, filename = nil + def initialize(tokenizer, set, platforms, filename = nil) @tokens = tokenizer @filename = filename @set = set @@ -41,10 +41,10 @@ def parse ## # Gets the next token for a Lockfile - def get expected_types = nil, expected_value = nil # :nodoc: + def get(expected_types = nil, expected_value = nil) # :nodoc: token = @tokens.shift - if expected_types and not Array(expected_types).include? token.type then + if expected_types and not Array(expected_types).include? token.type unget token message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " + @@ -53,7 +53,7 @@ def get expected_types = nil, expected_value = nil # :nodoc: raise Gem::RequestSet::Lockfile::ParseError.new message, token.column, token.line, @filename end - if expected_value and expected_value != token.value then + if expected_value and expected_value != token.value unget token message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " + @@ -93,7 +93,7 @@ def parse_DEPENDENCIES # :nodoc: get :r_paren - if peek[0] == :bang then + if peek[0] == :bang requirements.clear requirements << pinned_requirement(token.value) @@ -144,7 +144,7 @@ def parse_GEM # :nodoc: type = token.type data = token.value - if type == :text and column == 4 then + if type == :text and column == 4 version, platform = data.split '-', 2 platform = @@ -183,7 +183,7 @@ def parse_GIT # :nodoc: type = peek.type value = peek.value - if type == :entry and %w[branch ref tag].include? value then + if type == :entry and %w[branch ref tag].include? value get get :text @@ -214,7 +214,7 @@ def parse_GIT # :nodoc: type = token.type data = token.value - if type == :text and column == 4 then + if type == :text and column == 4 last_spec = set.add_git_spec name, data, repository, revision, true else dependency = parse_dependency name, data @@ -261,7 +261,7 @@ def parse_PATH # :nodoc: type = token.type data = token.value - if type == :text and column == 4 then + if type == :text and column == 4 last_spec = set.add_vendor_gem name, directory else dependency = parse_dependency name, data @@ -294,7 +294,7 @@ def parse_PLATFORMS # :nodoc: # Parses the requirements following the dependency +name+ and the +op+ for # the first token of the requirements and returns a Gem::Dependency object. - def parse_dependency name, op # :nodoc: + def parse_dependency(name, op) # :nodoc: return Gem::Dependency.new name, op unless peek[0] == :text version = get(:text).value @@ -314,7 +314,7 @@ def parse_dependency name, op # :nodoc: private - def skip type # :nodoc: + def skip(type) # :nodoc: @tokens.skip type end @@ -325,7 +325,7 @@ def peek # :nodoc: @tokens.peek end - def pinned_requirement name # :nodoc: + def pinned_requirement(name) # :nodoc: requirement = Gem::Dependency.new name specification = @set.sets.flat_map { |set| set.find_all(requirement) @@ -337,7 +337,7 @@ def pinned_requirement name # :nodoc: ## # Ungets the last token retrieved by #get - def unget token # :nodoc: + def unget(token) # :nodoc: @tokens.unshift token end end diff --git a/lib/rubygems/request_set/lockfile/tokenizer.rb b/lib/rubygems/request_set/lockfile/tokenizer.rb index a758743dda8d86..bb69c85fb45ed6 100644 --- a/lib/rubygems/request_set/lockfile/tokenizer.rb +++ b/lib/rubygems/request_set/lockfile/tokenizer.rb @@ -5,11 +5,11 @@ class Gem::RequestSet::Lockfile::Tokenizer Token = Struct.new :type, :value, :column, :line EOF = Token.new :EOF - def self.from_file file + def self.from_file(file) new File.read(file), file end - def initialize input, filename = nil, line = 0, pos = 0 + def initialize(input, filename = nil, line = 0, pos = 0) @line = line @line_pos = pos @tokens = [] @@ -17,7 +17,7 @@ def initialize input, filename = nil, line = 0, pos = 0 tokenize input end - def make_parser set, platforms + def make_parser(set, platforms) Gem::RequestSet::Lockfile::Parser.new self, set, platforms, @filename end @@ -25,7 +25,7 @@ def to_a @tokens.map { |token| [token.type, token.value, token.column, token.line] } end - def skip type + def skip(type) @tokens.shift while not @tokens.empty? and peek.type == type end @@ -33,7 +33,7 @@ def skip type # Calculates the column (by byte) and the line of the current token based on # +byte_offset+. - def token_pos byte_offset # :nodoc: + def token_pos(byte_offset) # :nodoc: [byte_offset - @line_pos, @line] end @@ -41,7 +41,7 @@ def empty? @tokens.empty? end - def unshift token + def unshift(token) @tokens.unshift token end @@ -56,7 +56,7 @@ def peek private - def tokenize input + def tokenize(input) require 'strscan' s = StringScanner.new input @@ -65,7 +65,7 @@ def tokenize input pos = s.pos if leading_whitespace = s.scan(/ +/) - if s.scan(/[<|=>]{7}/) then + if s.scan(/[<|=>]{7}/) message = "your #{@filename} contains merge conflict markers" column, line = token_pos pos @@ -80,7 +80,7 @@ def tokenize input @line += 1 token when s.scan(/[A-Z]+/) then - if leading_whitespace then + if leading_whitespace text = s.matched text += s.scan(/[^\s)]*/).to_s # in case of no match Token.new(:text, text, *token_pos(pos)) diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb index 93bfe7d02256ea..1a73274c0154b9 100644 --- a/lib/rubygems/requirement.rb +++ b/lib/rubygems/requirement.rb @@ -32,7 +32,7 @@ class Gem::Requirement ## # A regular expression that matches a requirement - PATTERN = /\A#{PATTERN_RAW}\z/ + PATTERN = /\A#{PATTERN_RAW}\z/.freeze ## # The default requirement matches any version @@ -51,7 +51,7 @@ class BadRequirementError < ArgumentError; end # If the input is "weird", the default version requirement is # returned. - def self.create *inputs + def self.create(*inputs) return new inputs if inputs.length > 1 input = inputs.shift @@ -64,7 +64,7 @@ def self.create *inputs when '!' then source_set else - if input.respond_to? :to_str then + if input.respond_to? :to_str new [input.to_str] else default @@ -98,7 +98,7 @@ def self.source_set # :nodoc: # parse("1.0") # => ["=", Gem::Version.new("1.0")] # parse(Gem::Version.new("1.0")) # => ["=, Gem::Version.new("1.0")] - def self.parse obj + def self.parse(obj) return ["=", obj] if Gem::Version === obj unless PATTERN =~ obj.to_s @@ -124,7 +124,7 @@ def self.parse obj # requirements are ignored. An empty set of +requirements+ is the # same as ">= 0". - def initialize *requirements + def initialize(*requirements) requirements = requirements.flatten requirements.compact! requirements.uniq! @@ -140,7 +140,7 @@ def initialize *requirements ## # Concatenates the +new+ requirements onto this requirement. - def concat new + def concat(new) new = new.flatten new.compact! new.uniq! @@ -198,7 +198,7 @@ def marshal_dump # :nodoc: [@requirements] end - def marshal_load array # :nodoc: + def marshal_load(array) # :nodoc: @requirements = array[0] fix_syck_default_key_in_requirements @@ -213,7 +213,7 @@ def yaml_initialize(tag, vals) # :nodoc: fix_syck_default_key_in_requirements end - def init_with coder # :nodoc: + def init_with(coder) # :nodoc: yaml_initialize coder.tag, coder.map end @@ -221,7 +221,7 @@ def to_yaml_properties # :nodoc: ["@requirements"] end - def encode_with coder # :nodoc: + def encode_with(coder) # :nodoc: coder.add 'requirements', @requirements end @@ -233,7 +233,7 @@ def prerelease? requirements.any? { |r| r.last.prerelease? } end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 1, 'Gem::Requirement.new(', ')' do q.pp as_list end @@ -242,7 +242,7 @@ def pretty_print q # :nodoc: ## # True if +version+ satisfies this Requirement. - def satisfied_by? version + def satisfied_by?(version) raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless Gem::Version === version # #28965: syck has a bug with unquoted '=' YAML.loading as YAML::DefaultKey @@ -265,7 +265,7 @@ def to_s # :nodoc: as_list.join ", " end - def == other # :nodoc: + def ==(other) # :nodoc: return unless Gem::Requirement === other requirements == other.requirements end diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 866998dc59a369..46276f3260eb8c 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -59,7 +59,7 @@ class Gem::Resolver # uniform manner. If one of the +sets+ is itself a ComposedSet its sets are # flattened into the result ComposedSet. - def self.compose_sets *sets + def self.compose_sets(*sets) sets.compact! sets = sets.map do |set| @@ -87,7 +87,7 @@ def self.compose_sets *sets # Creates a Resolver that queries only against the already installed gems # for the +needed+ dependencies. - def self.for_current_gems needed + def self.for_current_gems(needed) new needed, Gem::Resolver::CurrentSet.new end @@ -99,7 +99,7 @@ def self.for_current_gems needed # satisfy the Dependencies. This defaults to IndexSet, which will query # rubygems.org. - def initialize needed, set = nil + def initialize(needed, set = nil) @set = set || Gem::Resolver::IndexSet.new @needed = needed @@ -112,14 +112,14 @@ def initialize needed, set = nil @stats = Gem::Resolver::Stats.new end - def explain stage, *data # :nodoc: + def explain(stage, *data) # :nodoc: return unless DEBUG_RESOLVER d = data.map { |x| x.pretty_inspect }.join(", ") $stderr.printf "%10s %s\n", stage.to_s.upcase, d end - def explain_list stage # :nodoc: + def explain_list(stage) # :nodoc: return unless DEBUG_RESOLVER data = yield @@ -133,7 +133,7 @@ def explain_list stage # :nodoc: # # Returns the Specification and the ActivationRequest - def activation_request dep, possible # :nodoc: + def activation_request(dep, possible) # :nodoc: spec = possible.pop explain :activate, [spec.full_name, possible.size] @@ -145,7 +145,7 @@ def activation_request dep, possible # :nodoc: return spec, activation_request end - def requests s, act, reqs=[] # :nodoc: + def requests(s, act, reqs=[]) # :nodoc: return reqs if @ignore_dependencies s.fetch_development_dependencies if @development @@ -197,7 +197,7 @@ def resolve # Extracts the specifications that may be able to fulfill +dependency+ and # returns those that match the local platform and all those that match. - def find_possible dependency # :nodoc: + def find_possible(dependency) # :nodoc: all = @set.find_all dependency if (skip_dep_gems = skip_gems[dependency.name]) && !skip_dep_gems.empty? @@ -216,7 +216,7 @@ def find_possible dependency # :nodoc: ## # Returns the gems in +specs+ that match the local platform. - def select_local_platforms specs # :nodoc: + def select_local_platforms(specs) # :nodoc: specs.select do |spec| Gem::Platform.installable? spec end diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb index 135d75d6bc7d77..b28e1bef32f530 100644 --- a/lib/rubygems/resolver/activation_request.rb +++ b/lib/rubygems/resolver/activation_request.rb @@ -22,13 +22,13 @@ class Gem::Resolver::ActivationRequest # +others_possible+ indicates that other specifications may also match this # activation request. - def initialize spec, request, others_possible = true + def initialize(spec, request, others_possible = true) @spec = spec @request = request @others_possible = others_possible end - def == other # :nodoc: + def ==(other) # :nodoc: case other when Gem::Specification @spec == other @@ -49,7 +49,7 @@ def development? ## # Downloads a gem at +path+ and returns the file path. - def download path + def download(path) Gem.ensure_gem_subdirectories path if @spec.respond_to? :sources @@ -97,7 +97,7 @@ def inspect # :nodoc: when false then # TODO remove at RubyGems 3 nil else - unless @others_possible.empty? then + unless @others_possible.empty? others = @others_possible.map { |s| s.full_name } " (others possible: #{others.join ', '})" end @@ -152,7 +152,7 @@ def parent @request.requester end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[Activation request', ']' do q.breakable q.pp @spec @@ -167,7 +167,7 @@ def pretty_print q # :nodoc: q.breakable q.text 'others possible' else - unless @others_possible.empty? then + unless @others_possible.empty? q.breakable q.text 'others ' q.pp @others_possible.map { |s| s.full_name } diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb index ee3046af637a6b..6fd91e3b7300ac 100644 --- a/lib/rubygems/resolver/api_set.rb +++ b/lib/rubygems/resolver/api_set.rb @@ -25,7 +25,7 @@ class Gem::Resolver::APISet < Gem::Resolver::Set # API URL +dep_uri+ which is described at # http://guides.rubygems.org/rubygems-org-api - def initialize dep_uri = 'https://rubygems.org/api/v1/dependencies' + def initialize(dep_uri = 'https://rubygems.org/api/v1/dependencies') super() dep_uri = URI dep_uri unless URI === dep_uri # for ruby 1.8 @@ -43,7 +43,7 @@ def initialize dep_uri = 'https://rubygems.org/api/v1/dependencies' # Return an array of APISpecification objects matching # DependencyRequest +req+. - def find_all req + def find_all(req) res = [] return res unless @remote @@ -65,7 +65,7 @@ def find_all req # A hint run by the resolver to allow the Set to fetch # data for DependencyRequests +reqs+. - def prefetch reqs + def prefetch(reqs) return unless @remote names = reqs.map { |r| r.dependency.name } needed = names - @data.keys - @to_fetch @@ -93,7 +93,7 @@ def prefetch_now # :nodoc: end end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[APISet', ']' do q.breakable q.text "URI: #{@dep_uri}" @@ -107,7 +107,7 @@ def pretty_print q # :nodoc: ## # Return data for all versions of the gem +name+. - def versions name # :nodoc: + def versions(name) # :nodoc: if @data.key?(name) return @data[name] end @@ -123,4 +123,3 @@ def versions name # :nodoc: end end - diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb index c4e8a7cb54675b..9bbc09578865cc 100644 --- a/lib/rubygems/resolver/api_specification.rb +++ b/lib/rubygems/resolver/api_specification.rb @@ -27,7 +27,7 @@ def initialize(set, api_data) end end - def == other # :nodoc: + def ==(other) # :nodoc: self.class === other and @set == other.set and @name == other.name and @@ -46,7 +46,7 @@ def installable_platform? # :nodoc: Gem::Platform.match @platform end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[APISpecification', ']' do q.breakable q.text "name: #{name}" @@ -88,4 +88,3 @@ def source # :nodoc: end end - diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb index 4479535abe3a44..cc91b65c0bca46 100644 --- a/lib/rubygems/resolver/best_set.rb +++ b/lib/rubygems/resolver/best_set.rb @@ -10,7 +10,7 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet # Creates a BestSet for the given +sources+ or Gem::sources if none are # specified. +sources+ must be a Gem::SourceList. - def initialize sources = Gem.sources + def initialize(sources = Gem.sources) super() @sources = sources @@ -25,7 +25,7 @@ def pick_sets # :nodoc: end end - def find_all req # :nodoc: + def find_all(req) # :nodoc: pick_sets if @remote and @sets.empty? super @@ -35,13 +35,13 @@ def find_all req # :nodoc: retry end - def prefetch reqs # :nodoc: + def prefetch(reqs) # :nodoc: pick_sets if @remote and @sets.empty? super end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[BestSet', ']' do q.breakable q.text 'sets:' @@ -58,7 +58,7 @@ def pretty_print q # :nodoc: # # The calling method must retry the exception to repeat the lookup. - def replace_failed_api_set error # :nodoc: + def replace_failed_api_set(error) # :nodoc: uri = error.uri uri = URI uri unless URI === uri uri.query = nil @@ -76,4 +76,3 @@ def replace_failed_api_set error # :nodoc: end end - diff --git a/lib/rubygems/resolver/composed_set.rb b/lib/rubygems/resolver/composed_set.rb index 0b65942dcae061..4baac9c75bab5f 100644 --- a/lib/rubygems/resolver/composed_set.rb +++ b/lib/rubygems/resolver/composed_set.rb @@ -16,7 +16,7 @@ class Gem::Resolver::ComposedSet < Gem::Resolver::Set # Creates a new ComposedSet containing +sets+. Use # Gem::Resolver::compose_sets instead. - def initialize *sets + def initialize(*sets) super() @sets = sets @@ -26,7 +26,7 @@ def initialize *sets # When +allow_prerelease+ is set to +true+ prereleases gems are allowed to # match dependencies. - def prerelease= allow_prerelease + def prerelease=(allow_prerelease) super sets.each do |set| @@ -37,7 +37,7 @@ def prerelease= allow_prerelease ## # Sets the remote network access for all composed sets. - def remote= remote + def remote=(remote) super @sets.each { |set| set.remote = remote } @@ -50,7 +50,7 @@ def errors ## # Finds all specs matching +req+ in all sets. - def find_all req + def find_all(req) @sets.map do |s| s.find_all req end.flatten @@ -59,9 +59,8 @@ def find_all req ## # Prefetches +reqs+ in all sets. - def prefetch reqs + def prefetch(reqs) @sets.each { |s| s.prefetch(reqs) } end end - diff --git a/lib/rubygems/resolver/conflict.rb b/lib/rubygems/resolver/conflict.rb index 7997f92950d518..fb1e661b214f66 100644 --- a/lib/rubygems/resolver/conflict.rb +++ b/lib/rubygems/resolver/conflict.rb @@ -27,7 +27,7 @@ def initialize(dependency, activated, failed_dep=dependency) @failed_dep = failed_dep end - def == other # :nodoc: + def ==(other) # :nodoc: self.class === other and @dependency == other.dependency and @activated == other.activated and @@ -57,7 +57,7 @@ def explanation requirement = dependency.requirement alternates = dependency.matching_specs.map { |spec| spec.full_name } - unless alternates.empty? then + unless alternates.empty? matching = <<-MATCHING.chomp Gems matching %s: @@ -97,7 +97,7 @@ def for_spec?(spec) @dependency.name == spec.name end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[Dependency conflict: ', ']' do q.breakable @@ -109,7 +109,7 @@ def pretty_print q # :nodoc: q.pp @dependency q.breakable - if @dependency == @failed_dep then + if @dependency == @failed_dep q.text ' failed' else q.text ' failed dependency ' @@ -121,7 +121,7 @@ def pretty_print q # :nodoc: ## # Path of activations from the +current+ list. - def request_path current + def request_path(current) path = [] while current do diff --git a/lib/rubygems/resolver/current_set.rb b/lib/rubygems/resolver/current_set.rb index 265c639f153007..d60e46389d13b8 100644 --- a/lib/rubygems/resolver/current_set.rb +++ b/lib/rubygems/resolver/current_set.rb @@ -6,9 +6,8 @@ class Gem::Resolver::CurrentSet < Gem::Resolver::Set - def find_all req + def find_all(req) req.dependency.matching_specs end end - diff --git a/lib/rubygems/resolver/dependency_request.rb b/lib/rubygems/resolver/dependency_request.rb index c2918911cd30a9..1984aa9ddc63a4 100644 --- a/lib/rubygems/resolver/dependency_request.rb +++ b/lib/rubygems/resolver/dependency_request.rb @@ -19,12 +19,12 @@ class Gem::Resolver::DependencyRequest # Creates a new DependencyRequest for +dependency+ from +requester+. # +requester may be nil if the request came from a user. - def initialize dependency, requester + def initialize(dependency, requester) @dependency = dependency @requester = requester end - def == other # :nodoc: + def ==(other) # :nodoc: case other when Gem::Dependency @dependency == other @@ -48,7 +48,7 @@ def development? # NOTE: #match? only matches prerelease versions when #dependency is a # prerelease dependency. - def match? spec, allow_prerelease = false + def match?(spec, allow_prerelease = false) @dependency.match? spec, nil, allow_prerelease end @@ -95,7 +95,7 @@ def request_context @requester ? @requester.request : "(unknown)" end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[Dependency request ', ']' do q.breakable q.text @dependency.to_s diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb index 723a202d7aa605..6340b92faecf39 100644 --- a/lib/rubygems/resolver/git_set.rb +++ b/lib/rubygems/resolver/git_set.rb @@ -43,7 +43,7 @@ def initialize # :nodoc: @specs = {} end - def add_git_gem name, repository, reference, submodules # :nodoc: + def add_git_gem(name, repository, reference, submodules) # :nodoc: @repositories[name] = [repository, reference] @need_submodules[repository] = submodules end @@ -56,7 +56,7 @@ def add_git_gem name, repository, reference, submodules # :nodoc: # This fills in the prefetch information as enough information about the gem # is present in the arguments. - def add_git_spec name, version, repository, reference, submodules # :nodoc: + def add_git_spec(name, version, repository, reference, submodules) # :nodoc: add_git_gem name, repository, reference, submodules source = Gem::Source::Git.new name, repository, reference @@ -77,7 +77,7 @@ def add_git_spec name, version, repository, reference, submodules # :nodoc: ## # Finds all git gems matching +req+ - def find_all req + def find_all(req) prefetch nil specs.values.select do |spec| @@ -88,7 +88,7 @@ def find_all req ## # Prefetches specifications from the git repositories in this set. - def prefetch reqs + def prefetch(reqs) return unless @specs.empty? @repositories.each do |name, (repository, reference)| @@ -104,7 +104,7 @@ def prefetch reqs end end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[GitSet', ']' do next if @repositories.empty? q.breakable @@ -120,4 +120,3 @@ def pretty_print q # :nodoc: end end - diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb index 2448797d3fd183..f43cfba853e95e 100644 --- a/lib/rubygems/resolver/git_specification.rb +++ b/lib/rubygems/resolver/git_specification.rb @@ -6,14 +6,14 @@ class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification - def == other # :nodoc: + def ==(other) # :nodoc: self.class === other and @set == other.set and @spec == other.spec and @source == other.source end - def add_dependency dependency # :nodoc: + def add_dependency(dependency) # :nodoc: spec.dependencies << dependency end @@ -21,7 +21,7 @@ def add_dependency dependency # :nodoc: # Installing a git gem only involves building the extensions and generating # the executables. - def install options = {} + def install(options = {}) require 'rubygems/installer' installer = Gem::Installer.for_spec spec, options @@ -35,7 +35,7 @@ def install options = {} installer.run_post_install_hooks end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[GitSpecification', ']' do q.breakable q.text "name: #{name}" @@ -56,4 +56,3 @@ def pretty_print q # :nodoc: end end - diff --git a/lib/rubygems/resolver/index_set.rb b/lib/rubygems/resolver/index_set.rb index 2450f14b4f8af6..e32e1fa5baff13 100644 --- a/lib/rubygems/resolver/index_set.rb +++ b/lib/rubygems/resolver/index_set.rb @@ -5,11 +5,11 @@ class Gem::Resolver::IndexSet < Gem::Resolver::Set - def initialize source = nil # :nodoc: + def initialize(source = nil) # :nodoc: super() @f = - if source then + if source sources = Gem::SourceList.from [source] Gem::SpecFetcher.new sources @@ -36,7 +36,7 @@ def initialize source = nil # :nodoc: # Return an array of IndexSpecification objects matching # DependencyRequest +req+. - def find_all req + def find_all(req) res = [] return res unless @remote @@ -44,7 +44,7 @@ def find_all req name = req.dependency.name @all[name].each do |uri, n| - if req.match? n, @prerelease then + if req.match? n, @prerelease res << Gem::Resolver::IndexSpecification.new( self, n.name, n.version, uri, n.platform) end @@ -53,7 +53,7 @@ def find_all req res end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[IndexSet', ']' do q.breakable q.text 'sources:' @@ -78,4 +78,3 @@ def pretty_print q # :nodoc: end end - diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb index 4340f46943a700..ed9423791c4b34 100644 --- a/lib/rubygems/resolver/index_specification.rb +++ b/lib/rubygems/resolver/index_specification.rb @@ -15,7 +15,7 @@ class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification # The +name+, +version+ and +platform+ are the name, version and platform of # the gem. - def initialize set, name, version, source, platform + def initialize(set, name, version, source, platform) super() @set = set @@ -38,12 +38,12 @@ def inspect # :nodoc: '#<%s %s source %s>' % [self.class, full_name, @source] end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[Index specification', ']' do q.breakable q.text full_name - unless Gem::Platform::RUBY == @platform then + unless Gem::Platform::RUBY == @platform q.breakable q.text @platform.to_s end @@ -67,4 +67,3 @@ def spec # :nodoc: end end - diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb index d9c6a5e5cf7b82..9d996fc1dafbfa 100644 --- a/lib/rubygems/resolver/installed_specification.rb +++ b/lib/rubygems/resolver/installed_specification.rb @@ -5,7 +5,7 @@ class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification - def == other # :nodoc: + def ==(other) # :nodoc: self.class === other and @set == other.set and @spec == other.spec @@ -15,7 +15,7 @@ def == other # :nodoc: # This is a null install as this specification is already installed. # +options+ are ignored. - def install options = {} + def install(options = {}) yield nil end @@ -30,7 +30,7 @@ def installable_platform? super end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[InstalledSpecification', ']' do q.breakable q.text "name: #{name}" @@ -56,4 +56,3 @@ def source end end - diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb index f24293c0a03ee4..f3827ad4e9c90e 100644 --- a/lib/rubygems/resolver/installer_set.rb +++ b/lib/rubygems/resolver/installer_set.rb @@ -29,7 +29,7 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set ## # Creates a new InstallerSet that will look for gems in +domain+. - def initialize domain + def initialize(domain) super() @domain = domain @@ -50,7 +50,7 @@ def initialize domain # Looks up the latest specification for +dependency+ and adds it to the # always_install list. - def add_always_install dependency + def add_always_install(dependency) request = Gem::Resolver::DependencyRequest.new dependency, nil found = find_all request @@ -65,7 +65,7 @@ def add_always_install dependency Gem::Platform.local === s.platform end - if found.empty? then + if found.empty? exc = Gem::UnsatisfiableDependencyError.new request exc.errors = errors @@ -83,7 +83,7 @@ def add_always_install dependency # Adds a local gem requested using +dep_name+ with the given +spec+ that can # be loaded and installed using the +source+. - def add_local dep_name, spec, source + def add_local(dep_name, spec, source) @local[dep_name] = [spec, source] end @@ -112,7 +112,7 @@ def errors # Returns an array of IndexSpecification objects matching DependencyRequest # +req+. - def find_all req + def find_all(req) res = [] dep = req.dependency @@ -128,7 +128,7 @@ def find_all req res << Gem::Resolver::InstalledSpecification.new(self, gemspec) end unless @ignore_installed - if consider_local? then + if consider_local? matching_local = @local.values.select do |spec, _| req.match? spec end.map do |spec, source| @@ -138,7 +138,7 @@ def find_all req res.concat matching_local begin - if local_spec = @local_source.find_gem(name, dep.requirement) then + if local_spec = @local_source.find_gem(name, dep.requirement) res << Gem::Resolver::IndexSpecification.new( self, local_spec.name, local_spec.version, @local_source, local_spec.platform) @@ -161,7 +161,7 @@ def prefetch(reqs) @remote_set.prefetch(reqs) if consider_remote? end - def prerelease= allow_prerelease + def prerelease=(allow_prerelease) super @remote_set.prerelease = allow_prerelease @@ -179,7 +179,7 @@ def inspect # :nodoc: # Called from IndexSpecification to get a true Specification # object. - def load_spec name, ver, platform, source # :nodoc: + def load_spec(name, ver, platform, source) # :nodoc: key = "#{name}-#{ver}-#{platform}" @specs.fetch key do @@ -192,13 +192,13 @@ def load_spec name, ver, platform, source # :nodoc: ## # Has a local gem for +dep_name+ been added to this set? - def local? dep_name # :nodoc: + def local?(dep_name) # :nodoc: spec, _ = @local[dep_name] spec end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[InstallerSet', ']' do q.breakable q.text "domain: #{@domain}" @@ -213,7 +213,7 @@ def pretty_print q # :nodoc: end end - def remote= remote # :nodoc: + def remote=(remote) # :nodoc: case @domain when :local then @domain = :both if remote diff --git a/lib/rubygems/resolver/local_specification.rb b/lib/rubygems/resolver/local_specification.rb index 1d9d22f0acb543..7418cfcc8641b8 100644 --- a/lib/rubygems/resolver/local_specification.rb +++ b/lib/rubygems/resolver/local_specification.rb @@ -17,7 +17,7 @@ def local? # :nodoc: true end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[LocalSpecification', ']' do q.breakable q.text "name: #{name}" @@ -39,4 +39,3 @@ def pretty_print q # :nodoc: end end - diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb index 7fddc93e1c155b..4002a963a467e7 100644 --- a/lib/rubygems/resolver/lock_set.rb +++ b/lib/rubygems/resolver/lock_set.rb @@ -9,7 +9,7 @@ class Gem::Resolver::LockSet < Gem::Resolver::Set ## # Creates a new LockSet from the given +sources+ - def initialize sources + def initialize(sources) super() @sources = sources.map do |source| @@ -26,7 +26,7 @@ def initialize sources # The specification's set will be the current set, and the source will be # the current set's source. - def add name, version, platform # :nodoc: + def add(name, version, platform) # :nodoc: version = Gem::Version.new version specs = [ Gem::Resolver::LockSpecification.new(self, name, version, @sources, platform) @@ -41,7 +41,7 @@ def add name, version, platform # :nodoc: # Returns an Array of IndexSpecification objects matching the # DependencyRequest +req+. - def find_all req + def find_all(req) @specs.select do |spec| req.match? spec end @@ -51,7 +51,7 @@ def find_all req # Loads a Gem::Specification with the given +name+, +version+ and # +platform+. +source+ is ignored. - def load_spec name, version, platform, source # :nodoc: + def load_spec(name, version, platform, source) # :nodoc: dep = Gem::Dependency.new name, version found = @specs.find do |spec| @@ -63,7 +63,7 @@ def load_spec name, version, platform, source # :nodoc: found.source.fetch_spec tuple end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[LockSet', ']' do q.breakable q.text 'source:' @@ -80,4 +80,3 @@ def pretty_print q # :nodoc: end end - diff --git a/lib/rubygems/resolver/lock_specification.rb b/lib/rubygems/resolver/lock_specification.rb index f48567567313e7..e29b567de4eba6 100644 --- a/lib/rubygems/resolver/lock_specification.rb +++ b/lib/rubygems/resolver/lock_specification.rb @@ -9,7 +9,7 @@ class Gem::Resolver::LockSpecification < Gem::Resolver::Specification attr_reader :sources - def initialize set, name, version, sources, platform + def initialize(set, name, version, sources, platform) super() @name = name @@ -27,10 +27,10 @@ def initialize set, name, version, sources, platform # This is a null install as a locked specification is considered installed. # +options+ are ignored. - def install options = {} + def install(options = {}) destination = options[:install_dir] || Gem.dir - if File.exist? File.join(destination, 'specifications', spec.spec_name) then + if File.exist? File.join(destination, 'specifications', spec.spec_name) yield nil return end @@ -41,11 +41,11 @@ def install options = {} ## # Adds +dependency+ from the lockfile to this specification - def add_dependency dependency # :nodoc: + def add_dependency(dependency) # :nodoc: @dependencies << dependency end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[LockSpecification', ']' do q.breakable q.text "name: #{@name}" @@ -53,12 +53,12 @@ def pretty_print q # :nodoc: q.breakable q.text "version: #{@version}" - unless @platform == Gem::Platform::RUBY then + unless @platform == Gem::Platform::RUBY q.breakable q.text "platform: #{@platform}" end - unless @dependencies.empty? then + unless @dependencies.empty? q.breakable q.text 'dependencies:' q.breakable @@ -85,4 +85,3 @@ def spec end end - diff --git a/lib/rubygems/resolver/requirement_list.rb b/lib/rubygems/resolver/requirement_list.rb index 2768c80170d5f7..98d086e63cd89f 100644 --- a/lib/rubygems/resolver/requirement_list.rb +++ b/lib/rubygems/resolver/requirement_list.rb @@ -18,7 +18,7 @@ def initialize @list = [] end - def initialize_copy other # :nodoc: + def initialize_copy(other) # :nodoc: @exact = @exact.dup @list = @list.dup end diff --git a/lib/rubygems/resolver/set.rb b/lib/rubygems/resolver/set.rb index 11704d5c4c3ebd..242f9cd3dc9aa4 100644 --- a/lib/rubygems/resolver/set.rb +++ b/lib/rubygems/resolver/set.rb @@ -31,7 +31,7 @@ def initialize # :nodoc: # The find_all method must be implemented. It returns all Resolver # Specification objects matching the given DependencyRequest +req+. - def find_all req + def find_all(req) raise NotImplementedError end @@ -43,7 +43,7 @@ def find_all req # When overridden, the #prefetch method should look up specifications # matching +reqs+. - def prefetch reqs + def prefetch(reqs) end ## diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb index 696d963732ff5f..8e799514fd62f2 100644 --- a/lib/rubygems/resolver/source_set.rb +++ b/lib/rubygems/resolver/source_set.rb @@ -16,7 +16,7 @@ def initialize @sets = {} end - def find_all req # :nodoc: + def find_all(req) # :nodoc: if set = get_set(req.dependency.name) set.find_all req else @@ -25,7 +25,7 @@ def find_all req # :nodoc: end # potentially no-op - def prefetch reqs # :nodoc: + def prefetch(reqs) # :nodoc: reqs.each do |req| if set = get_set(req.dependency.name) set.prefetch reqs @@ -33,7 +33,7 @@ def prefetch reqs # :nodoc: end end - def add_source_gem name, source + def add_source_gem(name, source) @links[name] = source end @@ -45,4 +45,3 @@ def get_set(name) end end - diff --git a/lib/rubygems/resolver/spec_specification.rb b/lib/rubygems/resolver/spec_specification.rb index 35ee8cc247a8b8..d0e744f3a7bf18 100644 --- a/lib/rubygems/resolver/spec_specification.rb +++ b/lib/rubygems/resolver/spec_specification.rb @@ -10,7 +10,7 @@ class Gem::Resolver::SpecSpecification < Gem::Resolver::Specification # +spec+. The +source+ is either where the +spec+ came from, or should be # loaded from. - def initialize set, spec, source = nil + def initialize(set, spec, source = nil) @set = set @source = source @spec = spec @@ -54,4 +54,3 @@ def version end end - diff --git a/lib/rubygems/resolver/specification.rb b/lib/rubygems/resolver/specification.rb index 530ea9994d485f..7c1e9be702cb9c 100644 --- a/lib/rubygems/resolver/specification.rb +++ b/lib/rubygems/resolver/specification.rb @@ -81,7 +81,7 @@ def full_name # After installation #spec is updated to point to the just-installed # specification. - def install options = {} + def install(options = {}) require 'rubygems/installer' gem = download options @@ -93,7 +93,7 @@ def install options = {} @spec = installer.install end - def download options + def download(options) dir = options[:install_dir] || Gem.dir Gem.ensure_gem_subdirectories dir @@ -112,4 +112,3 @@ def local? # :nodoc: false end end - diff --git a/lib/rubygems/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb index f30ce534af35ee..7e2e917d5c3e20 100644 --- a/lib/rubygems/resolver/vendor_set.rb +++ b/lib/rubygems/resolver/vendor_set.rb @@ -32,7 +32,7 @@ def initialize # :nodoc: # Adds a specification to the set with the given +name+ which has been # unpacked into the given +directory+. - def add_vendor_gem name, directory # :nodoc: + def add_vendor_gem(name, directory) # :nodoc: gemspec = File.join directory, "#{name}.gemspec" spec = Gem::Specification.load gemspec @@ -52,7 +52,7 @@ def add_vendor_gem name, directory # :nodoc: # Returns an Array of VendorSpecification objects matching the # DependencyRequest +req+. - def find_all req + def find_all(req) @specs.values.select do |spec| req.match? spec end.map do |spec| @@ -65,11 +65,11 @@ def find_all req # Loads a spec with the given +name+. +version+, +platform+ and +source+ are # ignored. - def load_spec name, version, platform, source # :nodoc: + def load_spec(name, version, platform, source) # :nodoc: @specs.fetch name end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[VendorSet', ']' do next if @directories.empty? q.breakable @@ -85,4 +85,3 @@ def pretty_print q # :nodoc: end end - diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb index c624f3e8347b51..56f2e6eb2cf543 100644 --- a/lib/rubygems/resolver/vendor_specification.rb +++ b/lib/rubygems/resolver/vendor_specification.rb @@ -6,7 +6,7 @@ class Gem::Resolver::VendorSpecification < Gem::Resolver::SpecSpecification - def == other # :nodoc: + def ==(other) # :nodoc: self.class === other and @set == other.set and @spec == other.spec and @@ -17,9 +17,8 @@ def == other # :nodoc: # This is a null install as this gem was unpacked into a directory. # +options+ are ignored. - def install options = {} + def install(options = {}) yield nil end end - diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb index d6108899958ae2..3540fd74ddee83 100644 --- a/lib/rubygems/safe_yaml.rb +++ b/lib/rubygems/safe_yaml.rb @@ -7,7 +7,7 @@ module Gem # Psych.safe_load module SafeYAML - WHITELISTED_CLASSES = %w( + PERMITTED_CLASSES = %w( Symbol Time Date @@ -21,23 +21,23 @@ module SafeYAML Syck::DefaultKey ).freeze - WHITELISTED_SYMBOLS = %w( + PERMITTED_SYMBOLS = %w( development runtime ).freeze if ::YAML.respond_to? :safe_load - def self.safe_load input + def self.safe_load(input) if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') - ::YAML.safe_load(input, whitelist_classes: WHITELISTED_CLASSES, whitelist_symbols: WHITELISTED_SYMBOLS, aliases: true) + ::YAML.safe_load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, aliases: true) else - ::YAML.safe_load(input, WHITELISTED_CLASSES, WHITELISTED_SYMBOLS, true) + ::YAML.safe_load(input, PERMITTED_CLASSES, PERMITTED_SYMBOLS, true) end end - def self.load input + def self.load(input) if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') - ::YAML.safe_load(input, whitelist_classes: [::Symbol]) + ::YAML.safe_load(input, permitted_classes: [::Symbol]) else ::YAML.safe_load(input, [::Symbol]) end @@ -47,11 +47,11 @@ def self.load input warn "YAML safe loading is not available. Please upgrade psych to a version that supports safe loading (>= 2.0)." end - def self.safe_load input, *args + def self.safe_load(input, *args) ::YAML.load input end - def self.load input + def self.load(input) ::YAML.load input end end diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index f896039fa4e9c4..7b0a0b3c6af193 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -340,9 +340,9 @@ class Exception < Gem::Exception; end # Digest algorithm used to sign gems DIGEST_ALGORITHM = - if defined?(OpenSSL::Digest::SHA256) then + if defined?(OpenSSL::Digest::SHA256) OpenSSL::Digest::SHA256 - elsif defined?(OpenSSL::Digest::SHA1) then + elsif defined?(OpenSSL::Digest::SHA1) OpenSSL::Digest::SHA1 else require 'digest' @@ -353,7 +353,7 @@ class Exception < Gem::Exception; end # Used internally to select the signing digest from all computed digests DIGEST_NAME = # :nodoc: - if DIGEST_ALGORITHM.method_defined? :name then + if DIGEST_ALGORITHM.method_defined? :name DIGEST_ALGORITHM.new.name else DIGEST_ALGORITHM.name[/::([^:]+)\z/, 1] @@ -363,7 +363,7 @@ class Exception < Gem::Exception; end # Algorithm for creating the key pair used to sign gems KEY_ALGORITHM = - if defined?(OpenSSL::PKey::RSA) then + if defined?(OpenSSL::PKey::RSA) OpenSSL::PKey::RSA end @@ -403,7 +403,7 @@ class Exception < Gem::Exception; end 'subjectKeyIdentifier' => 'hash', }.freeze - def self.alt_name_or_x509_entry certificate, x509_entry + def self.alt_name_or_x509_entry(certificate, x509_entry) alt_name = certificate.extensions.find do |extension| extension.oid == "#{x509_entry}AltName" end @@ -419,8 +419,8 @@ def self.alt_name_or_x509_entry certificate, x509_entry # # The +extensions+ restrict the key to the indicated uses. - def self.create_cert subject, key, age = ONE_YEAR, extensions = EXTENSIONS, - serial = 1 + def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, + serial = 1) cert = OpenSSL::X509::Certificate.new cert.public_key = key.public_key @@ -446,7 +446,7 @@ def self.create_cert subject, key, age = ONE_YEAR, extensions = EXTENSIONS, # a subject alternative name of +email+ and the given +extensions+ for the # +key+. - def self.create_cert_email email, key, age = ONE_YEAR, extensions = EXTENSIONS + def self.create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS) subject = email_to_name email extensions = extensions.merge "subjectAltName" => "email:#{email}" @@ -458,8 +458,8 @@ def self.create_cert_email email, key, age = ONE_YEAR, extensions = EXTENSIONS # Creates a self-signed certificate with an issuer and subject of +subject+ # and the given +extensions+ for the +key+. - def self.create_cert_self_signed subject, key, age = ONE_YEAR, - extensions = EXTENSIONS, serial = 1 + def self.create_cert_self_signed(subject, key, age = ONE_YEAR, + extensions = EXTENSIONS, serial = 1) certificate = create_cert subject, key, age, extensions sign certificate, key, certificate, age, extensions, serial @@ -469,14 +469,14 @@ def self.create_cert_self_signed subject, key, age = ONE_YEAR, # Creates a new key pair of the specified +length+ and +algorithm+. The # default is a 3072 bit RSA key. - def self.create_key length = KEY_LENGTH, algorithm = KEY_ALGORITHM + def self.create_key(length = KEY_LENGTH, algorithm = KEY_ALGORITHM) algorithm.new length end ## # Turns +email_address+ into an OpenSSL::X509::Name - def self.email_to_name email_address + def self.email_to_name(email_address) email_address = email_address.gsub(/[^\w@.-]+/i, '_') cn, dcs = email_address.split '@' @@ -494,15 +494,15 @@ def self.email_to_name email_address #-- # TODO increment serial - def self.re_sign expired_certificate, private_key, age = ONE_YEAR, - extensions = EXTENSIONS + def self.re_sign(expired_certificate, private_key, age = ONE_YEAR, + extensions = EXTENSIONS) raise Gem::Security::Exception, "incorrect signing key for re-signing " + "#{expired_certificate.subject}" unless expired_certificate.public_key.to_pem == private_key.public_key.to_pem unless expired_certificate.subject.to_s == - expired_certificate.issuer.to_s then + expired_certificate.issuer.to_s subject = alt_name_or_x509_entry expired_certificate, :subject issuer = alt_name_or_x509_entry expired_certificate, :issuer @@ -531,8 +531,8 @@ def self.reset # # Returns the newly signed certificate. - def self.sign certificate, signing_key, signing_cert, - age = ONE_YEAR, extensions = EXTENSIONS, serial = 1 + def self.sign(certificate, signing_key, signing_cert, + age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) signee_subject = certificate.subject signee_key = certificate.public_key @@ -571,7 +571,7 @@ def self.trust_dir ## # Enumerates the trusted certificates via Gem::Security::TrustDir. - def self.trusted_certificates &block + def self.trusted_certificates(&block) trust_dir.each_certificate(&block) end @@ -580,7 +580,7 @@ def self.trusted_certificates &block # +permissions+. If passed +cipher+ and +passphrase+ those arguments will be # passed to +to_pem+. - def self.write pemmable, path, permissions = 0600, passphrase = nil, cipher = KEY_CIPHER + def self.write(pemmable, path, permissions = 0600, passphrase = nil, cipher = KEY_CIPHER) path = File.expand_path path File.open path, 'wb', permissions do |io| @@ -598,11 +598,10 @@ def self.write pemmable, path, permissions = 0600, passphrase = nil, cipher = KE end -if defined?(OpenSSL::SSL) then +if defined?(OpenSSL::SSL) require 'rubygems/security/policy' require 'rubygems/security/policies' require 'rubygems/security/trust_dir' end require 'rubygems/security/signer' - diff --git a/lib/rubygems/security/policies.rb b/lib/rubygems/security/policies.rb index 49ca8d860d4645..8f6ad9931608f8 100644 --- a/lib/rubygems/security/policies.rb +++ b/lib/rubygems/security/policies.rb @@ -113,4 +113,3 @@ module Gem::Security }.freeze end - diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 2e9159797c5b5c..1aa6eab18caae6 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -24,7 +24,7 @@ class Gem::Security::Policy # Create a new Gem::Security::Policy object with the given mode and # options. - def initialize name, policy = {}, opt = {} + def initialize(name, policy = {}, opt = {}) require 'openssl' @name = name @@ -55,7 +55,7 @@ def initialize name, policy = {}, opt = {} # Verifies each certificate in +chain+ has signed the following certificate # and is valid for the given +time+. - def check_chain chain, time + def check_chain(chain, time) raise Gem::Security::Exception, 'missing signing chain' unless chain raise Gem::Security::Exception, 'empty signing chain' if chain.empty? @@ -74,7 +74,7 @@ def check_chain chain, time # Verifies that +data+ matches the +signature+ created by +public_key+ and # the +digest+ algorithm. - def check_data public_key, digest, signature, data + def check_data(public_key, digest, signature, data) raise Gem::Security::Exception, "invalid signature" unless public_key.verify digest.new, signature, data.digest @@ -85,22 +85,22 @@ def check_data public_key, digest, signature, data # Ensures that +signer+ is valid for +time+ and was signed by the +issuer+. # If the +issuer+ is +nil+ no verification is performed. - def check_cert signer, issuer, time + def check_cert(signer, issuer, time) raise Gem::Security::Exception, 'missing signing certificate' unless signer message = "certificate #{signer.subject}" - if not_before = signer.not_before and not_before > time then + if not_before = signer.not_before and not_before > time raise Gem::Security::Exception, "#{message} not valid before #{not_before}" end - if not_after = signer.not_after and not_after < time then + if not_after = signer.not_after and not_after < time raise Gem::Security::Exception, "#{message} not valid after #{not_after}" end - if issuer and not signer.verify issuer.public_key then + if issuer and not signer.verify issuer.public_key raise Gem::Security::Exception, "#{message} was not issued by #{issuer.subject}" end @@ -111,8 +111,8 @@ def check_cert signer, issuer, time ## # Ensures the public key of +key+ matches the public key in +signer+ - def check_key signer, key - unless signer and key then + def check_key(signer, key) + unless signer and key return true unless @only_signed raise Gem::Security::Exception, 'missing key or signature' @@ -129,7 +129,7 @@ def check_key signer, key # Ensures the root certificate in +chain+ is self-signed and valid for # +time+. - def check_root chain, time + def check_root(chain, time) raise Gem::Security::Exception, 'missing signing chain' unless chain root = chain.first @@ -148,7 +148,7 @@ def check_root chain, time # Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and # the digests of the two certificates match according to +digester+ - def check_trust chain, digester, trust_dir + def check_trust(chain, digester, trust_dir) raise Gem::Security::Exception, 'missing signing chain' unless chain root = chain.first @@ -157,7 +157,7 @@ def check_trust chain, digester, trust_dir path = Gem::Security.trust_dir.cert_path root - unless File.exist? path then + unless File.exist? path message = "root cert #{root.subject} is not trusted".dup message << " (root of signing cert #{chain.last.subject})" if @@ -183,7 +183,7 @@ def check_trust chain, digester, trust_dir ## # Extracts the email or subject from +certificate+ - def subject certificate # :nodoc: + def subject(certificate) # :nodoc: certificate.extensions.each do |extension| next unless extension.oid == 'subjectAltName' @@ -208,13 +208,13 @@ def inspect # :nodoc: # # If +key+ is given it is used to validate the signing certificate. - def verify chain, key = nil, digests = {}, signatures = {}, - full_name = '(unknown)' - if signatures.empty? then - if @only_signed then + def verify(chain, key = nil, digests = {}, signatures = {}, + full_name = '(unknown)') + if signatures.empty? + if @only_signed raise Gem::Security::Exception, "unsigned gems are not allowed by the #{name} policy" - elsif digests.empty? then + elsif digests.empty? # lack of signatures is irrelevant if there is nothing to check # against else @@ -232,7 +232,7 @@ def verify chain, key = nil, digests = {}, signatures = {}, file_digests.values.first.name == Gem::Security::DIGEST_NAME end - if @verify_data then + if @verify_data raise Gem::Security::Exception, 'no digests provided (probable bug)' if signer_digests.nil? or signer_digests.empty? else @@ -249,9 +249,9 @@ def verify chain, key = nil, digests = {}, signatures = {}, check_root chain, time if @verify_root - if @only_trusted then + if @only_trusted check_trust chain, digester, trust_dir - elsif signatures.empty? and digests.empty? then + elsif signatures.empty? and digests.empty? # trust is irrelevant if there's no signatures to verify else alert_warning "#{subject signer} is not trusted for #{full_name}" @@ -280,7 +280,7 @@ def verify chain, key = nil, digests = {}, signatures = {}, # Extracts the certificate chain from the +spec+ and calls #verify to ensure # the signatures and certificate chain is valid according to the policy.. - def verify_signatures spec, digests, signatures + def verify_signatures(spec, digests, signatures) chain = spec.cert_chain.map do |cert_pem| OpenSSL::X509::Certificate.new cert_pem end diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index 32dab9fa81138b..34e86e921a4f68 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -65,18 +65,18 @@ def self.re_sign_cert(expired_cert, expired_cert_path, private_key) # +chain+ containing X509 certificates, encoding certificates or paths to # certificates. - def initialize key, cert_chain, passphrase = nil, options = {} + def initialize(key, cert_chain, passphrase = nil, options = {}) @cert_chain = cert_chain @key = key @passphrase = passphrase @options = DEFAULT_OPTIONS.merge(options) - unless @key then + unless @key default_key = File.join Gem.default_key_path @key = default_key if File.exist? default_key end - unless @cert_chain then + unless @cert_chain default_cert = File.join Gem.default_cert_path @cert_chain = [default_cert] if File.exist? default_cert end @@ -89,7 +89,7 @@ def initialize key, cert_chain, passphrase = nil, options = {} @key = OpenSSL::PKey::RSA.new(File.read(@key), @passphrase) end - if @cert_chain then + if @cert_chain @cert_chain = @cert_chain.compact.map do |cert| next cert if OpenSSL::X509::Certificate === cert @@ -106,10 +106,10 @@ def initialize key, cert_chain, passphrase = nil, options = {} # Extracts the full name of +cert+. If the certificate has a subjectAltName # this value is preferred, otherwise the subject is used. - def extract_name cert # :nodoc: + def extract_name(cert) # :nodoc: subject_alt_name = cert.extensions.find { |e| 'subjectAltName' == e.oid } - if subject_alt_name then + if subject_alt_name /\Aemail:/ =~ subject_alt_name.value $' || subject_alt_name.value @@ -138,12 +138,12 @@ def load_cert_chain # :nodoc: ## # Sign data with given digest algorithm - def sign data + def sign(data) return unless @key raise Gem::Security::Exception, 'no certs provided' if @cert_chain.empty? - if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now then + if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now re_sign_key( expiration_length: (Gem::Security::ONE_DAY * options[:expiration_length_days]) ) @@ -203,4 +203,3 @@ def re_sign_key(expiration_length: Gem::Security::ONE_YEAR) # :nodoc: end end - diff --git a/lib/rubygems/security/trust_dir.rb b/lib/rubygems/security/trust_dir.rb index 6d837affa17726..98031ea22b6db9 100644 --- a/lib/rubygems/security/trust_dir.rb +++ b/lib/rubygems/security/trust_dir.rb @@ -22,7 +22,7 @@ class Gem::Security::TrustDir # Creates a new TrustDir using +dir+ where the directory and file # permissions will be checked according to +permissions+ - def initialize dir, permissions = DEFAULT_PERMISSIONS + def initialize(dir, permissions = DEFAULT_PERMISSIONS) @dir = dir @permissions = permissions @@ -32,7 +32,7 @@ def initialize dir, permissions = DEFAULT_PERMISSIONS ## # Returns the path to the trusted +certificate+ - def cert_path certificate + def cert_path(certificate) name_path certificate.subject end @@ -59,7 +59,7 @@ def each_certificate # Returns the issuer certificate of the given +certificate+ if it exists in # the trust directory. - def issuer_of certificate + def issuer_of(certificate) path = name_path certificate.issuer return unless File.exist? path @@ -70,7 +70,7 @@ def issuer_of certificate ## # Returns the path to the trusted certificate with the given ASN.1 +name+ - def name_path name + def name_path(name) digest = @digester.hexdigest name.to_s File.join @dir, "cert-#{digest}.pem" @@ -79,7 +79,7 @@ def name_path name ## # Loads the given +certificate_file+ - def load_certificate certificate_file + def load_certificate(certificate_file) pem = File.read certificate_file OpenSSL::X509::Certificate.new pem @@ -88,7 +88,7 @@ def load_certificate certificate_file ## # Add a certificate to trusted certificate list. - def trust_cert certificate + def trust_cert(certificate) verify destination = cert_path certificate @@ -105,7 +105,7 @@ def trust_cert certificate # permissions. def verify - if File.exist? @dir then + if File.exist? @dir raise Gem::Security::Exception, "trust directory #{@dir} is not a directory" unless File.directory? @dir @@ -117,4 +117,3 @@ def verify end end - diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb index 5c65f74aa389b8..1453bf23235469 100644 --- a/lib/rubygems/server.rb +++ b/lib/rubygems/server.rb @@ -450,7 +450,7 @@ def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil) @have_rdoc_4_plus = nil end - def add_date res + def add_date(res) res['date'] = @spec_dirs.map do |spec_dir| File.stat(spec_dir).mtime end.max @@ -462,8 +462,8 @@ def uri_encode(str) end end - def doc_root gem_name - if have_rdoc_4_plus? then + def doc_root(gem_name) + if have_rdoc_4_plus? "/doc_root/#{u gem_name}/" else "/doc_root/#{u gem_name}/rdoc/index.html" @@ -491,14 +491,14 @@ def latest_specs(req, res) specs = Marshal.dump specs - if req.path =~ /\.gz$/ then + if req.path =~ /\.gz$/ specs = Gem::Util.gzip specs res['content-type'] = 'application/x-gzip' else res['content-type'] = 'application/octet-stream' end - if req.request_method == 'HEAD' then + if req.request_method == 'HEAD' res['content-length'] = specs.length else res.body << specs @@ -509,7 +509,7 @@ def latest_specs(req, res) # Creates server sockets based on the addresses option. If no addresses # were given a server socket for all interfaces is created. - def listen addresses = @addresses + def listen(addresses = @addresses) addresses = [nil] unless addresses listeners = 0 @@ -529,14 +529,14 @@ def listen addresses = @addresses end end - if @server.listeners.empty? then + if @server.listeners.empty? say "Unable to start a server." say "Check for running servers or your --bind and --port arguments" terminate_interaction 1 end end - def prerelease_specs req, res + def prerelease_specs(req, res) reset_gems res['content-type'] = 'application/x-gzip' @@ -552,14 +552,14 @@ def prerelease_specs req, res specs = Marshal.dump specs - if req.path =~ /\.gz$/ then + if req.path =~ /\.gz$/ specs = Gem::Util.gzip specs res['content-type'] = 'application/x-gzip' else res['content-type'] = 'application/octet-stream' end - if req.request_method == 'HEAD' then + if req.request_method == 'HEAD' res['content-length'] = specs.length else res.body << specs @@ -579,13 +579,13 @@ def quick(req, res) selector = full_name.inspect - if specs.empty? then + if specs.empty? res.status = 404 res.body = "No gems found matching #{selector}" - elsif specs.length > 1 then + elsif specs.length > 1 res.status = 500 res.body = "Multiple gems found matching #{selector}" - elsif marshal_format then + elsif marshal_format res['content-type'] = 'application/x-deflate' res.body << Gem.deflate(Marshal.dump(specs.first)) end @@ -818,7 +818,7 @@ def run '/gems' => '/cache/', } - if have_rdoc_4_plus? then + if have_rdoc_4_plus? @server.mount '/doc_root', RDoc::Servlet, '/doc_root' else file_handlers['/doc_root'] = '/doc/' @@ -851,14 +851,14 @@ def specs(req, res) specs = Marshal.dump specs - if req.path =~ /\.gz$/ then + if req.path =~ /\.gz$/ specs = Gem::Util.gzip specs res['content-type'] = 'application/x-gzip' else res['content-type'] = 'application/octet-stream' end - if req.request_method == 'HEAD' then + if req.request_method == 'HEAD' res['content-length'] = specs.length else res.body << specs diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 5530c2ce32680d..faed7bd350cf9b 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -68,7 +68,7 @@ def <=>(other) end end - def == other # :nodoc: + def ==(other) # :nodoc: self.class === other and @uri == other.uri end @@ -88,7 +88,7 @@ def dependency_resolver_set # :nodoc: rescue Gem::RemoteFetcher::FetchError Gem::Resolver::IndexSet.new self else - if response.respond_to? :uri then + if response.respond_to? :uri Gem::Resolver::APISet.new response.uri else Gem::Resolver::APISet.new bundler_api_uri @@ -126,7 +126,7 @@ def update_cache? ## # Fetches a specification for the given +name_tuple+. - def fetch_spec name_tuple + def fetch_spec(name_tuple) fetcher = Gem::RemoteFetcher.fetcher spec_file_name = name_tuple.spec_name @@ -137,7 +137,7 @@ def fetch_spec name_tuple local_spec = File.join cache_dir, spec_file_name - if File.exist? local_spec then + if File.exist? local_spec spec = Gem.read_binary local_spec spec = Marshal.load(spec) rescue nil return spec if spec @@ -148,7 +148,7 @@ def fetch_spec name_tuple spec = fetcher.fetch_path source_uri spec = Gem::Util.inflate spec - if update_cache? then + if update_cache? FileUtils.mkdir_p cache_dir File.open local_spec, 'wb' do |io| @@ -206,7 +206,7 @@ def download(spec, dir=Dir.pwd) fetcher.download spec, uri.to_s, dir end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[Remote:', ']' do q.breakable q.text @uri.to_s diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index 23f8928a4ebfc0..0b8a4339cc79b8 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -50,7 +50,7 @@ class Gem::Source::Git < Gem::Source # repository may contain multiple gems. If +submodules+ is true, submodules # will be checked out when the gem is installed. - def initialize name, repository, reference, submodules = false + def initialize(name, repository, reference, submodules = false) super repository @name = name @@ -63,7 +63,7 @@ def initialize name, repository, reference, submodules = false @git = ENV['git'] || 'git' end - def <=> other + def <=>(other) case other when Gem::Source::Git then 0 @@ -77,7 +77,7 @@ def <=> other end end - def == other # :nodoc: + def ==(other) # :nodoc: super and @name == other.name and @repository == other.repository and @@ -93,7 +93,7 @@ def checkout # :nodoc: return false unless File.exist? repo_cache_dir - unless File.exist? install_dir then + unless File.exist? install_dir system @git, 'clone', '--quiet', '--no-checkout', repo_cache_dir, install_dir end @@ -117,7 +117,7 @@ def checkout # :nodoc: def cache # :nodoc: return unless @remote - if File.exist? repo_cache_dir then + if File.exist? repo_cache_dir Dir.chdir repo_cache_dir do system @git, 'fetch', '--quiet', '--force', '--tags', @repository, 'refs/heads/*:refs/heads/*' @@ -145,7 +145,7 @@ def dir_shortref # :nodoc: ## # Nothing to download for git gems - def download full_spec, path # :nodoc: + def download(full_spec, path) # :nodoc: end ## @@ -157,7 +157,7 @@ def install_dir # :nodoc: File.join base_dir, 'gems', "#{@name}-#{dir_shortref}" end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[Git: ', ']' do q.breakable q.text @repository @@ -206,7 +206,7 @@ def specs Dir.chdir directory do spec = Gem::Specification.load file - if spec then + if spec spec.base_dir = base_dir spec.extension_dir = @@ -228,7 +228,7 @@ def uri_hash # :nodoc: require 'digest' # required here to avoid deadlocking in Gem.activate_bin_path (because digest is a gem on 2.5+) normalized = - if @repository =~ %r%^\w+://(\w+@)?% then + if @repository =~ %r%^\w+://(\w+@)?% uri = URI(@repository).normalize.to_s.sub %r%/$%,'' uri.sub(/\A(\w+)/) { $1.downcase } else @@ -239,4 +239,3 @@ def uri_hash # :nodoc: end end - diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb index 300491e467614d..8e20cbd76d8597 100644 --- a/lib/rubygems/source/installed.rb +++ b/lib/rubygems/source/installed.rb @@ -11,7 +11,7 @@ def initialize # :nodoc: ## # Installed sources sort before all other sources - def <=> other + def <=>(other) case other when Gem::Source::Git, Gem::Source::Lock, @@ -29,13 +29,12 @@ def <=> other ## # We don't need to download an installed gem - def download spec, path + def download(spec, path) nil end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.text '[Installed]' end end - diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb index 5ab7a467b57d62..875e992d85ac98 100644 --- a/lib/rubygems/source/local.rb +++ b/lib/rubygems/source/local.rb @@ -15,7 +15,7 @@ def initialize # :nodoc: ## # Local sorts before Gem::Source and after Gem::Source::Installed - def <=> other + def <=>(other) case other when Gem::Source::Installed, Gem::Source::Lock then @@ -34,7 +34,7 @@ def inspect # :nodoc: "#<%s specs: %p>" % [self.class, keys] end - def load_specs type # :nodoc: + def load_specs(type) # :nodoc: @load_specs_names[type] ||= begin names = [] @@ -78,8 +78,8 @@ def load_specs type # :nodoc: end end - def find_gem gem_name, version = Gem::Requirement.default, # :nodoc: - prerelease = false + def find_gem(gem_name, version = Gem::Requirement.default, # :nodoc: + prerelease = false) load_specs :complete found = [] @@ -101,7 +101,7 @@ def find_gem gem_name, version = Gem::Requirement.default, # :nodoc: found.max_by { |s| s.version } end - def fetch_spec name # :nodoc: + def fetch_spec(name) # :nodoc: load_specs :complete if data = @specs[name] @@ -111,7 +111,7 @@ def fetch_spec name # :nodoc: end end - def download spec, cache_dir = nil # :nodoc: + def download(spec, cache_dir = nil) # :nodoc: load_specs :complete @specs.each do |name, data| @@ -121,7 +121,7 @@ def download spec, cache_dir = nil # :nodoc: raise Gem::Exception, "Unable to find file for '#{spec.full_name}'" end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[Local gems:', ']' do q.breakable q.seplist @specs.keys do |v| diff --git a/lib/rubygems/source/lock.rb b/lib/rubygems/source/lock.rb index 59717be2c0bed6..3b3f491750efce 100644 --- a/lib/rubygems/source/lock.rb +++ b/lib/rubygems/source/lock.rb @@ -15,11 +15,11 @@ class Gem::Source::Lock < Gem::Source # Creates a new Lock source that wraps +source+ and moves it earlier in the # sort list. - def initialize source + def initialize(source) @wrapped = source end - def <=> other # :nodoc: + def <=>(other) # :nodoc: case other when Gem::Source::Lock then @wrapped <=> other.wrapped @@ -30,7 +30,7 @@ def <=> other # :nodoc: end end - def == other # :nodoc: + def ==(other) # :nodoc: 0 == (self <=> other) end @@ -41,7 +41,7 @@ def hash # :nodoc: ## # Delegates to the wrapped source's fetch_spec method. - def fetch_spec name_tuple + def fetch_spec(name_tuple) @wrapped.fetch_spec name_tuple end diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb index 459c803e1aa8ae..a22772b9c0bfe9 100644 --- a/lib/rubygems/source/specific_file.rb +++ b/lib/rubygems/source/specific_file.rb @@ -27,22 +27,22 @@ def initialize(file) attr_reader :spec - def load_specs *a # :nodoc: + def load_specs(*a) # :nodoc: [@name] end - def fetch_spec name # :nodoc: + def fetch_spec(name) # :nodoc: return @spec if name == @name raise Gem::Exception, "Unable to find '#{name}'" @spec end - def download spec, dir = nil # :nodoc: + def download(spec, dir = nil) # :nodoc: return @path if spec == @spec raise Gem::Exception, "Unable to download '#{spec.full_name}'" end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[SpecificFile:', ']' do q.breakable q.text @path @@ -59,7 +59,7 @@ def pretty_print q # :nodoc: # # Otherwise Gem::Source#<=> is used. - def <=> other + def <=>(other) case other when Gem::Source::SpecificFile then return nil if @spec.name != other.spec.name diff --git a/lib/rubygems/source/vendor.rb b/lib/rubygems/source/vendor.rb index e1b369860749ed..a87fa63331243f 100644 --- a/lib/rubygems/source/vendor.rb +++ b/lib/rubygems/source/vendor.rb @@ -7,11 +7,11 @@ class Gem::Source::Vendor < Gem::Source::Installed ## # Creates a new Vendor source for a gem that was unpacked at +path+. - def initialize path + def initialize(path) @uri = path end - def <=> other + def <=>(other) case other when Gem::Source::Lock then -1 @@ -25,4 +25,3 @@ def <=> other end end - diff --git a/lib/rubygems/source_list.rb b/lib/rubygems/source_list.rb index 66ce4d57ed76c4..83b689f78e2214 100644 --- a/lib/rubygems/source_list.rb +++ b/lib/rubygems/source_list.rb @@ -105,7 +105,7 @@ def empty? @sources.empty? end - def == other # :nodoc: + def ==(other) # :nodoc: to_a == other end @@ -140,7 +140,7 @@ def include?(other) ## # Deletes +source+ from the source list which may be a Gem::Source or a URI. - def delete source + def delete(source) if source.kind_of? Gem::Source @sources.delete source else diff --git a/lib/rubygems/source_local.rb b/lib/rubygems/source_local.rb index 9869158e7d76e9..5107069fd03dff 100644 --- a/lib/rubygems/source_local.rb +++ b/lib/rubygems/source_local.rb @@ -5,4 +5,3 @@ unless Gem::Deprecate.skip Kernel.warn "#{Gem.location_of_caller(3).join(':')}: Warning: Requiring rubygems/source_local is deprecated; please use rubygems/source/local instead." end - diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb index 7c77eeffdb0320..ca901cb8bf13b2 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -54,7 +54,7 @@ def self.fetcher=(fetcher) # :nodoc: # If you need to retrieve specifications from a different +source+, you can # send it as an argument. - def initialize sources = nil + def initialize(sources = nil) @sources = sources || Gem.sources @update_cache = @@ -271,4 +271,3 @@ def tuples_for(source, type, gracefully_ignore=false) # :nodoc: end end - diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 7b2f031d27af66..7eaa509d2b7988 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -114,7 +114,7 @@ class Gem::Specification < Gem::BasicSpecification private_constant :LOAD_CACHE if defined? private_constant - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: # :startdoc: @@ -274,7 +274,7 @@ def files # # spec.authors = ['John Jones', 'Mary Smith'] - def authors= value + def authors=(value) @authors = Array(value).flatten.grep(String) end @@ -342,7 +342,7 @@ def authors= value # Usage: # spec.license = 'MIT' - def license=o + def license=(o) self.licenses = [o] end @@ -359,7 +359,7 @@ def license=o # Usage: # spec.licenses = ['MIT', 'GPL-2.0'] - def licenses= licenses + def licenses=(licenses) @licenses = Array licenses end @@ -406,7 +406,7 @@ def licenses= licenses # # spec.author = 'John Jones' - def author= o + def author=(o) self.authors = [o] end @@ -457,9 +457,9 @@ def author= o # # spec.platform = Gem::Platform.local - def platform= platform + def platform=(platform) if @original_platform.nil? or - @original_platform == Gem::Platform::RUBY then + @original_platform == Gem::Platform::RUBY @original_platform = platform end @@ -625,7 +625,7 @@ def installed_by_version # :nodoc: # Sets the version of RubyGems that installed this gem. See also # #installed_by_version. - def installed_by_version= version # :nodoc: + def installed_by_version=(version) # :nodoc: @installed_by_version = Gem::Version.new version end @@ -650,9 +650,7 @@ def rdoc_options # ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.4.0] # # # - # Because patch-level is taken into account, be very careful specifying using - # `<=`: `<= 2.2.2` will not match any patch-level of 2.2.2 after the `p0` - # release. It is much safer to specify `< 2.2.3` instead + # Prereleases can also be specified. # # Usage: # @@ -662,17 +660,17 @@ def rdoc_options # # Only with ruby 2.0.x # spec.required_ruby_version = '~> 2.0' # - # # Only with ruby between 2.2.0 and 2.2.2 - # spec.required_ruby_version = ['>= 2.2.0', '< 2.2.3'] + # # Only prereleases or final releases after 2.6.0.preview2 + # spec.required_ruby_version = '> 2.6.0.preview2' - def required_ruby_version= req + def required_ruby_version=(req) @required_ruby_version = Gem::Requirement.create req end ## # The RubyGems version required by this gem - def required_rubygems_version= req + def required_rubygems_version=(req) @required_rubygems_version = Gem::Requirement.create req end @@ -697,7 +695,7 @@ def requirements # spec.test_files = Dir.glob('test/tc_*.rb') # spec.test_files = ['tests/test-suite.rb'] - def test_files= files # :nodoc: + def test_files=(files) # :nodoc: @test_files = Array files end @@ -747,7 +745,7 @@ def test_files= files # :nodoc: attr_accessor :specification_version def self._all # :nodoc: - unless defined?(@@all) && @@all then + unless defined?(@@all) && @@all @@all = stubs.map(&:to_spec) if @@all.any?(&:nil?) # TODO: remove once we're happy raise "pid: #{$$} nil spec! included in #{stubs.inspect}" @@ -774,12 +772,12 @@ def self.each_gemspec(dirs) # :nodoc: end end - def self.gemspec_stubs_in dir, pattern + def self.gemspec_stubs_in(dir, pattern) Gem::Util.glob_files_in_dir(pattern, dir).map { |path| yield path }.select(&:valid?) end private_class_method :gemspec_stubs_in - def self.default_stubs pattern + def self.default_stubs(pattern) base_dir = Gem.default_dir gems_dir = File.join base_dir, "gems" gemspec_stubs_in(default_specifications_dir, pattern) do |path| @@ -788,7 +786,7 @@ def self.default_stubs pattern end private_class_method :default_stubs - def self.installed_stubs dirs, pattern + def self.installed_stubs(dirs, pattern) map_stubs(dirs, pattern) do |path, base_dir, gems_dir| Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir) end @@ -809,7 +807,7 @@ def self.uniq_by(list, &block) # :nodoc: end private_class_method :uniq_by - def self.sort_by! list, &block + def self.sort_by!(list, &block) list.sort_by!(&block) end private_class_method :sort_by! @@ -842,7 +840,7 @@ def self.stubs # Returns a Gem::StubSpecification for installed gem named +name+ # only returns stubs that match Gem.platforms - def self.stubs_for name + def self.stubs_for(name) if @@stubs @@stubs_by_name[name] || [] else @@ -880,7 +878,7 @@ def self.load_defaults # Adds +spec+ to the known specifications, keeping the collection # properly sorted. - def self.add_spec spec + def self.add_spec(spec) warn "Gem::Specification.add_spec is deprecated and will be removed in RubyGems 3.0" unless Gem::Deprecate.skip # TODO: find all extraneous adds # puts @@ -905,7 +903,7 @@ def self.add_spec spec ## # Adds multiple specs to the known specifications. - def self.add_specs *specs + def self.add_specs(*specs) warn "Gem::Specification.add_specs is deprecated and will be removed in RubyGems 3.0" unless Gem::Deprecate.skip raise "nil spec!" if specs.any?(&:nil?) # TODO: remove once we're happy @@ -943,7 +941,7 @@ def self.all # # -- wilsonb - def self.all= specs + def self.all=(specs) raise "nil spec!" if specs.any?(&:nil?) # TODO: remove once we're happy @@stubs_by_name = specs.group_by(&:name) @@all = @@stubs = specs @@ -987,7 +985,7 @@ def self.dirs # Set the directories that Specification uses to find specs. Setting # this resets the list of known specs. - def self.dirs= dirs + def self.dirs=(dirs) self.reset @@dirs = Array(dirs).map { |dir| File.join dir, "specifications" } @@ -1010,7 +1008,7 @@ def self.each ## # Returns every spec that matches +name+ and optional +requirements+. - def self.find_all_by_name name, *requirements + def self.find_all_by_name(name, *requirements) requirements = Gem::Requirement.default if requirements.empty? # TODO: maybe try: find_all { |s| spec === dep } @@ -1029,7 +1027,7 @@ def self.find_all_by_full_name(full_name) # Find the best specification matching a +name+ and +requirements+. Raises # if the dependency doesn't resolve to a valid specification. - def self.find_by_name name, *requirements + def self.find_by_name(name, *requirements) requirements = Gem::Requirement.default if requirements.empty? # TODO: maybe try: find { |s| spec === dep } @@ -1040,7 +1038,7 @@ def self.find_by_name name, *requirements ## # Return the best specification that contains the file matching +path+. - def self.find_by_path path + def self.find_by_path(path) path = path.dup.freeze spec = @@spec_with_requirable_file[path] ||= (stubs.find { |s| next unless Gem::BundlerVersionFinder.compatible?(s) @@ -1053,7 +1051,7 @@ def self.find_by_path path # Return the best specification that contains the file matching +path+ # amongst the specs that are not activated. - def self.find_inactive_by_path path + def self.find_inactive_by_path(path) stub = stubs.find { |s| next if s.activated? next unless Gem::BundlerVersionFinder.compatible?(s) @@ -1062,7 +1060,7 @@ def self.find_inactive_by_path path stub && stub.to_spec end - def self.find_active_stub_by_path path + def self.find_active_stub_by_path(path) stub = @@active_stub_with_requirable_file[path] ||= (stubs.find { |s| s.activated? and s.contains_requirable_file? path } || NOT_FOUND) @@ -1072,7 +1070,7 @@ def self.find_active_stub_by_path path ## # Return currently unresolved specs that contain the file matching +path+. - def self.find_in_unresolved path + def self.find_in_unresolved(path) # TODO: do we need these?? Kill it specs = unresolved_deps.values.map { |dep| dep.to_specs }.flatten @@ -1083,7 +1081,7 @@ def self.find_in_unresolved path # Search through all unresolved deps and sub-dependencies and return # specs that contain the file matching +path+. - def self.find_in_unresolved_tree path + def self.find_in_unresolved_tree(path) specs = unresolved_deps.values.map { |dep| dep.to_specs }.flatten specs.each do |spec| @@ -1113,11 +1111,11 @@ def self.from_yaml(input) input = normalize_yaml_input input spec = Gem::SafeYAML.safe_load input - if spec && spec.class == FalseClass then + if spec && spec.class == FalseClass raise Gem::EndOfYAMLException end - unless Gem::Specification === spec then + unless Gem::Specification === spec raise Gem::Exception, "YAML data doesn't evaluate to gem specification" end @@ -1131,11 +1129,11 @@ def self.from_yaml(input) # Return the latest specs, optionally including prerelease specs if # +prerelease+ is true. - def self.latest_specs prerelease = false + def self.latest_specs(prerelease = false) _latest_specs Gem::Specification._all, prerelease end - def self._latest_specs specs, prerelease = false # :nodoc: + def self._latest_specs(specs, prerelease = false) # :nodoc: result = Hash.new { |h,k| h[k] = {} } native = {} @@ -1155,7 +1153,7 @@ def self._latest_specs specs, prerelease = false # :nodoc: ## # Loads Ruby format gemspec from +file+. - def self.load file + def self.load(file) return unless file _spec = LOAD_CACHE[file] @@ -1251,7 +1249,7 @@ def self.outdated_and_latest_version ## # Removes +spec+ from the known specs. - def self.remove_spec spec + def self.remove_spec(spec) warn "Gem::Specification.remove_spec is deprecated and will be removed in RubyGems 3.0" unless Gem::Deprecate.skip _all.delete spec stubs.delete_if { |s| s.full_name == spec.full_name } @@ -1287,7 +1285,7 @@ def self.reset @@active_stub_with_requirable_file = {} _clear_load_cache unresolved = unresolved_deps - unless unresolved.empty? then + unless unresolved.empty? w = "W" + "ARN" warn "#{w}: Unresolved or ambigious specs during Gem::Specification.reset:" unresolved.values.each do |dep| @@ -1322,7 +1320,7 @@ def self._load(str) current_version = CURRENT_SPECIFICATION_VERSION - field_count = if spec.specification_version > current_version then + field_count = if spec.specification_version > current_version spec.instance_variable_set :@specification_version, current_version MARSHAL_FIELDS[current_version] @@ -1330,7 +1328,7 @@ def self._load(str) MARSHAL_FIELDS[spec.specification_version] end - if array.size < field_count then + if array.size < field_count raise TypeError, "invalid Gem::Specification format #{array.inspect}" end @@ -1369,7 +1367,7 @@ def <=>(other) # :nodoc: sort_obj <=> other.sort_obj end - def == other # :nodoc: + def ==(other) # :nodoc: self.class === other && name == other.name && version == other.version && @@ -1414,7 +1412,7 @@ def _dump(limit) def activate other = Gem.loaded_specs[self.name] - if other then + if other check_version_conflict other return false end @@ -1452,7 +1450,7 @@ def activate_dependencies specs = spec_dep.to_specs - if specs.size == 1 then + if specs.size == 1 specs.first.activate else name = spec_dep.name @@ -1514,7 +1512,7 @@ def sanitize_string(string) def add_bindir(executables) return nil if executables.nil? - if @bindir then + if @bindir Array(executables).map { |e| File.join(@bindir, e) } else executables @@ -1529,7 +1527,7 @@ def add_bindir(executables) # :development. def add_dependency_with_type(dependency, type, requirements) - requirements = if requirements.empty? then + requirements = if requirements.empty? Gem::Requirement.default else requirements.flatten @@ -1558,7 +1556,7 @@ def add_self_to_load_path # gem directories must come after -I and ENV['RUBYLIB'] insert_index = Gem.load_path_insert_index - if insert_index then + if insert_index # gem directories must come after -I and ENV['RUBYLIB'] $LOAD_PATH.insert(insert_index, *paths) else @@ -1596,7 +1594,7 @@ def bin_dir ## # Returns the full path to an executable named +name+ in this gem. - def bin_file name + def bin_file(name) File.join bin_dir, name end @@ -1662,16 +1660,6 @@ def build_info_file File.join build_info_dir, "#{full_name}.info" end - ## - # Used to detect if the gem is bundled in older version of Ruby, but not - # detectable as default gem (see BasicSpecification#default_gem?). - - def bundled_gem_in_old_ruby? - !default_gem? && - RUBY_VERSION < "2.0.0" && - summary == "This #{name} is bundled with Ruby" - end - ## # Returns the full path to the cache directory containing this # spec's cached gem. @@ -1719,7 +1707,7 @@ def conficts_when_loaded_with?(list_of_specs) # :nodoc: def has_conflicts? return true unless Gem.env_requirement(name).satisfied_by?(version) self.dependencies.any? { |dep| - if dep.runtime? then + if dep.runtime? spec = Gem.loaded_specs[dep.name] spec and not spec.satisfies_requirement? dep else @@ -1749,26 +1737,26 @@ def DateLike.===(obj) # :nodoc: /\A (\d{4})-(\d{2})-(\d{2}) (\s+ \d{2}:\d{2}:\d{2}\.\d+ \s* (Z | [-+]\d\d:\d\d) )? - \Z/x + \Z/x.freeze ## # The date this gem was created # # DO NOT set this, it is set automatically when the gem is packaged. - def date= date + def date=(date) # We want to end up with a Time object with one-day resolution. # This is the cleanest, most-readable, faster-than-using-Date # way to do it. @date = case date when String then - if DateTimeFormat =~ date then + if DateTimeFormat =~ date Time.utc($1.to_i, $2.to_i, $3.to_i) # Workaround for where the date format output from psych isn't # parsed as a Time object by syck and thus comes through as a # string. - elsif /\A(\d{4})-(\d{2})-(\d{2}) \d{2}:\d{2}:\d{2}\.\d+?Z\z/ =~ date then + elsif /\A(\d{4})-(\d{2})-(\d{2}) \d{2}:\d{2}:\d{2}\.\d+?Z\z/ =~ date Time.utc($1.to_i, $2.to_i, $3.to_i) else raise(Gem::InvalidSpecificationException, @@ -1802,7 +1790,7 @@ def default_executable # :nodoc: ## # The default value for specification attribute +name+ - def default_value name + def default_value(name) @@default_value[name] end @@ -1826,7 +1814,7 @@ def dependent_gems out = [] Gem::Specification.each do |spec| spec.dependencies.each do |dep| - if self.satisfies_requirement?(dep) then + if self.satisfies_requirement?(dep) sats = [] find_all_satisfiers(dep) do |sat| sats << sat @@ -1848,7 +1836,7 @@ def dependent_specs ## # A detailed description of this gem. See also #summary - def description= str + def description=(str) @description = str.to_s end @@ -1867,17 +1855,17 @@ def development_dependencies # # spec.doc_dir 'ri' # => "/path/to/gem_repo/doc/a-1/ri" - def doc_dir type = nil + def doc_dir(type = nil) @doc_dir ||= File.join base_dir, 'doc', full_name - if type then + if type File.join @doc_dir, type else @doc_dir end end - def encode_with coder # :nodoc: + def encode_with(coder) # :nodoc: mark_version coder.add 'name', @name @@ -1898,7 +1886,7 @@ def encode_with coder # :nodoc: end end - def eql? other # :nodoc: + def eql?(other) # :nodoc: self.class === other && same_attributes?(other) end @@ -1912,7 +1900,7 @@ def executable ## # Singular accessor for #executables - def executable=o + def executable=(o) self.executables = [o] end @@ -1920,7 +1908,7 @@ def executable=o # Sets executables to +value+, ensuring it is an array. Don't # use this, push onto the array instead. - def executables= value + def executables=(value) # TODO: warn about setting instead of pushing @executables = Array(value) end @@ -1929,7 +1917,7 @@ def executables= value # Sets extensions to +extensions+, ensuring it is an array. Don't # use this, push onto the array instead. - def extensions= extensions + def extensions=(extensions) # TODO: warn about setting instead of pushing @extensions = Array extensions end @@ -1938,7 +1926,7 @@ def extensions= extensions # Sets extra_rdoc_files to +files+, ensuring it is an array. Don't # use this, push onto the array instead. - def extra_rdoc_files= files + def extra_rdoc_files=(files) # TODO: warn about setting instead of pushing @extra_rdoc_files = Array files end @@ -1955,14 +1943,14 @@ def file_name ## # Sets files to +files+, ensuring it is an array. - def files= files + def files=(files) @files = Array files end ## # Finds all gems that satisfy +dep+ - def find_all_satisfiers dep + def find_all_satisfiers(dep) Gem::Specification.each do |spec| yield spec if spec.satisfies_requirement? dep end @@ -2013,7 +2001,7 @@ def has_rdoc # :nodoc: # # Formerly used to indicate this gem was RDoc-capable. - def has_rdoc= ignored # :nodoc: + def has_rdoc=(ignored) # :nodoc: @has_rdoc = true end deprecate :has_rdoc=, :none, 2018, 12 @@ -2036,7 +2024,7 @@ def hash # :nodoc: name.hash ^ version.hash end - def init_with coder # :nodoc: + def init_with(coder) # :nodoc: @installed_by_version ||= nil yaml_initialize coder.tag, coder.map end @@ -2060,7 +2048,7 @@ def set_not_nil_attributes_to_default_values # and yields itself for further initialization. Optionally takes +name+ and # +version+. - def initialize name = nil, version = nil + def initialize(name = nil, version = nil) super() @gems_dir = nil @base_dir = nil @@ -2084,14 +2072,14 @@ def initialize name = nil, version = nil ## # Duplicates array_attributes from +other_spec+ so state isn't shared. - def initialize_copy other_spec + def initialize_copy(other_spec) self.class.array_attributes.each do |name| name = :"@#{name}" next unless other_spec.instance_variable_defined? name begin val = other_spec.instance_variable_get(name) - if val then + if val instance_variable_set name, val.dup elsif Gem.configuration.really_verbose warn "WARNING: #{full_name} has an invalid nil value for #{name}" @@ -2108,7 +2096,7 @@ def initialize_copy other_spec def base_dir return Gem.dir unless loaded_from - @base_dir ||= if default_gem? then + @base_dir ||= if default_gem? File.dirname File.dirname File.dirname loaded_from else File.dirname File.dirname loaded_from @@ -2184,7 +2172,7 @@ def mark_version def method_missing(sym, *a, &b) # :nodoc: if @specification_version > CURRENT_SPECIFICATION_VERSION and - sym.to_s =~ /=$/ then + sym.to_s =~ /=$/ warn "ignoring #{sym} loading #{full_name}" if $DEBUG else super @@ -2211,7 +2199,7 @@ def missing_extensions? # file list. def normalize - if defined?(@extra_rdoc_files) and @extra_rdoc_files then + if defined?(@extra_rdoc_files) and @extra_rdoc_files @extra_rdoc_files.uniq! @files ||= [] @files.concat(@extra_rdoc_files) @@ -2236,7 +2224,7 @@ def name_tuple # platform. For use with legacy gems. def original_name # :nodoc: - if platform == Gem::Platform::RUBY or platform.nil? then + if platform == Gem::Platform::RUBY or platform.nil? "#{@name}-#{@version}" else "#{@name}-#{@version}-#{@original_platform}" @@ -2269,11 +2257,11 @@ def pretty_print(q) # :nodoc: attributes.each do |attr_name| current_value = self.send attr_name if current_value != default_value(attr_name) or - self.class.required_attribute? attr_name then + self.class.required_attribute? attr_name q.text "s.#{attr_name} = " - if attr_name == :date then + if attr_name == :date current_value = current_value.utc q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})" @@ -2291,7 +2279,7 @@ def pretty_print(q) # :nodoc: # Raise an exception if the version of this spec conflicts with the one # that is already loaded (+other+) - def check_version_conflict other # :nodoc: + def check_version_conflict(other) # :nodoc: return if self.version == other.version # This gem is already loaded. If the currently loaded gem is not in the @@ -2312,7 +2300,7 @@ def check_version_conflict other # :nodoc: # Check the spec for possible conflicts and freak out if there are any. def raise_if_conflicts # :nodoc: - if has_conflicts? then + if has_conflicts? raise Gem::ConflictError.new self, conflicts end end @@ -2321,7 +2309,7 @@ def raise_if_conflicts # :nodoc: # Sets rdoc_options to +value+, ensuring it is an array. Don't # use this, push onto the array instead. - def rdoc_options= options + def rdoc_options=(options) # TODO: warn about setting instead of pushing @rdoc_options = Array options end @@ -2336,7 +2324,7 @@ def require_path ## # Singular accessor for #require_paths - def require_path= path + def require_path=(path) self.require_paths = Array(path) end @@ -2344,12 +2332,12 @@ def require_path= path # Set requirements to +req+, ensuring it is an array. Don't # use this, push onto the array instead. - def requirements= req + def requirements=(req) # TODO: warn about setting instead of pushing @requirements = Array req end - def respond_to_missing? m, include_private = false # :nodoc: + def respond_to_missing?(m, include_private = false) # :nodoc: false end @@ -2396,7 +2384,7 @@ def runtime_dependencies ## # True if this gem has the same attributes as +other+. - def same_attributes? spec + def same_attributes?(spec) @@attributes.all? { |name, default| self.send(name) == spec.send(name) } end @@ -2405,7 +2393,7 @@ def same_attributes? spec ## # Checks if this specification meets the requirement of +dependency+. - def satisfies_requirement? dependency + def satisfies_requirement?(dependency) return @name == dependency.name && dependency.requirement.satisfied_by?(@version) end @@ -2452,7 +2440,7 @@ def spec_name ## # A short summary of this gem's description. - def summary= str + def summary=(str) @summary = str.to_s.strip. gsub(/(\w-)\n[ \t]*(\w)/, '\1\2').gsub(/\n[ \t]*/, " ") # so. weird. end @@ -2467,7 +2455,7 @@ def test_file # :nodoc: ## # Singular mutator for #test_files - def test_file= file # :nodoc: + def test_file=(file) # :nodoc: self.test_files = [file] end @@ -2479,11 +2467,11 @@ def test_files # :nodoc: # Handle the possibility that we have @test_suite_file but not # @test_files. This will happen when an old gem is loaded via # YAML. - if defined? @test_suite_file then + if defined? @test_suite_file @test_files = [@test_suite_file].flatten @test_suite_file = nil end - if defined?(@test_files) and @test_files then + if defined?(@test_files) and @test_files @test_files else @test_files = [] @@ -2507,7 +2495,7 @@ def to_ruby result << " s.name = #{ruby_code name}" result << " s.version = #{ruby_code version}" - unless platform.nil? or platform == Gem::Platform::RUBY then + unless platform.nil? or platform == Gem::Platform::RUBY result << " s.platform = #{ruby_code original_platform}" end result << "" @@ -2535,17 +2523,17 @@ def to_ruby next if handled.include? attr_name current_value = self.send(attr_name) if current_value != default_value(attr_name) or - self.class.required_attribute? attr_name then + self.class.required_attribute? attr_name result << " s.#{attr_name} = #{ruby_code current_value}" end end - if @installed_by_version then + if @installed_by_version result << nil result << " s.installed_by_version = \"#{Gem::VERSION}\" if s.respond_to? :installed_by_version" end - unless dependencies.empty? then + unless dependencies.empty? result << nil result << " if s.respond_to? :specification_version then" result << " s.specification_version = #{specification_version}" @@ -2627,7 +2615,7 @@ def to_yaml(opts = {}) # :nodoc: # Recursively walk dependencies of this spec, executing the +block+ for each # hop. - def traverse trail = [], visited = {}, &block + def traverse(trail = [], visited = {}, &block) trail.push(self) begin dependencies.each do |dep| @@ -2660,7 +2648,7 @@ def traverse trail = [], visited = {}, &block # Raises InvalidSpecificationException if the spec does not pass the # checks.. - def validate packaging = true, strict = false + def validate(packaging = true, strict = false) require 'rubygems/user_interaction' extend Gem::UserInteraction normalize @@ -2701,7 +2689,7 @@ def validate_permissions # required_rubygems_version if +version+ indicates it is a # prerelease. - def version= version + def version=(version) @version = Gem::Version.create(version) # skip to set required_ruby_version when pre-released rubygems. # It caused to raise CircularDependencyError diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index e28408a5a55e23..bc552f82875120 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -2,11 +2,11 @@ require 'uri' class Gem::SpecificationPolicy < SimpleDelegator - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: - SPECIAL_CHARACTERS = /\A[#{Regexp.escape('.-_')}]+/ # :nodoc: + SPECIAL_CHARACTERS = /\A[#{Regexp.escape('.-_')}]+/.freeze # :nodoc: - VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z} # :nodoc: + VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}.freeze # :nodoc: METADATA_LINK_KEYS = %w[ bug_tracker_uri @@ -88,29 +88,29 @@ def validate(strict = false) # Implementation for Specification#validate_metadata def validate_metadata - unless Hash === metadata then + unless Hash === metadata error 'metadata must be a hash' end metadata.each do |key, value| - if !key.kind_of?(String) then + if !key.kind_of?(String) error "metadata keys must be a String" end - if key.size > 128 then + if key.size > 128 error "metadata key too large (#{key.size} > 128)" end - if !value.kind_of?(String) then + if !value.kind_of?(String) error "metadata values must be a String" end - if value.size > 1024 then + if value.size > 1024 error "metadata value too large (#{value.size} > 1024)" end - if METADATA_LINK_KEYS.include? key then - if value !~ VALID_URI_PATTERN then + if METADATA_LINK_KEYS.include? key + if value !~ VALID_URI_PATTERN error "metadata['#{key}'] has invalid link: #{value.inspect}" end end @@ -127,7 +127,7 @@ def validate_dependencies # :nodoc: error_messages = [] warning_messages = [] dependencies.each do |dep| - if prev = seen[dep.type][dep.name] then + if prev = seen[dep.type][dep.name] error_messages << <<-MESSAGE duplicate dependency on #{dep}, (#{prev.requirement}) use: add_#{dep.type}_dependency '#{dep.name}', '#{dep.requirement}', '#{prev.requirement}' @@ -147,14 +147,14 @@ def validate_dependencies # :nodoc: not version.prerelease? and (op == '>' or op == '>=') end - if open_ended then + if open_ended op, dep_version = dep.requirement.requirements.first base = dep_version.segments.first 2 - bugfix = if op == '>' then + bugfix = if op == '>' ", '> #{dep_version}'" - elsif op == '>=' and base != dep_version.segments then + elsif op == '>=' and base != dep_version.segments ", '>= #{dep_version}'" end @@ -165,10 +165,10 @@ def validate_dependencies # :nodoc: WARNING end end - if error_messages.any? then + if error_messages.any? error error_messages.join end - if warning_messages.any? then + if warning_messages.any? warning_messages.each { |warning_message| warning warning_message } end end @@ -214,7 +214,7 @@ def validate_rubygems_version def validate_required_attributes Gem::Specification.required_attributes.each do |symbol| - unless send symbol then + unless send symbol error "missing value for attribute #{symbol}" end end @@ -242,7 +242,7 @@ def validate_non_files return unless packaging non_files = files.reject {|x| File.file?(x) || File.symlink?(x)} - unless non_files.empty? then + unless non_files.empty? error "[\"#{non_files.join "\", \""}\"] are not files" end end @@ -261,7 +261,7 @@ def validate_specification_version def validate_platform case platform - when Gem::Platform, Gem::Platform::RUBY then # ok + when Gem::Platform, Gem::Platform::RUBY # ok else error "invalid platform #{platform.inspect}, see Gem::Platform" end @@ -282,7 +282,7 @@ def validate_array_attribute(field) String end - unless Array === val and val.all? {|x| x.kind_of?(klass)} then + unless Array === val and val.all? {|x| x.kind_of?(klass)} raise(Gem::InvalidSpecificationException, "#{field} must be an Array of #{klass}") end @@ -296,11 +296,11 @@ def validate_authors_field def validate_licenses licenses.each { |license| - if license.length > 64 then + if license.length > 64 error "each license must be 64 characters or less" end - if !Gem::Licenses.match?(license) then + if !Gem::Licenses.match?(license) suggestions = Gem::Licenses.suggestions(license) message = <<-warning license value '#{license}' is invalid. Use a license identifier from @@ -318,23 +318,23 @@ def validate_licenses end LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, '') - LAZY_PATTERN = /FI XME|TO DO/x - HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i + LAZY_PATTERN = /FI XME|TO DO/x.freeze + HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i.freeze def validate_lazy_metadata - unless authors.grep(LAZY_PATTERN).empty? then + unless authors.grep(LAZY_PATTERN).empty? error "#{LAZY} is not an author" end - unless Array(email).grep(LAZY_PATTERN).empty? then + unless Array(email).grep(LAZY_PATTERN).empty? error "#{LAZY} is not an email" end - if description =~ LAZY_PATTERN then + if description =~ LAZY_PATTERN error "#{LAZY} is not a description" end - if summary =~ LAZY_PATTERN then + if summary =~ LAZY_PATTERN error "#{LAZY} is not a summary" end @@ -356,7 +356,7 @@ def validate_values validate_attribute_present(attribute) end - if description == summary then + if description == summary warning "description and summary are identical" end @@ -384,13 +384,13 @@ def validate_shebang_line_in(executable) warning "#{executable_path} is missing #! line" end - def warning statement # :nodoc: + def warning(statement) # :nodoc: @warnings += 1 alert_warning statement end - def error statement # :nodoc: + def error(statement) # :nodoc: raise Gem::InvalidSpecificationException, statement ensure alert_warning help_text diff --git a/lib/rubygems/stub_specification.rb b/lib/rubygems/stub_specification.rb index 8dd6df17e319ac..022da9185d971f 100644 --- a/lib/rubygems/stub_specification.rb +++ b/lib/rubygems/stub_specification.rb @@ -32,7 +32,7 @@ class StubLine # :nodoc: all 'lib' => ['lib'].freeze }.freeze - def initialize data, extensions + def initialize(data, extensions) parts = data[PREFIX.length..-1].split(" ".freeze, 4) @name = parts[0].freeze @version = if Gem::Version.correct?(parts[1]) @@ -56,17 +56,17 @@ def initialize data, extensions end end - def self.default_gemspec_stub filename, base_dir, gems_dir + def self.default_gemspec_stub(filename, base_dir, gems_dir) new filename, base_dir, gems_dir, true end - def self.gemspec_stub filename, base_dir, gems_dir + def self.gemspec_stub(filename, base_dir, gems_dir) new filename, base_dir, gems_dir, false end attr_reader :base_dir, :gems_dir - def initialize filename, base_dir, gems_dir, default_gem + def initialize(filename, base_dir, gems_dir, default_gem) super() filename.untaint @@ -115,7 +115,7 @@ def data begin file.readline # discard encoding line stubline = file.readline.chomp - if stubline.start_with?(PREFIX) then + if stubline.start_with?(PREFIX) extensions = if /\A#{PREFIX}/ =~ file.readline.chomp $'.split "\0" else @@ -185,7 +185,7 @@ def full_name # The full Gem::Specification for this gem, loaded from evalling its gemspec def to_spec - @spec ||= if @data then + @spec ||= if @data loaded = Gem.loaded_specs[name] loaded if loaded && loaded.version == version end diff --git a/lib/rubygems/test_case.rb b/lib/rubygems/test_case.rb index 718571ea5789d1..7648ffdc8471c0 100644 --- a/lib/rubygems/test_case.rb +++ b/lib/rubygems/test_case.rb @@ -84,7 +84,7 @@ def self.win_platform=(val) # Allows setting path to Ruby. This method is available when requiring # 'rubygems/test_case' - def self.ruby= ruby + def self.ruby=(ruby) @ruby = ruby end @@ -115,7 +115,7 @@ class Gem::TestCase < (defined?(Minitest::Test) ? Minitest::Test : MiniTest::Uni attr_accessor :uri # :nodoc: - def assert_activate expected, *specs + def assert_activate(expected, *specs) specs.each do |spec| case spec when String then @@ -133,7 +133,7 @@ def assert_activate expected, *specs end # TODO: move to minitest - def assert_path_exists path, msg = nil + def assert_path_exists(path, msg = nil) msg = message(msg) { "Expected path '#{path}' to exist" } assert File.exist?(path), msg end @@ -142,13 +142,13 @@ def assert_path_exists path, msg = nil # Sets the ENABLE_SHARED entry in RbConfig::CONFIG to +value+ and restores # the original value when the block ends - def enable_shared value + def enable_shared(value) enable_shared = RbConfig::CONFIG['ENABLE_SHARED'] RbConfig::CONFIG['ENABLE_SHARED'] = value yield ensure - if enable_shared then + if enable_shared RbConfig::CONFIG['enable_shared'] = enable_shared else RbConfig::CONFIG.delete 'enable_shared' @@ -156,7 +156,7 @@ def enable_shared value end # TODO: move to minitest - def refute_path_exists path, msg = nil + def refute_path_exists(path, msg = nil) msg = message(msg) { "Expected path '#{path}' to not exist" } refute File.exist?(path), msg end @@ -270,7 +270,7 @@ def setup tmpdir = File.expand_path Dir.tmpdir tmpdir.untaint - if ENV['KEEP_FILES'] then + if ENV['KEEP_FILES'] @tempdir = File.join(tmpdir, "test_rubygems_#{$$}.#{Time.now.to_i}") else @tempdir = File.join(tmpdir, "test_rubygems_#{$$}") @@ -304,7 +304,7 @@ def setup @userhome = File.join @tempdir, 'userhome' ENV["GEM_SPEC_CACHE"] = File.join @tempdir, 'spec_cache' - @orig_ruby = if ENV['RUBY'] then + @orig_ruby = if ENV['RUBY'] ruby = Gem.ruby Gem.ruby = ENV['RUBY'] ruby @@ -426,7 +426,7 @@ def teardown end RbConfig::CONFIG['arch'] = @orig_arch - if defined? Gem::RemoteFetcher then + if defined? Gem::RemoteFetcher Gem::RemoteFetcher.fetcher = nil end @@ -449,7 +449,7 @@ def teardown Gem.ruby = @orig_ruby if @orig_ruby - if @orig_ENV_HOME then + if @orig_ENV_HOME ENV['HOME'] = @orig_ENV_HOME else ENV.delete 'HOME' @@ -513,7 +513,7 @@ def common_installer_teardown # # Yields the +specification+ to the block, if given - def git_gem name = 'a', version = 1 + def git_gem(name = 'a', version = 1) have_git? directory = File.join 'git', name @@ -534,7 +534,7 @@ def git_gem name = 'a', version = 1 head = nil Dir.chdir directory do - unless File.exist? '.git' then + unless File.exist? '.git' system @git, 'init', '--quiet' system @git, 'config', 'user.name', 'RubyGems Tests' system @git, 'config', 'user.email', 'rubygems@example' @@ -557,7 +557,7 @@ def have_git? skip 'cannot find git executable, use GIT environment variable to set' end - def in_path? executable # :nodoc: + def in_path?(executable) # :nodoc: return true if %r%\A([A-Z]:|/)% =~ executable and File.exist? executable ENV['PATH'].split(File::PATH_SEPARATOR).any? do |directory| @@ -568,12 +568,12 @@ def in_path? executable # :nodoc: ## # Builds and installs the Gem::Specification +spec+ - def install_gem spec, options = {} + def install_gem(spec, options = {}) require 'rubygems/installer' gem = File.join @tempdir, "gems", "#{spec.full_name}.gem" - unless File.exist? gem then + unless File.exist? gem use_ui Gem::MockGemUi.new do Dir.chdir @tempdir do Gem::Package.build spec @@ -589,17 +589,17 @@ def install_gem spec, options = {} ## # Builds and installs the Gem::Specification +spec+ into the user dir - def install_gem_user spec + def install_gem_user(spec) install_gem spec, :user_install => true end ## # Uninstalls the Gem::Specification +spec+ - def uninstall_gem spec + def uninstall_gem(spec) require 'rubygems/uninstaller' Class.new(Gem::Uninstaller) { - def ask_if_ok spec + def ask_if_ok(spec) true end }.new(spec.name, :executables => true, :user_install => true).uninstall @@ -704,7 +704,7 @@ def quick_gem(name, version='2') ## # TODO: remove in RubyGems 4.0 - def quick_spec name, version = '2' # :nodoc: + def quick_spec(name, version = '2') # :nodoc: util_spec name, version end deprecate :quick_spec, :util_spec, 2018, 12 @@ -808,7 +808,7 @@ def save_loaded_features # # TODO: remove in RubyGems 4.0 - def new_spec name, version, deps = nil, *files # :nodoc: + def new_spec(name, version, deps = nil, *files) # :nodoc: require 'rubygems/specification' spec = Gem::Specification.new do |s| @@ -832,7 +832,7 @@ def new_spec name, version, deps = nil, *files # :nodoc: spec.loaded_from = spec.spec_file - unless files.empty? then + unless files.empty? write_file spec.spec_file do |io| io.write spec.to_ruby_for_cache end @@ -872,7 +872,7 @@ def new_default_spec(name, version, deps = nil, *files) # Creates a spec with +name+, +version+. +deps+ can specify the dependency # or a +block+ can be given for full customization of the specification. - def util_spec name, version = 2, deps = nil, *files # :yields: specification + def util_spec(name, version = 2, deps = nil, *files) # :yields: specification raise "deps or block, not both" if deps and block_given? spec = Gem::Specification.new do |s| @@ -890,7 +890,7 @@ def util_spec name, version = 2, deps = nil, *files # :yields: specification yield s if block_given? end - if deps then + if deps # Since Hash#each is unordered in 1.8, sort the keys and iterate that # way so the tests are deterministic on all implementations. deps.keys.sort.each do |n| @@ -898,7 +898,7 @@ def util_spec name, version = 2, deps = nil, *files # :yields: specification end end - unless files.empty? then + unless files.empty? write_file spec.spec_file do |io| io.write spec.to_ruby_for_cache end @@ -926,7 +926,7 @@ def util_gem(name, version, deps = nil, &block) # TODO: deprecate raise "deps or block, not both" if deps and block - if deps then + if deps block = proc do |s| # Since Hash#each is unordered in 1.8, sort # the keys and iterate that way so the tests are @@ -1122,7 +1122,7 @@ def util_setup_spec_fetcher(*specs) end # HACK for test_download_to_cache - unless Gem::RemoteFetcher === @fetcher then + unless Gem::RemoteFetcher === @fetcher v = Gem.marshal_version specs = all.map { |spec| spec.name_tuple } @@ -1161,35 +1161,51 @@ def util_zip(data) Zlib::Deflate.deflate data end - def util_set_RUBY_VERSION(version, patchlevel = nil, revision = nil) + def util_set_RUBY_VERSION(version, patchlevel = nil, revision = nil, description = nil, engine = "ruby", engine_version = nil) if Gem.instance_variables.include? :@ruby_version or - Gem.instance_variables.include? '@ruby_version' then + Gem.instance_variables.include? '@ruby_version' Gem.send :remove_instance_variable, :@ruby_version end - @RUBY_VERSION = RUBY_VERSION - @RUBY_PATCHLEVEL = RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) - @RUBY_REVISION = RUBY_REVISION if defined?(RUBY_REVISION) + @RUBY_VERSION = RUBY_VERSION + @RUBY_PATCHLEVEL = RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) + @RUBY_REVISION = RUBY_REVISION if defined?(RUBY_REVISION) + @RUBY_DESCRIPTION = RUBY_DESCRIPTION if defined?(RUBY_DESCRIPTION) + @RUBY_ENGINE = RUBY_ENGINE + @RUBY_ENGINE_VERSION = RUBY_ENGINE_VERSION if defined?(RUBY_ENGINE_VERSION) - Object.send :remove_const, :RUBY_VERSION - Object.send :remove_const, :RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) - Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) + util_clear_RUBY_VERSION - Object.const_set :RUBY_VERSION, version - Object.const_set :RUBY_PATCHLEVEL, patchlevel if patchlevel - Object.const_set :RUBY_REVISION, revision if revision + Object.const_set :RUBY_VERSION, version + Object.const_set :RUBY_PATCHLEVEL, patchlevel if patchlevel + Object.const_set :RUBY_REVISION, revision if revision + Object.const_set :RUBY_DESCRIPTION, description if description + Object.const_set :RUBY_ENGINE, engine + Object.const_set :RUBY_ENGINE_VERSION, engine_version if engine_version end def util_restore_RUBY_VERSION - Object.send :remove_const, :RUBY_VERSION - Object.send :remove_const, :RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) - Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) + util_clear_RUBY_VERSION - Object.const_set :RUBY_VERSION, @RUBY_VERSION - Object.const_set :RUBY_PATCHLEVEL, @RUBY_PATCHLEVEL if + Object.const_set :RUBY_VERSION, @RUBY_VERSION + Object.const_set :RUBY_PATCHLEVEL, @RUBY_PATCHLEVEL if defined?(@RUBY_PATCHLEVEL) - Object.const_set :RUBY_REVISION, @RUBY_REVISION if + Object.const_set :RUBY_REVISION, @RUBY_REVISION if defined?(@RUBY_REVISION) + Object.const_set :RUBY_DESCRIPTION, @RUBY_DESCRIPTION if + defined?(@RUBY_DESCRIPTION) + Object.const_set :RUBY_ENGINE, @RUBY_ENGINE + Object.const_set :RUBY_ENGINE_VERSION, @RUBY_ENGINE_VERSION if + defined?(@RUBY_ENGINE_VERSION) + end + + def util_clear_RUBY_VERSION + Object.send :remove_const, :RUBY_VERSION + Object.send :remove_const, :RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) + Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) + Object.send :remove_const, :RUBY_DESCRIPTION if defined?(RUBY_DESCRIPTION) + Object.send :remove_const, :RUBY_ENGINE + Object.send :remove_const, :RUBY_ENGINE_VERSION if defined?(RUBY_ENGINE_VERSION) end ## @@ -1319,14 +1335,31 @@ def self.rubybin end end + class << self + # :nodoc: + ## + # Return the join path, with escaping backticks, dollars, and + # double-quotes. Unlike `shellescape`, equal-sign is not escaped. + private + def escape_path(*path) + path = File.join(*path) + if %r'\A[-+:/=@,.\w]+\z' =~ path + path + else + "\"#{path.gsub(/[`$"]/, '\\&')}\"" + end + end + end + @@ruby = rubybin - @@good_rake = "#{rubybin} \"#{File.expand_path('../../../test/rubygems/good_rake.rb', __FILE__)}\"" - @@bad_rake = "#{rubybin} \"#{File.expand_path('../../../test/rubygems/bad_rake.rb', __FILE__)}\"" + gempath = File.expand_path('../../../test/rubygems', __FILE__) + @@good_rake = "#{rubybin} #{escape_path(gempath, 'good_rake.rb')}" + @@bad_rake = "#{rubybin} #{escape_path(gempath, 'bad_rake.rb')}" ## # Construct a new Gem::Dependency. - def dep name, *requirements + def dep(name, *requirements) Gem::Dependency.new name, *requirements end @@ -1335,10 +1368,10 @@ def dep name, *requirements # Gem::Dependency +dep+, a +from_name+ and +from_version+ requesting the # dependency and a +parent+ DependencyRequest - def dependency_request dep, from_name, from_version, parent = nil + def dependency_request(dep, from_name, from_version, parent = nil) remote = Gem::Source.new @uri - unless parent then + unless parent parent_dep = dep from_name, from_version parent = Gem::Resolver::DependencyRequest.new parent_dep, nil end @@ -1353,7 +1386,7 @@ def dependency_request dep, from_name, from_version, parent = nil ## # Constructs a new Gem::Requirement. - def req *requirements + def req(*requirements) return requirements.first if Gem::Requirement === requirements.first Gem::Requirement.create requirements end @@ -1361,7 +1394,7 @@ def req *requirements ## # Constructs a new Gem::Specification. - def spec name, version, &block + def spec(name, version, &block) Gem::Specification.new name, v(version), &block end @@ -1385,7 +1418,7 @@ def spec name, version, &block # end # end - def spec_fetcher repository = @gem_repo + def spec_fetcher(repository = @gem_repo) Gem::TestCase::SpecFetcherSetup.declare self, repository do |spec_fetcher_setup| yield spec_fetcher_setup if block_given? end @@ -1394,7 +1427,7 @@ def spec_fetcher repository = @gem_repo ## # Construct a new Gem::Version. - def v string + def v(string) Gem::Version.create string end @@ -1404,7 +1437,7 @@ def v string # # Yields the +specification+ to the block, if given - def vendor_gem name = 'a', version = 1 + def vendor_gem(name = 'a', version = 1) directory = File.join 'vendor', name FileUtils.mkdir_p directory @@ -1418,7 +1451,7 @@ def vendor_gem name = 'a', version = 1 # # Yields the +specification+ to the block, if given - def save_gemspec name = 'a', version = 1, directory = '.' + def save_gemspec(name = 'a', version = 1, directory = '.') vendor_spec = Gem::Specification.new name, version do |specification| yield specification if block_given? end @@ -1455,7 +1488,7 @@ def initialize(specs) ## # Adds +spec+ to this set. - def add spec + def add(spec) @specs << spec end @@ -1479,7 +1512,7 @@ def find_all(dep) # Loads a Gem::Specification from this set which has the given +name+, # version +ver+, +platform+. The +source+ is ignored. - def load_spec name, ver, platform, source + def load_spec(name, ver, platform, source) dep = Gem::Dependency.new name, ver spec = find_spec dep @@ -1488,14 +1521,14 @@ def load_spec name, ver, platform, source end end - def prefetch reqs # :nodoc: + def prefetch(reqs) # :nodoc: end end ## # Loads certificate named +cert_name+ from test/rubygems/. - def self.load_cert cert_name + def self.load_cert(cert_name) cert_file = cert_path cert_name cert = File.read cert_file @@ -1507,8 +1540,8 @@ def self.load_cert cert_name # Returns the path to the certificate named +cert_name+ from # test/rubygems/. - def self.cert_path cert_name - if 32 == (Time.at(2**32) rescue 32) then + def self.cert_path(cert_name) + if 32 == (Time.at(2**32) rescue 32) cert_file = File.expand_path "../../../test/rubygems/#{cert_name}_cert_32.pem", __FILE__ @@ -1522,7 +1555,7 @@ def self.cert_path cert_name ## # Loads an RSA private key named +key_name+ with +passphrase+ in test/rubygems/ - def self.load_key key_name, passphrase = nil + def self.load_key(key_name, passphrase = nil) key_file = key_path key_name key = File.read key_file @@ -1533,7 +1566,7 @@ def self.load_key key_name, passphrase = nil ## # Returns the path to the key named +key_name+ from test/rubygems - def self.key_path key_name + def self.key_path(key_name) File.expand_path "../../../test/rubygems/#{key_name}_key.pem", __FILE__ end diff --git a/lib/rubygems/test_utilities.rb b/lib/rubygems/test_utilities.rb index 1ec142937a0537..fe7eff8d8c85f4 100644 --- a/lib/rubygems/test_utilities.rb +++ b/lib/rubygems/test_utilities.rb @@ -35,7 +35,7 @@ def initialize def find_data(path) return File.read path.path if URI === path and 'file' == path.scheme - if URI === path and "URI::#{path.scheme.upcase}" != path.class.name then + if URI === path and "URI::#{path.scheme.upcase}" != path.class.name raise ArgumentError, "mismatch for scheme #{path.scheme} and class #{path.class}" end @@ -44,20 +44,20 @@ def find_data(path) @paths << path raise ArgumentError, 'need full URI' unless path =~ %r'^https?://' - unless @data.key? path then + unless @data.key? path raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path) end @data[path] end - def fetch_path path, mtime = nil, head = false + def fetch_path(path, mtime = nil, head = false) data = find_data(path) - if data.respond_to?(:call) then + if data.respond_to?(:call) data.call else - if path.to_s =~ /gz$/ and not data.nil? and not data.empty? then + if path.to_s =~ /gz$/ and not data.nil? and not data.empty? data = Gem::Util.gunzip data end @@ -65,7 +65,7 @@ def fetch_path path, mtime = nil, head = false end end - def cache_update_path uri, path = nil, update = true + def cache_update_path(uri, path = nil, update = true) if data = fetch_path(uri) open(path, 'wb') { |io| io.write data } if path and update data @@ -98,7 +98,7 @@ def request(uri, request_class, last_modified = nil) response end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.group 2, '[FakeFetcher', ']' do q.breakable q.text 'URIs:' @@ -114,7 +114,7 @@ def fetch_size(path) raise ArgumentError, 'need full URI' unless path =~ %r'^http://' - unless @data.key? path then + unless @data.key? path raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path) end @@ -123,9 +123,9 @@ def fetch_size(path) data.respond_to?(:call) ? data.call : data.length end - def download spec, source_uri, install_dir = Gem.dir + def download(spec, source_uri, install_dir = Gem.dir) name = File.basename spec.cache_file - path = if Dir.pwd == install_dir then # see fetch_command + path = if Dir.pwd == install_dir # see fetch_command install_dir else File.join install_dir, "cache" @@ -133,7 +133,7 @@ def download spec, source_uri, install_dir = Gem.dir path = File.join path, name - if source_uri =~ /^http/ then + if source_uri =~ /^http/ File.open(path, "wb") do |f| f.write fetch_path(File.join(source_uri, "gems", name)) end @@ -144,7 +144,7 @@ def download spec, source_uri, install_dir = Gem.dir path end - def download_to_cache dependency + def download_to_cache(dependency) found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency return if found.empty? @@ -187,7 +187,7 @@ class Gem::TestCase::SpecFetcherSetup # Executes a SpecFetcher setup block. Yields an instance then creates the # gems and specifications defined in the instance. - def self.declare test, repository + def self.declare(test, repository) setup = new test, repository yield setup @@ -195,7 +195,7 @@ def self.declare test, repository setup.execute end - def initialize test, repository # :nodoc: + def initialize(test, repository) # :nodoc: @test = test @repository = repository @@ -263,7 +263,7 @@ def execute_operations # :nodoc: # The specification will be yielded before gem creation for customization, # but only the block or the dependencies may be set, not both. - def gem name, version, dependencies = nil, &block + def gem(name, version, dependencies = nil, &block) @operations << [:gem, name, version, dependencies, block] end @@ -274,7 +274,7 @@ def gem name, version, dependencies = nil, &block # The specification will be yielded before gem creation for customization, # but only the block or the dependencies may be set, not both. - def download name, version, dependencies = nil, &block + def download(name, version, dependencies = nil, &block) @operations << [:download, name, version, dependencies, block] end @@ -293,7 +293,7 @@ def setup_fetcher # :nodoc: require 'socket' require 'rubygems/remote_fetcher' - unless @test.fetcher then + unless @test.fetcher @test.fetcher = Gem::FakeFetcher.new Gem::RemoteFetcher.fetcher = @test.fetcher end @@ -327,11 +327,11 @@ def setup_fetcher # :nodoc: # The specification will be yielded before creation for customization, # but only the block or the dependencies may be set, not both. - def spec name, version, dependencies = nil, &block + def spec(name, version, dependencies = nil, &block) @operations << [:spec, name, version, dependencies, block] end - def write_spec spec # :nodoc: + def write_spec(spec) # :nodoc: File.open spec.spec_file, 'w' do |io| io.write spec.to_ruby_for_cache end @@ -367,4 +367,3 @@ def string Gem.read_binary path end end - diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb index 52580e780c8517..5d346b496b9e8e 100644 --- a/lib/rubygems/text.rb +++ b/lib/rubygems/text.rb @@ -28,7 +28,7 @@ def format_text(text, wrap, indent=0) work = clean_text(text) while work.length > wrap do - if work =~ /^(.{0,#{wrap}})[ \n]/ then + if work =~ /^(.{0,#{wrap}})[ \n]/ result << $1.rstrip work.slice!(0, $&.length) else @@ -40,10 +40,10 @@ def format_text(text, wrap, indent=0) result.join("\n").gsub(/^/, " " * indent) end - def min3 a, b, c # :nodoc: - if a < b && a < c then + def min3(a, b, c) # :nodoc: + if a < b && a < c a - elsif b < c then + elsif b < c b else c @@ -52,7 +52,7 @@ def min3 a, b, c # :nodoc: # This code is based directly on the Text gem implementation # Returns a value representing the "cost" of transforming str1 into str2 - def levenshtein_distance str1, str2 + def levenshtein_distance(str1, str2) s = str1 t = str2 n = s.length diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index ee0467c7bfdeff..c213cf656e8f3b 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -99,7 +99,7 @@ def uninstall list.sort! - if list.empty? then + if list.empty? if other_repo_specs.empty? if default_specs.empty? raise Gem::InstallError, "gem #{@gem.inspect} is not installed" @@ -119,19 +119,19 @@ def uninstall } raise Gem::InstallError, message.join("\n") - elsif @force_all then + elsif @force_all remove_all list - elsif list.size > 1 then + elsif list.size > 1 gem_names = list.map { |gem| gem.full_name } gem_names << "All versions" say _, index = choose_from_list "Select gem to uninstall:", gem_names - if index == list.size then + if index == list.size remove_all list - elsif index >= 0 && index < list.size then + elsif index >= 0 && index < list.size uninstall_gem list[index] else say "Error: must enter a number [1-#{list.size+1}]" @@ -194,7 +194,7 @@ def remove_executables(spec) executables = executables.map { |exec| formatted_program_filename exec } - remove = if @force_executables.nil? then + remove = if @force_executables.nil? ask_yes_no("Remove executables:\n" + "\t#{executables.join ', '}\n\n" + "in addition to the gem?", @@ -203,7 +203,7 @@ def remove_executables(spec) @force_executables end - if remove then + if remove bin_dir = @bin_dir || Gem.bindir(spec.base_dir) raise Gem::FilePermissionError, bin_dir unless File.writable? bin_dir @@ -239,7 +239,7 @@ def remove_all(list) def remove(spec) unless path_ok?(@gem_home, spec) or - (@user_install and path_ok?(Gem.user_dir, spec)) then + (@user_install and path_ok?(Gem.user_dir, spec)) e = Gem::GemNotInHomeException.new \ "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}" e.spec = spec @@ -265,7 +265,7 @@ def remove(spec) gemspec = spec.spec_file - unless File.exist? gemspec then + unless File.exist? gemspec gemspec = File.join(File.dirname(gemspec), "#{old_platform_name}.gemspec") end @@ -289,7 +289,7 @@ def path_ok?(gem_dir, spec) # Returns true if it is OK to remove +spec+ or this is a forced # uninstallation. - def dependencies_ok? spec # :nodoc: + def dependencies_ok?(spec) # :nodoc: return true if @force_ignore deplist = Gem::DependencyList.from_specs @@ -308,7 +308,7 @@ def abort_on_dependent? # :nodoc: ## # Asks if it is OK to remove +spec+. Returns true if it is OK. - def ask_if_ok spec # :nodoc: + def ask_if_ok(spec) # :nodoc: msg = [''] msg << 'You have requested to uninstall the gem:' msg << "\t#{spec.full_name}" @@ -332,11 +332,11 @@ def ask_if_ok spec # :nodoc: ## # Returns the formatted version of the executable +filename+ - def formatted_program_filename filename # :nodoc: + def formatted_program_filename(filename) # :nodoc: # TODO perhaps the installer should leave a small manifest # of what it did for us to find rather than trying to recreate # it again. - if @format_executable then + if @format_executable require 'rubygems/installer' Gem::Installer.exec_format % File.basename(filename) else diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb index bb128e4ef95e91..0a24dde24d4b62 100644 --- a/lib/rubygems/uri_formatter.rb +++ b/lib/rubygems/uri_formatter.rb @@ -19,7 +19,7 @@ class Gem::UriFormatter ## # Creates a new URI formatter for +uri+. - def initialize uri + def initialize(uri) @uri = uri end @@ -47,4 +47,3 @@ def unescape end end - diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 61e85549daad13..03eac76ea82d02 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -95,7 +95,7 @@ module Gem::UserInteraction ## # Displays an alert +statement+. Asks a +question+ if given. - def alert statement, question = nil + def alert(statement, question = nil) ui.alert statement, question end @@ -103,7 +103,7 @@ def alert statement, question = nil # Displays an error +statement+ to the error output location. Asks a # +question+ if given. - def alert_error statement, question = nil + def alert_error(statement, question = nil) ui.alert_error statement, question end @@ -111,49 +111,49 @@ def alert_error statement, question = nil # Displays a warning +statement+ to the warning output location. Asks a # +question+ if given. - def alert_warning statement, question = nil + def alert_warning(statement, question = nil) ui.alert_warning statement, question end ## # Asks a +question+ and returns the answer. - def ask question + def ask(question) ui.ask question end ## # Asks for a password with a +prompt+ - def ask_for_password prompt + def ask_for_password(prompt) ui.ask_for_password prompt end ## # Asks a yes or no +question+. Returns true for yes, false for no. - def ask_yes_no question, default = nil + def ask_yes_no(question, default = nil) ui.ask_yes_no question, default end ## # Asks the user to answer +question+ with an answer from the given +list+. - def choose_from_list question, list + def choose_from_list(question, list) ui.choose_from_list question, list end ## # Displays the given +statement+ on the standard output (or equivalent). - def say statement = '' + def say(statement = '') ui.say statement end ## # Terminates the RubyGems process with the given +exit_code+ - def terminate_interaction exit_code = 0 + def terminate_interaction(exit_code = 0) ui.terminate_interaction exit_code end @@ -161,7 +161,7 @@ def terminate_interaction exit_code = 0 # Calls +say+ with +msg+ or the results of the block if really_verbose # is true. - def verbose msg = nil + def verbose(msg = nil) say(msg || yield) if Gem.configuration.really_verbose end end @@ -212,7 +212,7 @@ def tty? # Prints a formatted backtrace to the errors stream if backtraces are # enabled. - def backtrace exception + def backtrace(exception) return unless Gem.configuration.backtrace @errs.puts "\t#{exception.backtrace.join "\n\t"}" @@ -247,8 +247,8 @@ def choose_from_list(question, list) # default. def ask_yes_no(question, default=nil) - unless tty? then - if default.nil? then + unless tty? + if default.nil? raise Gem::OperationNotSupportedError, "Not connected to a tty and no default specified" else @@ -568,6 +568,7 @@ class ThreadedDownloadReporter # +out_stream+. The other arguments are ignored. def initialize(out_stream, *args) + @file_name = nil @out = out_stream end diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index 6dbcd4ba21d3bd..60d552ddb3044c 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -46,7 +46,7 @@ def self.inflate(data) # and implements an IO.popen-like behavior where it does not accept an array # for a command. - def self.popen *command + def self.popen(*command) IO.popen command, &:read rescue TypeError # ruby 1.8 only supports string command r, w = IO.pipe @@ -70,7 +70,7 @@ def self.popen *command ## # Invokes system, but silences all output. - def self.silent_system *command + def self.silent_system(*command) opt = {:out => IO::NULL, :err => [:child, :out]} if Hash === command.last opt.update(command.last) @@ -103,7 +103,7 @@ def self.silent_system *command ## # Enumerates the parents of +directory+. - def self.traverse_parents directory, &block + def self.traverse_parents(directory, &block) return enum_for __method__, directory unless block_given? here = File.expand_path directory diff --git a/lib/rubygems/util/list.rb b/lib/rubygems/util/list.rb index 9c25f6b6dcfe8d..33c40af4bbfd88 100644 --- a/lib/rubygems/util/list.rb +++ b/lib/rubygems/util/list.rb @@ -25,7 +25,7 @@ def prepend(value) List.new value, self end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.pp to_a end diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb index 6842e4fa9c8d36..828497f7002389 100644 --- a/lib/rubygems/validator.rb +++ b/lib/rubygems/validator.rb @@ -62,7 +62,7 @@ def find_files_for_gem(gem_directory) # Describes a problem with a file in a gem. ErrorData = Struct.new :path, :problem do - def <=> other # :nodoc: + def <=>(other) # :nodoc: return nil unless self.class === other [path, problem] <=> [other.path, other.problem] @@ -94,13 +94,13 @@ def alien(gems=[]) spec_path = spec.spec_file gem_directory = spec.full_gem_path - unless File.directory? gem_directory then + unless File.directory? gem_directory errors[gem_name][spec.full_name] = "Gem registered but doesn't exist at #{gem_directory}" next end - unless File.exist? spec_path then + unless File.exist? spec_path errors[gem_name][spec_path] = "Spec file missing for installed gem" end @@ -135,7 +135,7 @@ def alien(gems=[]) source = File.join gem_directory, entry['path'] File.open source, Gem.binary_mode do |f| - unless f.read == data then + unless f.read == data errors[gem_name][entry['path']] = "Modified from original" end end @@ -163,4 +163,3 @@ def alien(gems=[]) errors end end - diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index d700aebd5d3783..f2f10569e88b84 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -155,7 +155,7 @@ class Gem::Version include Comparable VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?'.freeze # :nodoc: - ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc: + ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/.freeze # :nodoc: ## # A string representation of this Version. @@ -169,7 +169,7 @@ def version ## # True if the +version+ string matches RubyGems' requirements. - def self.correct? version + def self.correct?(version) unless Gem::Deprecate.skip warn "nil versions are discouraged and will be deprecated in Rubygems 4" if version.nil? end @@ -185,10 +185,10 @@ def self.correct? version # ver2 = Version.create(ver1) # -> (ver1) # ver3 = Version.create(nil) # -> nil - def self.create input - if self === input then # check yourself before you wreck yourself + def self.create(input) + if self === input # check yourself before you wreck yourself input - elsif input.nil? then + elsif input.nil? nil else new input @@ -197,7 +197,7 @@ def self.create input @@all = {} - def self.new version # :nodoc: + def self.new(version) # :nodoc: return super unless Gem::Version == self @@all[version] ||= super @@ -207,13 +207,13 @@ def self.new version # :nodoc: # Constructs a Version from the +version+ string. A version string is a # series of digits or ASCII letters separated by dots. - def initialize version + def initialize(version) unless self.class.correct?(version) raise ArgumentError, "Malformed version number string #{version}" end # If version is an empty string convert it to 0 - version = 0 if version =~ /\A\s*\Z/ + version = 0 if version.is_a?(String) && version =~ /\A\s*\Z/ @version = version.to_s.strip.gsub("-",".pre.") @segments = nil @@ -240,7 +240,7 @@ def bump # A Version is only eql? to another version if it's specified to the # same precision. Version "1.0" is not the same as version "1". - def eql? other + def eql?(other) self.class === other and @version == other._version end @@ -248,7 +248,7 @@ def hash # :nodoc: canonical_segments.hash end - def init_with coder # :nodoc: + def init_with(coder) # :nodoc: yaml_initialize coder.tag, coder.map end @@ -268,7 +268,7 @@ def marshal_dump # Load custom marshal format. It's a string for backwards (RubyGems # 1.3.5 and earlier) compatibility. - def marshal_load array + def marshal_load(array) initialize array[0] end @@ -282,7 +282,7 @@ def to_yaml_properties # :nodoc: ["@version"] end - def encode_with coder # :nodoc: + def encode_with(coder) # :nodoc: coder.add 'version', @version end @@ -296,7 +296,7 @@ def prerelease? @prerelease end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.text "Gem::Version.new(#{version.inspect})" end @@ -339,7 +339,7 @@ def approximate_recommendation # one. Attempts to compare to something that's not a # Gem::Version return +nil+. - def <=> other + def <=>(other) return unless Gem::Version === other return 0 if @version == other._version || canonical_segments == other.canonical_segments diff --git a/lib/rubygems/version_option.rb b/lib/rubygems/version_option.rb index c4d3306652f23c..458a7a6601427e 100644 --- a/lib/rubygems/version_option.rb +++ b/lib/rubygems/version_option.rb @@ -17,7 +17,7 @@ module Gem::VersionOption def add_platform_option(task = command, *wrap) OptionParser.accept Gem::Platform do |value| - if value == Gem::Platform::RUBY then + if value == Gem::Platform::RUBY value else Gem::Platform.new value @@ -27,7 +27,7 @@ def add_platform_option(task = command, *wrap) add_option('--platform PLATFORM', Gem::Platform, "Specify the platform of gem to #{task}", *wrap) do |value, options| - unless options[:added_platform] then + unless options[:added_platform] Gem.platforms = [Gem::Platform::RUBY] options[:added_platform] = true end @@ -74,4 +74,3 @@ def add_version_option(task = command, *wrap) end end - diff --git a/lib/shell/builtin-command.rb b/lib/shell/builtin-command.rb index e419a68c33cbd5..a6a9d232ade3f7 100644 --- a/lib/shell/builtin-command.rb +++ b/lib/shell/builtin-command.rb @@ -10,7 +10,7 @@ # # -require "shell/filter" +require_relative "filter" class Shell class BuiltInCommand < Filter diff --git a/lib/shell/command-processor.rb b/lib/shell/command-processor.rb index b52cb0043f759e..00357e06fdf0d7 100644 --- a/lib/shell/command-processor.rb +++ b/lib/shell/command-processor.rb @@ -12,10 +12,10 @@ require "e2mmap" -require "shell/error" -require "shell/filter" -require "shell/system-command" -require "shell/builtin-command" +require_relative "error" +require_relative "filter" +require_relative "system-command" +require_relative "builtin-command" class Shell # In order to execute a command on your OS, you need to define it as a diff --git a/lib/shell/system-command.rb b/lib/shell/system-command.rb index af22ed90d75593..767a9ee12cf6d9 100644 --- a/lib/shell/system-command.rb +++ b/lib/shell/system-command.rb @@ -10,7 +10,7 @@ # # -require "shell/filter" +require_relative "filter" class Shell class SystemCommand < Filter diff --git a/lib/time.rb b/lib/time.rb index a2ada7728becfb..b9913c19cdae7a 100644 --- a/lib/time.rb +++ b/lib/time.rb @@ -2,6 +2,8 @@ require 'date' +# :stopdoc: + # = time.rb # # When 'time' is required, Time is extended with additional methods for parsing @@ -18,73 +20,8 @@ # 8601}[http://www.iso.org/iso/date_and_time_format]) # * various formats handled by Date._parse # * custom formats handled by Date._strptime -# -# == Examples -# -# All examples assume you have loaded Time with: -# -# require 'time' -# -# All of these examples were done using the EST timezone which is GMT-5. -# -# === Converting to a String -# -# t = Time.now -# t.iso8601 # => "2011-10-05T22:26:12-04:00" -# t.rfc2822 # => "Wed, 05 Oct 2011 22:26:12 -0400" -# t.httpdate # => "Thu, 06 Oct 2011 02:26:12 GMT" -# -# === Time.parse -# -# #parse takes a string representation of a Time and attempts to parse it -# using a heuristic. -# -# Time.parse("2010-10-31") #=> 2010-10-31 00:00:00 -0500 -# -# Any missing pieces of the date are inferred based on the current date. -# -# # assuming the current date is "2011-10-31" -# Time.parse("12:00") #=> 2011-10-31 12:00:00 -0500 -# -# We can change the date used to infer our missing elements by passing a second -# object that responds to #mon, #day and #year, such as Date, Time or DateTime. -# We can also use our own object. -# -# class MyDate -# attr_reader :mon, :day, :year -# -# def initialize(mon, day, year) -# @mon, @day, @year = mon, day, year -# end -# end -# -# d = Date.parse("2010-10-28") -# t = Time.parse("2010-10-29") -# dt = DateTime.parse("2010-10-30") -# md = MyDate.new(10,31,2010) -# -# Time.parse("12:00", d) #=> 2010-10-28 12:00:00 -0500 -# Time.parse("12:00", t) #=> 2010-10-29 12:00:00 -0500 -# Time.parse("12:00", dt) #=> 2010-10-30 12:00:00 -0500 -# Time.parse("12:00", md) #=> 2010-10-31 12:00:00 -0500 -# -# #parse also accepts an optional block. You can use this block to specify how -# to handle the year component of the date. This is specifically designed for -# handling two digit years. For example, if you wanted to treat all two digit -# years prior to 70 as the year 2000+ you could write this: -# -# Time.parse("01-10-31") {|year| year + (year < 70 ? 2000 : 1900)} -# #=> 2001-10-31 00:00:00 -0500 -# Time.parse("70-10-31") {|year| year + (year < 70 ? 2000 : 1900)} -# #=> 1970-10-31 00:00:00 -0500 -# -# === Time.strptime -# -# #strptime works similar to +parse+ except that instead of using a heuristic -# to detect the format of the input string, you provide a second argument that -# describes the format of the string. For example: -# -# Time.strptime("2000-10-31", "%Y-%m-%d") #=> 2000-10-31 00:00:00 -0500 + +# :startdoc: class Time class << Time @@ -131,6 +68,13 @@ class << Time # # If +zone_offset+ is unable to determine the offset, nil will be # returned. + # + # require 'time' + # + # Time.zone_offset("EST") #=> -18000 + # + # You must require 'time' to use this method. + # def zone_offset(zone, year=self.now.year) off = nil zone = zone.upcase @@ -316,17 +260,62 @@ def make_time(date, year, yday, mon, day, hour, min, sec, sec_fraction, zone, no private :make_time # - # Parses +date+ using Date._parse and converts it to a Time object. + # Takes a string representation of a Time and attempts to parse it + # using a heuristic. # - # If a block is given, the year described in +date+ is converted by the - # block. For example: + # require 'time' + # + # Time.parse("2010-10-31") #=> 2010-10-31 00:00:00 -0500 # - # Time.parse(...) {|y| 0 <= y && y < 100 ? (y >= 69 ? y + 1900 : y + 2000) : y} + # Any missing pieces of the date are inferred based on the current date. + # + # require 'time' + # + # # assuming the current date is "2011-10-31" + # Time.parse("12:00") #=> 2011-10-31 12:00:00 -0500 + # + # We can change the date used to infer our missing elements by passing a second + # object that responds to #mon, #day and #year, such as Date, Time or DateTime. + # We can also use our own object. + # + # require 'time' + # + # class MyDate + # attr_reader :mon, :day, :year + # + # def initialize(mon, day, year) + # @mon, @day, @year = mon, day, year + # end + # end + # + # d = Date.parse("2010-10-28") + # t = Time.parse("2010-10-29") + # dt = DateTime.parse("2010-10-30") + # md = MyDate.new(10,31,2010) + # + # Time.parse("12:00", d) #=> 2010-10-28 12:00:00 -0500 + # Time.parse("12:00", t) #=> 2010-10-29 12:00:00 -0500 + # Time.parse("12:00", dt) #=> 2010-10-30 12:00:00 -0500 + # Time.parse("12:00", md) #=> 2010-10-31 12:00:00 -0500 + # + # If a block is given, the year described in +date+ is converted + # by the block. This is specifically designed for handling two + # digit years. For example, if you wanted to treat all two digit + # years prior to 70 as the year 2000+ you could write this: + # + # require 'time' + # + # Time.parse("01-10-31") {|year| year + (year < 70 ? 2000 : 1900)} + # #=> 2001-10-31 00:00:00 -0500 + # Time.parse("70-10-31") {|year| year + (year < 70 ? 2000 : 1900)} + # #=> 1970-10-31 00:00:00 -0500 # # If the upper components of the given time are broken or missing, they are # supplied with those of +now+. For the lower components, the minimum # values (1 or 0) are assumed if broken or missing. For example: # + # require 'time' + # # # Suppose it is "Thu Nov 29 14:33:20 2001" now and # # your time zone is EST which is GMT-5. # now = Time.parse("Thu Nov 29 14:33:20 2001") @@ -378,7 +367,9 @@ def parse(date, now=self.now) end # - # Parses +date+ using Date._strptime and converts it to a Time object. + # Works similar to +parse+ except that instead of using a + # heuristic to detect the format of the input string, you provide + # a second argument that describes the format of the string. # # If a block is given, the year described in +date+ is converted by the # block. For example: @@ -436,7 +427,13 @@ def parse(date, now=self.now) # %Z :: Time zone name # %% :: Literal "%" character # %+ :: date(1) (%a %b %e %H:%M:%S %Z %Y) - + # + # require 'time' + # + # Time.strptime("2000-10-31", "%Y-%m-%d") #=> 2000-10-31 00:00:00 -0500 + # + # You must require 'time' to use this method. + # def strptime(date, format, now=self.now) d = Date._strptime(date, format) raise ArgumentError, "invalid strptime format - `#{format}'" unless d @@ -474,6 +471,11 @@ def strptime(date, format, now=self.now) # # See #rfc2822 for more information on this format. # + # require 'time' + # + # Time.rfc2822("Wed, 05 Oct 2011 22:26:12 -0400") + # #=> 2010-10-05 22:26:12 -0400 + # # You must require 'time' to use this method. # def rfc2822(date) @@ -527,6 +529,11 @@ def rfc2822(date) # # See #httpdate for more information on this format. # + # require 'time' + # + # Time.httpdate("Thu, 06 Oct 2011 02:26:12 GMT") + # #=> 2011-10-06 02:26:12 UTC + # # You must require 'time' to use this method. # def httpdate(date) @@ -576,6 +583,11 @@ def httpdate(date) # # See #xmlschema for more information on this format. # + # require 'time' + # + # Time.xmlschema("2011-10-05T22:26:12-04:00") + # #=> 2011-10-05 22:26:12-04:00 + # # You must require 'time' to use this method. # def xmlschema(date) @@ -623,6 +635,11 @@ def xmlschema(date) # # If +self+ is a UTC time, -0000 is used as zone. # + # require 'time' + # + # t = Time.now + # t.rfc2822 # => "Wed, 05 Oct 2011 22:26:12 -0400" + # # You must require 'time' to use this method. # def rfc2822 @@ -658,6 +675,11 @@ def rfc2822 # # Note that the result is always UTC (GMT). # + # require 'time' + # + # t = Time.now + # t.httpdate # => "Thu, 06 Oct 2011 02:26:12 GMT" + # # You must require 'time' to use this method. # def httpdate @@ -682,6 +704,11 @@ def httpdate # +fractional_digits+ specifies a number of digits to use for fractional # seconds. Its default value is 0. # + # require 'time' + # + # t = Time.now + # t.iso8601 # => "2011-10-05T22:26:12-04:00" + # # You must require 'time' to use this method. # def xmlschema(fraction_digits=0) diff --git a/lib/un.rb b/lib/un.rb index c445dba4ec52bf..d109a3417bb002 100644 --- a/lib/un.rb +++ b/lib/un.rb @@ -313,6 +313,8 @@ def mkmf # --do-not-reverse-lookup disable reverse lookup # --request-timeout=SECOND request timeout in seconds # --http-version=VERSION HTTP version +# --server-name=NAME name of the server host +# --server-software=NAME name and version of the server # --ssl-certificate=CERT The SSL certificate file for the server # --ssl-private-key=KEY The SSL private key file for the server certificate # -v verbose @@ -321,6 +323,7 @@ def mkmf def httpd setup("", "BindAddress=ADDR", "Port=PORT", "MaxClients=NUM", "TempDir=DIR", "DoNotReverseLookup", "RequestTimeout=SECOND", "HTTPVersion=VERSION", + "ServerName=NAME", "ServerSoftware=NAME", "SSLCertificate=CERT", "SSLPrivateKey=KEY") do |argv, options| require 'webrick' diff --git a/lib/unicode_normalize/normalize.rb b/lib/unicode_normalize/normalize.rb index 460f784125cd7b..b27cdadaaac952 100644 --- a/lib/unicode_normalize/normalize.rb +++ b/lib/unicode_normalize/normalize.rb @@ -18,7 +18,7 @@ # content are purely an implementation detail, and should not be exposed in # any test or spec or otherwise. -require 'unicode_normalize/tables.rb' +require_relative 'tables' module UnicodeNormalize # :nodoc: diff --git a/lib/uri/common.rb b/lib/uri/common.rb index ab4a8e382b146d..17d9ffc28c7206 100644 --- a/lib/uri/common.rb +++ b/lib/uri/common.rb @@ -10,8 +10,8 @@ # See URI for general documentation # -require "uri/rfc2396_parser" -require "uri/rfc3986_parser" +require_relative "rfc2396_parser" +require_relative "rfc3986_parser" module URI REGEXP = RFC2396_REGEXP diff --git a/lib/uri/file.rb b/lib/uri/file.rb index 6c959be50d8334..561ec703c46520 100644 --- a/lib/uri/file.rb +++ b/lib/uri/file.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'uri/generic' +require_relative 'generic' module URI diff --git a/lib/uri/ftp.rb b/lib/uri/ftp.rb index bc9c9c3c65c99e..f57b4b7df9abc5 100644 --- a/lib/uri/ftp.rb +++ b/lib/uri/ftp.rb @@ -8,7 +8,7 @@ # See URI for general documentation # -require 'uri/generic' +require_relative 'generic' module URI diff --git a/lib/uri/generic.rb b/lib/uri/generic.rb index 0948cba66dec35..ea79e7950a5bde 100644 --- a/lib/uri/generic.rb +++ b/lib/uri/generic.rb @@ -9,7 +9,7 @@ # See URI for general documentation # -require 'uri/common' +require_relative 'common' autoload :IPSocket, 'socket' autoload :IPAddr, 'ipaddr' diff --git a/lib/uri/http.rb b/lib/uri/http.rb index 7d499f08d86d7a..2e2ebcc1d37340 100644 --- a/lib/uri/http.rb +++ b/lib/uri/http.rb @@ -8,7 +8,7 @@ # See URI for general documentation # -require 'uri/generic' +require_relative 'generic' module URI diff --git a/lib/uri/https.rb b/lib/uri/https.rb index 3c8c905cc3e171..4780ee0a4482d2 100644 --- a/lib/uri/https.rb +++ b/lib/uri/https.rb @@ -8,7 +8,7 @@ # See URI for general documentation # -require 'uri/http' +require_relative 'http' module URI diff --git a/lib/uri/ldap.rb b/lib/uri/ldap.rb index 6863fcfbc1f769..228c793cbd510b 100644 --- a/lib/uri/ldap.rb +++ b/lib/uri/ldap.rb @@ -12,7 +12,7 @@ # See URI for general documentation # -require 'uri/generic' +require_relative 'generic' module URI diff --git a/lib/uri/ldaps.rb b/lib/uri/ldaps.rb index d03f8efa2d8890..227e7fab354b3e 100644 --- a/lib/uri/ldaps.rb +++ b/lib/uri/ldaps.rb @@ -6,7 +6,7 @@ # See URI for general documentation # -require 'uri/ldap' +require_relative 'ldap' module URI diff --git a/lib/uri/mailto.rb b/lib/uri/mailto.rb index 4dd41bca3e4953..9c06871c7a9ef5 100644 --- a/lib/uri/mailto.rb +++ b/lib/uri/mailto.rb @@ -8,7 +8,7 @@ # See URI for general documentation # -require 'uri/generic' +require_relative 'generic' module URI diff --git a/lib/webrick/cgi.rb b/lib/webrick/cgi.rb index 33f1542731ca52..bb0ae2fc844214 100644 --- a/lib/webrick/cgi.rb +++ b/lib/webrick/cgi.rb @@ -8,9 +8,9 @@ # # $Id$ -require "webrick/httprequest" -require "webrick/httpresponse" -require "webrick/config" +require_relative "httprequest" +require_relative "httpresponse" +require_relative "config" require "stringio" module WEBrick diff --git a/lib/webrick/config.rb b/lib/webrick/config.rb index af4b561534fba0..9f2ab44f49d80c 100644 --- a/lib/webrick/config.rb +++ b/lib/webrick/config.rb @@ -9,11 +9,11 @@ # # $IPR: config.rb,v 1.52 2003/07/22 19:20:42 gotoyuzo Exp $ -require 'webrick/version' -require 'webrick/httpversion' -require 'webrick/httputils' -require 'webrick/utils' -require 'webrick/log' +require_relative 'version' +require_relative 'httpversion' +require_relative 'httputils' +require_relative 'utils' +require_relative 'log' module WEBrick module Config diff --git a/lib/webrick/cookie.rb b/lib/webrick/cookie.rb index 24bf92ec001a48..5fd3bfb22862ee 100644 --- a/lib/webrick/cookie.rb +++ b/lib/webrick/cookie.rb @@ -10,7 +10,7 @@ # $IPR: cookie.rb,v 1.16 2002/09/21 12:23:35 gotoyuzo Exp $ require 'time' -require 'webrick/httputils' +require_relative 'httputils' module WEBrick diff --git a/lib/webrick/httpauth.rb b/lib/webrick/httpauth.rb index bbb67765284801..f8bf09a6f1972c 100644 --- a/lib/webrick/httpauth.rb +++ b/lib/webrick/httpauth.rb @@ -9,11 +9,11 @@ # # $IPR: httpauth.rb,v 1.14 2003/07/22 19:20:42 gotoyuzo Exp $ -require 'webrick/httpauth/basicauth' -require 'webrick/httpauth/digestauth' -require 'webrick/httpauth/htpasswd' -require 'webrick/httpauth/htdigest' -require 'webrick/httpauth/htgroup' +require_relative 'httpauth/basicauth' +require_relative 'httpauth/digestauth' +require_relative 'httpauth/htpasswd' +require_relative 'httpauth/htdigest' +require_relative 'httpauth/htgroup' module WEBrick diff --git a/lib/webrick/httpauth/basicauth.rb b/lib/webrick/httpauth/basicauth.rb index 751885bc3e3fdc..7d0a9cfc8fd43d 100644 --- a/lib/webrick/httpauth/basicauth.rb +++ b/lib/webrick/httpauth/basicauth.rb @@ -8,9 +8,9 @@ # # $IPR: basicauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ -require 'webrick/config' -require 'webrick/httpstatus' -require 'webrick/httpauth/authenticator' +require_relative '../config' +require_relative '../httpstatus' +require_relative 'authenticator' module WEBrick module HTTPAuth diff --git a/lib/webrick/httpauth/digestauth.rb b/lib/webrick/httpauth/digestauth.rb index 94f849a02ef117..6416a40998f584 100644 --- a/lib/webrick/httpauth/digestauth.rb +++ b/lib/webrick/httpauth/digestauth.rb @@ -12,9 +12,9 @@ # # $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ -require 'webrick/config' -require 'webrick/httpstatus' -require 'webrick/httpauth/authenticator' +require_relative '../config' +require_relative '../httpstatus' +require_relative 'authenticator' require 'digest/md5' require 'digest/sha1' diff --git a/lib/webrick/httpauth/htdigest.rb b/lib/webrick/httpauth/htdigest.rb index c35b38433bf8bf..93b18e2c750bd9 100644 --- a/lib/webrick/httpauth/htdigest.rb +++ b/lib/webrick/httpauth/htdigest.rb @@ -8,8 +8,8 @@ # # $IPR: htdigest.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ -require 'webrick/httpauth/userdb' -require 'webrick/httpauth/digestauth' +require_relative 'userdb' +require_relative 'digestauth' require 'tempfile' module WEBrick diff --git a/lib/webrick/httpauth/htpasswd.rb b/lib/webrick/httpauth/htpasswd.rb index cff18a8012d9de..abca30532e0429 100644 --- a/lib/webrick/httpauth/htpasswd.rb +++ b/lib/webrick/httpauth/htpasswd.rb @@ -8,8 +8,8 @@ # # $IPR: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ -require 'webrick/httpauth/userdb' -require 'webrick/httpauth/basicauth' +require_relative 'userdb' +require_relative 'basicauth' require 'tempfile' module WEBrick diff --git a/lib/webrick/httpproxy.rb b/lib/webrick/httpproxy.rb index d180ff483185f5..d05d59514c7291 100644 --- a/lib/webrick/httpproxy.rb +++ b/lib/webrick/httpproxy.rb @@ -10,7 +10,7 @@ # $IPR: httpproxy.rb,v 1.18 2003/03/08 18:58:10 gotoyuzo Exp $ # $kNotwork: straw.rb,v 1.3 2002/02/12 15:13:07 gotoken Exp $ -require "webrick/httpserver" +require_relative "httpserver" require "net/http" module WEBrick diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index ac60c3937fcdeb..e402099a3045e4 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -10,10 +10,10 @@ # $IPR: httprequest.rb,v 1.64 2003/07/13 17:18:22 gotoyuzo Exp $ require 'uri' -require 'webrick/httpversion' -require 'webrick/httpstatus' -require 'webrick/httputils' -require 'webrick/cookie' +require_relative 'httpversion' +require_relative 'httpstatus' +require_relative 'httputils' +require_relative 'cookie' module WEBrick diff --git a/lib/webrick/httpresponse.rb b/lib/webrick/httpresponse.rb index 255a27f6b962cb..41a2510e6f8209 100644 --- a/lib/webrick/httpresponse.rb +++ b/lib/webrick/httpresponse.rb @@ -11,10 +11,10 @@ require 'time' require 'uri' -require 'webrick/httpversion' -require 'webrick/htmlutils' -require 'webrick/httputils' -require 'webrick/httpstatus' +require_relative 'httpversion' +require_relative 'htmlutils' +require_relative 'httputils' +require_relative 'httpstatus' module WEBrick ## @@ -254,7 +254,7 @@ def setup_header() # :nodoc: @header.delete('content-length') elsif @header['content-length'].nil? unless @body.is_a?(IO) - @header['content-length'] = @body ? @body.bytesize : 0 + @header['content-length'] = (@body ? @body.bytesize : 0).to_s end end @@ -277,7 +277,7 @@ def setup_header() # :nodoc: # Location is a single absoluteURI. if location = @header['location'] if @request_uri - @header['location'] = @request_uri.merge(location) + @header['location'] = @request_uri.merge(location).to_s end end end diff --git a/lib/webrick/https.rb b/lib/webrick/https.rb index 4826654d3a4d02..b0a49bc40bae05 100644 --- a/lib/webrick/https.rb +++ b/lib/webrick/https.rb @@ -9,8 +9,8 @@ # # $IPR: https.rb,v 1.15 2003/07/22 19:20:42 gotoyuzo Exp $ -require 'webrick/ssl' -require 'webrick/httpserver' +require_relative 'ssl' +require_relative 'httpserver' module WEBrick module Config diff --git a/lib/webrick/httpserver.rb b/lib/webrick/httpserver.rb index e46b3bd1ad8243..6945868ec827be 100644 --- a/lib/webrick/httpserver.rb +++ b/lib/webrick/httpserver.rb @@ -10,13 +10,13 @@ # $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $ require 'io/wait' -require 'webrick/server' -require 'webrick/httputils' -require 'webrick/httpstatus' -require 'webrick/httprequest' -require 'webrick/httpresponse' -require 'webrick/httpservlet' -require 'webrick/accesslog' +require_relative 'server' +require_relative 'httputils' +require_relative 'httpstatus' +require_relative 'httprequest' +require_relative 'httpresponse' +require_relative 'httpservlet' +require_relative 'accesslog' module WEBrick class HTTPServerError < ServerError; end diff --git a/lib/webrick/httpservlet.rb b/lib/webrick/httpservlet.rb index 1ee04ec86fd5fc..da49a1405b3af9 100644 --- a/lib/webrick/httpservlet.rb +++ b/lib/webrick/httpservlet.rb @@ -9,11 +9,11 @@ # # $IPR: httpservlet.rb,v 1.21 2003/02/23 12:24:46 gotoyuzo Exp $ -require 'webrick/httpservlet/abstract' -require 'webrick/httpservlet/filehandler' -require 'webrick/httpservlet/cgihandler' -require 'webrick/httpservlet/erbhandler' -require 'webrick/httpservlet/prochandler' +require_relative 'httpservlet/abstract' +require_relative 'httpservlet/filehandler' +require_relative 'httpservlet/cgihandler' +require_relative 'httpservlet/erbhandler' +require_relative 'httpservlet/prochandler' module WEBrick module HTTPServlet diff --git a/lib/webrick/httpservlet/abstract.rb b/lib/webrick/httpservlet/abstract.rb index fc4cd2275a8113..bccb091861d7fa 100644 --- a/lib/webrick/httpservlet/abstract.rb +++ b/lib/webrick/httpservlet/abstract.rb @@ -9,9 +9,9 @@ # # $IPR: abstract.rb,v 1.24 2003/07/11 11:16:46 gotoyuzo Exp $ -require 'webrick/htmlutils' -require 'webrick/httputils' -require 'webrick/httpstatus' +require_relative '../htmlutils' +require_relative '../httputils' +require_relative '../httpstatus' module WEBrick module HTTPServlet diff --git a/lib/webrick/httpservlet/cgihandler.rb b/lib/webrick/httpservlet/cgihandler.rb index b1fb471c54dbc8..981f649750e0f6 100644 --- a/lib/webrick/httpservlet/cgihandler.rb +++ b/lib/webrick/httpservlet/cgihandler.rb @@ -11,8 +11,8 @@ require 'rbconfig' require 'tempfile' -require 'webrick/config' -require 'webrick/httpservlet/abstract' +require_relative '../config' +require_relative 'abstract' module WEBrick module HTTPServlet diff --git a/lib/webrick/httpservlet/erbhandler.rb b/lib/webrick/httpservlet/erbhandler.rb index aa02ce8a1da51d..cd09e5f2161540 100644 --- a/lib/webrick/httpservlet/erbhandler.rb +++ b/lib/webrick/httpservlet/erbhandler.rb @@ -9,7 +9,7 @@ # # $IPR: erbhandler.rb,v 1.25 2003/02/24 19:25:31 gotoyuzo Exp $ -require 'webrick/httpservlet/abstract.rb' +require_relative 'abstract' require 'erb' diff --git a/lib/webrick/httpservlet/filehandler.rb b/lib/webrick/httpservlet/filehandler.rb index 0072e81ac64472..601882ef4cc007 100644 --- a/lib/webrick/httpservlet/filehandler.rb +++ b/lib/webrick/httpservlet/filehandler.rb @@ -11,9 +11,9 @@ require 'time' -require 'webrick/htmlutils' -require 'webrick/httputils' -require 'webrick/httpstatus' +require_relative '../htmlutils' +require_relative '../httputils' +require_relative '../httpstatus' module WEBrick module HTTPServlet @@ -55,7 +55,7 @@ def do_GET(req, res) else mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes]) res['content-type'] = mtype - res['content-length'] = st.size + res['content-length'] = st.size.to_s res['last-modified'] = mtime.httpdate res.body = File.open(@local_path, "rb") end @@ -144,7 +144,7 @@ def make_partial_content(req, res, filename, filesize) raise HTTPStatus::RequestRangeNotSatisfiable if first < 0 res['content-type'] = mtype res['content-range'] = "bytes #{first}-#{last}/#{filesize}" - res['content-length'] = last - first + 1 + res['content-length'] = (last - first + 1).to_s res.body = io.dup else raise HTTPStatus::BadRequest diff --git a/lib/webrick/httpservlet/prochandler.rb b/lib/webrick/httpservlet/prochandler.rb index c1f454e2f6f487..599ffc43408d37 100644 --- a/lib/webrick/httpservlet/prochandler.rb +++ b/lib/webrick/httpservlet/prochandler.rb @@ -9,7 +9,7 @@ # # $IPR: prochandler.rb,v 1.7 2002/09/21 12:23:42 gotoyuzo Exp $ -require 'webrick/httpservlet/abstract.rb' +require_relative 'abstract' module WEBrick module HTTPServlet diff --git a/lib/webrick/httpstatus.rb b/lib/webrick/httpstatus.rb index 0630219ada2d47..c811f21964990c 100644 --- a/lib/webrick/httpstatus.rb +++ b/lib/webrick/httpstatus.rb @@ -9,7 +9,7 @@ # # $IPR: httpstatus.rb,v 1.11 2003/03/24 20:18:55 gotoyuzo Exp $ -require 'webrick/accesslog' +require_relative 'accesslog' module WEBrick diff --git a/lib/webrick/server.rb b/lib/webrick/server.rb index 88e160d9814edf..4a6e74c4f91883 100644 --- a/lib/webrick/server.rb +++ b/lib/webrick/server.rb @@ -10,8 +10,8 @@ # $IPR: server.rb,v 1.62 2003/07/22 19:20:43 gotoyuzo Exp $ require 'socket' -require 'webrick/config' -require 'webrick/log' +require_relative 'config' +require_relative 'log' module WEBrick diff --git a/libexec/bundle b/libexec/bundle new file mode 100755 index 00000000000000..aaf773745d3669 --- /dev/null +++ b/libexec/bundle @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Exit cleanly from an early interrupt +Signal.trap("INT") do + Bundler.ui.debug("\n#{caller.join("\n")}") if defined?(Bundler) + exit 1 +end + +require "bundler" +# Check if an older version of bundler is installed +$LOAD_PATH.each do |path| + next unless path =~ %r{/bundler-0\.(\d+)} && $1.to_i < 9 + err = String.new + err << "Looks like you have a version of bundler that's older than 0.9.\n" + err << "Please remove your old versions.\n" + err << "An easy way to do this is by running `gem cleanup bundler`." + abort(err) +end + +require "bundler/friendly_errors" +Bundler.with_friendly_errors do + require "bundler/cli" + + # Allow any command to use --help flag to show help for that command + help_flags = %w[--help -h] + help_flag_used = ARGV.any? {|a| help_flags.include? a } + args = help_flag_used ? Bundler::CLI.reformatted_help_args(ARGV) : ARGV + + Bundler::CLI.start(args, :debug => true) +end diff --git a/libexec/bundle_ruby b/libexec/bundle_ruby new file mode 100755 index 00000000000000..2209c6195f8c50 --- /dev/null +++ b/libexec/bundle_ruby @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/shared_helpers" + +Bundler::SharedHelpers.major_deprecation(3, "the bundle_ruby executable has been removed in favor of `bundle platform --ruby`") + +Signal.trap("INT") { exit 1 } + +require "bundler/errors" +require "bundler/ruby_version" +require "bundler/ruby_dsl" + +module Bundler + class Dsl + include RubyDsl + + attr_accessor :ruby_version + + def initialize + @ruby_version = nil + end + + def eval_gemfile(gemfile, contents = nil) + contents ||= File.open(gemfile, "rb", &:read) + instance_eval(contents, gemfile.to_s, 1) + rescue SyntaxError => e + bt = e.message.split("\n")[1..-1] + raise GemfileError, ["Gemfile syntax error:", *bt].join("\n") + rescue ScriptError, RegexpError, NameError, ArgumentError => e + e.backtrace[0] = "#{e.backtrace[0]}: #{e.message} (#{e.class})" + STDERR.puts e.backtrace.join("\n ") + raise GemfileError, "There was an error in your Gemfile," \ + " and Bundler cannot continue." + end + + def source(source, options = {}) + end + + def gem(name, *args) + end + + def group(*args) + end + end +end + +dsl = Bundler::Dsl.new +begin + dsl.eval_gemfile(Bundler::SharedHelpers.default_gemfile) + ruby_version = dsl.ruby_version + if ruby_version + puts ruby_version + else + puts "No ruby version specified" + end +rescue Bundler::GemfileError => e + puts e.message + exit(-1) +end diff --git a/libexec/bundler b/libexec/bundler new file mode 100755 index 00000000000000..d9131fe834b8a8 --- /dev/null +++ b/libexec/bundler @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +load File.expand_path("../bundle", __FILE__) diff --git a/libexec/irb b/libexec/irb new file mode 100755 index 00000000000000..c64ee85fbdcd41 --- /dev/null +++ b/libexec/irb @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# +# irb.rb - interactive ruby +# $Release Version: 0.9.6 $ +# $Revision$ +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +require "irb" + +IRB.start(__FILE__) diff --git a/libexec/rdoc b/libexec/rdoc new file mode 100755 index 00000000000000..aaa23292dfd1ec --- /dev/null +++ b/libexec/rdoc @@ -0,0 +1,44 @@ +#!/usr/bin/env ruby +# +# RDoc: Documentation tool for source code +# (see lib/rdoc/rdoc.rb for more information) +# +# Copyright (c) 2003 Dave Thomas +# Released under the same terms as Ruby + +begin + gem 'rdoc' +rescue NameError => e # --disable-gems + raise unless e.name == :gem +rescue Gem::LoadError +end + +require 'rdoc/rdoc' + +begin + r = RDoc::RDoc.new + r.document ARGV +rescue Errno::ENOSPC + $stderr.puts 'Ran out of space creating documentation' + $stderr.puts + $stderr.puts 'Please free up some space and try again' +rescue SystemExit + raise +rescue Exception => e + if $DEBUG_RDOC then + $stderr.puts e.message + $stderr.puts "#{e.backtrace.join "\n\t"}" + $stderr.puts + elsif Interrupt === e then + $stderr.puts + $stderr.puts 'Interrupted' + else + $stderr.puts "uh-oh! RDoc had a problem:" + $stderr.puts e.message + $stderr.puts + $stderr.puts "run with --debug for full backtrace" + end + + exit 1 +end + diff --git a/libexec/ri b/libexec/ri new file mode 100755 index 00000000000000..7fbed0c09900f5 --- /dev/null +++ b/libexec/ri @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby + +begin + gem 'rdoc' +rescue NameError => e # --disable-gems + raise unless e.name == :gem +rescue Gem::LoadError +end + +require 'rdoc/ri/driver' + +RDoc::RI::Driver.run ARGV diff --git a/man/bundle-add.1 b/man/bundle-add.1 new file mode 100644 index 00000000000000..3adf3e81636f56 --- /dev/null +++ b/man/bundle-add.1 @@ -0,0 +1,58 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-ADD" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install +. +.SH "SYNOPSIS" +\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-skip\-install] [\-\-strict] [\-\-optimistic] +. +.SH "DESCRIPTION" +Adds the named gem to the Gemfile and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\. +. +.P +Example: +. +.P +bundle add rails +. +.P +bundle add rails \-\-version "< 3\.0, > 1\.1" +. +.P +bundle add rails \-\-version "~> 5\.0\.0" \-\-source "https://gems\.example\.com" \-\-group "development" +. +.P +bundle add rails \-\-skip\-install +. +.P +bundle add rails \-\-group "development, test" +. +.SH "OPTIONS" +. +.TP +\fB\-\-version\fR, \fB\-v\fR +Specify version requirements(s) for the added gem\. +. +.TP +\fB\-\-group\fR, \fB\-g\fR +Specify the group(s) for the added gem\. Multiple groups should be separated by commas\. +. +.TP +\fB\-\-source\fR, , \fB\-s\fR +Specify the source for the added gem\. +. +.TP +\fB\-\-skip\-install\fR +Adds the gem to the Gemfile but does not install it\. +. +.TP +\fB\-\-optimistic\fR +Adds optimistic declaration of version +. +.TP +\fB\-\-strict\fR +Adds strict declaration of version + diff --git a/man/bundle-add.1.txt b/man/bundle-add.1.txt new file mode 100644 index 00000000000000..653a16604427a9 --- /dev/null +++ b/man/bundle-add.1.txt @@ -0,0 +1,52 @@ +BUNDLE-ADD(1) BUNDLE-ADD(1) + + + +1mNAME0m + 1mbundle-add 22m- Add gem to the Gemfile and run bundle install + +1mSYNOPSIS0m + 1mbundle add 4m22mGEM_NAME24m [--group=GROUP] [--version=VERSION] + [--source=SOURCE] [--skip-install] [--strict] [--optimistic] + +1mDESCRIPTION0m + Adds the named gem to the Gemfile and run 1mbundle install22m. 1mbundle0m + 1minstall 22mcan be avoided by using the flag 1m--skip-install22m. + + Example: + + bundle add rails + + bundle add rails --version "< 3.0, > 1.1" + + bundle add rails --version "~> 5.0.0" --source "https://gems.exam- + ple.com" --group "development" + + bundle add rails --skip-install + + bundle add rails --group "development, test" + +1mOPTIONS0m + 1m--version22m, 1m-v0m + Specify version requirements(s) for the added gem. + + 1m--group22m, 1m-g0m + Specify the group(s) for the added gem. Multiple groups should + be separated by commas. + + 1m--source22m, , 1m-s0m + Specify the source for the added gem. + + 1m--skip-install0m + Adds the gem to the Gemfile but does not install it. + + 1m--optimistic0m + Adds optimistic declaration of version + + 1m--strict0m + Adds strict declaration of version + + + + + November 2018 BUNDLE-ADD(1) diff --git a/man/bundle-add.ronn b/man/bundle-add.ronn new file mode 100644 index 00000000000000..1e2d732ec6fe5e --- /dev/null +++ b/man/bundle-add.ronn @@ -0,0 +1,40 @@ +bundle-add(1) -- Add gem to the Gemfile and run bundle install +================================================================ + +## SYNOPSIS + +`bundle add` [--group=GROUP] [--version=VERSION] [--source=SOURCE] [--skip-install] [--strict] [--optimistic] + +## DESCRIPTION +Adds the named gem to the Gemfile and run `bundle install`. `bundle install` can be avoided by using the flag `--skip-install`. + +Example: + +bundle add rails + +bundle add rails --version "< 3.0, > 1.1" + +bundle add rails --version "~> 5.0.0" --source "https://gems.example.com" --group "development" + +bundle add rails --skip-install + +bundle add rails --group "development, test" + +## OPTIONS +* `--version`, `-v`: + Specify version requirements(s) for the added gem. + +* `--group`, `-g`: + Specify the group(s) for the added gem. Multiple groups should be separated by commas. + +* `--source`, , `-s`: + Specify the source for the added gem. + +* `--skip-install`: + Adds the gem to the Gemfile but does not install it. + +* `--optimistic`: + Adds optimistic declaration of version + +* `--strict`: + Adds strict declaration of version diff --git a/man/bundle-binstubs.1 b/man/bundle-binstubs.1 new file mode 100644 index 00000000000000..583ab83abc7b41 --- /dev/null +++ b/man/bundle-binstubs.1 @@ -0,0 +1,40 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-BINSTUBS" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-binstubs\fR \- Install the binstubs of the listed gems +. +.SH "SYNOPSIS" +\fBbundle binstubs\fR \fIGEM_NAME\fR [\-\-force] [\-\-path PATH] [\-\-standalone] +. +.SH "DESCRIPTION" +Binstubs are scripts that wrap around executables\. Bundler creates a small Ruby file (a binstub) that loads Bundler, runs the command, and puts it into \fBbin/\fR\. Binstubs are a shortcut\-or alternative\- to always using \fBbundle exec\fR\. This gives you a file that can by run directly, and one that will always run the correct gem version used by the application\. +. +.P +For example, if you run \fBbundle binstubs rspec\-core\fR, Bundler will create the file \fBbin/rspec\fR\. That file will contain enough code to load Bundler, tell it to load the bundled gems, and then run rspec\. +. +.P +This command generates binstubs for executables in \fBGEM_NAME\fR\. Binstubs are put into \fBbin\fR, or the \fB\-\-path\fR directory if one has been set\. Calling binstubs with [GEM [GEM]] will create binstubs for all given gems\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-force\fR +Overwrite existing binstubs if they exist\. +. +.TP +\fB\-\-path\fR +The location to install the specified binstubs to\. This defaults to \fBbin\fR\. +. +.TP +\fB\-\-standalone\fR +Makes binstubs that can work without depending on Rubygems or Bundler at runtime\. +. +.TP +\fB\-\-shebang\fR +Specify a different shebang executable name than the default (default \'ruby\') +. +.SH "BUNDLE INSTALL \-\-BINSTUBS" +To create binstubs for all the gems in the bundle you can use the \fB\-\-binstubs\fR flag in bundle install(1) \fIbundle\-install\.1\.html\fR\. diff --git a/man/bundle-binstubs.1.txt b/man/bundle-binstubs.1.txt new file mode 100644 index 00000000000000..f15a875e2da087 --- /dev/null +++ b/man/bundle-binstubs.1.txt @@ -0,0 +1,48 @@ +BUNDLE-BINSTUBS(1) BUNDLE-BINSTUBS(1) + + + +1mNAME0m + 1mbundle-binstubs 22m- Install the binstubs of the listed gems + +1mSYNOPSIS0m + 1mbundle binstubs 4m22mGEM_NAME24m [--force] [--path PATH] [--standalone] + +1mDESCRIPTION0m + Binstubs are scripts that wrap around executables. Bundler creates a + small Ruby file (a binstub) that loads Bundler, runs the command, and + puts it into 1mbin/22m. Binstubs are a shortcut-or alternative- to always + using 1mbundle exec22m. This gives you a file that can by run directly, and + one that will always run the correct gem version used by the applica- + tion. + + For example, if you run 1mbundle binstubs rspec-core22m, Bundler will create + the file 1mbin/rspec22m. That file will contain enough code to load Bundler, + tell it to load the bundled gems, and then run rspec. + + This command generates binstubs for executables in 1mGEM_NAME22m. Binstubs + are put into 1mbin22m, or the 1m--path 22mdirectory if one has been set. Calling + binstubs with [GEM [GEM]] will create binstubs for all given gems. + +1mOPTIONS0m + 1m--force0m + Overwrite existing binstubs if they exist. + + 1m--path 22mThe location to install the specified binstubs to. This defaults + to 1mbin22m. + + 1m--standalone0m + Makes binstubs that can work without depending on Rubygems or + Bundler at runtime. + + 1m--shebang0m + Specify a different shebang executable name than the default + (default 'ruby') + +1mBUNDLE INSTALL --BINSTUBS0m + To create binstubs for all the gems in the bundle you can use the + 1m--binstubs 22mflag in bundle install(1) 4mbundle-install.1.html24m. + + + + November 2018 BUNDLE-BINSTUBS(1) diff --git a/man/bundle-binstubs.ronn b/man/bundle-binstubs.ronn new file mode 100644 index 00000000000000..c1ae0988cd1ab2 --- /dev/null +++ b/man/bundle-binstubs.ronn @@ -0,0 +1,43 @@ +bundle-binstubs(1) -- Install the binstubs of the listed gems +============================================================= + +## SYNOPSIS + +`bundle binstubs` [--force] [--path PATH] [--standalone] + +## DESCRIPTION + +Binstubs are scripts that wrap around executables. Bundler creates a +small Ruby file (a binstub) that loads Bundler, runs the command, +and puts it into `bin/`. Binstubs are a shortcut-or alternative- +to always using `bundle exec`. This gives you a file that can by run +directly, and one that will always run the correct gem version +used by the application. + +For example, if you run `bundle binstubs rspec-core`, Bundler will create +the file `bin/rspec`. That file will contain enough code to load Bundler, +tell it to load the bundled gems, and then run rspec. + +This command generates binstubs for executables in `GEM_NAME`. +Binstubs are put into `bin`, or the `--path` directory if one has been set. +Calling binstubs with [GEM [GEM]] will create binstubs for all given gems. + +## OPTIONS + +* `--force`: + Overwrite existing binstubs if they exist. + +* `--path`: + The location to install the specified binstubs to. This defaults to `bin`. + +* `--standalone`: + Makes binstubs that can work without depending on Rubygems or Bundler at + runtime. + +* `--shebang`: + Specify a different shebang executable name than the default (default 'ruby') + +## BUNDLE INSTALL --BINSTUBS + +To create binstubs for all the gems in the bundle you can use the `--binstubs` +flag in [bundle install(1)](bundle-install.1.html). diff --git a/man/bundle-check.1 b/man/bundle-check.1 new file mode 100644 index 00000000000000..35431972d5e834 --- /dev/null +++ b/man/bundle-check.1 @@ -0,0 +1,31 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-CHECK" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems +. +.SH "SYNOPSIS" +\fBbundle check\fR [\-\-dry\-run] [\-\-gemfile=FILE] [\-\-path=PATH] +. +.SH "DESCRIPTION" +\fBcheck\fR searches the local machine for each of the gems requested in the Gemfile\. If all gems are found, Bundler prints a success message and exits with a status of 0\. +. +.P +If not, the first missing gem is listed and Bundler exits status 1\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-dry\-run\fR +Locks the [\fBGemfile(5)\fR][Gemfile(5)] before running the command\. +. +.TP +\fB\-\-gemfile\fR +Use the specified gemfile instead of the [\fBGemfile(5)\fR][Gemfile(5)]\. +. +.TP +\fB\-\-path\fR +Specify a different path than the system default (\fB$BUNDLE_PATH\fR or \fB$GEM_HOME\fR)\. Bundler will remember this value for future installs on this machine\. + diff --git a/man/bundle-check.1.txt b/man/bundle-check.1.txt new file mode 100644 index 00000000000000..20cfe264469ecc --- /dev/null +++ b/man/bundle-check.1.txt @@ -0,0 +1,33 @@ +BUNDLE-CHECK(1) BUNDLE-CHECK(1) + + + +1mNAME0m + 1mbundle-check 22m- Verifies if dependencies are satisfied by installed gems + +1mSYNOPSIS0m + 1mbundle check 22m[--dry-run] [--gemfile=FILE] [--path=PATH] + +1mDESCRIPTION0m + 1mcheck 22msearches the local machine for each of the gems requested in the + Gemfile. If all gems are found, Bundler prints a success message and + exits with a status of 0. + + If not, the first missing gem is listed and Bundler exits status 1. + +1mOPTIONS0m + 1m--dry-run0m + Locks the [1mGemfile(5)22m][Gemfile(5)] before running the command. + + 1m--gemfile0m + Use the specified gemfile instead of the [1mGemfile(5)22m][Gem- + file(5)]. + + 1m--path 22mSpecify a different path than the system default (1m$BUNDLE_PATH0m + or 1m$GEM_HOME22m). Bundler will remember this value for future + installs on this machine. + + + + + November 2018 BUNDLE-CHECK(1) diff --git a/man/bundle-check.ronn b/man/bundle-check.ronn new file mode 100644 index 00000000000000..f2846b8ff2a066 --- /dev/null +++ b/man/bundle-check.ronn @@ -0,0 +1,26 @@ +bundle-check(1) -- Verifies if dependencies are satisfied by installed gems +=========================================================================== + +## SYNOPSIS + +`bundle check` [--dry-run] + [--gemfile=FILE] + [--path=PATH] + +## DESCRIPTION + +`check` searches the local machine for each of the gems requested in the +Gemfile. If all gems are found, Bundler prints a success message and exits with +a status of 0. + +If not, the first missing gem is listed and Bundler exits status 1. + +## OPTIONS + +* `--dry-run`: + Locks the [`Gemfile(5)`][Gemfile(5)] before running the command. +* `--gemfile`: + Use the specified gemfile instead of the [`Gemfile(5)`][Gemfile(5)]. +* `--path`: + Specify a different path than the system default (`$BUNDLE_PATH` or `$GEM_HOME`). + Bundler will remember this value for future installs on this machine. diff --git a/man/bundle-clean.1 b/man/bundle-clean.1 new file mode 100644 index 00000000000000..681f41effc0d69 --- /dev/null +++ b/man/bundle-clean.1 @@ -0,0 +1,24 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-CLEAN" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory +. +.SH "SYNOPSIS" +\fBbundle clean\fR [\-\-dry\-run] [\-\-force] +. +.SH "DESCRIPTION" +This command will remove all unused gems in your bundler directory\. This is useful when you have made many changes to your gem dependencies\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-dry\-run\fR +Print the changes, but do not clean the unused gems\. +. +.TP +\fB\-\-force\fR +Force a clean even if \fB\-\-path\fR is not set\. + diff --git a/man/bundle-clean.1.txt b/man/bundle-clean.1.txt new file mode 100644 index 00000000000000..f4b507db655066 --- /dev/null +++ b/man/bundle-clean.1.txt @@ -0,0 +1,26 @@ +BUNDLE-CLEAN(1) BUNDLE-CLEAN(1) + + + +1mNAME0m + 1mbundle-clean 22m- Cleans up unused gems in your bundler directory + +1mSYNOPSIS0m + 1mbundle clean 22m[--dry-run] [--force] + +1mDESCRIPTION0m + This command will remove all unused gems in your bundler directory. + This is useful when you have made many changes to your gem dependen- + cies. + +1mOPTIONS0m + 1m--dry-run0m + Print the changes, but do not clean the unused gems. + + 1m--force0m + Force a clean even if 1m--path 22mis not set. + + + + + November 2018 BUNDLE-CLEAN(1) diff --git a/man/bundle-clean.ronn b/man/bundle-clean.ronn new file mode 100644 index 00000000000000..de239917822fa4 --- /dev/null +++ b/man/bundle-clean.ronn @@ -0,0 +1,18 @@ +bundle-clean(1) -- Cleans up unused gems in your bundler directory +================================================================== + +## SYNOPSIS + +`bundle clean` [--dry-run] [--force] + +## DESCRIPTION + +This command will remove all unused gems in your bundler directory. This is +useful when you have made many changes to your gem dependencies. + +## OPTIONS + +* `--dry-run`: + Print the changes, but do not clean the unused gems. +* `--force`: + Force a clean even if `--path` is not set. diff --git a/man/bundle-config.1 b/man/bundle-config.1 new file mode 100644 index 00000000000000..8c0b3683c6677d --- /dev/null +++ b/man/bundle-config.1 @@ -0,0 +1,497 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-CONFIG" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-config\fR \- Set bundler configuration options +. +.SH "SYNOPSIS" +\fBbundle config\fR [\fIname\fR [\fIvalue\fR]] +. +.SH "DESCRIPTION" +This command allows you to interact with Bundler\'s configuration system\. +. +.P +Bundler loads configuration settings in this order: +. +.IP "1." 4 +Local config (\fBapp/\.bundle/config\fR) +. +.IP "2." 4 +Environmental variables (\fBENV\fR) +. +.IP "3." 4 +Global config (\fB~/\.bundle/config\fR) +. +.IP "4." 4 +Bundler default config +. +.IP "" 0 +. +.P +Executing \fBbundle config\fR with no parameters will print a list of all bundler configuration for the current bundle, and where that configuration was set\. +. +.P +Executing \fBbundle config \fR will print the value of that configuration setting, and where it was set\. +. +.P +Executing \fBbundle config \fR will set that configuration to the value specified for all bundles executed as the current user\. The configuration will be stored in \fB~/\.bundle/config\fR\. If \fIname\fR already is set, \fIname\fR will be overridden and user will be warned\. +. +.P +Executing \fBbundle config \-\-global \fR works the same as above\. +. +.P +Executing \fBbundle config \-\-local \fR will set that configuration to the local application\. The configuration will be stored in \fBapp/\.bundle/config\fR\. +. +.P +Executing \fBbundle config \-\-delete \fR will delete the configuration in both local and global sources\. Not compatible with \-\-global or \-\-local flag\. +. +.P +Executing bundle with the \fBBUNDLE_IGNORE_CONFIG\fR environment variable set will cause it to ignore all configuration\. +. +.P +Executing \fBbundle config disable_multisource true\fR upgrades the warning about the Gemfile containing multiple primary sources to an error\. Executing \fBbundle config \-\-delete disable_multisource\fR downgrades this error to a warning\. +. +.SH "REMEMBERING OPTIONS" +Flags passed to \fBbundle install\fR or the Bundler runtime, such as \fB\-\-path foo\fR or \fB\-\-without production\fR, are not remembered between commands\. If these options must be remembered,they must be set using \fBbundle config\fR (e\.g\., \fBbundle config path foo\fR)\. +. +.P +The options that can be configured are: +. +.TP +\fBbin\fR +Creates a directory (defaults to \fB~/bin\fR) and place any executables from the gem there\. These executables run in Bundler\'s context\. If used, you might add this directory to your environment\'s \fBPATH\fR variable\. For instance, if the \fBrails\fR gem comes with a \fBrails\fR executable, this flag will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. +. +.TP +\fBdeployment\fR +In deployment mode, Bundler will \'roll\-out\' the bundle for \fBproduction\fR use\. Please check carefully if you want to have this option enabled in \fBdevelopment\fR or \fBtest\fR environments\. +. +.TP +\fBpath\fR +The location to install the specified gems to\. This defaults to Rubygems\' setting\. Bundler shares this location with Rubygems, \fBgem install \.\.\.\fR will have gem installed there, too\. Therefore, gems installed without a \fB\-\-path \.\.\.\fR setting will show up by calling \fBgem list\fR\. Accordingly, gems installed to other locations will not get listed\. +. +.TP +\fBwithout\fR +A space\-separated list of groups referencing gems to skip during installation\. +. +.TP +\fBwith\fR +A space\-separated list of groups referencing gems to include during installation\. +. +.SH "BUILD OPTIONS" +You can use \fBbundle config\fR to give Bundler the flags to pass to the gem installer every time bundler tries to install a particular gem\. +. +.P +A very common example, the \fBmysql\fR gem, requires Snow Leopard users to pass configuration flags to \fBgem install\fR to specify where to find the \fBmysql_config\fR executable\. +. +.IP "" 4 +. +.nf + +gem install mysql \-\- \-\-with\-mysql\-config=/usr/local/mysql/bin/mysql_config +. +.fi +. +.IP "" 0 +. +.P +Since the specific location of that executable can change from machine to machine, you can specify these flags on a per\-machine basis\. +. +.IP "" 4 +. +.nf + +bundle config build\.mysql \-\-with\-mysql\-config=/usr/local/mysql/bin/mysql_config +. +.fi +. +.IP "" 0 +. +.P +After running this command, every time bundler needs to install the \fBmysql\fR gem, it will pass along the flags you specified\. +. +.SH "CONFIGURATION KEYS" +Configuration keys in bundler have two forms: the canonical form and the environment variable form\. +. +.P +For instance, passing the \fB\-\-without\fR flag to bundle install(1) \fIbundle\-install\.1\.html\fR prevents Bundler from installing certain groups specified in the Gemfile(5)\. Bundler persists this value in \fBapp/\.bundle/config\fR so that calls to \fBBundler\.setup\fR do not try to find gems from the \fBGemfile\fR that you didn\'t install\. Additionally, subsequent calls to bundle install(1) \fIbundle\-install\.1\.html\fR remember this setting and skip those groups\. +. +.P +The canonical form of this configuration is \fB"without"\fR\. To convert the canonical form to the environment variable form, capitalize it, and prepend \fBBUNDLE_\fR\. The environment variable form of \fB"without"\fR is \fBBUNDLE_WITHOUT\fR\. +. +.P +Any periods in the configuration keys must be replaced with two underscores when setting it via environment variables\. The configuration key \fBlocal\.rack\fR becomes the environment variable \fBBUNDLE_LOCAL__RACK\fR\. +. +.SH "LIST OF AVAILABLE KEYS" +The following is a list of all configuration keys and their purpose\. You can learn more about their operation in bundle install(1) \fIbundle\-install\.1\.html\fR\. +. +.IP "\(bu" 4 +\fBallow_bundler_dependency_conflicts\fR (\fBBUNDLE_ALLOW_BUNDLER_DEPENDENCY_CONFLICTS\fR): Allow resolving to specifications that have dependencies on \fBbundler\fR that are incompatible with the running Bundler version\. +. +.IP "\(bu" 4 +\fBallow_deployment_source_credential_changes\fR (\fBBUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES\fR): When in deployment mode, allow changing the credentials to a gem\'s source\. Ex: \fBhttps://some\.host\.com/gems/path/\fR \-> \fBhttps://user_name:password@some\.host\.com/gems/path\fR +. +.IP "\(bu" 4 +\fBallow_offline_install\fR (\fBBUNDLE_ALLOW_OFFLINE_INSTALL\fR): Allow Bundler to use cached data when installing without network access\. +. +.IP "\(bu" 4 +\fBauto_clean_without_path\fR (\fBBUNDLE_AUTO_CLEAN_WITHOUT_PATH\fR): Automatically run \fBbundle clean\fR after installing when an explicit \fBpath\fR has not been set and Bundler is not installing into the system gems\. +. +.IP "\(bu" 4 +\fBauto_install\fR (\fBBUNDLE_AUTO_INSTALL\fR): Automatically run \fBbundle install\fR when gems are missing\. +. +.IP "\(bu" 4 +\fBbin\fR (\fBBUNDLE_BIN\fR): Install executables from gems in the bundle to the specified directory\. Defaults to \fBfalse\fR\. +. +.IP "\(bu" 4 +\fBcache_all\fR (\fBBUNDLE_CACHE_ALL\fR): Cache all gems, including path and git gems\. +. +.IP "\(bu" 4 +\fBcache_all_platforms\fR (\fBBUNDLE_CACHE_ALL_PLATFORMS\fR): Cache gems for all platforms\. +. +.IP "\(bu" 4 +\fBcache_path\fR (\fBBUNDLE_CACHE_PATH\fR): The directory that bundler will place cached gems in when running \fBbundle package\fR, and that bundler will look in when installing gems\. Defaults to \fBvendor/bundle\fR\. +. +.IP "\(bu" 4 +\fBclean\fR (\fBBUNDLE_CLEAN\fR): Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. +. +.IP "\(bu" 4 +\fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. +. +.IP "\(bu" 4 +\fBdefault_install_uses_path\fR (\fBBUNDLE_DEFAULT_INSTALL_USES_PATH\fR): Whether a \fBbundle install\fR without an explicit \fB\-\-path\fR argument defaults to installing gems in \fB\.bundle\fR\. +. +.IP "\(bu" 4 +\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR): Disallow changes to the \fBGemfile\fR\. When the \fBGemfile\fR is changed and the lockfile has not been updated, running Bundler commands will be blocked\. +. +.IP "\(bu" 4 +\fBdisable_checksum_validation\fR (\fBBUNDLE_DISABLE_CHECKSUM_VALIDATION\fR): Allow installing gems even if they do not match the checksum provided by RubyGems\. +. +.IP "\(bu" 4 +\fBdisable_exec_load\fR (\fBBUNDLE_DISABLE_EXEC_LOAD\fR): Stop Bundler from using \fBload\fR to launch an executable in\-process in \fBbundle exec\fR\. +. +.IP "\(bu" 4 +\fBdisable_local_branch_check\fR (\fBBUNDLE_DISABLE_LOCAL_BRANCH_CHECK\fR): Allow Bundler to use a local git override without a branch specified in the Gemfile\. +. +.IP "\(bu" 4 +\fBdisable_multisource\fR (\fBBUNDLE_DISABLE_MULTISOURCE\fR): When set, Gemfiles containing multiple sources will produce errors instead of warnings\. Use \fBbundle config \-\-delete disable_multisource\fR to unset\. +. +.IP "\(bu" 4 +\fBdisable_platform_warnings\fR (\fBBUNDLE_DISABLE_PLATFORM_WARNINGS\fR): Disable warnings during bundle install when a dependency is unused on the current platform\. +. +.IP "\(bu" 4 +\fBdisable_shared_gems\fR (\fBBUNDLE_DISABLE_SHARED_GEMS\fR): Stop Bundler from accessing gems installed to RubyGems\' normal location\. +. +.IP "\(bu" 4 +\fBdisable_version_check\fR (\fBBUNDLE_DISABLE_VERSION_CHECK\fR): Stop Bundler from checking if a newer Bundler version is available on rubygems\.org\. +. +.IP "\(bu" 4 +\fBerror_on_stderr\fR (\fBBUNDLE_ERROR_ON_STDERR\fR): Print Bundler errors to stderr\. +. +.IP "\(bu" 4 +\fBforce_ruby_platform\fR (\fBBUNDLE_FORCE_RUBY_PLATFORM\fR): Ignore the current machine\'s platform and install only \fBruby\fR platform gems\. As a result, gems with native extensions will be compiled from source\. +. +.IP "\(bu" 4 +\fBfrozen\fR (\fBBUNDLE_FROZEN\fR): Disallow changes to the \fBGemfile\fR\. When the \fBGemfile\fR is changed and the lockfile has not been updated, running Bundler commands will be blocked\. Defaults to \fBtrue\fR when \fB\-\-deployment\fR is used\. +. +.IP "\(bu" 4 +\fBgem\.push_key\fR (\fBBUNDLE_GEM__PUSH_KEY\fR): Sets the \fB\-\-key\fR parameter for \fBgem push\fR when using the \fBrake release\fR command with a private gemstash server\. +. +.IP "\(bu" 4 +\fBgemfile\fR (\fBBUNDLE_GEMFILE\fR): The name of the file that bundler should use as the \fBGemfile\fR\. This location of this file also sets the root of the project, which is used to resolve relative paths in the \fBGemfile\fR, among other things\. By default, bundler will search up from the current working directory until it finds a \fBGemfile\fR\. +. +.IP "\(bu" 4 +\fBglobal_gem_cache\fR (\fBBUNDLE_GLOBAL_GEM_CACHE\fR): Whether Bundler should cache all gems globally, rather than locally to the installing Ruby installation\. +. +.IP "\(bu" 4 +\fBglobal_path_appends_ruby_scope\fR (\fBBUNDLE_GLOBAL_PATH_APPENDS_RUBY_SCOPE\fR): Whether Bundler should append the Ruby scope (e\.g\. engine and ABI version) to a globally\-configured path\. +. +.IP "\(bu" 4 +\fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR): When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\. +. +.IP "\(bu" 4 +\fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR) Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\. +. +.IP "\(bu" 4 +\fBjobs\fR (\fBBUNDLE_JOBS\fR): The number of gems Bundler can install in parallel\. Defaults to 1\. +. +.IP "\(bu" 4 +\fBlist_command\fR (\fBBUNDLE_LIST_COMMAND\fR) Enable new list command feature +. +.IP "\(bu" 4 +\fBmajor_deprecations\fR (\fBBUNDLE_MAJOR_DEPRECATIONS\fR): Whether Bundler should print deprecation warnings for behavior that will be changed in the next major version\. +. +.IP "\(bu" 4 +\fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR): Whether \fBbundle package\fR should skip installing gems\. +. +.IP "\(bu" 4 +\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR): Whether Bundler should leave outdated gems unpruned when caching\. +. +.IP "\(bu" 4 +\fBonly_update_to_newer_versions\fR (\fBBUNDLE_ONLY_UPDATE_TO_NEWER_VERSIONS\fR): During \fBbundle update\fR, only resolve to newer versions of the gems in the lockfile\. +. +.IP "\(bu" 4 +\fBpath\fR (\fBBUNDLE_PATH\fR): The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. Defaults to \fBGem\.dir\fR\. When \-\-deployment is used, defaults to vendor/bundle\. +. +.IP "\(bu" 4 +\fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR): Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\. +. +.IP "\(bu" 4 +\fBpath_relative_to_cwd\fR (\fBBUNDLE_PATH_RELATIVE_TO_CWD\fR) Makes \fB\-\-path\fR relative to the CWD instead of the \fBGemfile\fR\. +. +.IP "\(bu" 4 +\fBplugins\fR (\fBBUNDLE_PLUGINS\fR): Enable Bundler\'s experimental plugin system\. +. +.IP "\(bu" 4 +\fBprefer_gems_rb\fR (\fBBUNDLE_PREFER_GEMS_RB\fR) Prefer \fBgems\.rb\fR to \fBGemfile\fR when Bundler is searching for a Gemfile\. +. +.IP "\(bu" 4 +\fBprint_only_version_number\fR (\fBBUNDLE_PRINT_ONLY_VERSION_NUMBER\fR) Print only version number from \fBbundler \-\-version\fR\. +. +.IP "\(bu" 4 +\fBredirect\fR (\fBBUNDLE_REDIRECT\fR): The number of redirects allowed for network requests\. Defaults to \fB5\fR\. +. +.IP "\(bu" 4 +\fBretry\fR (\fBBUNDLE_RETRY\fR): The number of times to retry failed network requests\. Defaults to \fB3\fR\. +. +.IP "\(bu" 4 +\fBsetup_makes_kernel_gem_public\fR (\fBBUNDLE_SETUP_MAKES_KERNEL_GEM_PUBLIC\fR): Have \fBBundler\.setup\fR make the \fBKernel#gem\fR method public, even though RubyGems declares it as private\. +. +.IP "\(bu" 4 +\fBshebang\fR (\fBBUNDLE_SHEBANG\fR): The program name that should be invoked for generated binstubs\. Defaults to the ruby install name used to generate the binstub\. +. +.IP "\(bu" 4 +\fBsilence_root_warning\fR (\fBBUNDLE_SILENCE_ROOT_WARNING\fR): Silence the warning Bundler prints when installing gems as root\. +. +.IP "\(bu" 4 +\fBskip_default_git_sources\fR (\fBBUNDLE_SKIP_DEFAULT_GIT_SOURCES\fR): Whether Bundler should skip adding default git source shortcuts to the Gemfile DSL\. +. +.IP "\(bu" 4 +\fBspecific_platform\fR (\fBBUNDLE_SPECIFIC_PLATFORM\fR): Allow bundler to resolve for the specific running platform and store it in the lockfile, instead of only using a generic platform\. A specific platform is the exact platform triple reported by \fBGem::Platform\.local\fR, such as \fBx86_64\-darwin\-16\fR or \fBuniversal\-java\-1\.8\fR\. On the other hand, generic platforms are those such as \fBruby\fR, \fBmswin\fR, or \fBjava\fR\. In this example, \fBx86_64\-darwin\-16\fR would map to \fBruby\fR and \fBuniversal\-java\-1\.8\fR to \fBjava\fR\. +. +.IP "\(bu" 4 +\fBssl_ca_cert\fR (\fBBUNDLE_SSL_CA_CERT\fR): Path to a designated CA certificate file or folder containing multiple certificates for trusted CAs in PEM format\. +. +.IP "\(bu" 4 +\fBssl_client_cert\fR (\fBBUNDLE_SSL_CLIENT_CERT\fR): Path to a designated file containing a X\.509 client certificate and key in PEM format\. +. +.IP "\(bu" 4 +\fBssl_verify_mode\fR (\fBBUNDLE_SSL_VERIFY_MODE\fR): The SSL verification mode Bundler uses when making HTTPS requests\. Defaults to verify peer\. +. +.IP "\(bu" 4 +\fBsuppress_install_using_messages\fR (\fBBUNDLE_SUPPRESS_INSTALL_USING_MESSAGES\fR): Avoid printing \fBUsing \.\.\.\fR messages during installation when the version of a gem has not changed\. +. +.IP "\(bu" 4 +\fBsystem_bindir\fR (\fBBUNDLE_SYSTEM_BINDIR\fR): The location where RubyGems installs binstubs\. Defaults to \fBGem\.bindir\fR\. +. +.IP "\(bu" 4 +\fBtimeout\fR (\fBBUNDLE_TIMEOUT\fR): The seconds allowed before timing out for network requests\. Defaults to \fB10\fR\. +. +.IP "\(bu" 4 +\fBunlock_source_unlocks_spec\fR (\fBBUNDLE_UNLOCK_SOURCE_UNLOCKS_SPEC\fR): Whether running \fBbundle update \-\-source NAME\fR unlocks a gem with the given name\. Defaults to \fBtrue\fR\. +. +.IP "\(bu" 4 +\fBupdate_requires_all_flag\fR (\fBBUNDLE_UPDATE_REQUIRES_ALL_FLAG\fR) Require passing \fB\-\-all\fR to \fBbundle update\fR when everything should be updated, and disallow passing no options to \fBbundle update\fR\. +. +.IP "\(bu" 4 +\fBuser_agent\fR (\fBBUNDLE_USER_AGENT\fR): The custom user agent fragment Bundler includes in API requests\. +. +.IP "\(bu" 4 +\fBwith\fR (\fBBUNDLE_WITH\fR): A \fB:\fR\-separated list of groups whose gems bundler should install\. +. +.IP "\(bu" 4 +\fBwithout\fR (\fBBUNDLE_WITHOUT\fR): A \fB:\fR\-separated list of groups whose gems bundler should not install\. +. +.IP "" 0 +. +.P +In general, you should set these settings per\-application by using the applicable flag to the bundle install(1) \fIbundle\-install\.1\.html\fR or bundle package(1) \fIbundle\-package\.1\.html\fR command\. +. +.P +You can set them globally either via environment variables or \fBbundle config\fR, whichever is preferable for your setup\. If you use both, environment variables will take preference over global settings\. +. +.SH "LOCAL GIT REPOS" +Bundler also allows you to work against a git repository locally instead of using the remote version\. This can be achieved by setting up a local override: +. +.IP "" 4 +. +.nf + +bundle config local\.GEM_NAME /path/to/local/git/repository +. +.fi +. +.IP "" 0 +. +.P +For example, in order to use a local Rack repository, a developer could call: +. +.IP "" 4 +. +.nf + +bundle config local\.rack ~/Work/git/rack +. +.fi +. +.IP "" 0 +. +.P +Now instead of checking out the remote git repository, the local override will be used\. Similar to a path source, every time the local git repository change, changes will be automatically picked up by Bundler\. This means a commit in the local git repo will update the revision in the \fBGemfile\.lock\fR to the local git repo revision\. This requires the same attention as git submodules\. Before pushing to the remote, you need to ensure the local override was pushed, otherwise you may point to a commit that only exists in your local machine\. You\'ll also need to CGI escape your usernames and passwords as well\. +. +.P +Bundler does many checks to ensure a developer won\'t work with invalid references\. Particularly, we force a developer to specify a branch in the \fBGemfile\fR in order to use this feature\. If the branch specified in the \fBGemfile\fR and the current branch in the local git repository do not match, Bundler will abort\. This ensures that a developer is always working against the correct branches, and prevents accidental locking to a different branch\. +. +.P +Finally, Bundler also ensures that the current revision in the \fBGemfile\.lock\fR exists in the local git repository\. By doing this, Bundler forces you to fetch the latest changes in the remotes\. +. +.SH "MIRRORS OF GEM SOURCES" +Bundler supports overriding gem sources with mirrors\. This allows you to configure rubygems\.org as the gem source in your Gemfile while still using your mirror to fetch gems\. +. +.IP "" 4 +. +.nf + +bundle config mirror\.SOURCE_URL MIRROR_URL +. +.fi +. +.IP "" 0 +. +.P +For example, to use a mirror of rubygems\.org hosted at rubygems\-mirror\.org: +. +.IP "" 4 +. +.nf + +bundle config mirror\.http://rubygems\.org http://rubygems\-mirror\.org +. +.fi +. +.IP "" 0 +. +.P +Each mirror also provides a fallback timeout setting\. If the mirror does not respond within the fallback timeout, Bundler will try to use the original server instead of the mirror\. +. +.IP "" 4 +. +.nf + +bundle config mirror\.SOURCE_URL\.fallback_timeout TIMEOUT +. +.fi +. +.IP "" 0 +. +.P +For example, to fall back to rubygems\.org after 3 seconds: +. +.IP "" 4 +. +.nf + +bundle config mirror\.https://rubygems\.org\.fallback_timeout 3 +. +.fi +. +.IP "" 0 +. +.P +The default fallback timeout is 0\.1 seconds, but the setting can currently only accept whole seconds (for example, 1, 15, or 30)\. +. +.SH "CREDENTIALS FOR GEM SOURCES" +Bundler allows you to configure credentials for any gem source, which allows you to avoid putting secrets into your Gemfile\. +. +.IP "" 4 +. +.nf + +bundle config SOURCE_HOSTNAME USERNAME:PASSWORD +. +.fi +. +.IP "" 0 +. +.P +For example, to save the credentials of user \fBclaudette\fR for the gem source at \fBgems\.longerous\.com\fR, you would run: +. +.IP "" 4 +. +.nf + +bundle config gems\.longerous\.com claudette:s00pers3krit +. +.fi +. +.IP "" 0 +. +.P +Or you can set the credentials as an environment variable like this: +. +.IP "" 4 +. +.nf + +export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit" +. +.fi +. +.IP "" 0 +. +.P +For gems with a git source with HTTP(S) URL you can specify credentials like so: +. +.IP "" 4 +. +.nf + +bundle config https://github\.com/bundler/bundler\.git username:password +. +.fi +. +.IP "" 0 +. +.P +Or you can set the credentials as an environment variable like so: +. +.IP "" 4 +. +.nf + +export BUNDLE_GITHUB__COM=username:password +. +.fi +. +.IP "" 0 +. +.P +This is especially useful for private repositories on hosts such as Github, where you can use personal OAuth tokens: +. +.IP "" 4 +. +.nf + +export BUNDLE_GITHUB__COM=abcd0123generatedtoken:x\-oauth\-basic +. +.fi +. +.IP "" 0 +. +.SH "CONFIGURE BUNDLER DIRECTORIES" +Bundler\'s home, config, cache and plugin directories are able to be configured through environment variables\. The default location for Bundler\'s home directory is \fB~/\.bundle\fR, which all directories inherit from by default\. The following outlines the available environment variables and their default values +. +.IP "" 4 +. +.nf + +BUNDLE_USER_HOME : $HOME/\.bundle +BUNDLE_USER_CACHE : $BUNDLE_USER_HOME/cache +BUNDLE_USER_CONFIG : $BUNDLE_USER_HOME/config +BUNDLE_USER_PLUGIN : $BUNDLE_USER_HOME/plugin +. +.fi +. +.IP "" 0 + diff --git a/man/bundle-config.1.txt b/man/bundle-config.1.txt new file mode 100644 index 00000000000000..9b071593425fdd --- /dev/null +++ b/man/bundle-config.1.txt @@ -0,0 +1,529 @@ +BUNDLE-CONFIG(1) BUNDLE-CONFIG(1) + + + +1mNAME0m + 1mbundle-config 22m- Set bundler configuration options + +1mSYNOPSIS0m + 1mbundle config 22m[4mname24m [4mvalue24m]] + +1mDESCRIPTION0m + This command allows you to interact with Bundler's configuration sys- + tem. + + Bundler loads configuration settings in this order: + + 1. Local config (1mapp/.bundle/config22m) + + 2. Environmental variables (1mENV22m) + + 3. Global config (1m~/.bundle/config22m) + + 4. Bundler default config + + + + Executing 1mbundle config 22mwith no parameters will print a list of all + bundler configuration for the current bundle, and where that configura- + tion was set. + + Executing 1mbundle config 22mwill print the value of that configura- + tion setting, and where it was set. + + Executing 1mbundle config 22mwill set that configuration to + the value specified for all bundles executed as the current user. The + configuration will be stored in 1m~/.bundle/config22m. If 4mname24m already is + set, 4mname24m will be overridden and user will be warned. + + Executing 1mbundle config --global 22mworks the same as + above. + + Executing 1mbundle config --local 22mwill set that configura- + tion to the local application. The configuration will be stored in + 1mapp/.bundle/config22m. + + Executing 1mbundle config --delete 22mwill delete the configuration + in both local and global sources. Not compatible with --global or + --local flag. + + Executing bundle with the 1mBUNDLE_IGNORE_CONFIG 22menvironment variable set + will cause it to ignore all configuration. + + Executing 1mbundle config disable_multisource true 22mupgrades the warning + about the Gemfile containing multiple primary sources to an error. Exe- + cuting 1mbundle config --delete disable_multisource 22mdowngrades this error + to a warning. + +1mREMEMBERING OPTIONS0m + Flags passed to 1mbundle install 22mor the Bundler runtime, such as 1m--path0m + 1mfoo 22mor 1m--without production22m, are not remembered between commands. If + these options must be remembered,they must be set using 1mbundle config0m + (e.g., 1mbundle config path foo22m). + + The options that can be configured are: + + 1mbin 22mCreates a directory (defaults to 1m~/bin22m) and place any executa- + bles from the gem there. These executables run in Bundler's con- + text. If used, you might add this directory to your environ- + ment's 1mPATH 22mvariable. For instance, if the 1mrails 22mgem comes with + a 1mrails 22mexecutable, this flag will create a 1mbin/rails 22mexecutable + that ensures that all referred dependencies will be resolved + using the bundled gems. + + 1mdeployment0m + In deployment mode, Bundler will 'roll-out' the bundle for 1mpro-0m + 1mduction 22muse. Please check carefully if you want to have this + option enabled in 1mdevelopment 22mor 1mtest 22menvironments. + + 1mpath 22mThe location to install the specified gems to. This defaults to + Rubygems' setting. Bundler shares this location with Rubygems, + 1mgem install ... 22mwill have gem installed there, too. Therefore, + gems installed without a 1m--path ... 22msetting will show up by + calling 1mgem list22m. Accordingly, gems installed to other locations + will not get listed. + + 1mwithout0m + A space-separated list of groups referencing gems to skip during + installation. + + 1mwith 22mA space-separated list of groups referencing gems to include + during installation. + +1mBUILD OPTIONS0m + You can use 1mbundle config 22mto give Bundler the flags to pass to the gem + installer every time bundler tries to install a particular gem. + + A very common example, the 1mmysql 22mgem, requires Snow Leopard users to + pass configuration flags to 1mgem install 22mto specify where to find the + 1mmysql_config 22mexecutable. + + + + gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config + + + + Since the specific location of that executable can change from machine + to machine, you can specify these flags on a per-machine basis. + + + + bundle config build.mysql --with-mysql-config=/usr/local/mysql/bin/mysql_config + + + + After running this command, every time bundler needs to install the + 1mmysql 22mgem, it will pass along the flags you specified. + +1mCONFIGURATION KEYS0m + Configuration keys in bundler have two forms: the canonical form and + the environment variable form. + + For instance, passing the 1m--without 22mflag to bundle install(1) 4mbun-0m + 4mdle-install.1.html24m prevents Bundler from installing certain groups + specified in the Gemfile(5). Bundler persists this value in 1mapp/.bun-0m + 1mdle/config 22mso that calls to 1mBundler.setup 22mdo not try to find gems from + the 1mGemfile 22mthat you didn't install. Additionally, subsequent calls to + bundle install(1) 4mbundle-install.1.html24m remember this setting and skip + those groups. + + The canonical form of this configuration is 1m"without"22m. To convert the + canonical form to the environment variable form, capitalize it, and + prepend 1mBUNDLE_22m. The environment variable form of 1m"without" 22mis 1mBUN-0m + 1mDLE_WITHOUT22m. + + Any periods in the configuration keys must be replaced with two under- + scores when setting it via environment variables. The configuration key + 1mlocal.rack 22mbecomes the environment variable 1mBUNDLE_LOCAL__RACK22m. + +1mLIST OF AVAILABLE KEYS0m + The following is a list of all configuration keys and their purpose. + You can learn more about their operation in bundle install(1) 4mbun-0m + 4mdle-install.1.html24m. + + o 1mallow_bundler_dependency_conflicts 22m(1mBUNDLE_ALLOW_BUNDLER_DEPEN-0m + 1mDENCY_CONFLICTS22m): Allow resolving to specifications that have + dependencies on 1mbundler 22mthat are incompatible with the running + Bundler version. + + o 1mallow_deployment_source_credential_changes 22m(1mBUNDLE_ALLOW_DEPLOY-0m + 1mMENT_SOURCE_CREDENTIAL_CHANGES22m): When in deployment mode, allow + changing the credentials to a gem's source. Ex: + 1mhttps://some.host.com/gems/path/ 22m-> 1mhttps://user_name:pass-0m + 1mword@some.host.com/gems/path0m + + o 1mallow_offline_install 22m(1mBUNDLE_ALLOW_OFFLINE_INSTALL22m): Allow Bundler + to use cached data when installing without network access. + + o 1mauto_clean_without_path 22m(1mBUNDLE_AUTO_CLEAN_WITHOUT_PATH22m): Automati- + cally run 1mbundle clean 22mafter installing when an explicit 1mpath 22mhas + not been set and Bundler is not installing into the system gems. + + o 1mauto_install 22m(1mBUNDLE_AUTO_INSTALL22m): Automatically run 1mbundle0m + 1minstall 22mwhen gems are missing. + + o 1mbin 22m(1mBUNDLE_BIN22m): Install executables from gems in the bundle to + the specified directory. Defaults to 1mfalse22m. + + o 1mcache_all 22m(1mBUNDLE_CACHE_ALL22m): Cache all gems, including path and + git gems. + + o 1mcache_all_platforms 22m(1mBUNDLE_CACHE_ALL_PLATFORMS22m): Cache gems for + all platforms. + + o 1mcache_path 22m(1mBUNDLE_CACHE_PATH22m): The directory that bundler will + place cached gems in when running 1mbundle package22m, and that bundler + will look in when installing gems. Defaults to 1mvendor/bundle22m. + + o 1mclean 22m(1mBUNDLE_CLEAN22m): Whether Bundler should run 1mbundle clean 22mauto- + matically after 1mbundle install22m. + + o 1mconsole 22m(1mBUNDLE_CONSOLE22m): The console that 1mbundle console 22mstarts. + Defaults to 1mirb22m. + + o 1mdefault_install_uses_path 22m(1mBUNDLE_DEFAULT_INSTALL_USES_PATH22m): + Whether a 1mbundle install 22mwithout an explicit 1m--path 22margument + defaults to installing gems in 1m.bundle22m. + + o 1mdeployment 22m(1mBUNDLE_DEPLOYMENT22m): Disallow changes to the 1mGemfile22m. + When the 1mGemfile 22mis changed and the lockfile has not been updated, + running Bundler commands will be blocked. + + o 1mdisable_checksum_validation 22m(1mBUNDLE_DISABLE_CHECKSUM_VALIDATION22m): + Allow installing gems even if they do not match the checksum pro- + vided by RubyGems. + + o 1mdisable_exec_load 22m(1mBUNDLE_DISABLE_EXEC_LOAD22m): Stop Bundler from + using 1mload 22mto launch an executable in-process in 1mbundle exec22m. + + o 1mdisable_local_branch_check 22m(1mBUNDLE_DISABLE_LOCAL_BRANCH_CHECK22m): + Allow Bundler to use a local git override without a branch speci- + fied in the Gemfile. + + o 1mdisable_multisource 22m(1mBUNDLE_DISABLE_MULTISOURCE22m): When set, Gem- + files containing multiple sources will produce errors instead of + warnings. Use 1mbundle config --delete disable_multisource 22mto unset. + + o 1mdisable_platform_warnings 22m(1mBUNDLE_DISABLE_PLATFORM_WARNINGS22m): Dis- + able warnings during bundle install when a dependency is unused on + the current platform. + + o 1mdisable_shared_gems 22m(1mBUNDLE_DISABLE_SHARED_GEMS22m): Stop Bundler from + accessing gems installed to RubyGems' normal location. + + o 1mdisable_version_check 22m(1mBUNDLE_DISABLE_VERSION_CHECK22m): Stop Bundler + from checking if a newer Bundler version is available on + rubygems.org. + + o 1merror_on_stderr 22m(1mBUNDLE_ERROR_ON_STDERR22m): Print Bundler errors to + stderr. + + o 1mforce_ruby_platform 22m(1mBUNDLE_FORCE_RUBY_PLATFORM22m): Ignore the cur- + rent machine's platform and install only 1mruby 22mplatform gems. As a + result, gems with native extensions will be compiled from source. + + o 1mfrozen 22m(1mBUNDLE_FROZEN22m): Disallow changes to the 1mGemfile22m. When the + 1mGemfile 22mis changed and the lockfile has not been updated, running + Bundler commands will be blocked. Defaults to 1mtrue 22mwhen 1m--deploy-0m + 1mment 22mis used. + + o 1mgem.push_key 22m(1mBUNDLE_GEM__PUSH_KEY22m): Sets the 1m--key 22mparameter for + 1mgem push 22mwhen using the 1mrake release 22mcommand with a private gem- + stash server. + + o 1mgemfile 22m(1mBUNDLE_GEMFILE22m): The name of the file that bundler should + use as the 1mGemfile22m. This location of this file also sets the root + of the project, which is used to resolve relative paths in the 1mGem-0m + 1mfile22m, among other things. By default, bundler will search up from + the current working directory until it finds a 1mGemfile22m. + + o 1mglobal_gem_cache 22m(1mBUNDLE_GLOBAL_GEM_CACHE22m): Whether Bundler should + cache all gems globally, rather than locally to the installing Ruby + installation. + + o 1mglobal_path_appends_ruby_scope 22m(1mBUN-0m + 1mDLE_GLOBAL_PATH_APPENDS_RUBY_SCOPE22m): Whether Bundler should append + the Ruby scope (e.g. engine and ABI version) to a globally-config- + ured path. + + o 1mignore_messages 22m(1mBUNDLE_IGNORE_MESSAGES22m): When set, no post install + messages will be printed. To silence a single gem, use dot notation + like 1mignore_messages.httparty true22m. + + o 1minit_gems_rb 22m(1mBUNDLE_INIT_GEMS_RB22m) Generate a 1mgems.rb 22minstead of a + 1mGemfile 22mwhen running 1mbundle init22m. + + o 1mjobs 22m(1mBUNDLE_JOBS22m): The number of gems Bundler can install in par- + allel. Defaults to 1. + + o 1mlist_command 22m(1mBUNDLE_LIST_COMMAND22m) Enable new list command feature + + o 1mmajor_deprecations 22m(1mBUNDLE_MAJOR_DEPRECATIONS22m): Whether Bundler + should print deprecation warnings for behavior that will be changed + in the next major version. + + o 1mno_install 22m(1mBUNDLE_NO_INSTALL22m): Whether 1mbundle package 22mshould skip + installing gems. + + o 1mno_prune 22m(1mBUNDLE_NO_PRUNE22m): Whether Bundler should leave outdated + gems unpruned when caching. + + o 1monly_update_to_newer_versions 22m(1mBUNDLE_ONLY_UPDATE_TO_NEWER_VER-0m + 1mSIONS22m): During 1mbundle update22m, only resolve to newer versions of the + gems in the lockfile. + + o 1mpath 22m(1mBUNDLE_PATH22m): The location on disk where all gems in your + bundle will be located regardless of 1m$GEM_HOME 22mor 1m$GEM_PATH 22mvalues. + Bundle gems not found in this location will be installed by 1mbundle0m + 1minstall22m. Defaults to 1mGem.dir22m. When --deployment is used, defaults + to vendor/bundle. + + o 1mpath.system 22m(1mBUNDLE_PATH__SYSTEM22m): Whether Bundler will install + gems into the default system path (1mGem.dir22m). + + o 1mpath_relative_to_cwd 22m(1mBUNDLE_PATH_RELATIVE_TO_CWD22m) Makes 1m--path0m + relative to the CWD instead of the 1mGemfile22m. + + o 1mplugins 22m(1mBUNDLE_PLUGINS22m): Enable Bundler's experimental plugin sys- + tem. + + o 1mprefer_gems_rb 22m(1mBUNDLE_PREFER_GEMS_RB22m) Prefer 1mgems.rb 22mto 1mGemfile0m + when Bundler is searching for a Gemfile. + + o 1mprint_only_version_number 22m(1mBUNDLE_PRINT_ONLY_VERSION_NUMBER22m) Print + only version number from 1mbundler --version22m. + + o 1mredirect 22m(1mBUNDLE_REDIRECT22m): The number of redirects allowed for + network requests. Defaults to 1m522m. + + o 1mretry 22m(1mBUNDLE_RETRY22m): The number of times to retry failed network + requests. Defaults to 1m322m. + + o 1msetup_makes_kernel_gem_public 22m(1mBUNDLE_SETUP_MAKES_KERNEL_GEM_PUB-0m + 1mLIC22m): Have 1mBundler.setup 22mmake the 1mKernel#gem 22mmethod public, even + though RubyGems declares it as private. + + o 1mshebang 22m(1mBUNDLE_SHEBANG22m): The program name that should be invoked + for generated binstubs. Defaults to the ruby install name used to + generate the binstub. + + o 1msilence_root_warning 22m(1mBUNDLE_SILENCE_ROOT_WARNING22m): Silence the + warning Bundler prints when installing gems as root. + + o 1mskip_default_git_sources 22m(1mBUNDLE_SKIP_DEFAULT_GIT_SOURCES22m): Whether + Bundler should skip adding default git source shortcuts to the Gem- + file DSL. + + o 1mspecific_platform 22m(1mBUNDLE_SPECIFIC_PLATFORM22m): Allow bundler to + resolve for the specific running platform and store it in the lock- + file, instead of only using a generic platform. A specific platform + is the exact platform triple reported by 1mGem::Platform.local22m, such + as 1mx86_64-darwin-16 22mor 1muniversal-java-1.822m. On the other hand, + generic platforms are those such as 1mruby22m, 1mmswin22m, or 1mjava22m. In this + example, 1mx86_64-darwin-16 22mwould map to 1mruby 22mand 1muniversal-java-1.80m + to 1mjava22m. + + o 1mssl_ca_cert 22m(1mBUNDLE_SSL_CA_CERT22m): Path to a designated CA certifi- + cate file or folder containing multiple certificates for trusted + CAs in PEM format. + + o 1mssl_client_cert 22m(1mBUNDLE_SSL_CLIENT_CERT22m): Path to a designated file + containing a X.509 client certificate and key in PEM format. + + o 1mssl_verify_mode 22m(1mBUNDLE_SSL_VERIFY_MODE22m): The SSL verification mode + Bundler uses when making HTTPS requests. Defaults to verify peer. + + o 1msuppress_install_using_messages 22m(1mBUNDLE_SUPPRESS_INSTALL_USING_MES-0m + 1mSAGES22m): Avoid printing 1mUsing ... 22mmessages during installation when + the version of a gem has not changed. + + o 1msystem_bindir 22m(1mBUNDLE_SYSTEM_BINDIR22m): The location where RubyGems + installs binstubs. Defaults to 1mGem.bindir22m. + + o 1mtimeout 22m(1mBUNDLE_TIMEOUT22m): The seconds allowed before timing out for + network requests. Defaults to 1m1022m. + + o 1munlock_source_unlocks_spec 22m(1mBUNDLE_UNLOCK_SOURCE_UNLOCKS_SPEC22m): + Whether running 1mbundle update --source NAME 22munlocks a gem with the + given name. Defaults to 1mtrue22m. + + o 1mupdate_requires_all_flag 22m(1mBUNDLE_UPDATE_REQUIRES_ALL_FLAG22m) Require + passing 1m--all 22mto 1mbundle update 22mwhen everything should be updated, + and disallow passing no options to 1mbundle update22m. + + o 1muser_agent 22m(1mBUNDLE_USER_AGENT22m): The custom user agent fragment + Bundler includes in API requests. + + o 1mwith 22m(1mBUNDLE_WITH22m): A 1m:22m-separated list of groups whose gems bundler + should install. + + o 1mwithout 22m(1mBUNDLE_WITHOUT22m): A 1m:22m-separated list of groups whose gems + bundler should not install. + + + + In general, you should set these settings per-application by using the + applicable flag to the bundle install(1) 4mbundle-install.1.html24m or bun- + dle package(1) 4mbundle-package.1.html24m command. + + You can set them globally either via environment variables or 1mbundle0m + 1mconfig22m, whichever is preferable for your setup. If you use both, envi- + ronment variables will take preference over global settings. + +1mLOCAL GIT REPOS0m + Bundler also allows you to work against a git repository locally + instead of using the remote version. This can be achieved by setting up + a local override: + + + + bundle config local.GEM_NAME /path/to/local/git/repository + + + + For example, in order to use a local Rack repository, a developer could + call: + + + + bundle config local.rack ~/Work/git/rack + + + + Now instead of checking out the remote git repository, the local over- + ride will be used. Similar to a path source, every time the local git + repository change, changes will be automatically picked up by Bundler. + This means a commit in the local git repo will update the revision in + the 1mGemfile.lock 22mto the local git repo revision. This requires the same + attention as git submodules. Before pushing to the remote, you need to + ensure the local override was pushed, otherwise you may point to a com- + mit that only exists in your local machine. You'll also need to CGI + escape your usernames and passwords as well. + + Bundler does many checks to ensure a developer won't work with invalid + references. Particularly, we force a developer to specify a branch in + the 1mGemfile 22min order to use this feature. If the branch specified in + the 1mGemfile 22mand the current branch in the local git repository do not + match, Bundler will abort. This ensures that a developer is always + working against the correct branches, and prevents accidental locking + to a different branch. + + Finally, Bundler also ensures that the current revision in the 1mGem-0m + 1mfile.lock 22mexists in the local git repository. By doing this, Bundler + forces you to fetch the latest changes in the remotes. + +1mMIRRORS OF GEM SOURCES0m + Bundler supports overriding gem sources with mirrors. This allows you + to configure rubygems.org as the gem source in your Gemfile while still + using your mirror to fetch gems. + + + + bundle config mirror.SOURCE_URL MIRROR_URL + + + + For example, to use a mirror of rubygems.org hosted at rubygems-mir- + ror.org: + + + + bundle config mirror.http://rubygems.org http://rubygems-mirror.org + + + + Each mirror also provides a fallback timeout setting. If the mirror + does not respond within the fallback timeout, Bundler will try to use + the original server instead of the mirror. + + + + bundle config mirror.SOURCE_URL.fallback_timeout TIMEOUT + + + + For example, to fall back to rubygems.org after 3 seconds: + + + + bundle config mirror.https://rubygems.org.fallback_timeout 3 + + + + The default fallback timeout is 0.1 seconds, but the setting can cur- + rently only accept whole seconds (for example, 1, 15, or 30). + +1mCREDENTIALS FOR GEM SOURCES0m + Bundler allows you to configure credentials for any gem source, which + allows you to avoid putting secrets into your Gemfile. + + + + bundle config SOURCE_HOSTNAME USERNAME:PASSWORD + + + + For example, to save the credentials of user 1mclaudette 22mfor the gem + source at 1mgems.longerous.com22m, you would run: + + + + bundle config gems.longerous.com claudette:s00pers3krit + + + + Or you can set the credentials as an environment variable like this: + + + + export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit" + + + + For gems with a git source with HTTP(S) URL you can specify credentials + like so: + + + + bundle config https://github.com/bundler/bundler.git username:password + + + + Or you can set the credentials as an environment variable like so: + + + + export BUNDLE_GITHUB__COM=username:password + + + + This is especially useful for private repositories on hosts such as + Github, where you can use personal OAuth tokens: + + + + export BUNDLE_GITHUB__COM=abcd0123generatedtoken:x-oauth-basic + + + +1mCONFIGURE BUNDLER DIRECTORIES0m + Bundler's home, config, cache and plugin directories are able to be + configured through environment variables. The default location for + Bundler's home directory is 1m~/.bundle22m, which all directories inherit + from by default. The following outlines the available environment vari- + ables and their default values + + + + BUNDLE_USER_HOME : $HOME/.bundle + BUNDLE_USER_CACHE : $BUNDLE_USER_HOME/cache + BUNDLE_USER_CONFIG : $BUNDLE_USER_HOME/config + BUNDLE_USER_PLUGIN : $BUNDLE_USER_HOME/plugin + + + + + + + November 2018 BUNDLE-CONFIG(1) diff --git a/man/bundle-config.ronn b/man/bundle-config.ronn new file mode 100644 index 00000000000000..b5c97ae82d9bfd --- /dev/null +++ b/man/bundle-config.ronn @@ -0,0 +1,397 @@ +bundle-config(1) -- Set bundler configuration options +===================================================== + +## SYNOPSIS + +`bundle config` [ []] + +## DESCRIPTION + +This command allows you to interact with Bundler's configuration system. + +Bundler loads configuration settings in this order: + +1. Local config (`app/.bundle/config`) +2. Environmental variables (`ENV`) +3. Global config (`~/.bundle/config`) +4. Bundler default config + +Executing `bundle config` with no parameters will print a list of all +bundler configuration for the current bundle, and where that configuration +was set. + +Executing `bundle config ` will print the value of that configuration +setting, and where it was set. + +Executing `bundle config ` will set that configuration to the +value specified for all bundles executed as the current user. The configuration +will be stored in `~/.bundle/config`. If already is set, will be +overridden and user will be warned. + +Executing `bundle config --global ` works the same as above. + +Executing `bundle config --local ` will set that configuration to +the local application. The configuration will be stored in `app/.bundle/config`. + +Executing `bundle config --delete ` will delete the configuration in both +local and global sources. Not compatible with --global or --local flag. + +Executing bundle with the `BUNDLE_IGNORE_CONFIG` environment variable set will +cause it to ignore all configuration. + +Executing `bundle config disable_multisource true` upgrades the warning about +the Gemfile containing multiple primary sources to an error. Executing `bundle +config --delete disable_multisource` downgrades this error to a warning. + +## REMEMBERING OPTIONS + +Flags passed to `bundle install` or the Bundler runtime, +such as `--path foo` or `--without production`, are not remembered between commands. +If these options must be remembered,they must be set using `bundle config` +(e.g., `bundle config path foo`). + +The options that can be configured are: + +* `bin`: + Creates a directory (defaults to `~/bin`) and place any executables from the + gem there. These executables run in Bundler's context. If used, you might add + this directory to your environment's `PATH` variable. For instance, if the + `rails` gem comes with a `rails` executable, this flag will create a + `bin/rails` executable that ensures that all referred dependencies will be + resolved using the bundled gems. + +* `deployment`: + In deployment mode, Bundler will 'roll-out' the bundle for + `production` use. Please check carefully if you want to have this option + enabled in `development` or `test` environments. + +* `path`: + The location to install the specified gems to. This defaults to Rubygems' + setting. Bundler shares this location with Rubygems, `gem install ...` will + have gem installed there, too. Therefore, gems installed without a + `--path ...` setting will show up by calling `gem list`. Accordingly, gems + installed to other locations will not get listed. + +* `without`: + A space-separated list of groups referencing gems to skip during installation. + +* `with`: + A space-separated list of groups referencing gems to include during installation. + +## BUILD OPTIONS + +You can use `bundle config` to give Bundler the flags to pass to the gem +installer every time bundler tries to install a particular gem. + +A very common example, the `mysql` gem, requires Snow Leopard users to +pass configuration flags to `gem install` to specify where to find the +`mysql_config` executable. + + gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config + +Since the specific location of that executable can change from machine +to machine, you can specify these flags on a per-machine basis. + + bundle config build.mysql --with-mysql-config=/usr/local/mysql/bin/mysql_config + +After running this command, every time bundler needs to install the +`mysql` gem, it will pass along the flags you specified. + +## CONFIGURATION KEYS + +Configuration keys in bundler have two forms: the canonical form and the +environment variable form. + +For instance, passing the `--without` flag to [bundle install(1)](bundle-install.1.html) +prevents Bundler from installing certain groups specified in the Gemfile(5). Bundler +persists this value in `app/.bundle/config` so that calls to `Bundler.setup` +do not try to find gems from the `Gemfile` that you didn't install. Additionally, +subsequent calls to [bundle install(1)](bundle-install.1.html) remember this setting +and skip those groups. + +The canonical form of this configuration is `"without"`. To convert the canonical +form to the environment variable form, capitalize it, and prepend `BUNDLE_`. The +environment variable form of `"without"` is `BUNDLE_WITHOUT`. + +Any periods in the configuration keys must be replaced with two underscores when +setting it via environment variables. The configuration key `local.rack` becomes +the environment variable `BUNDLE_LOCAL__RACK`. + +## LIST OF AVAILABLE KEYS + +The following is a list of all configuration keys and their purpose. You can +learn more about their operation in [bundle install(1)](bundle-install.1.html). + +* `allow_bundler_dependency_conflicts` (`BUNDLE_ALLOW_BUNDLER_DEPENDENCY_CONFLICTS`): + Allow resolving to specifications that have dependencies on `bundler` that + are incompatible with the running Bundler version. +* `allow_deployment_source_credential_changes` (`BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES`): + When in deployment mode, allow changing the credentials to a gem's source. + Ex: `https://some.host.com/gems/path/` -> `https://user_name:password@some.host.com/gems/path` +* `allow_offline_install` (`BUNDLE_ALLOW_OFFLINE_INSTALL`): + Allow Bundler to use cached data when installing without network access. +* `auto_clean_without_path` (`BUNDLE_AUTO_CLEAN_WITHOUT_PATH`): + Automatically run `bundle clean` after installing when an explicit `path` + has not been set and Bundler is not installing into the system gems. +* `auto_install` (`BUNDLE_AUTO_INSTALL`): + Automatically run `bundle install` when gems are missing. +* `bin` (`BUNDLE_BIN`): + Install executables from gems in the bundle to the specified directory. + Defaults to `false`. +* `cache_all` (`BUNDLE_CACHE_ALL`): + Cache all gems, including path and git gems. +* `cache_all_platforms` (`BUNDLE_CACHE_ALL_PLATFORMS`): + Cache gems for all platforms. +* `cache_path` (`BUNDLE_CACHE_PATH`): + The directory that bundler will place cached gems in when running + bundle package, and that bundler will look in when installing gems. + Defaults to `vendor/bundle`. +* `clean` (`BUNDLE_CLEAN`): + Whether Bundler should run `bundle clean` automatically after + `bundle install`. +* `console` (`BUNDLE_CONSOLE`): + The console that `bundle console` starts. Defaults to `irb`. +* `default_install_uses_path` (`BUNDLE_DEFAULT_INSTALL_USES_PATH`): + Whether a `bundle install` without an explicit `--path` argument defaults + to installing gems in `.bundle`. +* `deployment` (`BUNDLE_DEPLOYMENT`): + Disallow changes to the `Gemfile`. When the `Gemfile` is changed and the + lockfile has not been updated, running Bundler commands will be blocked. +* `disable_checksum_validation` (`BUNDLE_DISABLE_CHECKSUM_VALIDATION`): + Allow installing gems even if they do not match the checksum provided by + RubyGems. +* `disable_exec_load` (`BUNDLE_DISABLE_EXEC_LOAD`): + Stop Bundler from using `load` to launch an executable in-process in + `bundle exec`. +* `disable_local_branch_check` (`BUNDLE_DISABLE_LOCAL_BRANCH_CHECK`): + Allow Bundler to use a local git override without a branch specified in the + Gemfile. +* `disable_multisource` (`BUNDLE_DISABLE_MULTISOURCE`): + When set, Gemfiles containing multiple sources will produce errors + instead of warnings. + Use `bundle config --delete disable_multisource` to unset. +* `disable_platform_warnings` (`BUNDLE_DISABLE_PLATFORM_WARNINGS`): + Disable warnings during bundle install when a dependency is unused on the current platform. +* `disable_shared_gems` (`BUNDLE_DISABLE_SHARED_GEMS`): + Stop Bundler from accessing gems installed to RubyGems' normal location. +* `disable_version_check` (`BUNDLE_DISABLE_VERSION_CHECK`): + Stop Bundler from checking if a newer Bundler version is available on + rubygems.org. +* `error_on_stderr` (`BUNDLE_ERROR_ON_STDERR`): + Print Bundler errors to stderr. +* `force_ruby_platform` (`BUNDLE_FORCE_RUBY_PLATFORM`): + Ignore the current machine's platform and install only `ruby` platform gems. + As a result, gems with native extensions will be compiled from source. +* `frozen` (`BUNDLE_FROZEN`): + Disallow changes to the `Gemfile`. When the `Gemfile` is changed and the + lockfile has not been updated, running Bundler commands will be blocked. + Defaults to `true` when `--deployment` is used. +* `gem.push_key` (`BUNDLE_GEM__PUSH_KEY`): + Sets the `--key` parameter for `gem push` when using the `rake release` + command with a private gemstash server. +* `gemfile` (`BUNDLE_GEMFILE`): + The name of the file that bundler should use as the `Gemfile`. This location + of this file also sets the root of the project, which is used to resolve + relative paths in the `Gemfile`, among other things. By default, bundler + will search up from the current working directory until it finds a + `Gemfile`. +* `global_gem_cache` (`BUNDLE_GLOBAL_GEM_CACHE`): + Whether Bundler should cache all gems globally, rather than locally to the + installing Ruby installation. +* `global_path_appends_ruby_scope` (`BUNDLE_GLOBAL_PATH_APPENDS_RUBY_SCOPE`): + Whether Bundler should append the Ruby scope (e.g. engine and ABI version) + to a globally-configured path. +* `ignore_messages` (`BUNDLE_IGNORE_MESSAGES`): When set, no post install + messages will be printed. To silence a single gem, use dot notation like + `ignore_messages.httparty true`. +* `init_gems_rb` (`BUNDLE_INIT_GEMS_RB`) + Generate a `gems.rb` instead of a `Gemfile` when running `bundle init`. +* `jobs` (`BUNDLE_JOBS`): + The number of gems Bundler can install in parallel. Defaults to 1. +* `list_command` (`BUNDLE_LIST_COMMAND`) + Enable new list command feature +* `major_deprecations` (`BUNDLE_MAJOR_DEPRECATIONS`): + Whether Bundler should print deprecation warnings for behavior that will + be changed in the next major version. +* `no_install` (`BUNDLE_NO_INSTALL`): + Whether `bundle package` should skip installing gems. +* `no_prune` (`BUNDLE_NO_PRUNE`): + Whether Bundler should leave outdated gems unpruned when caching. +* `only_update_to_newer_versions` (`BUNDLE_ONLY_UPDATE_TO_NEWER_VERSIONS`): + During `bundle update`, only resolve to newer versions of the gems in the + lockfile. +* `path` (`BUNDLE_PATH`): + The location on disk where all gems in your bundle will be located regardless + of `$GEM_HOME` or `$GEM_PATH` values. Bundle gems not found in this location + will be installed by `bundle install`. Defaults to `Gem.dir`. When --deployment + is used, defaults to vendor/bundle. +* `path.system` (`BUNDLE_PATH__SYSTEM`): + Whether Bundler will install gems into the default system path (`Gem.dir`). +* `path_relative_to_cwd` (`PATH_RELATIVE_TO_CWD`) + Makes `--path` relative to the CWD instead of the `Gemfile`. +* `plugins` (`BUNDLE_PLUGINS`): + Enable Bundler's experimental plugin system. +* `prefer_gems_rb` (`BUNDLE_PREFER_GEMS_RB`) + Prefer `gems.rb` to `Gemfile` when Bundler is searching for a Gemfile. +* `print_only_version_number` (`BUNDLE_PRINT_ONLY_VERSION_NUMBER`) + Print only version number from `bundler --version`. +* `redirect` (`BUNDLE_REDIRECT`): + The number of redirects allowed for network requests. Defaults to `5`. +* `retry` (`BUNDLE_RETRY`): + The number of times to retry failed network requests. Defaults to `3`. +* `setup_makes_kernel_gem_public` (`BUNDLE_SETUP_MAKES_KERNEL_GEM_PUBLIC`): + Have `Bundler.setup` make the `Kernel#gem` method public, even though + RubyGems declares it as private. +* `shebang` (`BUNDLE_SHEBANG`): + The program name that should be invoked for generated binstubs. Defaults to + the ruby install name used to generate the binstub. +* `silence_root_warning` (`BUNDLE_SILENCE_ROOT_WARNING`): + Silence the warning Bundler prints when installing gems as root. +* `skip_default_git_sources` (`BUNDLE_SKIP_DEFAULT_GIT_SOURCES`): + Whether Bundler should skip adding default git source shortcuts to the + Gemfile DSL. +* `specific_platform` (`BUNDLE_SPECIFIC_PLATFORM`): + Allow bundler to resolve for the specific running platform and store it in + the lockfile, instead of only using a generic platform. + A specific platform is the exact platform triple reported by + `Gem::Platform.local`, such as `x86_64-darwin-16` or `universal-java-1.8`. + On the other hand, generic platforms are those such as `ruby`, `mswin`, or + `java`. In this example, `x86_64-darwin-16` would map to `ruby` and + `universal-java-1.8` to `java`. +* `ssl_ca_cert` (`BUNDLE_SSL_CA_CERT`): + Path to a designated CA certificate file or folder containing multiple + certificates for trusted CAs in PEM format. +* `ssl_client_cert` (`BUNDLE_SSL_CLIENT_CERT`): + Path to a designated file containing a X.509 client certificate + and key in PEM format. +* `ssl_verify_mode` (`BUNDLE_SSL_VERIFY_MODE`): + The SSL verification mode Bundler uses when making HTTPS requests. + Defaults to verify peer. +* `suppress_install_using_messages` (`BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES`): + Avoid printing `Using ...` messages during installation when the version of + a gem has not changed. +* `system_bindir` (`BUNDLE_SYSTEM_BINDIR`): + The location where RubyGems installs binstubs. Defaults to `Gem.bindir`. +* `timeout` (`BUNDLE_TIMEOUT`): + The seconds allowed before timing out for network requests. Defaults to `10`. +* `unlock_source_unlocks_spec` (`BUNDLE_UNLOCK_SOURCE_UNLOCKS_SPEC`): + Whether running `bundle update --source NAME` unlocks a gem with the given + name. Defaults to `true`. +* `update_requires_all_flag` (`BUNDLE_UPDATE_REQUIRES_ALL_FLAG`) + Require passing `--all` to `bundle update` when everything should be updated, + and disallow passing no options to `bundle update`. +* `user_agent` (`BUNDLE_USER_AGENT`): + The custom user agent fragment Bundler includes in API requests. +* `with` (`BUNDLE_WITH`): + A `:`-separated list of groups whose gems bundler should install. +* `without` (`BUNDLE_WITHOUT`): + A `:`-separated list of groups whose gems bundler should not install. + +In general, you should set these settings per-application by using the applicable +flag to the [bundle install(1)](bundle-install.1.html) or [bundle package(1)](bundle-package.1.html) command. + +You can set them globally either via environment variables or `bundle config`, +whichever is preferable for your setup. If you use both, environment variables +will take preference over global settings. + +## LOCAL GIT REPOS + +Bundler also allows you to work against a git repository locally +instead of using the remote version. This can be achieved by setting +up a local override: + + bundle config local.GEM_NAME /path/to/local/git/repository + +For example, in order to use a local Rack repository, a developer could call: + + bundle config local.rack ~/Work/git/rack + +Now instead of checking out the remote git repository, the local +override will be used. Similar to a path source, every time the local +git repository change, changes will be automatically picked up by +Bundler. This means a commit in the local git repo will update the +revision in the `Gemfile.lock` to the local git repo revision. This +requires the same attention as git submodules. Before pushing to +the remote, you need to ensure the local override was pushed, otherwise +you may point to a commit that only exists in your local machine. +You'll also need to CGI escape your usernames and passwords as well. + +Bundler does many checks to ensure a developer won't work with +invalid references. Particularly, we force a developer to specify +a branch in the `Gemfile` in order to use this feature. If the branch +specified in the `Gemfile` and the current branch in the local git +repository do not match, Bundler will abort. This ensures that +a developer is always working against the correct branches, and prevents +accidental locking to a different branch. + +Finally, Bundler also ensures that the current revision in the +`Gemfile.lock` exists in the local git repository. By doing this, Bundler +forces you to fetch the latest changes in the remotes. + +## MIRRORS OF GEM SOURCES + +Bundler supports overriding gem sources with mirrors. This allows you to +configure rubygems.org as the gem source in your Gemfile while still using your +mirror to fetch gems. + + bundle config mirror.SOURCE_URL MIRROR_URL + +For example, to use a mirror of rubygems.org hosted at rubygems-mirror.org: + + bundle config mirror.http://rubygems.org http://rubygems-mirror.org + +Each mirror also provides a fallback timeout setting. If the mirror does not +respond within the fallback timeout, Bundler will try to use the original +server instead of the mirror. + + bundle config mirror.SOURCE_URL.fallback_timeout TIMEOUT + +For example, to fall back to rubygems.org after 3 seconds: + + bundle config mirror.https://rubygems.org.fallback_timeout 3 + +The default fallback timeout is 0.1 seconds, but the setting can currently +only accept whole seconds (for example, 1, 15, or 30). + +## CREDENTIALS FOR GEM SOURCES + +Bundler allows you to configure credentials for any gem source, which allows +you to avoid putting secrets into your Gemfile. + + bundle config SOURCE_HOSTNAME USERNAME:PASSWORD + +For example, to save the credentials of user `claudette` for the gem source at +`gems.longerous.com`, you would run: + + bundle config gems.longerous.com claudette:s00pers3krit + +Or you can set the credentials as an environment variable like this: + + export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit" + +For gems with a git source with HTTP(S) URL you can specify credentials like so: + + bundle config https://github.com/bundler/bundler.git username:password + +Or you can set the credentials as an environment variable like so: + + export BUNDLE_GITHUB__COM=username:password + +This is especially useful for private repositories on hosts such as Github, +where you can use personal OAuth tokens: + + export BUNDLE_GITHUB__COM=abcd0123generatedtoken:x-oauth-basic + + +## CONFIGURE BUNDLER DIRECTORIES + +Bundler's home, config, cache and plugin directories are able to be configured +through environment variables. The default location for Bundler's home directory is +`~/.bundle`, which all directories inherit from by default. The following +outlines the available environment variables and their default values + + BUNDLE_USER_HOME : $HOME/.bundle + BUNDLE_USER_CACHE : $BUNDLE_USER_HOME/cache + BUNDLE_USER_CONFIG : $BUNDLE_USER_HOME/config + BUNDLE_USER_PLUGIN : $BUNDLE_USER_HOME/plugin + diff --git a/man/bundle-doctor.1 b/man/bundle-doctor.1 new file mode 100644 index 00000000000000..53291ba7b986d0 --- /dev/null +++ b/man/bundle-doctor.1 @@ -0,0 +1,44 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-DOCTOR" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-doctor\fR \- Checks the bundle for common problems +. +.SH "SYNOPSIS" +\fBbundle doctor\fR [\-\-quiet] [\-\-gemfile=GEMFILE] +. +.SH "DESCRIPTION" +Checks your Gemfile and gem environment for common problems\. If issues are detected, Bundler prints them and exits status 1\. Otherwise, Bundler prints a success message and exits status 0\. +. +.P +Examples of common problems caught by bundle\-doctor include: +. +.IP "\(bu" 4 +Invalid Bundler settings +. +.IP "\(bu" 4 +Mismatched Ruby versions +. +.IP "\(bu" 4 +Mismatched platforms +. +.IP "\(bu" 4 +Uninstalled gems +. +.IP "\(bu" 4 +Missing dependencies +. +.IP "" 0 +. +.SH "OPTIONS" +. +.TP +\fB\-\-quiet\fR +Only output warnings and errors\. +. +.TP +\fB\-\-gemfile=\fR +The location of the Gemfile(5) which Bundler should use\. This defaults to a Gemfile(5) in the current working directory\. In general, Bundler will assume that the location of the Gemfile(5) is also the project\'s root and will try to find \fBGemfile\.lock\fR and \fBvendor/cache\fR relative to this location\. + diff --git a/man/bundle-doctor.1.txt b/man/bundle-doctor.1.txt new file mode 100644 index 00000000000000..b21291ac0fe244 --- /dev/null +++ b/man/bundle-doctor.1.txt @@ -0,0 +1,44 @@ +BUNDLE-DOCTOR(1) BUNDLE-DOCTOR(1) + + + +1mNAME0m + 1mbundle-doctor 22m- Checks the bundle for common problems + +1mSYNOPSIS0m + 1mbundle doctor 22m[--quiet] [--gemfile=GEMFILE] + +1mDESCRIPTION0m + Checks your Gemfile and gem environment for common problems. If issues + are detected, Bundler prints them and exits status 1. Otherwise, + Bundler prints a success message and exits status 0. + + Examples of common problems caught by bundle-doctor include: + + o Invalid Bundler settings + + o Mismatched Ruby versions + + o Mismatched platforms + + o Uninstalled gems + + o Missing dependencies + + + +1mOPTIONS0m + 1m--quiet0m + Only output warnings and errors. + + 1m--gemfile=0m + The location of the Gemfile(5) which Bundler should use. This + defaults to a Gemfile(5) in the current working directory. In + general, Bundler will assume that the location of the Gemfile(5) + is also the project's root and will try to find 1mGemfile.lock 22mand + 1mvendor/cache 22mrelative to this location. + + + + + November 2018 BUNDLE-DOCTOR(1) diff --git a/man/bundle-doctor.ronn b/man/bundle-doctor.ronn new file mode 100644 index 00000000000000..271ee800ad2f94 --- /dev/null +++ b/man/bundle-doctor.ronn @@ -0,0 +1,33 @@ +bundle-doctor(1) -- Checks the bundle for common problems +========================================================= + +## SYNOPSIS + +`bundle doctor` [--quiet] + [--gemfile=GEMFILE] + +## DESCRIPTION + +Checks your Gemfile and gem environment for common problems. If issues +are detected, Bundler prints them and exits status 1. Otherwise, +Bundler prints a success message and exits status 0. + +Examples of common problems caught by bundle-doctor include: + +* Invalid Bundler settings +* Mismatched Ruby versions +* Mismatched platforms +* Uninstalled gems +* Missing dependencies + +## OPTIONS + +* `--quiet`: + Only output warnings and errors. + +* `--gemfile=`: + The location of the Gemfile(5) which Bundler should use. This defaults + to a Gemfile(5) in the current working directory. In general, Bundler + will assume that the location of the Gemfile(5) is also the project's + root and will try to find `Gemfile.lock` and `vendor/cache` relative + to this location. diff --git a/man/bundle-exec.1 b/man/bundle-exec.1 new file mode 100644 index 00000000000000..e7421417697d49 --- /dev/null +++ b/man/bundle-exec.1 @@ -0,0 +1,165 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-EXEC" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-exec\fR \- Execute a command in the context of the bundle +. +.SH "SYNOPSIS" +\fBbundle exec\fR [\-\-keep\-file\-descriptors] \fIcommand\fR +. +.SH "DESCRIPTION" +This command executes the command, making all gems specified in the [\fBGemfile(5)\fR][Gemfile(5)] available to \fBrequire\fR in Ruby programs\. +. +.P +Essentially, if you would normally have run something like \fBrspec spec/my_spec\.rb\fR, and you want to use the gems specified in the [\fBGemfile(5)\fR][Gemfile(5)] and installed via bundle install(1) \fIbundle\-install\.1\.html\fR, you should run \fBbundle exec rspec spec/my_spec\.rb\fR\. +. +.P +Note that \fBbundle exec\fR does not require that an executable is available on your shell\'s \fB$PATH\fR\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-keep\-file\-descriptors\fR +Exec in Ruby 2\.0 began discarding non\-standard file descriptors\. When this flag is passed, exec will revert to the 1\.9 behaviour of passing all file descriptors to the new process\. +. +.SH "BUNDLE INSTALL \-\-BINSTUBS" +If you use the \fB\-\-binstubs\fR flag in bundle install(1) \fIbundle\-install\.1\.html\fR, Bundler will automatically create a directory (which defaults to \fBapp_root/bin\fR) containing all of the executables available from gems in the bundle\. +. +.P +After using \fB\-\-binstubs\fR, \fBbin/rspec spec/my_spec\.rb\fR is identical to \fBbundle exec rspec spec/my_spec\.rb\fR\. +. +.SH "ENVIRONMENT MODIFICATIONS" +\fBbundle exec\fR makes a number of changes to the shell environment, then executes the command you specify in full\. +. +.IP "\(bu" 4 +make sure that it\'s still possible to shell out to \fBbundle\fR from inside a command invoked by \fBbundle exec\fR (using \fB$BUNDLE_BIN_PATH\fR) +. +.IP "\(bu" 4 +put the directory containing executables (like \fBrails\fR, \fBrspec\fR, \fBrackup\fR) for your bundle on \fB$PATH\fR +. +.IP "\(bu" 4 +make sure that if bundler is invoked in the subshell, it uses the same \fBGemfile\fR (by setting \fBBUNDLE_GEMFILE\fR) +. +.IP "\(bu" 4 +add \fB\-rbundler/setup\fR to \fB$RUBYOPT\fR, which makes sure that Ruby programs invoked in the subshell can see the gems in the bundle +. +.IP "" 0 +. +.P +It also modifies Rubygems: +. +.IP "\(bu" 4 +disallow loading additional gems not in the bundle +. +.IP "\(bu" 4 +modify the \fBgem\fR method to be a no\-op if a gem matching the requirements is in the bundle, and to raise a \fBGem::LoadError\fR if it\'s not +. +.IP "\(bu" 4 +Define \fBGem\.refresh\fR to be a no\-op, since the source index is always frozen when using bundler, and to prevent gems from the system leaking into the environment +. +.IP "\(bu" 4 +Override \fBGem\.bin_path\fR to use the gems in the bundle, making system executables work +. +.IP "\(bu" 4 +Add all gems in the bundle into Gem\.loaded_specs +. +.IP "" 0 +. +.P +Finally, \fBbundle exec\fR also implicitly modifies \fBGemfile\.lock\fR if the lockfile and the Gemfile do not match\. Bundler needs the Gemfile to determine things such as a gem\'s groups, \fBautorequire\fR, and platforms, etc\., and that information isn\'t stored in the lockfile\. The Gemfile and lockfile must be synced in order to \fBbundle exec\fR successfully, so \fBbundle exec\fR updates the lockfile beforehand\. +. +.SS "Loading" +By default, when attempting to \fBbundle exec\fR to a file with a ruby shebang, Bundler will \fBKernel\.load\fR that file instead of using \fBKernel\.exec\fR\. For the vast majority of cases, this is a performance improvement\. In a rare few cases, this could cause some subtle side\-effects (such as dependence on the exact contents of \fB$0\fR or \fB__FILE__\fR) and the optimization can be disabled by enabling the \fBdisable_exec_load\fR setting\. +. +.SS "Shelling out" +Any Ruby code that opens a subshell (like \fBsystem\fR, backticks, or \fB%x{}\fR) will automatically use the current Bundler environment\. If you need to shell out to a Ruby command that is not part of your current bundle, use the \fBwith_clean_env\fR method with a block\. Any subshells created inside the block will be given the environment present before Bundler was activated\. For example, Homebrew commands run Ruby, but don\'t work inside a bundle: +. +.IP "" 4 +. +.nf + +Bundler\.with_clean_env do + `brew install wget` +end +. +.fi +. +.IP "" 0 +. +.P +Using \fBwith_clean_env\fR is also necessary if you are shelling out to a different bundle\. Any Bundler commands run in a subshell will inherit the current Gemfile, so commands that need to run in the context of a different bundle also need to use \fBwith_clean_env\fR\. +. +.IP "" 4 +. +.nf + +Bundler\.with_clean_env do + Dir\.chdir "/other/bundler/project" do + `bundle exec \./script` + end +end +. +.fi +. +.IP "" 0 +. +.P +Bundler provides convenience helpers that wrap \fBsystem\fR and \fBexec\fR, and they can be used like this: +. +.IP "" 4 +. +.nf + +Bundler\.clean_system(\'brew install wget\') +Bundler\.clean_exec(\'brew install wget\') +. +.fi +. +.IP "" 0 +. +.SH "RUBYGEMS PLUGINS" +At present, the Rubygems plugin system requires all files named \fBrubygems_plugin\.rb\fR on the load path of \fIany\fR installed gem when any Ruby code requires \fBrubygems\.rb\fR\. This includes executables installed into the system, like \fBrails\fR, \fBrackup\fR, and \fBrspec\fR\. +. +.P +Since Rubygems plugins can contain arbitrary Ruby code, they commonly end up activating themselves or their dependencies\. +. +.P +For instance, the \fBgemcutter 0\.5\fR gem depended on \fBjson_pure\fR\. If you had that version of gemcutter installed (even if you \fIalso\fR had a newer version without this problem), Rubygems would activate \fBgemcutter 0\.5\fR and \fBjson_pure \fR\. +. +.P +If your Gemfile(5) also contained \fBjson_pure\fR (or a gem with a dependency on \fBjson_pure\fR), the latest version on your system might conflict with the version in your Gemfile(5), or the snapshot version in your \fBGemfile\.lock\fR\. +. +.P +If this happens, bundler will say: +. +.IP "" 4 +. +.nf + +You have already activated json_pure 1\.4\.6 but your Gemfile +requires json_pure 1\.4\.3\. Consider using bundle exec\. +. +.fi +. +.IP "" 0 +. +.P +In this situation, you almost certainly want to remove the underlying gem with the problematic gem plugin\. In general, the authors of these plugins (in this case, the \fBgemcutter\fR gem) have released newer versions that are more careful in their plugins\. +. +.P +You can find a list of all the gems containing gem plugins by running +. +.IP "" 4 +. +.nf + +ruby \-rubygems \-e "puts Gem\.find_files(\'rubygems_plugin\.rb\')" +. +.fi +. +.IP "" 0 +. +.P +At the very least, you should remove all but the newest version of each gem plugin, and also remove all gem plugins that you aren\'t using (\fBgem uninstall gem_name\fR)\. diff --git a/man/bundle-exec.1.txt b/man/bundle-exec.1.txt new file mode 100644 index 00000000000000..fa55d2a0c2c12c --- /dev/null +++ b/man/bundle-exec.1.txt @@ -0,0 +1,178 @@ +BUNDLE-EXEC(1) BUNDLE-EXEC(1) + + + +1mNAME0m + 1mbundle-exec 22m- Execute a command in the context of the bundle + +1mSYNOPSIS0m + 1mbundle exec 22m[--keep-file-descriptors] 4mcommand0m + +1mDESCRIPTION0m + This command executes the command, making all gems specified in the + [1mGemfile(5)22m][Gemfile(5)] available to 1mrequire 22min Ruby programs. + + Essentially, if you would normally have run something like 1mrspec0m + 1mspec/my_spec.rb22m, and you want to use the gems specified in the [1mGem-0m + 1mfile(5)22m][Gemfile(5)] and installed via bundle install(1) 4mbun-0m + 4mdle-install.1.html24m, you should run 1mbundle exec rspec spec/my_spec.rb22m. + + Note that 1mbundle exec 22mdoes not require that an executable is available + on your shell's 1m$PATH22m. + +1mOPTIONS0m + 1m--keep-file-descriptors0m + Exec in Ruby 2.0 began discarding non-standard file descriptors. + When this flag is passed, exec will revert to the 1.9 behaviour + of passing all file descriptors to the new process. + +1mBUNDLE INSTALL --BINSTUBS0m + If you use the 1m--binstubs 22mflag in bundle install(1) 4mbun-0m + 4mdle-install.1.html24m, Bundler will automatically create a directory + (which defaults to 1mapp_root/bin22m) containing all of the executables + available from gems in the bundle. + + After using 1m--binstubs22m, 1mbin/rspec spec/my_spec.rb 22mis identical to 1mbun-0m + 1mdle exec rspec spec/my_spec.rb22m. + +1mENVIRONMENT MODIFICATIONS0m + 1mbundle exec 22mmakes a number of changes to the shell environment, then + executes the command you specify in full. + + o make sure that it's still possible to shell out to 1mbundle 22mfrom + inside a command invoked by 1mbundle exec 22m(using 1m$BUNDLE_BIN_PATH22m) + + o put the directory containing executables (like 1mrails22m, 1mrspec22m, + 1mrackup22m) for your bundle on 1m$PATH0m + + o make sure that if bundler is invoked in the subshell, it uses the + same 1mGemfile 22m(by setting 1mBUNDLE_GEMFILE22m) + + o add 1m-rbundler/setup 22mto 1m$RUBYOPT22m, which makes sure that Ruby pro- + grams invoked in the subshell can see the gems in the bundle + + + + It also modifies Rubygems: + + o disallow loading additional gems not in the bundle + + o modify the 1mgem 22mmethod to be a no-op if a gem matching the require- + ments is in the bundle, and to raise a 1mGem::LoadError 22mif it's not + + o Define 1mGem.refresh 22mto be a no-op, since the source index is always + frozen when using bundler, and to prevent gems from the system + leaking into the environment + + o Override 1mGem.bin_path 22mto use the gems in the bundle, making system + executables work + + o Add all gems in the bundle into Gem.loaded_specs + + + + Finally, 1mbundle exec 22malso implicitly modifies 1mGemfile.lock 22mif the lock- + file and the Gemfile do not match. Bundler needs the Gemfile to deter- + mine things such as a gem's groups, 1mautorequire22m, and platforms, etc., + and that information isn't stored in the lockfile. The Gemfile and + lockfile must be synced in order to 1mbundle exec 22msuccessfully, so 1mbundle0m + 1mexec 22mupdates the lockfile beforehand. + + 1mLoading0m + By default, when attempting to 1mbundle exec 22mto a file with a ruby she- + bang, Bundler will 1mKernel.load 22mthat file instead of using 1mKernel.exec22m. + For the vast majority of cases, this is a performance improvement. In a + rare few cases, this could cause some subtle side-effects (such as + dependence on the exact contents of 1m$0 22mor 1m__FILE__22m) and the optimiza- + tion can be disabled by enabling the 1mdisable_exec_load 22msetting. + + 1mShelling out0m + Any Ruby code that opens a subshell (like 1msystem22m, backticks, or 1m%x{}22m) + will automatically use the current Bundler environment. If you need to + shell out to a Ruby command that is not part of your current bundle, + use the 1mwith_clean_env 22mmethod with a block. Any subshells created + inside the block will be given the environment present before Bundler + was activated. For example, Homebrew commands run Ruby, but don't work + inside a bundle: + + + + Bundler.with_clean_env do + `brew install wget` + end + + + + Using 1mwith_clean_env 22mis also necessary if you are shelling out to a + different bundle. Any Bundler commands run in a subshell will inherit + the current Gemfile, so commands that need to run in the context of a + different bundle also need to use 1mwith_clean_env22m. + + + + Bundler.with_clean_env do + Dir.chdir "/other/bundler/project" do + `bundle exec ./script` + end + end + + + + Bundler provides convenience helpers that wrap 1msystem 22mand 1mexec22m, and + they can be used like this: + + + + Bundler.clean_system('brew install wget') + Bundler.clean_exec('brew install wget') + + + +1mRUBYGEMS PLUGINS0m + At present, the Rubygems plugin system requires all files named + 1mrubygems_plugin.rb 22mon the load path of 4many24m installed gem when any Ruby + code requires 1mrubygems.rb22m. This includes executables installed into the + system, like 1mrails22m, 1mrackup22m, and 1mrspec22m. + + Since Rubygems plugins can contain arbitrary Ruby code, they commonly + end up activating themselves or their dependencies. + + For instance, the 1mgemcutter 0.5 22mgem depended on 1mjson_pure22m. If you had + that version of gemcutter installed (even if you 4malso24m had a newer ver- + sion without this problem), Rubygems would activate 1mgemcutter 0.5 22mand + 1mjson_pure 22m. + + If your Gemfile(5) also contained 1mjson_pure 22m(or a gem with a dependency + on 1mjson_pure22m), the latest version on your system might conflict with + the version in your Gemfile(5), or the snapshot version in your 1mGem-0m + 1mfile.lock22m. + + If this happens, bundler will say: + + + + You have already activated json_pure 1.4.6 but your Gemfile + requires json_pure 1.4.3. Consider using bundle exec. + + + + In this situation, you almost certainly want to remove the underlying + gem with the problematic gem plugin. In general, the authors of these + plugins (in this case, the 1mgemcutter 22mgem) have released newer versions + that are more careful in their plugins. + + You can find a list of all the gems containing gem plugins by running + + + + ruby -rubygems -e "puts Gem.find_files('rubygems_plugin.rb')" + + + + At the very least, you should remove all but the newest version of each + gem plugin, and also remove all gem plugins that you aren't using (1mgem0m + 1muninstall gem_name22m). + + + + November 2018 BUNDLE-EXEC(1) diff --git a/man/bundle-exec.ronn b/man/bundle-exec.ronn new file mode 100644 index 00000000000000..aa680f4c5de846 --- /dev/null +++ b/man/bundle-exec.ronn @@ -0,0 +1,152 @@ +bundle-exec(1) -- Execute a command in the context of the bundle +================================================================ + +## SYNOPSIS + +`bundle exec` [--keep-file-descriptors] + +## DESCRIPTION + +This command executes the command, making all gems specified in the +[`Gemfile(5)`][Gemfile(5)] available to `require` in Ruby programs. + +Essentially, if you would normally have run something like +`rspec spec/my_spec.rb`, and you want to use the gems specified +in the [`Gemfile(5)`][Gemfile(5)] and installed via [bundle install(1)](bundle-install.1.html), you +should run `bundle exec rspec spec/my_spec.rb`. + +Note that `bundle exec` does not require that an executable is +available on your shell's `$PATH`. + +## OPTIONS + +* `--keep-file-descriptors`: + Exec in Ruby 2.0 began discarding non-standard file descriptors. When this + flag is passed, exec will revert to the 1.9 behaviour of passing all file + descriptors to the new process. + +## BUNDLE INSTALL --BINSTUBS + +If you use the `--binstubs` flag in [bundle install(1)](bundle-install.1.html), Bundler will +automatically create a directory (which defaults to `app_root/bin`) +containing all of the executables available from gems in the bundle. + +After using `--binstubs`, `bin/rspec spec/my_spec.rb` is identical +to `bundle exec rspec spec/my_spec.rb`. + +## ENVIRONMENT MODIFICATIONS + +`bundle exec` makes a number of changes to the shell environment, +then executes the command you specify in full. + +* make sure that it's still possible to shell out to `bundle` + from inside a command invoked by `bundle exec` (using + `$BUNDLE_BIN_PATH`) +* put the directory containing executables (like `rails`, `rspec`, + `rackup`) for your bundle on `$PATH` +* make sure that if bundler is invoked in the subshell, it uses + the same `Gemfile` (by setting `BUNDLE_GEMFILE`) +* add `-rbundler/setup` to `$RUBYOPT`, which makes sure that + Ruby programs invoked in the subshell can see the gems in + the bundle + +It also modifies Rubygems: + +* disallow loading additional gems not in the bundle +* modify the `gem` method to be a no-op if a gem matching + the requirements is in the bundle, and to raise a + `Gem::LoadError` if it's not +* Define `Gem.refresh` to be a no-op, since the source + index is always frozen when using bundler, and to + prevent gems from the system leaking into the environment +* Override `Gem.bin_path` to use the gems in the bundle, + making system executables work +* Add all gems in the bundle into Gem.loaded_specs + +Finally, `bundle exec` also implicitly modifies `Gemfile.lock` if the lockfile +and the Gemfile do not match. Bundler needs the Gemfile to determine things +such as a gem's groups, `autorequire`, and platforms, etc., and that +information isn't stored in the lockfile. The Gemfile and lockfile must be +synced in order to `bundle exec` successfully, so `bundle exec` +updates the lockfile beforehand. + +### Loading + +By default, when attempting to `bundle exec` to a file with a ruby shebang, +Bundler will `Kernel.load` that file instead of using `Kernel.exec`. For the +vast majority of cases, this is a performance improvement. In a rare few cases, +this could cause some subtle side-effects (such as dependence on the exact +contents of `$0` or `__FILE__`) and the optimization can be disabled by enabling +the `disable_exec_load` setting. + +### Shelling out + +Any Ruby code that opens a subshell (like `system`, backticks, or `%x{}`) will +automatically use the current Bundler environment. If you need to shell out to +a Ruby command that is not part of your current bundle, use the +`with_clean_env` method with a block. Any subshells created inside the block +will be given the environment present before Bundler was activated. For +example, Homebrew commands run Ruby, but don't work inside a bundle: + + Bundler.with_clean_env do + `brew install wget` + end + +Using `with_clean_env` is also necessary if you are shelling out to a different +bundle. Any Bundler commands run in a subshell will inherit the current +Gemfile, so commands that need to run in the context of a different bundle also +need to use `with_clean_env`. + + Bundler.with_clean_env do + Dir.chdir "/other/bundler/project" do + `bundle exec ./script` + end + end + +Bundler provides convenience helpers that wrap `system` and `exec`, and they +can be used like this: + + Bundler.clean_system('brew install wget') + Bundler.clean_exec('brew install wget') + + +## RUBYGEMS PLUGINS + +At present, the Rubygems plugin system requires all files +named `rubygems_plugin.rb` on the load path of _any_ installed +gem when any Ruby code requires `rubygems.rb`. This includes +executables installed into the system, like `rails`, `rackup`, +and `rspec`. + +Since Rubygems plugins can contain arbitrary Ruby code, they +commonly end up activating themselves or their dependencies. + +For instance, the `gemcutter 0.5` gem depended on `json_pure`. +If you had that version of gemcutter installed (even if +you _also_ had a newer version without this problem), Rubygems +would activate `gemcutter 0.5` and `json_pure `. + +If your Gemfile(5) also contained `json_pure` (or a gem +with a dependency on `json_pure`), the latest version on +your system might conflict with the version in your +Gemfile(5), or the snapshot version in your `Gemfile.lock`. + +If this happens, bundler will say: + + You have already activated json_pure 1.4.6 but your Gemfile + requires json_pure 1.4.3. Consider using bundle exec. + +In this situation, you almost certainly want to remove the +underlying gem with the problematic gem plugin. In general, +the authors of these plugins (in this case, the `gemcutter` +gem) have released newer versions that are more careful in +their plugins. + +You can find a list of all the gems containing gem plugins +by running + + ruby -rubygems -e "puts Gem.find_files('rubygems_plugin.rb')" + +At the very least, you should remove all but the newest +version of each gem plugin, and also remove all gem plugins +that you aren't using (`gem uninstall gem_name`). diff --git a/man/bundle-gem.1 b/man/bundle-gem.1 new file mode 100644 index 00000000000000..1087a0888c53ea --- /dev/null +++ b/man/bundle-gem.1 @@ -0,0 +1,80 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-GEM" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem +. +.SH "SYNOPSIS" +\fBbundle gem\fR \fIGEM_NAME\fR \fIOPTIONS\fR +. +.SH "DESCRIPTION" +Generates a directory named \fBGEM_NAME\fR with a \fBRakefile\fR, \fBGEM_NAME\.gemspec\fR, and other supporting files and directories that can be used to develop a rubygem with that name\. +. +.P +Run \fBrake \-T\fR in the resulting project for a list of Rake tasks that can be used to test and publish the gem to rubygems\.org\. +. +.P +The generated project skeleton can be customized with OPTIONS, as explained below\. Note that these options can also be specified via Bundler\'s global configuration file using the following names: +. +.IP "\(bu" 4 +\fBgem\.coc\fR +. +.IP "\(bu" 4 +\fBgem\.mit\fR +. +.IP "\(bu" 4 +\fBgem\.test\fR +. +.IP "" 0 +. +.SH "OPTIONS" +. +.TP +\fB\-\-exe\fR or \fB\-b\fR or \fB\-\-bin\fR +Specify that Bundler should create a binary executable (as \fBexe/GEM_NAME\fR) in the generated rubygem project\. This binary will also be added to the \fBGEM_NAME\.gemspec\fR manifest\. This behavior is disabled by default\. +. +.TP +\fB\-\-no\-exe\fR +Do not create a binary (overrides \fB\-\-exe\fR specified in the global config)\. +. +.TP +\fB\-\-coc\fR +Add a \fBCODE_OF_CONDUCT\.md\fR file to the root of the generated project\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler\'s global config for future \fBbundle gem\fR use\. +. +.TP +\fB\-\-no\-coc\fR +Do not create a \fBCODE_OF_CONDUCT\.md\fR (overrides \fB\-\-coc\fR specified in the global config)\. +. +.TP +\fB\-\-ext\fR +Add boilerplate for C extension code to the generated project\. This behavior is disabled by default\. +. +.TP +\fB\-\-no\-ext\fR +Do not add C extension code (overrides \fB\-\-ext\fR specified in the global config)\. +. +.TP +\fB\-\-mit\fR +Add an MIT license to a \fBLICENSE\.txt\fR file in the root of the generated project\. Your name from the global git config is used for the copyright statement\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler\'s global config for future \fBbundle gem\fR use\. +. +.TP +\fB\-\-no\-mit\fR +Do not create a \fBLICENSE\.txt\fR (overrides \fB\-\-mit\fR specified in the global config)\. +. +.TP +\fB\-t\fR, \fB\-\-test=minitest\fR, \fB\-\-test=rspec\fR +Specify the test framework that Bundler should use when generating the project\. Acceptable values are \fBminitest\fR and \fBrspec\fR\. The \fBGEM_NAME\.gemspec\fR will be configured and a skeleton test/spec directory will be created based on this option\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler\'s global config for future \fBbundle gem\fR use\. If no option is specified, the default testing framework is RSpec\. +. +.TP +\fB\-e\fR, \fB\-\-edit[=EDITOR]\fR +Open the resulting GEM_NAME\.gemspec in EDITOR, or the default editor if not specified\. The default is \fB$BUNDLER_EDITOR\fR, \fB$VISUAL\fR, or \fB$EDITOR\fR\. +. +.SH "SEE ALSO" +. +.IP "\(bu" 4 +bundle config(1) \fIbundle\-config\.1\.html\fR +. +.IP "" 0 + diff --git a/man/bundle-gem.1.txt b/man/bundle-gem.1.txt new file mode 100644 index 00000000000000..59e5e0ddcc652f --- /dev/null +++ b/man/bundle-gem.1.txt @@ -0,0 +1,91 @@ +BUNDLE-GEM(1) BUNDLE-GEM(1) + + + +1mNAME0m + 1mbundle-gem 22m- Generate a project skeleton for creating a rubygem + +1mSYNOPSIS0m + 1mbundle gem 4m22mGEM_NAME24m 4mOPTIONS0m + +1mDESCRIPTION0m + Generates a directory named 1mGEM_NAME 22mwith a 1mRakefile22m, 1mGEM_NAME.gemspec22m, + and other supporting files and directories that can be used to develop + a rubygem with that name. + + Run 1mrake -T 22min the resulting project for a list of Rake tasks that can + be used to test and publish the gem to rubygems.org. + + The generated project skeleton can be customized with OPTIONS, as + explained below. Note that these options can also be specified via + Bundler's global configuration file using the following names: + + o 1mgem.coc0m + + o 1mgem.mit0m + + o 1mgem.test0m + + + +1mOPTIONS0m + 1m--exe 22mor 1m-b 22mor 1m--bin0m + Specify that Bundler should create a binary executable (as + 1mexe/GEM_NAME22m) in the generated rubygem project. This binary will + also be added to the 1mGEM_NAME.gemspec 22mmanifest. This behavior is + disabled by default. + + 1m--no-exe0m + Do not create a binary (overrides 1m--exe 22mspecified in the global + config). + + 1m--coc 22mAdd a 1mCODE_OF_CONDUCT.md 22mfile to the root of the generated + project. If this option is unspecified, an interactive prompt + will be displayed and the answer will be saved in Bundler's + global config for future 1mbundle gem 22muse. + + 1m--no-coc0m + Do not create a 1mCODE_OF_CONDUCT.md 22m(overrides 1m--coc 22mspecified in + the global config). + + 1m--ext 22mAdd boilerplate for C extension code to the generated project. + This behavior is disabled by default. + + 1m--no-ext0m + Do not add C extension code (overrides 1m--ext 22mspecified in the + global config). + + 1m--mit 22mAdd an MIT license to a 1mLICENSE.txt 22mfile in the root of the gen- + erated project. Your name from the global git config is used for + the copyright statement. If this option is unspecified, an + interactive prompt will be displayed and the answer will be + saved in Bundler's global config for future 1mbundle gem 22muse. + + 1m--no-mit0m + Do not create a 1mLICENSE.txt 22m(overrides 1m--mit 22mspecified in the + global config). + + 1m-t22m, 1m--test=minitest22m, 1m--test=rspec0m + Specify the test framework that Bundler should use when generat- + ing the project. Acceptable values are 1mminitest 22mand 1mrspec22m. The + 1mGEM_NAME.gemspec 22mwill be configured and a skeleton test/spec + directory will be created based on this option. If this option + is unspecified, an interactive prompt will be displayed and the + answer will be saved in Bundler's global config for future 1mbun-0m + 1mdle gem 22muse. If no option is specified, the default testing + framework is RSpec. + + 1m-e22m, 1m--edit[=EDITOR]0m + Open the resulting GEM_NAME.gemspec in EDITOR, or the default + editor if not specified. The default is 1m$BUNDLER_EDITOR22m, 1m$VIS-0m + 1mUAL22m, or 1m$EDITOR22m. + +1mSEE ALSO0m + o bundle config(1) 4mbundle-config.1.html0m + + + + + + + November 2018 BUNDLE-GEM(1) diff --git a/man/bundle-gem.ronn b/man/bundle-gem.ronn new file mode 100644 index 00000000000000..cf3d037df2159c --- /dev/null +++ b/man/bundle-gem.ronn @@ -0,0 +1,78 @@ +bundle-gem(1) -- Generate a project skeleton for creating a rubygem +==================================================================== + +## SYNOPSIS + +`bundle gem` [OPTIONS] + +## DESCRIPTION + +Generates a directory named `GEM_NAME` with a `Rakefile`, `GEM_NAME.gemspec`, +and other supporting files and directories that can be used to develop a +rubygem with that name. + +Run `rake -T` in the resulting project for a list of Rake tasks that can be used +to test and publish the gem to rubygems.org. + +The generated project skeleton can be customized with OPTIONS, as explained +below. Note that these options can also be specified via Bundler's global +configuration file using the following names: + +* `gem.coc` +* `gem.mit` +* `gem.test` + +## OPTIONS + +* `--exe` or `-b` or `--bin`: + Specify that Bundler should create a binary executable (as `exe/GEM_NAME`) + in the generated rubygem project. This binary will also be added to the + `GEM_NAME.gemspec` manifest. This behavior is disabled by default. + +* `--no-exe`: + Do not create a binary (overrides `--exe` specified in the global config). + +* `--coc`: + Add a `CODE_OF_CONDUCT.md` file to the root of the generated project. If + this option is unspecified, an interactive prompt will be displayed and the + answer will be saved in Bundler's global config for future `bundle gem` use. + +* `--no-coc`: + Do not create a `CODE_OF_CONDUCT.md` (overrides `--coc` specified in the + global config). + +* `--ext`: + Add boilerplate for C extension code to the generated project. This behavior + is disabled by default. + +* `--no-ext`: + Do not add C extension code (overrides `--ext` specified in the global + config). + +* `--mit`: + Add an MIT license to a `LICENSE.txt` file in the root of the generated + project. Your name from the global git config is used for the copyright + statement. If this option is unspecified, an interactive prompt will be + displayed and the answer will be saved in Bundler's global config for future + `bundle gem` use. + +* `--no-mit`: + Do not create a `LICENSE.txt` (overrides `--mit` specified in the global + config). + +* `-t`, `--test=minitest`, `--test=rspec`: + Specify the test framework that Bundler should use when generating the + project. Acceptable values are `minitest` and `rspec`. The `GEM_NAME.gemspec` + will be configured and a skeleton test/spec directory will be created based + on this option. If this option is unspecified, an interactive prompt will be + displayed and the answer will be saved in Bundler's global config for future + `bundle gem` use. + If no option is specified, the default testing framework is RSpec. + +* `-e`, `--edit[=EDITOR]`: + Open the resulting GEM_NAME.gemspec in EDITOR, or the default editor if not + specified. The default is `$BUNDLER_EDITOR`, `$VISUAL`, or `$EDITOR`. + +## SEE ALSO + +* [bundle config(1)](bundle-config.1.html) diff --git a/man/bundle-info.1 b/man/bundle-info.1 new file mode 100644 index 00000000000000..9140d27c73a7fe --- /dev/null +++ b/man/bundle-info.1 @@ -0,0 +1,20 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-INFO" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-info\fR \- Show information for the given gem in your bundle +. +.SH "SYNOPSIS" +\fBbundle info\fR [GEM] [\-\-path] +. +.SH "DESCRIPTION" +Print the basic information about the provided GEM such as homepage, version, path and summary\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-path\fR +Print the path of the given gem + diff --git a/man/bundle-info.1.txt b/man/bundle-info.1.txt new file mode 100644 index 00000000000000..4d328416e72a97 --- /dev/null +++ b/man/bundle-info.1.txt @@ -0,0 +1,21 @@ +BUNDLE-INFO(1) BUNDLE-INFO(1) + + + +1mNAME0m + 1mbundle-info 22m- Show information for the given gem in your bundle + +1mSYNOPSIS0m + 1mbundle info 22m[GEM] [--path] + +1mDESCRIPTION0m + Print the basic information about the provided GEM such as homepage, + version, path and summary. + +1mOPTIONS0m + 1m--path 22mPrint the path of the given gem + + + + + November 2018 BUNDLE-INFO(1) diff --git a/man/bundle-info.ronn b/man/bundle-info.ronn new file mode 100644 index 00000000000000..47e457aa3c8155 --- /dev/null +++ b/man/bundle-info.ronn @@ -0,0 +1,17 @@ +bundle-info(1) -- Show information for the given gem in your bundle +========================================================================= + +## SYNOPSIS + +`bundle info` [GEM] + [--path] + +## DESCRIPTION + +Print the basic information about the provided GEM such as homepage, version, +path and summary. + +## OPTIONS + +* `--path`: +Print the path of the given gem diff --git a/man/bundle-init.1 b/man/bundle-init.1 new file mode 100644 index 00000000000000..70519fda39b455 --- /dev/null +++ b/man/bundle-init.1 @@ -0,0 +1,25 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-INIT" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-init\fR \- Generates a Gemfile into the current working directory +. +.SH "SYNOPSIS" +\fBbundle init\fR [\-\-gemspec=FILE] +. +.SH "DESCRIPTION" +Init generates a default [\fBGemfile(5)\fR][Gemfile(5)] in the current working directory\. When adding a [\fBGemfile(5)\fR][Gemfile(5)] to a gem with a gemspec, the \fB\-\-gemspec\fR option will automatically add each dependency listed in the gemspec file to the newly created [\fBGemfile(5)\fR][Gemfile(5)]\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-gemspec\fR +Use the specified \.gemspec to create the [\fBGemfile(5)\fR][Gemfile(5)] +. +.SH "FILES" +Included in the default [\fBGemfile(5)\fR][Gemfile(5)] generated is the line \fB# frozen_string_literal: true\fR\. This is a magic comment supported for the first time in Ruby 2\.3\. The presence of this line results in all string literals in the file being implicitly frozen\. +. +.SH "SEE ALSO" +Gemfile(5) \fIhttp://bundler\.io/man/gemfile\.5\.html\fR diff --git a/man/bundle-init.1.txt b/man/bundle-init.1.txt new file mode 100644 index 00000000000000..eb6091bab2e6eb --- /dev/null +++ b/man/bundle-init.1.txt @@ -0,0 +1,34 @@ +BUNDLE-INIT(1) BUNDLE-INIT(1) + + + +1mNAME0m + 1mbundle-init 22m- Generates a Gemfile into the current working directory + +1mSYNOPSIS0m + 1mbundle init 22m[--gemspec=FILE] + +1mDESCRIPTION0m + Init generates a default [1mGemfile(5)22m][Gemfile(5)] in the current work- + ing directory. When adding a [1mGemfile(5)22m][Gemfile(5)] to a gem with a + gemspec, the 1m--gemspec 22moption will automatically add each dependency + listed in the gemspec file to the newly created [1mGemfile(5)22m][Gem- + file(5)]. + +1mOPTIONS0m + 1m--gemspec0m + Use the specified .gemspec to create the [1mGemfile(5)22m][Gem- + file(5)] + +1mFILES0m + Included in the default [1mGemfile(5)22m][Gemfile(5)] generated is the line + 1m# frozen_string_literal: true22m. This is a magic comment supported for + the first time in Ruby 2.3. The presence of this line results in all + string literals in the file being implicitly frozen. + +1mSEE ALSO0m + Gemfile(5) 4mhttp://bundler.io/man/gemfile.5.html0m + + + + November 2018 BUNDLE-INIT(1) diff --git a/man/bundle-init.ronn b/man/bundle-init.ronn new file mode 100644 index 00000000000000..7504af7bab6e8d --- /dev/null +++ b/man/bundle-init.ronn @@ -0,0 +1,29 @@ +bundle-init(1) -- Generates a Gemfile into the current working directory +======================================================================== + +## SYNOPSIS + +`bundle init` [--gemspec=FILE] + +## DESCRIPTION + +Init generates a default [`Gemfile(5)`][Gemfile(5)] in the current working directory. When +adding a [`Gemfile(5)`][Gemfile(5)] to a gem with a gemspec, the `--gemspec` option will +automatically add each dependency listed in the gemspec file to the newly +created [`Gemfile(5)`][Gemfile(5)]. + +## OPTIONS + +* `--gemspec`: + Use the specified .gemspec to create the [`Gemfile(5)`][Gemfile(5)] + +## FILES + +Included in the default [`Gemfile(5)`][Gemfile(5)] +generated is the line `# frozen_string_literal: true`. This is a magic comment +supported for the first time in Ruby 2.3. The presence of this line +results in all string literals in the file being implicitly frozen. + +## SEE ALSO + +[Gemfile(5)](http://bundler.io/man/gemfile.5.html) diff --git a/man/bundle-inject.1 b/man/bundle-inject.1 new file mode 100644 index 00000000000000..4f663e1a858802 --- /dev/null +++ b/man/bundle-inject.1 @@ -0,0 +1,33 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-INJECT" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-inject\fR \- Add named gem(s) with version requirements to Gemfile +. +.SH "SYNOPSIS" +\fBbundle inject\fR [GEM] [VERSION] +. +.SH "DESCRIPTION" +Adds the named gem(s) with their version requirements to the resolved [\fBGemfile(5)\fR][Gemfile(5)]\. +. +.P +This command will add the gem to both your [\fBGemfile(5)\fR][Gemfile(5)] and Gemfile\.lock if it isn\'t listed yet\. +. +.P +Example: +. +.IP "" 4 +. +.nf + +bundle install +bundle inject \'rack\' \'> 0\' +. +.fi +. +.IP "" 0 +. +.P +This will inject the \'rack\' gem with a version greater than 0 in your [\fBGemfile(5)\fR][Gemfile(5)] and Gemfile\.lock diff --git a/man/bundle-inject.1.txt b/man/bundle-inject.1.txt new file mode 100644 index 00000000000000..0011dea0c17d49 --- /dev/null +++ b/man/bundle-inject.1.txt @@ -0,0 +1,32 @@ +BUNDLE-INJECT(1) BUNDLE-INJECT(1) + + + +1mNAME0m + 1mbundle-inject 22m- Add named gem(s) with version requirements to Gemfile + +1mSYNOPSIS0m + 1mbundle inject 22m[GEM] [VERSION] + +1mDESCRIPTION0m + Adds the named gem(s) with their version requirements to the resolved + [1mGemfile(5)22m][Gemfile(5)]. + + This command will add the gem to both your [1mGemfile(5)22m][Gemfile(5)] and + Gemfile.lock if it isn't listed yet. + + Example: + + + + bundle install + bundle inject 'rack' '> 0' + + + + This will inject the 'rack' gem with a version greater than 0 in your + [1mGemfile(5)22m][Gemfile(5)] and Gemfile.lock + + + + November 2018 BUNDLE-INJECT(1) diff --git a/man/bundle-inject.ronn b/man/bundle-inject.ronn new file mode 100644 index 00000000000000..f45434189647e3 --- /dev/null +++ b/man/bundle-inject.ronn @@ -0,0 +1,22 @@ +bundle-inject(1) -- Add named gem(s) with version requirements to Gemfile +========================================================================= + +## SYNOPSIS + +`bundle inject` [GEM] [VERSION] + +## DESCRIPTION + +Adds the named gem(s) with their version requirements to the resolved +[`Gemfile(5)`][Gemfile(5)]. + +This command will add the gem to both your [`Gemfile(5)`][Gemfile(5)] and Gemfile.lock if it +isn't listed yet. + +Example: + + bundle install + bundle inject 'rack' '> 0' + +This will inject the 'rack' gem with a version greater than 0 in your +[`Gemfile(5)`][Gemfile(5)] and Gemfile.lock diff --git a/man/bundle-install.1 b/man/bundle-install.1 new file mode 100644 index 00000000000000..2549562b7606cc --- /dev/null +++ b/man/bundle-install.1 @@ -0,0 +1,311 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-INSTALL" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-install\fR \- Install the dependencies specified in your Gemfile +. +.SH "SYNOPSIS" +\fBbundle install\fR [\-\-binstubs[=DIRECTORY]] [\-\-clean] [\-\-deployment] [\-\-frozen] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-no\-cache] [\-\-no\-prune] [\-\-path PATH] [\-\-quiet] [\-\-redownload] [\-\-retry=NUMBER] [\-\-shebang] [\-\-standalone[=GROUP[ GROUP\.\.\.]]] [\-\-system] [\-\-trust\-policy=POLICY] [\-\-with=GROUP[ GROUP\.\.\.]] [\-\-without=GROUP[ GROUP\.\.\.]] +. +.SH "DESCRIPTION" +Install the gems specified in your Gemfile(5)\. If this is the first time you run bundle install (and a \fBGemfile\.lock\fR does not exist), Bundler will fetch all remote sources, resolve dependencies and install all needed gems\. +. +.P +If a \fBGemfile\.lock\fR does exist, and you have not updated your Gemfile(5), Bundler will fetch all remote sources, but use the dependencies specified in the \fBGemfile\.lock\fR instead of resolving dependencies\. +. +.P +If a \fBGemfile\.lock\fR does exist, and you have updated your Gemfile(5), Bundler will use the dependencies in the \fBGemfile\.lock\fR for all gems that you did not update, but will re\-resolve the dependencies of gems that you did update\. You can find more information about this update process below under \fICONSERVATIVE UPDATING\fR\. +. +.SH "OPTIONS" +To apply any of \fB\-\-binstubs\fR, \fB\-\-deployment\fR, \fB\-\-path\fR, or \fB\-\-without\fR every time \fBbundle install\fR is run, use \fBbundle config\fR (see bundle\-config(1))\. +. +.TP +\fB\-\-binstubs[=]\fR +Binstubs are scripts that wrap around executables\. Bundler creates a small Ruby file (a binstub) that loads Bundler, runs the command, and puts it in \fBbin/\fR\. This lets you link the binstub inside of an application to the exact gem version the application needs\. +. +.IP +Creates a directory (defaults to \fB~/bin\fR) and places any executables from the gem there\. These executables run in Bundler\'s context\. If used, you might add this directory to your environment\'s \fBPATH\fR variable\. For instance, if the \fBrails\fR gem comes with a \fBrails\fR executable, this flag will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. +. +.TP +\fB\-\-clean\fR +On finishing the installation Bundler is going to remove any gems not present in the current Gemfile(5)\. Don\'t worry, gems currently in use will not be removed\. +. +.TP +\fB\-\-deployment\fR +In \fIdeployment mode\fR, Bundler will \'roll\-out\' the bundle for production or CI use\. Please check carefully if you want to have this option enabled in your development environment\. +. +.TP +\fB\-\-redownload\fR +Force download every gem, even if the required versions are already available locally\. +. +.TP +\fB\-\-frozen\fR +Do not allow the Gemfile\.lock to be updated after this install\. Exits non\-zero if there are going to be changes to the Gemfile\.lock\. +. +.TP +\fB\-\-full\-index\fR +Bundler will not call Rubygems\' API endpoint (default) but download and cache a (currently big) index file of all gems\. Performance can be improved for large bundles that seldom change by enabling this option\. +. +.TP +\fB\-\-gemfile=\fR +The location of the Gemfile(5) which Bundler should use\. This defaults to a Gemfile(5) in the current working directory\. In general, Bundler will assume that the location of the Gemfile(5) is also the project\'s root and will try to find \fBGemfile\.lock\fR and \fBvendor/cache\fR relative to this location\. +. +.TP +\fB\-\-jobs=[]\fR, \fB\-j[]\fR +The maximum number of parallel download and install jobs\. The default is \fB1\fR\. +. +.TP +\fB\-\-local\fR +Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems\' cache or in \fBvendor/cache\fR\. Note that if a appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\. +. +.TP +\fB\-\-no\-cache\fR +Do not update the cache in \fBvendor/cache\fR with the newly bundled gems\. This does not remove any gems in the cache but keeps the newly bundled gems from being cached during the install\. +. +.TP +\fB\-\-no\-prune\fR +Don\'t remove stale gems from the cache when the installation finishes\. +. +.TP +\fB\-\-path=\fR +The location to install the specified gems to\. This defaults to Rubygems\' setting\. Bundler shares this location with Rubygems, \fBgem install \.\.\.\fR will have gem installed there, too\. Therefore, gems installed without a \fB\-\-path \.\.\.\fR setting will show up by calling \fBgem list\fR\. Accordingly, gems installed to other locations will not get listed\. +. +.TP +\fB\-\-quiet\fR +Do not print progress information to the standard output\. Instead, Bundler will exit using a status code (\fB$?\fR)\. +. +.TP +\fB\-\-retry=[]\fR +Retry failed network or git requests for \fInumber\fR times\. +. +.TP +\fB\-\-shebang=\fR +Uses the specified ruby executable (usually \fBruby\fR) to execute the scripts created with \fB\-\-binstubs\fR\. In addition, if you use \fB\-\-binstubs\fR together with \fB\-\-shebang jruby\fR these executables will be changed to execute \fBjruby\fR instead\. +. +.TP +\fB\-\-standalone[=]\fR +Makes a bundle that can work without depending on Rubygems or Bundler at runtime\. A space separated list of groups to install has to be specified\. Bundler creates a directory named \fBbundle\fR and installs the bundle there\. It also generates a \fBbundle/bundler/setup\.rb\fR file to replace Bundler\'s own setup in the manner required\. Using this option implicitly sets \fBpath\fR, which is a [remembered option][REMEMBERED OPTIONS]\. +. +.TP +\fB\-\-system\fR +Installs the gems specified in the bundle to the system\'s Rubygems location\. This overrides any previous configuration of \fB\-\-path\fR\. +. +.TP +\fB\-\-trust\-policy=[]\fR +Apply the Rubygems security policy \fIpolicy\fR, where policy is one of \fBHighSecurity\fR, \fBMediumSecurity\fR, \fBLowSecurity\fR, \fBAlmostNoSecurity\fR, or \fBNoSecurity\fR\. For more details, please see the Rubygems signing documentation linked below in \fISEE ALSO\fR\. +. +.TP +\fB\-\-with=\fR +A space\-separated list of groups referencing gems to install\. If an optional group is given it is installed\. If a group is given that is in the remembered list of groups given to \-\-without, it is removed from that list\. +. +.TP +\fB\-\-without=\fR +A space\-separated list of groups referencing gems to skip during installation\. If a group is given that is in the remembered list of groups given to \-\-with, it is removed from that list\. +. +.SH "DEPLOYMENT MODE" +Bundler\'s defaults are optimized for development\. To switch to defaults optimized for deployment and for CI, use the \fB\-\-deployment\fR flag\. Do not activate deployment mode on development machines, as it will cause an error when the Gemfile(5) is modified\. +. +.IP "1." 4 +A \fBGemfile\.lock\fR is required\. +. +.IP +To ensure that the same versions of the gems you developed with and tested with are also used in deployments, a \fBGemfile\.lock\fR is required\. +. +.IP +This is mainly to ensure that you remember to check your \fBGemfile\.lock\fR into version control\. +. +.IP "2." 4 +The \fBGemfile\.lock\fR must be up to date +. +.IP +In development, you can modify your Gemfile(5) and re\-run \fBbundle install\fR to \fIconservatively update\fR your \fBGemfile\.lock\fR snapshot\. +. +.IP +In deployment, your \fBGemfile\.lock\fR should be up\-to\-date with changes made in your Gemfile(5)\. +. +.IP "3." 4 +Gems are installed to \fBvendor/bundle\fR not your default system location +. +.IP +In development, it\'s convenient to share the gems used in your application with other applications and other scripts that run on the system\. +. +.IP +In deployment, isolation is a more important default\. In addition, the user deploying the application may not have permission to install gems to the system, or the web server may not have permission to read them\. +. +.IP +As a result, \fBbundle install \-\-deployment\fR installs gems to the \fBvendor/bundle\fR directory in the application\. This may be overridden using the \fB\-\-path\fR option\. +. +.IP "" 0 +. +.SH "SUDO USAGE" +By default, Bundler installs gems to the same location as \fBgem install\fR\. +. +.P +In some cases, that location may not be writable by your Unix user\. In that case, Bundler will stage everything in a temporary directory, then ask you for your \fBsudo\fR password in order to copy the gems into their system location\. +. +.P +From your perspective, this is identical to installing the gems directly into the system\. +. +.P +You should never use \fBsudo bundle install\fR\. This is because several other steps in \fBbundle install\fR must be performed as the current user: +. +.IP "\(bu" 4 +Updating your \fBGemfile\.lock\fR +. +.IP "\(bu" 4 +Updating your \fBvendor/cache\fR, if necessary +. +.IP "\(bu" 4 +Checking out private git repositories using your user\'s SSH keys +. +.IP "" 0 +. +.P +Of these three, the first two could theoretically be performed by \fBchown\fRing the resulting files to \fB$SUDO_USER\fR\. The third, however, can only be performed by invoking the \fBgit\fR command as the current user\. Therefore, git gems are downloaded and installed into \fB~/\.bundle\fR rather than $GEM_HOME or $BUNDLE_PATH\. +. +.P +As a result, you should run \fBbundle install\fR as the current user, and Bundler will ask for your password if it is needed to put the gems into their final location\. +. +.SH "INSTALLING GROUPS" +By default, \fBbundle install\fR will install all gems in all groups in your Gemfile(5), except those declared for a different platform\. +. +.P +However, you can explicitly tell Bundler to skip installing certain groups with the \fB\-\-without\fR option\. This option takes a space\-separated list of groups\. +. +.P +While the \fB\-\-without\fR option will skip \fIinstalling\fR the gems in the specified groups, it will still \fIdownload\fR those gems and use them to resolve the dependencies of every gem in your Gemfile(5)\. +. +.P +This is so that installing a different set of groups on another machine (such as a production server) will not change the gems and versions that you have already developed and tested against\. +. +.P +\fBBundler offers a rock\-solid guarantee that the third\-party code you are running in development and testing is also the third\-party code you are running in production\. You can choose to exclude some of that code in different environments, but you will never be caught flat\-footed by different versions of third\-party code being used in different environments\.\fR +. +.P +For a simple illustration, consider the following Gemfile(5): +. +.IP "" 4 +. +.nf + +source \'https://rubygems\.org\' + +gem \'sinatra\' + +group :production do + gem \'rack\-perftools\-profiler\' +end +. +.fi +. +.IP "" 0 +. +.P +In this case, \fBsinatra\fR depends on any version of Rack (\fB>= 1\.0\fR), while \fBrack\-perftools\-profiler\fR depends on 1\.x (\fB~> 1\.0\fR)\. +. +.P +When you run \fBbundle install \-\-without production\fR in development, we look at the dependencies of \fBrack\-perftools\-profiler\fR as well\. That way, you do not spend all your time developing against Rack 2\.0, using new APIs unavailable in Rack 1\.x, only to have Bundler switch to Rack 1\.2 when the \fBproduction\fR group \fIis\fR used\. +. +.P +This should not cause any problems in practice, because we do not attempt to \fBinstall\fR the gems in the excluded groups, and only evaluate as part of the dependency resolution process\. +. +.P +This also means that you cannot include different versions of the same gem in different groups, because doing so would result in different sets of dependencies used in development and production\. Because of the vagaries of the dependency resolution process, this usually affects more than the gems you list in your Gemfile(5), and can (surprisingly) radically change the gems you are using\. +. +.SH "THE GEMFILE\.LOCK" +When you run \fBbundle install\fR, Bundler will persist the full names and versions of all gems that you used (including dependencies of the gems specified in the Gemfile(5)) into a file called \fBGemfile\.lock\fR\. +. +.P +Bundler uses this file in all subsequent calls to \fBbundle install\fR, which guarantees that you always use the same exact code, even as your application moves across machines\. +. +.P +Because of the way dependency resolution works, even a seemingly small change (for instance, an update to a point\-release of a dependency of a gem in your Gemfile(5)) can result in radically different gems being needed to satisfy all dependencies\. +. +.P +As a result, you \fBSHOULD\fR check your \fBGemfile\.lock\fR into version control, in both applications and gems\. If you do not, every machine that checks out your repository (including your production server) will resolve all dependencies again, which will result in different versions of third\-party code being used if \fBany\fR of the gems in the Gemfile(5) or any of their dependencies have been updated\. +. +.P +When Bundler first shipped, the \fBGemfile\.lock\fR was included in the \fB\.gitignore\fR file included with generated gems\. Over time, however, it became clear that this practice forces the pain of broken dependencies onto new contributors, while leaving existing contributors potentially unaware of the problem\. Since \fBbundle install\fR is usually the first step towards a contribution, the pain of broken dependencies would discourage new contributors from contributing\. As a result, we have revised our guidance for gem authors to now recommend checking in the lock for gems\. +. +.SH "CONSERVATIVE UPDATING" +When you make a change to the Gemfile(5) and then run \fBbundle install\fR, Bundler will update only the gems that you modified\. +. +.P +In other words, if a gem that you \fBdid not modify\fR worked before you called \fBbundle install\fR, it will continue to use the exact same versions of all dependencies as it used before the update\. +. +.P +Let\'s take a look at an example\. Here\'s your original Gemfile(5): +. +.IP "" 4 +. +.nf + +source \'https://rubygems\.org\' + +gem \'actionpack\', \'2\.3\.8\' +gem \'activemerchant\' +. +.fi +. +.IP "" 0 +. +.P +In this case, both \fBactionpack\fR and \fBactivemerchant\fR depend on \fBactivesupport\fR\. The \fBactionpack\fR gem depends on \fBactivesupport 2\.3\.8\fR and \fBrack ~> 1\.1\.0\fR, while the \fBactivemerchant\fR gem depends on \fBactivesupport >= 2\.3\.2\fR, \fBbraintree >= 2\.0\.0\fR, and \fBbuilder >= 2\.0\.0\fR\. +. +.P +When the dependencies are first resolved, Bundler will select \fBactivesupport 2\.3\.8\fR, which satisfies the requirements of both gems in your Gemfile(5)\. +. +.P +Next, you modify your Gemfile(5) to: +. +.IP "" 4 +. +.nf + +source \'https://rubygems\.org\' + +gem \'actionpack\', \'3\.0\.0\.rc\' +gem \'activemerchant\' +. +.fi +. +.IP "" 0 +. +.P +The \fBactionpack 3\.0\.0\.rc\fR gem has a number of new dependencies, and updates the \fBactivesupport\fR dependency to \fB= 3\.0\.0\.rc\fR and the \fBrack\fR dependency to \fB~> 1\.2\.1\fR\. +. +.P +When you run \fBbundle install\fR, Bundler notices that you changed the \fBactionpack\fR gem, but not the \fBactivemerchant\fR gem\. It evaluates the gems currently being used to satisfy its requirements: +. +.TP +\fBactivesupport 2\.3\.8\fR +also used to satisfy a dependency in \fBactivemerchant\fR, which is not being updated +. +.TP +\fBrack ~> 1\.1\.0\fR +not currently being used to satisfy another dependency +. +.P +Because you did not explicitly ask to update \fBactivemerchant\fR, you would not expect it to suddenly stop working after updating \fBactionpack\fR\. However, satisfying the new \fBactivesupport 3\.0\.0\.rc\fR dependency of actionpack requires updating one of its dependencies\. +. +.P +Even though \fBactivemerchant\fR declares a very loose dependency that theoretically matches \fBactivesupport 3\.0\.0\.rc\fR, Bundler treats gems in your Gemfile(5) that have not changed as an atomic unit together with their dependencies\. In this case, the \fBactivemerchant\fR dependency is treated as \fBactivemerchant 1\.7\.1 + activesupport 2\.3\.8\fR, so \fBbundle install\fR will report that it cannot update \fBactionpack\fR\. +. +.P +To explicitly update \fBactionpack\fR, including its dependencies which other gems in the Gemfile(5) still depend on, run \fBbundle update actionpack\fR (see \fBbundle update(1)\fR)\. +. +.P +\fBSummary\fR: In general, after making a change to the Gemfile(5) , you should first try to run \fBbundle install\fR, which will guarantee that no other gem in the Gemfile(5) is impacted by the change\. If that does not work, run bundle update(1) \fIbundle\-update\.1\.html\fR\. +. +.SH "SEE ALSO" +. +.IP "\(bu" 4 +Gem install docs \fIhttp://guides\.rubygems\.org/rubygems\-basics/#installing\-gems\fR +. +.IP "\(bu" 4 +Rubygems signing docs \fIhttp://guides\.rubygems\.org/security/\fR +. +.IP "" 0 + diff --git a/man/bundle-install.1.txt b/man/bundle-install.1.txt new file mode 100644 index 00000000000000..004a5224b22d23 --- /dev/null +++ b/man/bundle-install.1.txt @@ -0,0 +1,401 @@ +BUNDLE-INSTALL(1) BUNDLE-INSTALL(1) + + + +1mNAME0m + 1mbundle-install 22m- Install the dependencies specified in your Gemfile + +1mSYNOPSIS0m + 1mbundle install 22m[--binstubs[=DIRECTORY]] [--clean] [--deployment] + [--frozen] [--full-index] [--gemfile=GEMFILE] [--jobs=NUMBER] [--local] + [--no-cache] [--no-prune] [--path PATH] [--quiet] [--redownload] + [--retry=NUMBER] [--shebang] [--standalone[=GROUP[ GROUP...]]] [--sys- + tem] [--trust-policy=POLICY] [--with=GROUP[ GROUP...]] [--with- + out=GROUP[ GROUP...]] + +1mDESCRIPTION0m + Install the gems specified in your Gemfile(5). If this is the first + time you run bundle install (and a 1mGemfile.lock 22mdoes not exist), + Bundler will fetch all remote sources, resolve dependencies and install + all needed gems. + + If a 1mGemfile.lock 22mdoes exist, and you have not updated your Gemfile(5), + Bundler will fetch all remote sources, but use the dependencies speci- + fied in the 1mGemfile.lock 22minstead of resolving dependencies. + + If a 1mGemfile.lock 22mdoes exist, and you have updated your Gemfile(5), + Bundler will use the dependencies in the 1mGemfile.lock 22mfor all gems that + you did not update, but will re-resolve the dependencies of gems that + you did update. You can find more information about this update process + below under 4mCONSERVATIVE24m 4mUPDATING24m. + +1mOPTIONS0m + To apply any of 1m--binstubs22m, 1m--deployment22m, 1m--path22m, or 1m--without 22mevery + time 1mbundle install 22mis run, use 1mbundle config 22m(see bundle-config(1)). + + 1m--binstubs[=]0m + Binstubs are scripts that wrap around executables. Bundler cre- + ates a small Ruby file (a binstub) that loads Bundler, runs the + command, and puts it in 1mbin/22m. This lets you link the binstub + inside of an application to the exact gem version the applica- + tion needs. + + Creates a directory (defaults to 1m~/bin22m) and places any executa- + bles from the gem there. These executables run in Bundler's con- + text. If used, you might add this directory to your environ- + ment's 1mPATH 22mvariable. For instance, if the 1mrails 22mgem comes with + a 1mrails 22mexecutable, this flag will create a 1mbin/rails 22mexecutable + that ensures that all referred dependencies will be resolved + using the bundled gems. + + 1m--clean0m + On finishing the installation Bundler is going to remove any + gems not present in the current Gemfile(5). Don't worry, gems + currently in use will not be removed. + + 1m--deployment0m + In 4mdeployment24m 4mmode24m, Bundler will 'roll-out' the bundle for pro- + duction or CI use. Please check carefully if you want to have + this option enabled in your development environment. + + 1m--redownload0m + Force download every gem, even if the required versions are + already available locally. + + 1m--frozen0m + Do not allow the Gemfile.lock to be updated after this install. + Exits non-zero if there are going to be changes to the Gem- + file.lock. + + 1m--full-index0m + Bundler will not call Rubygems' API endpoint (default) but down- + load and cache a (currently big) index file of all gems. Perfor- + mance can be improved for large bundles that seldom change by + enabling this option. + + 1m--gemfile=0m + The location of the Gemfile(5) which Bundler should use. This + defaults to a Gemfile(5) in the current working directory. In + general, Bundler will assume that the location of the Gemfile(5) + is also the project's root and will try to find 1mGemfile.lock 22mand + 1mvendor/cache 22mrelative to this location. + + 1m--jobs=[]22m, 1m-j[]0m + The maximum number of parallel download and install jobs. The + default is 1m122m. + + 1m--local0m + Do not attempt to connect to 1mrubygems.org22m. Instead, Bundler will + use the gems already present in Rubygems' cache or in 1mven-0m + 1mdor/cache22m. Note that if a appropriate platform-specific gem + exists on 1mrubygems.org 22mit will not be found. + + 1m--no-cache0m + Do not update the cache in 1mvendor/cache 22mwith the newly bundled + gems. This does not remove any gems in the cache but keeps the + newly bundled gems from being cached during the install. + + 1m--no-prune0m + Don't remove stale gems from the cache when the installation + finishes. + + 1m--path=0m + The location to install the specified gems to. This defaults to + Rubygems' setting. Bundler shares this location with Rubygems, + 1mgem install ... 22mwill have gem installed there, too. Therefore, + gems installed without a 1m--path ... 22msetting will show up by + calling 1mgem list22m. Accordingly, gems installed to other locations + will not get listed. + + 1m--quiet0m + Do not print progress information to the standard output. + Instead, Bundler will exit using a status code (1m$?22m). + + 1m--retry=[]0m + Retry failed network or git requests for 4mnumber24m times. + + 1m--shebang=0m + Uses the specified ruby executable (usually 1mruby22m) to execute the + scripts created with 1m--binstubs22m. In addition, if you use 1m--bin-0m + 1mstubs 22mtogether with 1m--shebang jruby 22mthese executables will be + changed to execute 1mjruby 22minstead. + + 1m--standalone[=]0m + Makes a bundle that can work without depending on Rubygems or + Bundler at runtime. A space separated list of groups to install + has to be specified. Bundler creates a directory named 1mbundle0m + and installs the bundle there. It also generates a 1mbun-0m + 1mdle/bundler/setup.rb 22mfile to replace Bundler's own setup in the + manner required. Using this option implicitly sets 1mpath22m, which + is a [remembered option][REMEMBERED OPTIONS]. + + 1m--system0m + Installs the gems specified in the bundle to the system's + Rubygems location. This overrides any previous configuration of + 1m--path22m. + + 1m--trust-policy=[]0m + Apply the Rubygems security policy 4mpolicy24m, where policy is one + of 1mHighSecurity22m, 1mMediumSecurity22m, 1mLowSecurity22m, 1mAlmostNoSecurity22m, + or 1mNoSecurity22m. For more details, please see the Rubygems signing + documentation linked below in 4mSEE24m 4mALSO24m. + + 1m--with=0m + A space-separated list of groups referencing gems to install. If + an optional group is given it is installed. If a group is given + that is in the remembered list of groups given to --without, it + is removed from that list. + + 1m--without=0m + A space-separated list of groups referencing gems to skip during + installation. If a group is given that is in the remembered list + of groups given to --with, it is removed from that list. + +1mDEPLOYMENT MODE0m + Bundler's defaults are optimized for development. To switch to defaults + optimized for deployment and for CI, use the 1m--deployment 22mflag. Do not + activate deployment mode on development machines, as it will cause an + error when the Gemfile(5) is modified. + + 1. A 1mGemfile.lock 22mis required. + + To ensure that the same versions of the gems you developed with and + tested with are also used in deployments, a 1mGemfile.lock 22mis + required. + + This is mainly to ensure that you remember to check your 1mGem-0m + 1mfile.lock 22minto version control. + + 2. The 1mGemfile.lock 22mmust be up to date + + In development, you can modify your Gemfile(5) and re-run 1mbundle0m + 1minstall 22mto 4mconservatively24m 4mupdate24m your 1mGemfile.lock 22msnapshot. + + In deployment, your 1mGemfile.lock 22mshould be up-to-date with changes + made in your Gemfile(5). + + 3. Gems are installed to 1mvendor/bundle 22mnot your default system loca- + tion + + In development, it's convenient to share the gems used in your + application with other applications and other scripts that run on + the system. + + In deployment, isolation is a more important default. In addition, + the user deploying the application may not have permission to + install gems to the system, or the web server may not have permis- + sion to read them. + + As a result, 1mbundle install --deployment 22minstalls gems to the 1mven-0m + 1mdor/bundle 22mdirectory in the application. This may be overridden + using the 1m--path 22moption. + + + +1mSUDO USAGE0m + By default, Bundler installs gems to the same location as 1mgem install22m. + + In some cases, that location may not be writable by your Unix user. In + that case, Bundler will stage everything in a temporary directory, then + ask you for your 1msudo 22mpassword in order to copy the gems into their + system location. + + From your perspective, this is identical to installing the gems + directly into the system. + + You should never use 1msudo bundle install22m. This is because several other + steps in 1mbundle install 22mmust be performed as the current user: + + o Updating your 1mGemfile.lock0m + + o Updating your 1mvendor/cache22m, if necessary + + o Checking out private git repositories using your user's SSH keys + + + + Of these three, the first two could theoretically be performed by + 1mchown22ming the resulting files to 1m$SUDO_USER22m. The third, however, can + only be performed by invoking the 1mgit 22mcommand as the current user. + Therefore, git gems are downloaded and installed into 1m~/.bundle 22mrather + than $GEM_HOME or $BUNDLE_PATH. + + As a result, you should run 1mbundle install 22mas the current user, and + Bundler will ask for your password if it is needed to put the gems into + their final location. + +1mINSTALLING GROUPS0m + By default, 1mbundle install 22mwill install all gems in all groups in your + Gemfile(5), except those declared for a different platform. + + However, you can explicitly tell Bundler to skip installing certain + groups with the 1m--without 22moption. This option takes a space-separated + list of groups. + + While the 1m--without 22moption will skip 4minstalling24m the gems in the speci- + fied groups, it will still 4mdownload24m those gems and use them to resolve + the dependencies of every gem in your Gemfile(5). + + This is so that installing a different set of groups on another machine + (such as a production server) will not change the gems and versions + that you have already developed and tested against. + + 1mBundler offers a rock-solid guarantee that the third-party code you are0m + 1mrunning in development and testing is also the third-party code you are0m + 1mrunning in production. You can choose to exclude some of that code in0m + 1mdifferent environments, but you will never be caught flat-footed by0m + 1mdifferent versions of third-party code being used in different environ-0m + 1mments.0m + + For a simple illustration, consider the following Gemfile(5): + + + + source 'https://rubygems.org' + + gem 'sinatra' + + group :production do + gem 'rack-perftools-profiler' + end + + + + In this case, 1msinatra 22mdepends on any version of Rack (1m>= 1.022m), while + 1mrack-perftools-profiler 22mdepends on 1.x (1m~> 1.022m). + + When you run 1mbundle install --without production 22min development, we + look at the dependencies of 1mrack-perftools-profiler 22mas well. That way, + you do not spend all your time developing against Rack 2.0, using new + APIs unavailable in Rack 1.x, only to have Bundler switch to Rack 1.2 + when the 1mproduction 22mgroup 4mis24m used. + + This should not cause any problems in practice, because we do not + attempt to 1minstall 22mthe gems in the excluded groups, and only evaluate + as part of the dependency resolution process. + + This also means that you cannot include different versions of the same + gem in different groups, because doing so would result in different + sets of dependencies used in development and production. Because of the + vagaries of the dependency resolution process, this usually affects + more than the gems you list in your Gemfile(5), and can (surprisingly) + radically change the gems you are using. + +1mTHE GEMFILE.LOCK0m + When you run 1mbundle install22m, Bundler will persist the full names and + versions of all gems that you used (including dependencies of the gems + specified in the Gemfile(5)) into a file called 1mGemfile.lock22m. + + Bundler uses this file in all subsequent calls to 1mbundle install22m, which + guarantees that you always use the same exact code, even as your appli- + cation moves across machines. + + Because of the way dependency resolution works, even a seemingly small + change (for instance, an update to a point-release of a dependency of a + gem in your Gemfile(5)) can result in radically different gems being + needed to satisfy all dependencies. + + As a result, you 1mSHOULD 22mcheck your 1mGemfile.lock 22minto version control, + in both applications and gems. If you do not, every machine that checks + out your repository (including your production server) will resolve all + dependencies again, which will result in different versions of + third-party code being used if 1many 22mof the gems in the Gemfile(5) or any + of their dependencies have been updated. + + When Bundler first shipped, the 1mGemfile.lock 22mwas included in the 1m.git-0m + 1mignore 22mfile included with generated gems. Over time, however, it became + clear that this practice forces the pain of broken dependencies onto + new contributors, while leaving existing contributors potentially + unaware of the problem. Since 1mbundle install 22mis usually the first step + towards a contribution, the pain of broken dependencies would discour- + age new contributors from contributing. As a result, we have revised + our guidance for gem authors to now recommend checking in the lock for + gems. + +1mCONSERVATIVE UPDATING0m + When you make a change to the Gemfile(5) and then run 1mbundle install22m, + Bundler will update only the gems that you modified. + + In other words, if a gem that you 1mdid not modify 22mworked before you + called 1mbundle install22m, it will continue to use the exact same versions + of all dependencies as it used before the update. + + Let's take a look at an example. Here's your original Gemfile(5): + + + + source 'https://rubygems.org' + + gem 'actionpack', '2.3.8' + gem 'activemerchant' + + + + In this case, both 1mactionpack 22mand 1mactivemerchant 22mdepend on 1mactivesup-0m + 1mport22m. The 1mactionpack 22mgem depends on 1mactivesupport 2.3.8 22mand 1mrack ~>0m + 1m1.1.022m, while the 1mactivemerchant 22mgem depends on 1mactivesupport >= 2.3.222m, + 1mbraintree >= 2.0.022m, and 1mbuilder >= 2.0.022m. + + When the dependencies are first resolved, Bundler will select + 1mactivesupport 2.3.822m, which satisfies the requirements of both gems in + your Gemfile(5). + + Next, you modify your Gemfile(5) to: + + + + source 'https://rubygems.org' + + gem 'actionpack', '3.0.0.rc' + gem 'activemerchant' + + + + The 1mactionpack 3.0.0.rc 22mgem has a number of new dependencies, and + updates the 1mactivesupport 22mdependency to 1m= 3.0.0.rc 22mand the 1mrack 22mdepen- + dency to 1m~> 1.2.122m. + + When you run 1mbundle install22m, Bundler notices that you changed the + 1mactionpack 22mgem, but not the 1mactivemerchant 22mgem. It evaluates the gems + currently being used to satisfy its requirements: + + 1mactivesupport 2.3.80m + also used to satisfy a dependency in 1mactivemerchant22m, which is + not being updated + + 1mrack ~> 1.1.00m + not currently being used to satisfy another dependency + + Because you did not explicitly ask to update 1mactivemerchant22m, you would + not expect it to suddenly stop working after updating 1mactionpack22m. How- + ever, satisfying the new 1mactivesupport 3.0.0.rc 22mdependency of action- + pack requires updating one of its dependencies. + + Even though 1mactivemerchant 22mdeclares a very loose dependency that theo- + retically matches 1mactivesupport 3.0.0.rc22m, Bundler treats gems in your + Gemfile(5) that have not changed as an atomic unit together with their + dependencies. In this case, the 1mactivemerchant 22mdependency is treated as + 1mactivemerchant 1.7.1 + activesupport 2.3.822m, so 1mbundle install 22mwill + report that it cannot update 1mactionpack22m. + + To explicitly update 1mactionpack22m, including its dependencies which other + gems in the Gemfile(5) still depend on, run 1mbundle update actionpack0m + (see 1mbundle update(1)22m). + + 1mSummary22m: In general, after making a change to the Gemfile(5) , you + should first try to run 1mbundle install22m, which will guarantee that no + other gem in the Gemfile(5) is impacted by the change. If that does not + work, run bundle update(1) 4mbundle-update.1.html24m. + +1mSEE ALSO0m + o Gem install docs + 4mhttp://guides.rubygems.org/rubygems-basics/#installing-gems0m + + o Rubygems signing docs 4mhttp://guides.rubygems.org/security/0m + + + + + + + November 2018 BUNDLE-INSTALL(1) diff --git a/man/bundle-install.ronn b/man/bundle-install.ronn new file mode 100644 index 00000000000000..a9e375c87c0671 --- /dev/null +++ b/man/bundle-install.ronn @@ -0,0 +1,378 @@ +bundle-install(1) -- Install the dependencies specified in your Gemfile +======================================================================= + +## SYNOPSIS + +`bundle install` [--binstubs[=DIRECTORY]] + [--clean] + [--deployment] + [--force] + [--frozen] + [--full-index] + [--gemfile=GEMFILE] + [--jobs=NUMBER] + [--local] + [--no-cache] + [--no-prune] + [--path PATH] + [--quiet] + [--retry=NUMBER] + [--shebang] + [--standalone[=GROUP[ GROUP...]]] + [--system] + [--trust-policy=POLICY] + [--with=GROUP[ GROUP...]] + [--without=GROUP[ GROUP...]] + +## DESCRIPTION + +Install the gems specified in your Gemfile(5). If this is the first +time you run bundle install (and a `Gemfile.lock` does not exist), +Bundler will fetch all remote sources, resolve dependencies and +install all needed gems. + +If a `Gemfile.lock` does exist, and you have not updated your Gemfile(5), +Bundler will fetch all remote sources, but use the dependencies +specified in the `Gemfile.lock` instead of resolving dependencies. + +If a `Gemfile.lock` does exist, and you have updated your Gemfile(5), +Bundler will use the dependencies in the `Gemfile.lock` for all gems +that you did not update, but will re-resolve the dependencies of +gems that you did update. You can find more information about this +update process below under [CONSERVATIVE UPDATING][]. + +## OPTIONS + +To apply any of `--binstubs`, `--deployment`, `--path`, or `--without` every +time `bundle install` is run, use `bundle config` (see bundle-config(1)). + +* `--binstubs[=]`: + Creates a directory (defaults to `~/bin`) and place any executables from the + gem there. These executables run in Bundler's context. If used, you might add + this directory to your environment's `PATH` variable. For instance, if the + `rails` gem comes with a `rails` executable, this flag will create a + `bin/rails` executable that ensures that all referred dependencies will be + resolved using the bundled gems. + +* `--clean`: + On finishing the installation Bundler is going to remove any gems not present + in the current Gemfile(5). Don't worry, gems currently in use will not be + removed. + +* `--deployment`: + In [deployment mode][DEPLOYMENT MODE], Bundler will 'roll-out' the bundle for + production or CI use. Please check carefully if you want to have this option + enabled in your development environment. + +* `--force`: + Force download every gem, even if the required versions are already available + locally. `--redownload` is an alias of this option. + +* `--frozen`: + Do not allow the Gemfile.lock to be updated after this install. Exits + non-zero if there are going to be changes to the Gemfile.lock. + +* `--full-index`: + Bundler will not call Rubygems' API endpoint (default) but download and cache + a (currently big) index file of all gems. Performance can be improved for + large bundles that seldom change by enabling this option. + +* `--gemfile=`: + The location of the Gemfile(5) which Bundler should use. This defaults + to a Gemfile(5) in the current working directory. In general, Bundler + will assume that the location of the Gemfile(5) is also the project's + root and will try to find `Gemfile.lock` and `vendor/cache` relative + to this location. + +* `--jobs=[]`, `-j[]`: + The maximum number of parallel download and install jobs. The default + is `1`. + +* `--local`: + Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the + gems already present in Rubygems' cache or in `vendor/cache`. Note that if a + appropriate platform-specific gem exists on `rubygems.org` it will not be + found. + +* `--no-cache`: + Do not update the cache in `vendor/cache` with the newly bundled gems. This + does not remove any gems in the cache but keeps the newly bundled gems from + being cached during the install. + +* `--no-prune`: + Don't remove stale gems from the cache when the installation finishes. + +* `--path=`: + The location to install the specified gems to. This defaults to Rubygems' + setting. Bundler shares this location with Rubygems, `gem install ...` will + have gem installed there, too. Therefore, gems installed without a + `--path ...` setting will show up by calling `gem list`. Accordingly, gems + installed to other locations will not get listed. + +* `--quiet`: + Do not print progress information to the standard output. Instead, Bundler + will exit using a status code (`$?`). + +* `--retry=[]`: + Retry failed network or git requests for times. + +* `--shebang=`: + Uses the specified ruby executable (usually `ruby`) to execute the scripts + created with `--binstubs`. In addition, if you use `--binstubs` together with + `--shebang jruby` these executables will be changed to execute `jruby` + instead. + +* `--standalone[=]`: + Makes a bundle that can work without depending on Rubygems or Bundler at + runtime. A space separated list of groups to install has to be specified. + Bundler creates a directory named `bundle` and installs the bundle there. It + also generates a `bundle/bundler/setup.rb` file to replace Bundler's own setup + in the manner required. Using this option implicitly sets `path`, which is a + [remembered option][REMEMBERED OPTIONS]. + +* `--system`: + Installs the gems specified in the bundle to the system's Rubygems location. + This overrides any previous configuration of `--path`. + +* `--trust-policy=[]`: + Apply the Rubygems security policy , where policy is one of + `HighSecurity`, `MediumSecurity`, `LowSecurity`, `AlmostNoSecurity`, or + `NoSecurity`. For more details, please see the Rubygems signing documentation + linked below in [SEE ALSO][]. + +* `--with=`: + A space-separated list of groups referencing gems to install. If an + optional group is given it is installed. If a group is given that is + in the remembered list of groups given to --without, it is removed + from that list. + +* `--without=`: + A space-separated list of groups referencing gems to skip during installation. + If a group is given that is in the remembered list of groups given + to --with, it is removed from that list. + +## DEPLOYMENT MODE + +Bundler's defaults are optimized for development. To switch to +defaults optimized for deployment and for CI, use the `--deployment` +flag. Do not activate deployment mode on development machines, as it +will cause an error when the Gemfile(5) is modified. + +1. A `Gemfile.lock` is required. + + To ensure that the same versions of the gems you developed with + and tested with are also used in deployments, a `Gemfile.lock` + is required. + + This is mainly to ensure that you remember to check your + `Gemfile.lock` into version control. + +2. The `Gemfile.lock` must be up to date + + In development, you can modify your Gemfile(5) and re-run + `bundle install` to [conservatively update][CONSERVATIVE UPDATING] + your `Gemfile.lock` snapshot. + + In deployment, your `Gemfile.lock` should be up-to-date with + changes made in your Gemfile(5). + +3. Gems are installed to `vendor/bundle` not your default system location + + In development, it's convenient to share the gems used in your + application with other applications and other scripts that run on + the system. + + In deployment, isolation is a more important default. In addition, + the user deploying the application may not have permission to install + gems to the system, or the web server may not have permission to + read them. + + As a result, `bundle install --deployment` installs gems to + the `vendor/bundle` directory in the application. This may be + overridden using the `--path` option. + +## SUDO USAGE + +By default, Bundler installs gems to the same location as `gem install`. + +In some cases, that location may not be writable by your Unix user. In +that case, Bundler will stage everything in a temporary directory, +then ask you for your `sudo` password in order to copy the gems into +their system location. + +From your perspective, this is identical to installing the gems +directly into the system. + +You should never use `sudo bundle install`. This is because several +other steps in `bundle install` must be performed as the current user: + +* Updating your `Gemfile.lock` +* Updating your `vendor/cache`, if necessary +* Checking out private git repositories using your user's SSH keys + +Of these three, the first two could theoretically be performed by +`chown`ing the resulting files to `$SUDO_USER`. The third, however, +can only be performed by invoking the `git` command as +the current user. Therefore, git gems are downloaded and installed +into `~/.bundle` rather than $GEM_HOME or $BUNDLE_PATH. + +As a result, you should run `bundle install` as the current user, +and Bundler will ask for your password if it is needed to put the +gems into their final location. + +## INSTALLING GROUPS + +By default, `bundle install` will install all gems in all groups +in your Gemfile(5), except those declared for a different platform. + +However, you can explicitly tell Bundler to skip installing +certain groups with the `--without` option. This option takes +a space-separated list of groups. + +While the `--without` option will skip _installing_ the gems in the +specified groups, it will still _download_ those gems and use them to +resolve the dependencies of every gem in your Gemfile(5). + +This is so that installing a different set of groups on another + machine (such as a production server) will not change the +gems and versions that you have already developed and tested against. + +`Bundler offers a rock-solid guarantee that the third-party +code you are running in development and testing is also the +third-party code you are running in production. You can choose +to exclude some of that code in different environments, but you +will never be caught flat-footed by different versions of +third-party code being used in different environments.` + +For a simple illustration, consider the following Gemfile(5): + + source 'https://rubygems.org' + + gem 'sinatra' + + group :production do + gem 'rack-perftools-profiler' + end + +In this case, `sinatra` depends on any version of Rack (`>= 1.0`), while +`rack-perftools-profiler` depends on 1.x (`~> 1.0`). + +When you run `bundle install --without production` in development, we +look at the dependencies of `rack-perftools-profiler` as well. That way, +you do not spend all your time developing against Rack 2.0, using new +APIs unavailable in Rack 1.x, only to have Bundler switch to Rack 1.2 +when the `production` group _is_ used. + +This should not cause any problems in practice, because we do not +attempt to `install` the gems in the excluded groups, and only evaluate +as part of the dependency resolution process. + +This also means that you cannot include different versions of the same +gem in different groups, because doing so would result in different +sets of dependencies used in development and production. Because of +the vagaries of the dependency resolution process, this usually +affects more than the gems you list in your Gemfile(5), and can +(surprisingly) radically change the gems you are using. + +## THE GEMFILE.LOCK + +When you run `bundle install`, Bundler will persist the full names +and versions of all gems that you used (including dependencies of +the gems specified in the Gemfile(5)) into a file called `Gemfile.lock`. + +Bundler uses this file in all subsequent calls to `bundle install`, +which guarantees that you always use the same exact code, even +as your application moves across machines. + +Because of the way dependency resolution works, even a +seemingly small change (for instance, an update to a point-release +of a dependency of a gem in your Gemfile(5)) can result in radically +different gems being needed to satisfy all dependencies. + +As a result, you `SHOULD` check your `Gemfile.lock` into version +control, in both applications and gems. If you do not, every machine that +checks out your repository (including your production server) will resolve all +dependencies again, which will result in different versions of +third-party code being used if `any` of the gems in the Gemfile(5) +or any of their dependencies have been updated. + +When Bundler first shipped, the `Gemfile.lock` was included in the `.gitignore` +file included with generated gems. Over time, however, it became clear that +this practice forces the pain of broken dependencies onto new contributors, +while leaving existing contributors potentially unaware of the problem. Since +`bundle install` is usually the first step towards a contribution, the pain of +broken dependencies would discourage new contributors from contributing. As a +result, we have revised our guidance for gem authors to now recommend checking +in the lock for gems. + +## CONSERVATIVE UPDATING + +When you make a change to the Gemfile(5) and then run `bundle install`, +Bundler will update only the gems that you modified. + +In other words, if a gem that you `did not modify` worked before +you called `bundle install`, it will continue to use the exact +same versions of all dependencies as it used before the update. + +Let's take a look at an example. Here's your original Gemfile(5): + + source 'https://rubygems.org' + + gem 'actionpack', '2.3.8' + gem 'activemerchant' + +In this case, both `actionpack` and `activemerchant` depend on +`activesupport`. The `actionpack` gem depends on `activesupport 2.3.8` +and `rack ~> 1.1.0`, while the `activemerchant` gem depends on +`activesupport >= 2.3.2`, `braintree >= 2.0.0`, and `builder >= 2.0.0`. + +When the dependencies are first resolved, Bundler will select +`activesupport 2.3.8`, which satisfies the requirements of both +gems in your Gemfile(5). + +Next, you modify your Gemfile(5) to: + + source 'https://rubygems.org' + + gem 'actionpack', '3.0.0.rc' + gem 'activemerchant' + +The `actionpack 3.0.0.rc` gem has a number of new dependencies, +and updates the `activesupport` dependency to `= 3.0.0.rc` and +the `rack` dependency to `~> 1.2.1`. + +When you run `bundle install`, Bundler notices that you changed +the `actionpack` gem, but not the `activemerchant` gem. It +evaluates the gems currently being used to satisfy its requirements: + + * `activesupport 2.3.8`: + also used to satisfy a dependency in `activemerchant`, + which is not being updated + * `rack ~> 1.1.0`: + not currently being used to satisfy another dependency + +Because you did not explicitly ask to update `activemerchant`, +you would not expect it to suddenly stop working after updating +`actionpack`. However, satisfying the new `activesupport 3.0.0.rc` +dependency of actionpack requires updating one of its dependencies. + +Even though `activemerchant` declares a very loose dependency +that theoretically matches `activesupport 3.0.0.rc`, Bundler treats +gems in your Gemfile(5) that have not changed as an atomic unit +together with their dependencies. In this case, the `activemerchant` +dependency is treated as `activemerchant 1.7.1 + activesupport 2.3.8`, +so `bundle install` will report that it cannot update `actionpack`. + +To explicitly update `actionpack`, including its dependencies +which other gems in the Gemfile(5) still depend on, run +`bundle update actionpack` (see `bundle update(1)`). + +`Summary`: In general, after making a change to the Gemfile(5) , you +should first try to run `bundle install`, which will guarantee that no +other gem in the Gemfile(5) is impacted by the change. If that +does not work, run [bundle update(1)](bundle-update.1.html). + +## SEE ALSO + +* [Gem install docs](http://guides.rubygems.org/rubygems-basics/#installing-gems) +* [Rubygems signing docs](http://guides.rubygems.org/security/) diff --git a/man/bundle-list.1 b/man/bundle-list.1 new file mode 100644 index 00000000000000..c41948f1b57185 --- /dev/null +++ b/man/bundle-list.1 @@ -0,0 +1,50 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-LIST" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-list\fR \- List all the gems in the bundle +. +.SH "SYNOPSIS" +\fBbundle list\fR [\-\-name\-only] [\-\-paths] [\-\-without\-group=GROUP] [\-\-only\-group=GROUP] +. +.SH "DESCRIPTION" +Prints a list of all the gems in the bundle including their version\. +. +.P +Example: +. +.P +bundle list \-\-name\-only +. +.P +bundle list \-\-paths +. +.P +bundle list \-\-without\-group test +. +.P +bundle list \-\-only\-group dev +. +.P +bundle list \-\-only\-group dev \-\-paths +. +.SH "OPTIONS" +. +.TP +\fB\-\-name\-only\fR +Print only the name of each gem\. +. +.TP +\fB\-\-paths\fR +Print the path to each gem in the bundle\. +. +.TP +\fB\-\-without\-group\fR +Print all gems expect from a group\. +. +.TP +\fB\-\-only\-group\fR +Print gems from a particular group\. + diff --git a/man/bundle-list.1.txt b/man/bundle-list.1.txt new file mode 100644 index 00000000000000..aa20d4ebeac939 --- /dev/null +++ b/man/bundle-list.1.txt @@ -0,0 +1,43 @@ +BUNDLE-LIST(1) BUNDLE-LIST(1) + + + +1mNAME0m + 1mbundle-list 22m- List all the gems in the bundle + +1mSYNOPSIS0m + 1mbundle list 22m[--name-only] [--paths] [--without-group=GROUP] + [--only-group=GROUP] + +1mDESCRIPTION0m + Prints a list of all the gems in the bundle including their version. + + Example: + + bundle list --name-only + + bundle list --paths + + bundle list --without-group test + + bundle list --only-group dev + + bundle list --only-group dev --paths + +1mOPTIONS0m + 1m--name-only0m + Print only the name of each gem. + + 1m--paths0m + Print the path to each gem in the bundle. + + 1m--without-group0m + Print all gems expect from a group. + + 1m--only-group0m + Print gems from a particular group. + + + + + November 2018 BUNDLE-LIST(1) diff --git a/man/bundle-list.ronn b/man/bundle-list.ronn new file mode 100644 index 00000000000000..120cf5e30756e1 --- /dev/null +++ b/man/bundle-list.ronn @@ -0,0 +1,33 @@ +bundle-list(1) -- List all the gems in the bundle +========================================================================= + +## SYNOPSIS + +`bundle list` [--name-only] [--paths] [--without-group=GROUP] [--only-group=GROUP] + +## DESCRIPTION + +Prints a list of all the gems in the bundle including their version. + +Example: + +bundle list --name-only + +bundle list --paths + +bundle list --without-group test + +bundle list --only-group dev + +bundle list --only-group dev --paths + +## OPTIONS + +* `--name-only`: + Print only the name of each gem. +* `--paths`: + Print the path to each gem in the bundle. +* `--without-group`: + Print all gems expect from a group. +* `--only-group`: + Print gems from a particular group. diff --git a/man/bundle-lock.1 b/man/bundle-lock.1 new file mode 100644 index 00000000000000..03b7e8f9cc84cf --- /dev/null +++ b/man/bundle-lock.1 @@ -0,0 +1,84 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-LOCK" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-lock\fR \- Creates / Updates a lockfile without installing +. +.SH "SYNOPSIS" +\fBbundle lock\fR [\-\-update] [\-\-local] [\-\-print] [\-\-lockfile=PATH] [\-\-full\-index] [\-\-add\-platform] [\-\-remove\-platform] [\-\-patch] [\-\-minor] [\-\-major] [\-\-strict] [\-\-conservative] +. +.SH "DESCRIPTION" +Lock the gems specified in Gemfile\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-update=<*gems>\fR +Ignores the existing lockfile\. Resolve then updates lockfile\. Taking a list of gems or updating all gems if no list is given\. +. +.TP +\fB\-\-local\fR +Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems\' cache or in \fBvendor/cache\fR\. Note that if a appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\. +. +.TP +\fB\-\-print\fR +Prints the lockfile to STDOUT instead of writing to the file system\. +. +.TP +\fB\-\-lockfile=\fR +The path where the lockfile should be written to\. +. +.TP +\fB\-\-full\-index\fR +Fall back to using the single\-file index of all gems\. +. +.TP +\fB\-\-add\-platform\fR +Add a new platform to the lockfile, re\-resolving for the addition of that platform\. +. +.TP +\fB\-\-remove\-platform\fR +Remove a platform from the lockfile\. +. +.TP +\fB\-\-patch\fR +If updating, prefer updating only to next patch version\. +. +.TP +\fB\-\-minor\fR +If updating, prefer updating only to next minor version\. +. +.TP +\fB\-\-major\fR +If updating, prefer updating to next major version (default)\. +. +.TP +\fB\-\-strict\fR +If updating, do not allow any gem to be updated past latest \-\-patch | \-\-minor | \-\-major\. +. +.TP +\fB\-\-conservative\fR +If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated\. +. +.SH "UPDATING ALL GEMS" +If you run \fBbundle lock\fR with \fB\-\-update\fR option without list of gems, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. +. +.SH "UPDATING A LIST OF GEMS" +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\. +. +.P +For instance, you only want to update \fBnokogiri\fR, run \fBbundle lock \-\-update nokogiri\fR\. +. +.P +Bundler will update \fBnokogiri\fR and any of its dependencies, but leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\. +. +.SH "SUPPORTING OTHER PLATFORMS" +If you want your bundle to support platforms other than the one you\'re running locally, you can run \fBbundle lock \-\-add\-platform PLATFORM\fR to add PLATFORM to the lockfile, force bundler to re\-resolve and consider the new platform when picking gems, all without needing to have a machine that matches PLATFORM handy to install those platform\-specific gems on\. +. +.P +For a full explanation of gem platforms, see \fBgem help platform\fR\. +. +.SH "PATCH LEVEL OPTIONS" +See bundle update(1) \fIbundle\-update\.1\.html\fR for details\. diff --git a/man/bundle-lock.1.txt b/man/bundle-lock.1.txt new file mode 100644 index 00000000000000..2c757f02019fe2 --- /dev/null +++ b/man/bundle-lock.1.txt @@ -0,0 +1,93 @@ +BUNDLE-LOCK(1) BUNDLE-LOCK(1) + + + +1mNAME0m + 1mbundle-lock 22m- Creates / Updates a lockfile without installing + +1mSYNOPSIS0m + 1mbundle lock 22m[--update] [--local] [--print] [--lockfile=PATH] + [--full-index] [--add-platform] [--remove-platform] [--patch] [--minor] + [--major] [--strict] [--conservative] + +1mDESCRIPTION0m + Lock the gems specified in Gemfile. + +1mOPTIONS0m + 1m--update=<*gems>0m + Ignores the existing lockfile. Resolve then updates lockfile. + Taking a list of gems or updating all gems if no list is given. + + 1m--local0m + Do not attempt to connect to 1mrubygems.org22m. Instead, Bundler will + use the gems already present in Rubygems' cache or in 1mven-0m + 1mdor/cache22m. Note that if a appropriate platform-specific gem + exists on 1mrubygems.org 22mit will not be found. + + 1m--print0m + Prints the lockfile to STDOUT instead of writing to the file + system. + + 1m--lockfile=0m + The path where the lockfile should be written to. + + 1m--full-index0m + Fall back to using the single-file index of all gems. + + 1m--add-platform0m + Add a new platform to the lockfile, re-resolving for the addi- + tion of that platform. + + 1m--remove-platform0m + Remove a platform from the lockfile. + + 1m--patch0m + If updating, prefer updating only to next patch version. + + 1m--minor0m + If updating, prefer updating only to next minor version. + + 1m--major0m + If updating, prefer updating to next major version (default). + + 1m--strict0m + If updating, do not allow any gem to be updated past latest + --patch | --minor | --major. + + 1m--conservative0m + If updating, use bundle install conservative update behavior and + do not allow shared dependencies to be updated. + +1mUPDATING ALL GEMS0m + If you run 1mbundle lock 22mwith 1m--update 22moption without list of gems, + bundler will ignore any previously installed gems and resolve all + dependencies again based on the latest versions of all gems available + in the sources. + +1mUPDATING A LIST OF GEMS0m + Sometimes, you want to update a single gem in the Gemfile(5), and leave + the rest of the gems that you specified locked to the versions in the + 1mGemfile.lock22m. + + For instance, you only want to update 1mnokogiri22m, run 1mbundle lock0m + 1m--update nokogiri22m. + + Bundler will update 1mnokogiri 22mand any of its dependencies, but leave the + rest of the gems that you specified locked to the versions in the 1mGem-0m + 1mfile.lock22m. + +1mSUPPORTING OTHER PLATFORMS0m + If you want your bundle to support platforms other than the one you're + running locally, you can run 1mbundle lock --add-platform PLATFORM 22mto add + PLATFORM to the lockfile, force bundler to re-resolve and consider the + new platform when picking gems, all without needing to have a machine + that matches PLATFORM handy to install those platform-specific gems on. + + For a full explanation of gem platforms, see 1mgem help platform22m. + +1mPATCH LEVEL OPTIONS0m + See bundle update(1) 4mbundle-update.1.html24m for details. + + + + November 2018 BUNDLE-LOCK(1) diff --git a/man/bundle-lock.ronn b/man/bundle-lock.ronn new file mode 100644 index 00000000000000..3aa5920f5a1f83 --- /dev/null +++ b/man/bundle-lock.ronn @@ -0,0 +1,94 @@ +bundle-lock(1) -- Creates / Updates a lockfile without installing +================================================================= + +## SYNOPSIS + +`bundle lock` [--update] + [--local] + [--print] + [--lockfile=PATH] + [--full-index] + [--add-platform] + [--remove-platform] + [--patch] + [--minor] + [--major] + [--strict] + [--conservative] + +## DESCRIPTION + +Lock the gems specified in Gemfile. + +## OPTIONS + +* `--update=<*gems>`: + Ignores the existing lockfile. Resolve then updates lockfile. Taking a list + of gems or updating all gems if no list is given. + +* `--local`: + Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the + gems already present in Rubygems' cache or in `vendor/cache`. Note that if a + appropriate platform-specific gem exists on `rubygems.org` it will not be + found. + +* `--print`: + Prints the lockfile to STDOUT instead of writing to the file system. + +* `--lockfile=`: + The path where the lockfile should be written to. + +* `--full-index`: + Fall back to using the single-file index of all gems. + +* `--add-platform`: + Add a new platform to the lockfile, re-resolving for the addition of that + platform. + +* `--remove-platform`: + Remove a platform from the lockfile. + +* `--patch`: + If updating, prefer updating only to next patch version. + +* `--minor`: + If updating, prefer updating only to next minor version. + +* `--major`: + If updating, prefer updating to next major version (default). + +* `--strict`: + If updating, do not allow any gem to be updated past latest --patch | --minor | --major. + +* `--conservative`: + If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated. + +## UPDATING ALL GEMS + +If you run `bundle lock` with `--update` option without list of gems, bundler will +ignore any previously installed gems and resolve all dependencies again based +on the latest versions of all gems available in the sources. + +## UPDATING A LIST OF GEMS + +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of +the gems that you specified locked to the versions in the `Gemfile.lock`. + +For instance, you only want to update `nokogiri`, run `bundle lock --update nokogiri`. + +Bundler will update `nokogiri` and any of its dependencies, but leave the rest of the +gems that you specified locked to the versions in the `Gemfile.lock`. + +## SUPPORTING OTHER PLATFORMS + +If you want your bundle to support platforms other than the one you're running +locally, you can run `bundle lock --add-platform PLATFORM` to add PLATFORM to +the lockfile, force bundler to re-resolve and consider the new platform when +picking gems, all without needing to have a machine that matches PLATFORM handy +to install those platform-specific gems on. + +For a full explanation of gem platforms, see `gem help platform`. + +## PATCH LEVEL OPTIONS + +See [bundle update(1)](bundle-update.1.html) for details. diff --git a/man/bundle-open.1 b/man/bundle-open.1 new file mode 100644 index 00000000000000..6301cb1fbc1d95 --- /dev/null +++ b/man/bundle-open.1 @@ -0,0 +1,32 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-OPEN" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-open\fR \- Opens the source directory for a gem in your bundle +. +.SH "SYNOPSIS" +\fBbundle open\fR [GEM] +. +.SH "DESCRIPTION" +Opens the source directory of the provided GEM in your editor\. +. +.P +For this to work the \fBEDITOR\fR or \fBBUNDLER_EDITOR\fR environment variable has to be set\. +. +.P +Example: +. +.IP "" 4 +. +.nf + +bundle open \'rack\' +. +.fi +. +.IP "" 0 +. +.P +Will open the source directory for the \'rack\' gem in your bundle\. diff --git a/man/bundle-open.1.txt b/man/bundle-open.1.txt new file mode 100644 index 00000000000000..833744ae5e9c60 --- /dev/null +++ b/man/bundle-open.1.txt @@ -0,0 +1,29 @@ +BUNDLE-OPEN(1) BUNDLE-OPEN(1) + + + +1mNAME0m + 1mbundle-open 22m- Opens the source directory for a gem in your bundle + +1mSYNOPSIS0m + 1mbundle open 22m[GEM] + +1mDESCRIPTION0m + Opens the source directory of the provided GEM in your editor. + + For this to work the 1mEDITOR 22mor 1mBUNDLER_EDITOR 22menvironment variable has + to be set. + + Example: + + + + bundle open 'rack' + + + + Will open the source directory for the 'rack' gem in your bundle. + + + + November 2018 BUNDLE-OPEN(1) diff --git a/man/bundle-open.ronn b/man/bundle-open.ronn new file mode 100644 index 00000000000000..497beac93f9310 --- /dev/null +++ b/man/bundle-open.ronn @@ -0,0 +1,19 @@ +bundle-open(1) -- Opens the source directory for a gem in your bundle +===================================================================== + +## SYNOPSIS + +`bundle open` [GEM] + +## DESCRIPTION + +Opens the source directory of the provided GEM in your editor. + +For this to work the `EDITOR` or `BUNDLER_EDITOR` environment variable has to +be set. + +Example: + + bundle open 'rack' + +Will open the source directory for the 'rack' gem in your bundle. diff --git a/man/bundle-outdated.1 b/man/bundle-outdated.1 new file mode 100644 index 00000000000000..cde4bb09a11027 --- /dev/null +++ b/man/bundle-outdated.1 @@ -0,0 +1,155 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-OUTDATED" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-outdated\fR \- List installed gems with newer versions available +. +.SH "SYNOPSIS" +\fBbundle outdated\fR [GEM] [\-\-local] [\-\-pre] [\-\-source] [\-\-strict] [\-\-parseable | \-\-porcelain] [\-\-group=GROUP] [\-\-groups] [\-\-update\-strict] [\-\-patch|\-\-minor|\-\-major] [\-\-filter\-major] [\-\-filter\-minor] [\-\-filter\-patch] [\-\-only\-explicit] +. +.SH "DESCRIPTION" +Outdated lists the names and versions of gems that have a newer version available in the given source\. Calling outdated with [GEM [GEM]] will only check for newer versions of the given gems\. Prerelease gems are ignored by default\. If your gems are up to date, Bundler will exit with a status of 0\. Otherwise, it will exit 1\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-local\fR +Do not attempt to fetch gems remotely and use the gem cache instead\. +. +.TP +\fB\-\-pre\fR +Check for newer pre\-release gems\. +. +.TP +\fB\-\-source\fR +Check against a specific source\. +. +.TP +\fB\-\-strict\fR +Only list newer versions allowed by your Gemfile requirements\. +. +.TP +\fB\-\-parseable\fR, \fB\-\-porcelain\fR +Use minimal formatting for more parseable output\. +. +.TP +\fB\-\-group\fR +List gems from a specific group\. +. +.TP +\fB\-\-groups\fR +List gems organized by groups\. +. +.TP +\fB\-\-update\-strict\fR +Strict conservative resolution, do not allow any gem to be updated past latest \-\-patch | \-\-minor| \-\-major\. +. +.TP +\fB\-\-minor\fR +Prefer updating only to next minor version\. +. +.TP +\fB\-\-major\fR +Prefer updating to next major version (default)\. +. +.TP +\fB\-\-patch\fR +Prefer updating only to next patch version\. +. +.TP +\fB\-\-filter\-major\fR +Only list major newer versions\. +. +.TP +\fB\-\-filter\-minor\fR +Only list minor newer versions\. +. +.TP +\fB\-\-filter\-patch\fR +Only list patch newer versions\. +. +.TP +\fB\-\-only\-explicit\fR +Only list gems specified in your Gemfile, not their dependencies\. +. +.SH "PATCH LEVEL OPTIONS" +See bundle update(1) \fIbundle\-update\.1\.html\fR for details\. +. +.P +One difference between the patch level options in \fBbundle update\fR and here is the \fB\-\-strict\fR option\. \fB\-\-strict\fR was already an option on outdated before the patch level options were added\. \fB\-\-strict\fR wasn\'t altered, and the \fB\-\-update\-strict\fR option on \fBoutdated\fR reflects what \fB\-\-strict\fR does on \fBbundle update\fR\. +. +.SH "FILTERING OUTPUT" +The 3 filtering options do not affect the resolution of versions, merely what versions are shown in the output\. +. +.P +If the regular output shows the following: +. +.IP "" 4 +. +.nf + +* faker (newest 1\.6\.6, installed 1\.6\.5, requested ~> 1\.4) in groups "development, test" +* hashie (newest 3\.4\.6, installed 1\.2\.0, requested = 1\.2\.0) in groups "default" +* headless (newest 2\.3\.1, installed 2\.2\.3) in groups "test" +. +.fi +. +.IP "" 0 +. +.P +\fB\-\-filter\-major\fR would only show: +. +.IP "" 4 +. +.nf + +* hashie (newest 3\.4\.6, installed 1\.2\.0, requested = 1\.2\.0) in groups "default" +. +.fi +. +.IP "" 0 +. +.P +\fB\-\-filter\-minor\fR would only show: +. +.IP "" 4 +. +.nf + +* headless (newest 2\.3\.1, installed 2\.2\.3) in groups "test" +. +.fi +. +.IP "" 0 +. +.P +\fB\-\-filter\-patch\fR would only show: +. +.IP "" 4 +. +.nf + +* faker (newest 1\.6\.6, installed 1\.6\.5, requested ~> 1\.4) in groups "development, test" +. +.fi +. +.IP "" 0 +. +.P +Filter options can be combined\. \fB\-\-filter\-minor\fR and \fB\-\-filter\-patch\fR would show: +. +.IP "" 4 +. +.nf + +* faker (newest 1\.6\.6, installed 1\.6\.5, requested ~> 1\.4) in groups "development, test" +* headless (newest 2\.3\.1, installed 2\.2\.3) in groups "test" +. +.fi +. +.IP "" 0 +. +.P +Combining all three \fBfilter\fR options would be the same result as providing none of them\. diff --git a/man/bundle-outdated.1.txt b/man/bundle-outdated.1.txt new file mode 100644 index 00000000000000..00779962fca9be --- /dev/null +++ b/man/bundle-outdated.1.txt @@ -0,0 +1,131 @@ +BUNDLE-OUTDATED(1) BUNDLE-OUTDATED(1) + + + +1mNAME0m + 1mbundle-outdated 22m- List installed gems with newer versions available + +1mSYNOPSIS0m + 1mbundle outdated 22m[GEM] [--local] [--pre] [--source] [--strict] + [--parseable | --porcelain] [--group=GROUP] [--groups] + [--update-strict] [--patch|--minor|--major] [--filter-major] [--fil- + ter-minor] [--filter-patch] [--only-explicit] + +1mDESCRIPTION0m + Outdated lists the names and versions of gems that have a newer version + available in the given source. Calling outdated with [GEM [GEM]] will + only check for newer versions of the given gems. Prerelease gems are + ignored by default. If your gems are up to date, Bundler will exit with + a status of 0. Otherwise, it will exit 1. + +1mOPTIONS0m + 1m--local0m + Do not attempt to fetch gems remotely and use the gem cache + instead. + + 1m--pre 22mCheck for newer pre-release gems. + + 1m--source0m + Check against a specific source. + + 1m--strict0m + Only list newer versions allowed by your Gemfile requirements. + + 1m--parseable22m, 1m--porcelain0m + Use minimal formatting for more parseable output. + + 1m--group0m + List gems from a specific group. + + 1m--groups0m + List gems organized by groups. + + 1m--update-strict0m + Strict conservative resolution, do not allow any gem to be + updated past latest --patch | --minor| --major. + + 1m--minor0m + Prefer updating only to next minor version. + + 1m--major0m + Prefer updating to next major version (default). + + 1m--patch0m + Prefer updating only to next patch version. + + 1m--filter-major0m + Only list major newer versions. + + 1m--filter-minor0m + Only list minor newer versions. + + 1m--filter-patch0m + Only list patch newer versions. + + 1m--only-explicit0m + Only list gems specified in your Gemfile, not their dependen- + cies. + +1mPATCH LEVEL OPTIONS0m + See bundle update(1) 4mbundle-update.1.html24m for details. + + One difference between the patch level options in 1mbundle update 22mand + here is the 1m--strict 22moption. 1m--strict 22mwas already an option on outdated + before the patch level options were added. 1m--strict 22mwasn't altered, and + the 1m--update-strict 22moption on 1moutdated 22mreflects what 1m--strict 22mdoes on + 1mbundle update22m. + +1mFILTERING OUTPUT0m + The 3 filtering options do not affect the resolution of versions, + merely what versions are shown in the output. + + If the regular output shows the following: + + + + * faker (newest 1.6.6, installed 1.6.5, requested ~> 1.4) in groups "development, test" + * hashie (newest 3.4.6, installed 1.2.0, requested = 1.2.0) in groups "default" + * headless (newest 2.3.1, installed 2.2.3) in groups "test" + + + + 1m--filter-major 22mwould only show: + + + + * hashie (newest 3.4.6, installed 1.2.0, requested = 1.2.0) in groups "default" + + + + 1m--filter-minor 22mwould only show: + + + + * headless (newest 2.3.1, installed 2.2.3) in groups "test" + + + + 1m--filter-patch 22mwould only show: + + + + * faker (newest 1.6.6, installed 1.6.5, requested ~> 1.4) in groups "development, test" + + + + Filter options can be combined. 1m--filter-minor 22mand 1m--filter-patch 22mwould + show: + + + + * faker (newest 1.6.6, installed 1.6.5, requested ~> 1.4) in groups "development, test" + * headless (newest 2.3.1, installed 2.2.3) in groups "test" + + + + Combining all three 1mfilter 22moptions would be the same result as provid- + ing none of them. + + + + November 2018 BUNDLE-OUTDATED(1) diff --git a/man/bundle-outdated.ronn b/man/bundle-outdated.ronn new file mode 100644 index 00000000000000..a991d23789360e --- /dev/null +++ b/man/bundle-outdated.ronn @@ -0,0 +1,111 @@ +bundle-outdated(1) -- List installed gems with newer versions available +======================================================================= + +## SYNOPSIS + +`bundle outdated` [GEM] [--local] + [--pre] + [--source] + [--strict] + [--parseable | --porcelain] + [--group=GROUP] + [--groups] + [--update-strict] + [--patch|--minor|--major] + [--filter-major] + [--filter-minor] + [--filter-patch] + [--only-explicit] + +## DESCRIPTION + +Outdated lists the names and versions of gems that have a newer version available +in the given source. Calling outdated with [GEM [GEM]] will only check for newer +versions of the given gems. Prerelease gems are ignored by default. If your gems +are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. + +## OPTIONS + +* `--local`: + Do not attempt to fetch gems remotely and use the gem cache instead. + +* `--pre`: + Check for newer pre-release gems. + +* `--source`: + Check against a specific source. + +* `--strict`: + Only list newer versions allowed by your Gemfile requirements. + +* `--parseable`, `--porcelain`: + Use minimal formatting for more parseable output. + +* `--group`: + List gems from a specific group. + +* `--groups`: + List gems organized by groups. + +* `--update-strict`: + Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor| --major. + +* `--minor`: + Prefer updating only to next minor version. + +* `--major`: + Prefer updating to next major version (default). + +* `--patch`: + Prefer updating only to next patch version. + +* `--filter-major`: + Only list major newer versions. + +* `--filter-minor`: + Only list minor newer versions. + +* `--filter-patch`: + Only list patch newer versions. + +* `--only-explicit`: + Only list gems specified in your Gemfile, not their dependencies. + +## PATCH LEVEL OPTIONS + +See [bundle update(1)](bundle-update.1.html) for details. + +One difference between the patch level options in `bundle update` and here is the `--strict` option. +`--strict` was already an option on outdated before the patch level options were added. `--strict` +wasn't altered, and the `--update-strict` option on `outdated` reflects what `--strict` does on +`bundle update`. + +## FILTERING OUTPUT + +The 3 filtering options do not affect the resolution of versions, merely what versions are shown +in the output. + +If the regular output shows the following: + + * faker (newest 1.6.6, installed 1.6.5, requested ~> 1.4) in groups "development, test" + * hashie (newest 3.4.6, installed 1.2.0, requested = 1.2.0) in groups "default" + * headless (newest 2.3.1, installed 2.2.3) in groups "test" + +`--filter-major` would only show: + + * hashie (newest 3.4.6, installed 1.2.0, requested = 1.2.0) in groups "default" + +`--filter-minor` would only show: + + * headless (newest 2.3.1, installed 2.2.3) in groups "test" + +`--filter-patch` would only show: + + * faker (newest 1.6.6, installed 1.6.5, requested ~> 1.4) in groups "development, test" + +Filter options can be combined. `--filter-minor` and `--filter-patch` would show: + + * faker (newest 1.6.6, installed 1.6.5, requested ~> 1.4) in groups "development, test" + * headless (newest 2.3.1, installed 2.2.3) in groups "test" + +Combining all three `filter` options would be the same result as providing none of them. diff --git a/man/bundle-package.1 b/man/bundle-package.1 new file mode 100644 index 00000000000000..db0447be83a7e2 --- /dev/null +++ b/man/bundle-package.1 @@ -0,0 +1,55 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-PACKAGE" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-package\fR \- Package your needed \fB\.gem\fR files into your application +. +.SH "SYNOPSIS" +\fBbundle package\fR +. +.SH "DESCRIPTION" +Copy all of the \fB\.gem\fR files needed to run the application into the \fBvendor/cache\fR directory\. In the future, when running [bundle install(1)][bundle\-install], use the gems in the cache in preference to the ones on \fBrubygems\.org\fR\. +. +.SH "GIT AND PATH GEMS" +Since Bundler 1\.2, the \fBbundle package\fR command can also package \fB:git\fR and \fB:path\fR dependencies besides \.gem files\. This needs to be explicitly enabled via the \fB\-\-all\fR option\. Once used, the \fB\-\-all\fR option will be remembered\. +. +.SH "SUPPORT FOR MULTIPLE PLATFORMS" +When using gems that have different packages for different platforms, Bundler 1\.8 and newer support caching of gems for other platforms where the Gemfile has been resolved (i\.e\. present in the lockfile) in \fBvendor/cache\fR\. This needs to be enabled via the \fB\-\-all\-platforms\fR option\. This setting will be remembered in your local bundler configuration\. +. +.SH "REMOTE FETCHING" +By default, if you run \fBbundle install(1)\fR](bundle\-install\.1\.html) after running bundle package(1) \fIbundle\-package\.1\.html\fR, bundler will still connect to \fBrubygems\.org\fR to check whether a platform\-specific gem exists for any of the gems in \fBvendor/cache\fR\. +. +.P +For instance, consider this Gemfile(5): +. +.IP "" 4 +. +.nf + +source "https://rubygems\.org" + +gem "nokogiri" +. +.fi +. +.IP "" 0 +. +.P +If you run \fBbundle package\fR under C Ruby, bundler will retrieve the version of \fBnokogiri\fR for the \fB"ruby"\fR platform\. If you deploy to JRuby and run \fBbundle install\fR, bundler is forced to check to see whether a \fB"java"\fR platformed \fBnokogiri\fR exists\. +. +.P +Even though the \fBnokogiri\fR gem for the Ruby platform is \fItechnically\fR acceptable on JRuby, it has a C extension that does not run on JRuby\. As a result, bundler will, by default, still connect to \fBrubygems\.org\fR to check whether it has a version of one of your gems more specific to your platform\. +. +.P +This problem is also not limited to the \fB"java"\fR platform\. A similar (common) problem can happen when developing on Windows and deploying to Linux, or even when developing on OSX and deploying to Linux\. +. +.P +If you know for sure that the gems packaged in \fBvendor/cache\fR are appropriate for the platform you are on, you can run \fBbundle install \-\-local\fR to skip checking for more appropriate gems, and use the ones in \fBvendor/cache\fR\. +. +.P +One way to be sure that you have the right platformed versions of all your gems is to run \fBbundle package\fR on an identical machine and check in the gems\. For instance, you can run \fBbundle package\fR on an identical staging box during your staging process, and check in the \fBvendor/cache\fR before deploying to production\. +. +.P +By default, bundle package(1) \fIbundle\-package\.1\.html\fR fetches and also installs the gems to the default location\. To package the dependencies to \fBvendor/cache\fR without installing them to the local install location, you can run \fBbundle package \-\-no\-install\fR\. diff --git a/man/bundle-package.1.txt b/man/bundle-package.1.txt new file mode 100644 index 00000000000000..e581e9450756a6 --- /dev/null +++ b/man/bundle-package.1.txt @@ -0,0 +1,79 @@ +BUNDLE-PACKAGE(1) BUNDLE-PACKAGE(1) + + + +1mNAME0m + 1mbundle-package 22m- Package your needed 1m.gem 22mfiles into your application + +1mSYNOPSIS0m + 1mbundle package0m + +1mDESCRIPTION0m + Copy all of the 1m.gem 22mfiles needed to run the application into the 1mven-0m + 1mdor/cache 22mdirectory. In the future, when running [bundle + install(1)][bundle-install], use the gems in the cache in preference to + the ones on 1mrubygems.org22m. + +1mGIT AND PATH GEMS0m + Since Bundler 1.2, the 1mbundle package 22mcommand can also package 1m:git 22mand + 1m:path 22mdependencies besides .gem files. This needs to be explicitly + enabled via the 1m--all 22moption. Once used, the 1m--all 22moption will be + remembered. + +1mSUPPORT FOR MULTIPLE PLATFORMS0m + When using gems that have different packages for different platforms, + Bundler 1.8 and newer support caching of gems for other platforms where + the Gemfile has been resolved (i.e. present in the lockfile) in 1mven-0m + 1mdor/cache22m. This needs to be enabled via the 1m--all-platforms 22moption. + This setting will be remembered in your local bundler configuration. + +1mREMOTE FETCHING0m + By default, if you run 1mbundle install(1)22m](bundle-install.1.html) after + running bundle package(1) 4mbundle-package.1.html24m, bundler will still + connect to 1mrubygems.org 22mto check whether a platform-specific gem exists + for any of the gems in 1mvendor/cache22m. + + For instance, consider this Gemfile(5): + + + + source "https://rubygems.org" + + gem "nokogiri" + + + + If you run 1mbundle package 22munder C Ruby, bundler will retrieve the ver- + sion of 1mnokogiri 22mfor the 1m"ruby" 22mplatform. If you deploy to JRuby and + run 1mbundle install22m, bundler is forced to check to see whether a 1m"java"0m + platformed 1mnokogiri 22mexists. + + Even though the 1mnokogiri 22mgem for the Ruby platform is 4mtechnically0m + acceptable on JRuby, it has a C extension that does not run on JRuby. + As a result, bundler will, by default, still connect to 1mrubygems.org 22mto + check whether it has a version of one of your gems more specific to + your platform. + + This problem is also not limited to the 1m"java" 22mplatform. A similar + (common) problem can happen when developing on Windows and deploying to + Linux, or even when developing on OSX and deploying to Linux. + + If you know for sure that the gems packaged in 1mvendor/cache 22mare appro- + priate for the platform you are on, you can run 1mbundle install --local0m + to skip checking for more appropriate gems, and use the ones in 1mven-0m + 1mdor/cache22m. + + One way to be sure that you have the right platformed versions of all + your gems is to run 1mbundle package 22mon an identical machine and check in + the gems. For instance, you can run 1mbundle package 22mon an identical + staging box during your staging process, and check in the 1mvendor/cache0m + before deploying to production. + + By default, bundle package(1) 4mbundle-package.1.html24m fetches and also + installs the gems to the default location. To package the dependencies + to 1mvendor/cache 22mwithout installing them to the local install location, + you can run 1mbundle package --no-install22m. + + + + November 2018 BUNDLE-PACKAGE(1) diff --git a/man/bundle-package.ronn b/man/bundle-package.ronn new file mode 100644 index 00000000000000..bc137374da934a --- /dev/null +++ b/man/bundle-package.ronn @@ -0,0 +1,72 @@ +bundle-package(1) -- Package your needed `.gem` files into your application +=========================================================================== + +## SYNOPSIS + +`bundle package` + +## DESCRIPTION + +Copy all of the `.gem` files needed to run the application into the +`vendor/cache` directory. In the future, when running [bundle install(1)][bundle-install], +use the gems in the cache in preference to the ones on `rubygems.org`. + +## GIT AND PATH GEMS + +Since Bundler 1.2, the `bundle package` command can also package `:git` and +`:path` dependencies besides .gem files. This needs to be explicitly enabled +via the `--all` option. Once used, the `--all` option will be remembered. + +## SUPPORT FOR MULTIPLE PLATFORMS + +When using gems that have different packages for different platforms, Bundler +1.8 and newer support caching of gems for other platforms where the Gemfile +has been resolved (i.e. present in the lockfile) in `vendor/cache`. This needs +to be enabled via the `--all-platforms` option. This setting will be remembered +in your local bundler configuration. + +## REMOTE FETCHING + +By default, if you run `bundle install(1)`](bundle-install.1.html) after running +[bundle package(1)](bundle-package.1.html), bundler will still connect to `rubygems.org` +to check whether a platform-specific gem exists for any of the gems +in `vendor/cache`. + +For instance, consider this Gemfile(5): + + source "https://rubygems.org" + + gem "nokogiri" + +If you run `bundle package` under C Ruby, bundler will retrieve +the version of `nokogiri` for the `"ruby"` platform. If you deploy +to JRuby and run `bundle install`, bundler is forced to check to +see whether a `"java"` platformed `nokogiri` exists. + +Even though the `nokogiri` gem for the Ruby platform is +_technically_ acceptable on JRuby, it has a C extension +that does not run on JRuby. As a result, bundler will, by default, +still connect to `rubygems.org` to check whether it has a version +of one of your gems more specific to your platform. + +This problem is also not limited to the `"java"` platform. +A similar (common) problem can happen when developing on Windows +and deploying to Linux, or even when developing on OSX and +deploying to Linux. + +If you know for sure that the gems packaged in `vendor/cache` +are appropriate for the platform you are on, you can run +`bundle install --local` to skip checking for more appropriate +gems, and use the ones in `vendor/cache`. + +One way to be sure that you have the right platformed versions +of all your gems is to run `bundle package` on an identical +machine and check in the gems. For instance, you can run +`bundle package` on an identical staging box during your +staging process, and check in the `vendor/cache` before +deploying to production. + +By default, [bundle package(1)](bundle-package.1.html) fetches and also +installs the gems to the default location. To package the +dependencies to `vendor/cache` without installing them to the +local install location, you can run `bundle package --no-install`. diff --git a/man/bundle-platform.1 b/man/bundle-platform.1 new file mode 100644 index 00000000000000..94b5e13cc1bf7b --- /dev/null +++ b/man/bundle-platform.1 @@ -0,0 +1,61 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-PLATFORM" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-platform\fR \- Displays platform compatibility information +. +.SH "SYNOPSIS" +\fBbundle platform\fR [\-\-ruby] +. +.SH "DESCRIPTION" +\fBplatform\fR will display information from your Gemfile, Gemfile\.lock, and Ruby VM about your platform\. +. +.P +For instance, using this Gemfile(5): +. +.IP "" 4 +. +.nf + +source "https://rubygems\.org" + +ruby "1\.9\.3" + +gem "rack" +. +.fi +. +.IP "" 0 +. +.P +If you run \fBbundle platform\fR on Ruby 1\.9\.3, it will display the following output: +. +.IP "" 4 +. +.nf + +Your platform is: x86_64\-linux + +Your app has gems that work on these platforms: +* ruby + +Your Gemfile specifies a Ruby version requirement: +* ruby 1\.9\.3 + +Your current platform satisfies the Ruby version requirement\. +. +.fi +. +.IP "" 0 +. +.P +\fBplatform\fR will list all the platforms in your \fBGemfile\.lock\fR as well as the \fBruby\fR directive if applicable from your Gemfile(5)\. It will also let you know if the \fBruby\fR directive requirement has been met\. If \fBruby\fR directive doesn\'t match the running Ruby VM, it will tell you what part does not\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-ruby\fR +It will display the ruby directive information, so you don\'t have to parse it from the Gemfile(5)\. + diff --git a/man/bundle-platform.1.txt b/man/bundle-platform.1.txt new file mode 100644 index 00000000000000..26079cc3d5dffe --- /dev/null +++ b/man/bundle-platform.1.txt @@ -0,0 +1,57 @@ +BUNDLE-PLATFORM(1) BUNDLE-PLATFORM(1) + + + +1mNAME0m + 1mbundle-platform 22m- Displays platform compatibility information + +1mSYNOPSIS0m + 1mbundle platform 22m[--ruby] + +1mDESCRIPTION0m + 1mplatform 22mwill display information from your Gemfile, Gemfile.lock, and + Ruby VM about your platform. + + For instance, using this Gemfile(5): + + + + source "https://rubygems.org" + + ruby "1.9.3" + + gem "rack" + + + + If you run 1mbundle platform 22mon Ruby 1.9.3, it will display the following + output: + + + + Your platform is: x86_64-linux + + Your app has gems that work on these platforms: + * ruby + + Your Gemfile specifies a Ruby version requirement: + * ruby 1.9.3 + + Your current platform satisfies the Ruby version requirement. + + + + 1mplatform 22mwill list all the platforms in your 1mGemfile.lock 22mas well as + the 1mruby 22mdirective if applicable from your Gemfile(5). It will also let + you know if the 1mruby 22mdirective requirement has been met. If 1mruby 22mdirec- + tive doesn't match the running Ruby VM, it will tell you what part does + not. + +1mOPTIONS0m + 1m--ruby 22mIt will display the ruby directive information, so you don't + have to parse it from the Gemfile(5). + + + + + November 2018 BUNDLE-PLATFORM(1) diff --git a/man/bundle-platform.ronn b/man/bundle-platform.ronn new file mode 100644 index 00000000000000..b5d3283fb6e3cb --- /dev/null +++ b/man/bundle-platform.ronn @@ -0,0 +1,42 @@ +bundle-platform(1) -- Displays platform compatibility information +================================================================= + +## SYNOPSIS + +`bundle platform` [--ruby] + +## DESCRIPTION + +`platform` will display information from your Gemfile, Gemfile.lock, and Ruby +VM about your platform. + +For instance, using this Gemfile(5): + + source "https://rubygems.org" + + ruby "1.9.3" + + gem "rack" + +If you run `bundle platform` on Ruby 1.9.3, it will display the following output: + + Your platform is: x86_64-linux + + Your app has gems that work on these platforms: + * ruby + + Your Gemfile specifies a Ruby version requirement: + * ruby 1.9.3 + + Your current platform satisfies the Ruby version requirement. + +`platform` will list all the platforms in your `Gemfile.lock` as well as the +`ruby` directive if applicable from your Gemfile(5). It will also let you know +if the `ruby` directive requirement has been met. If `ruby` directive doesn't +match the running Ruby VM, it will tell you what part does not. + +## OPTIONS + +* `--ruby`: + It will display the ruby directive information, so you don't have to + parse it from the Gemfile(5). diff --git a/man/bundle-pristine.1 b/man/bundle-pristine.1 new file mode 100644 index 00000000000000..d3881ade3dc22d --- /dev/null +++ b/man/bundle-pristine.1 @@ -0,0 +1,34 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-PRISTINE" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-pristine\fR \- Restores installed gems to their pristine condition +. +.SH "SYNOPSIS" +\fBbundle pristine\fR +. +.SH "DESCRIPTION" +\fBpristine\fR restores the installed gems in the bundle to their pristine condition using the local gem cache from RubyGems\. For git gems, a forced checkout will be performed\. +. +.P +For further explanation, \fBbundle pristine\fR ignores unpacked files on disk\. In other words, this command utilizes the local \fB\.gem\fR cache or the gem\'s git repository as if one were installing from scratch\. +. +.P +Note: the Bundler gem cannot be restored to its original state with \fBpristine\fR\. One also cannot use \fBbundle pristine\fR on gems with a \'path\' option in the Gemfile, because bundler has no original copy it can restore from\. +. +.P +When is it practical to use \fBbundle pristine\fR? +. +.P +It comes in handy when a developer is debugging a gem\. \fBbundle pristine\fR is a great way to get rid of experimental changes to a gem that one may not want\. +. +.P +Why use \fBbundle pristine\fR over \fBgem pristine \-\-all\fR? +. +.P +Both commands are very similar\. For context: \fBbundle pristine\fR, without arguments, cleans all gems from the lockfile\. Meanwhile, \fBgem pristine \-\-all\fR cleans all installed gems for that Ruby version\. +. +.P +If a developer forgets which gems in their project they might have been debugging, the Rubygems \fBgem pristine [GEMNAME]\fR command may be inconvenient\. One can avoid waiting for \fBgem pristine \-\-all\fR, and instead run \fBbundle pristine\fR\. diff --git a/man/bundle-pristine.1.txt b/man/bundle-pristine.1.txt new file mode 100644 index 00000000000000..a46f3c830c3092 --- /dev/null +++ b/man/bundle-pristine.1.txt @@ -0,0 +1,44 @@ +BUNDLE-PRISTINE(1) BUNDLE-PRISTINE(1) + + + +1mNAME0m + 1mbundle-pristine 22m- Restores installed gems to their pristine condition + +1mSYNOPSIS0m + 1mbundle pristine0m + +1mDESCRIPTION0m + 1mpristine 22mrestores the installed gems in the bundle to their pristine + condition using the local gem cache from RubyGems. For git gems, a + forced checkout will be performed. + + For further explanation, 1mbundle pristine 22mignores unpacked files on + disk. In other words, this command utilizes the local 1m.gem 22mcache or the + gem's git repository as if one were installing from scratch. + + Note: the Bundler gem cannot be restored to its original state with + 1mpristine22m. One also cannot use 1mbundle pristine 22mon gems with a 'path' + option in the Gemfile, because bundler has no original copy it can + restore from. + + When is it practical to use 1mbundle pristine22m? + + It comes in handy when a developer is debugging a gem. 1mbundle pristine0m + is a great way to get rid of experimental changes to a gem that one may + not want. + + Why use 1mbundle pristine 22mover 1mgem pristine --all22m? + + Both commands are very similar. For context: 1mbundle pristine22m, without + arguments, cleans all gems from the lockfile. Meanwhile, 1mgem pristine0m + 1m--all 22mcleans all installed gems for that Ruby version. + + If a developer forgets which gems in their project they might have been + debugging, the Rubygems 1mgem pristine [GEMNAME] 22mcommand may be inconve- + nient. One can avoid waiting for 1mgem pristine --all22m, and instead run + 1mbundle pristine22m. + + + + November 2018 BUNDLE-PRISTINE(1) diff --git a/man/bundle-pristine.ronn b/man/bundle-pristine.ronn new file mode 100644 index 00000000000000..e2d6b6a348f07e --- /dev/null +++ b/man/bundle-pristine.ronn @@ -0,0 +1,34 @@ +bundle-pristine(1) -- Restores installed gems to their pristine condition +=========================================================================== + +## SYNOPSIS + +`bundle pristine` + +## DESCRIPTION + +`pristine` restores the installed gems in the bundle to their pristine condition +using the local gem cache from RubyGems. For git gems, a forced checkout will be performed. + +For further explanation, `bundle pristine` ignores unpacked files on disk. In other +words, this command utilizes the local `.gem` cache or the gem's git repository +as if one were installing from scratch. + +Note: the Bundler gem cannot be restored to its original state with `pristine`. +One also cannot use `bundle pristine` on gems with a 'path' option in the Gemfile, +because bundler has no original copy it can restore from. + +When is it practical to use `bundle pristine`? + +It comes in handy when a developer is debugging a gem. `bundle pristine` is a +great way to get rid of experimental changes to a gem that one may not want. + +Why use `bundle pristine` over `gem pristine --all`? + +Both commands are very similar. +For context: `bundle pristine`, without arguments, cleans all gems from the lockfile. +Meanwhile, `gem pristine --all` cleans all installed gems for that Ruby version. + +If a developer forgets which gems in their project they might +have been debugging, the Rubygems `gem pristine [GEMNAME]` command may be inconvenient. +One can avoid waiting for `gem pristine --all`, and instead run `bundle pristine`. diff --git a/man/bundle-remove.1 b/man/bundle-remove.1 new file mode 100644 index 00000000000000..8232ab28877bb5 --- /dev/null +++ b/man/bundle-remove.1 @@ -0,0 +1,31 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-REMOVE" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-remove\fR \- Removes gems from the Gemfile +. +.SH "SYNOPSIS" +\fBbundle remove [GEM [GEM \.\.\.]] [\-\-install]\fR +. +.SH "DESCRIPTION" +Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid\. If a gem cannot be removed, a warning is printed\. If a gem is already absent from the Gemfile, and error is raised\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-install\fR +Runs \fBbundle install\fR after the given gems have been removed from the Gemfile, which ensures that both the lockfile and the installed gems on disk are also updated to remove the given gem(s)\. +. +.P +Example: +. +.P +bundle remove rails +. +.P +bundle remove rails rack +. +.P +bundle remove rails rack \-\-install diff --git a/man/bundle-remove.1.txt b/man/bundle-remove.1.txt new file mode 100644 index 00000000000000..2cf2ba6010e7b0 --- /dev/null +++ b/man/bundle-remove.1.txt @@ -0,0 +1,34 @@ +BUNDLE-REMOVE(1) BUNDLE-REMOVE(1) + + + +1mNAME0m + 1mbundle-remove 22m- Removes gems from the Gemfile + +1mSYNOPSIS0m + 1mbundle remove [GEM [GEM ...]] [--install]0m + +1mDESCRIPTION0m + Removes the given gems from the Gemfile while ensuring that the result- + ing Gemfile is still valid. If a gem cannot be removed, a warning is + printed. If a gem is already absent from the Gemfile, and error is + raised. + +1mOPTIONS0m + 1m--install0m + Runs 1mbundle install 22mafter the given gems have been removed from + the Gemfile, which ensures that both the lockfile and the + installed gems on disk are also updated to remove the given + gem(s). + + Example: + + bundle remove rails + + bundle remove rails rack + + bundle remove rails rack --install + + + + November 2018 BUNDLE-REMOVE(1) diff --git a/man/bundle-remove.ronn b/man/bundle-remove.ronn new file mode 100644 index 00000000000000..40a239b4a29e8e --- /dev/null +++ b/man/bundle-remove.ronn @@ -0,0 +1,23 @@ +bundle-remove(1) -- Removes gems from the Gemfile +=========================================================================== + +## SYNOPSIS + +`bundle remove [GEM [GEM ...]] [--install]` + +## DESCRIPTION + +Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid. If a gem cannot be removed, a warning is printed. If a gem is already absent from the Gemfile, and error is raised. + +## OPTIONS + +* `--install`: + Runs `bundle install` after the given gems have been removed from the Gemfile, which ensures that both the lockfile and the installed gems on disk are also updated to remove the given gem(s). + +Example: + +bundle remove rails + +bundle remove rails rack + +bundle remove rails rack --install diff --git a/man/bundle-show.1 b/man/bundle-show.1 new file mode 100644 index 00000000000000..72ce37aedd31c8 --- /dev/null +++ b/man/bundle-show.1 @@ -0,0 +1,23 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-SHOW" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem +. +.SH "SYNOPSIS" +\fBbundle show\fR [GEM] [\-\-paths] +. +.SH "DESCRIPTION" +Without the [GEM] option, \fBshow\fR will print a list of the names and versions of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by name\. +. +.P +Calling show with [GEM] will list the exact location of that gem on your machine\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-paths\fR +List the paths of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by gem name\. + diff --git a/man/bundle-show.1.txt b/man/bundle-show.1.txt new file mode 100644 index 00000000000000..542ee55f7840c1 --- /dev/null +++ b/man/bundle-show.1.txt @@ -0,0 +1,27 @@ +BUNDLE-SHOW(1) BUNDLE-SHOW(1) + + + +1mNAME0m + 1mbundle-show 22m- Shows all the gems in your bundle, or the path to a gem + +1mSYNOPSIS0m + 1mbundle show 22m[GEM] [--paths] + +1mDESCRIPTION0m + Without the [GEM] option, 1mshow 22mwill print a list of the names and ver- + sions of all gems that are required by your [1mGemfile(5)22m][Gemfile(5)], + sorted by name. + + Calling show with [GEM] will list the exact location of that gem on + your machine. + +1mOPTIONS0m + 1m--paths0m + List the paths of all gems that are required by your [1mGem-0m + 1mfile(5)22m][Gemfile(5)], sorted by gem name. + + + + + November 2018 BUNDLE-SHOW(1) diff --git a/man/bundle-show.ronn b/man/bundle-show.ronn new file mode 100644 index 00000000000000..a6a59a1445dcd3 --- /dev/null +++ b/man/bundle-show.ronn @@ -0,0 +1,21 @@ +bundle-show(1) -- Shows all the gems in your bundle, or the path to a gem +========================================================================= + +## SYNOPSIS + +`bundle show` [GEM] + [--paths] + +## DESCRIPTION + +Without the [GEM] option, `show` will print a list of the names and versions of +all gems that are required by your [`Gemfile(5)`][Gemfile(5)], sorted by name. + +Calling show with [GEM] will list the exact location of that gem on your +machine. + +## OPTIONS + +* `--paths`: + List the paths of all gems that are required by your [`Gemfile(5)`][Gemfile(5)], + sorted by gem name. diff --git a/man/bundle-update.1 b/man/bundle-update.1 new file mode 100644 index 00000000000000..3a303b582d907f --- /dev/null +++ b/man/bundle-update.1 @@ -0,0 +1,394 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-UPDATE" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-update\fR \- Update your gems to the latest available versions +. +.SH "SYNOPSIS" +\fBbundle update\fR \fI*gems\fR [\-\-all] [\-\-group=NAME] [\-\-source=NAME] [\-\-local] [\-\-ruby] [\-\-bundler[=VERSION]] [\-\-full\-index] [\-\-jobs=JOBS] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-redownload] [\-\-strict] [\-\-conservative] +. +.SH "DESCRIPTION" +Update the gems specified (all gems, if \fB\-\-all\fR flag is used), ignoring the previously installed gems specified in the \fBGemfile\.lock\fR\. In general, you should use bundle install(1) \fIbundle\-install\.1\.html\fR to install the same exact gems and versions across machines\. +. +.P +You would use \fBbundle update\fR to explicitly update the version of a gem\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-all\fR +Update all gems specified in Gemfile\. +. +.TP +\fB\-\-group=\fR, \fB\-g=[]\fR +Only update the gems in the specified group\. For instance, you can update all gems in the development group with \fBbundle update \-\-group development\fR\. You can also call \fBbundle update rails \-\-group test\fR to update the rails gem and all gems in the test group, for example\. +. +.TP +\fB\-\-source=\fR +The name of a \fB:git\fR or \fB:path\fR source used in the Gemfile(5)\. For instance, with a \fB:git\fR source of \fBhttp://github\.com/rails/rails\.git\fR, you would call \fBbundle update \-\-source rails\fR +. +.TP +\fB\-\-local\fR +Do not attempt to fetch gems remotely and use the gem cache instead\. +. +.TP +\fB\-\-ruby\fR +Update the locked version of Ruby to the current version of Ruby\. +. +.TP +\fB\-\-bundler\fR +Update the locked version of bundler to the invoked bundler version\. +. +.TP +\fB\-\-full\-index\fR +Fall back to using the single\-file index of all gems\. +. +.TP +\fB\-\-jobs=[]\fR, \fB\-j[]\fR +Specify the number of jobs to run in parallel\. The default is \fB1\fR\. +. +.TP +\fB\-\-retry=[]\fR +Retry failed network or git requests for \fInumber\fR times\. +. +.TP +\fB\-\-quiet\fR +Only output warnings and errors\. +. +.TP +\fB\-\-redownload\fR +Force downloading every gem\. +. +.TP +\fB\-\-patch\fR +Prefer updating only to next patch version\. +. +.TP +\fB\-\-minor\fR +Prefer updating only to next minor version\. +. +.TP +\fB\-\-major\fR +Prefer updating to next major version (default)\. +. +.TP +\fB\-\-strict\fR +Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR | \fB\-\-major\fR\. +. +.TP +\fB\-\-conservative\fR +Use bundle install conservative update behavior and do not allow shared dependencies to be updated\. +. +.SH "UPDATING ALL GEMS" +If you run \fBbundle update \-\-all\fR, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. +. +.P +Consider the following Gemfile(5): +. +.IP "" 4 +. +.nf + +source "https://rubygems\.org" + +gem "rails", "3\.0\.0\.rc" +gem "nokogiri" +. +.fi +. +.IP "" 0 +. +.P +When you run bundle install(1) \fIbundle\-install\.1\.html\fR the first time, bundler will resolve all of the dependencies, all the way down, and install what you need: +. +.IP "" 4 +. +.nf + +Fetching gem metadata from https://rubygems\.org/\.\.\.\.\.\.\.\.\. +Resolving dependencies\.\.\. +Installing builder 2\.1\.2 +Installing abstract 1\.0\.0 +Installing rack 1\.2\.8 +Using bundler 1\.7\.6 +Installing rake 10\.4\.0 +Installing polyglot 0\.3\.5 +Installing mime\-types 1\.25\.1 +Installing i18n 0\.4\.2 +Installing mini_portile 0\.6\.1 +Installing tzinfo 0\.3\.42 +Installing rack\-mount 0\.6\.14 +Installing rack\-test 0\.5\.7 +Installing treetop 1\.4\.15 +Installing thor 0\.14\.6 +Installing activesupport 3\.0\.0\.rc +Installing erubis 2\.6\.6 +Installing activemodel 3\.0\.0\.rc +Installing arel 0\.4\.0 +Installing mail 2\.2\.20 +Installing activeresource 3\.0\.0\.rc +Installing actionpack 3\.0\.0\.rc +Installing activerecord 3\.0\.0\.rc +Installing actionmailer 3\.0\.0\.rc +Installing railties 3\.0\.0\.rc +Installing rails 3\.0\.0\.rc +Installing nokogiri 1\.6\.5 + +Bundle complete! 2 Gemfile dependencies, 26 gems total\. +Use `bundle show [gemname]` to see where a bundled gem is installed\. +. +.fi +. +.IP "" 0 +. +.P +As you can see, even though you have two gems in the Gemfile(5), your application needs 26 different gems in order to run\. Bundler remembers the exact versions it installed in \fBGemfile\.lock\fR\. The next time you run bundle install(1) \fIbundle\-install\.1\.html\fR, bundler skips the dependency resolution and installs the same gems as it installed last time\. +. +.P +After checking in the \fBGemfile\.lock\fR into version control and cloning it on another machine, running bundle install(1) \fIbundle\-install\.1\.html\fR will \fIstill\fR install the gems that you installed last time\. You don\'t need to worry that a new release of \fBerubis\fR or \fBmail\fR changes the gems you use\. +. +.P +However, from time to time, you might want to update the gems you are using to the newest versions that still match the gems in your Gemfile(5)\. +. +.P +To do this, run \fBbundle update \-\-all\fR, which will ignore the \fBGemfile\.lock\fR, and resolve all the dependencies again\. Keep in mind that this process can result in a significantly different set of the 25 gems, based on the requirements of new gems that the gem authors released since the last time you ran \fBbundle update \-\-all\fR\. +. +.SH "UPDATING A LIST OF GEMS" +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\. +. +.P +For instance, in the scenario above, imagine that \fBnokogiri\fR releases version \fB1\.4\.4\fR, and you want to update it \fIwithout\fR updating Rails and all of its dependencies\. To do this, run \fBbundle update nokogiri\fR\. +. +.P +Bundler will update \fBnokogiri\fR and any of its dependencies, but leave alone Rails and its dependencies\. +. +.SH "OVERLAPPING DEPENDENCIES" +Sometimes, multiple gems declared in your Gemfile(5) are satisfied by the same second\-level dependency\. For instance, consider the case of \fBthin\fR and \fBrack\-perftools\-profiler\fR\. +. +.IP "" 4 +. +.nf + +source "https://rubygems\.org" + +gem "thin" +gem "rack\-perftools\-profiler" +. +.fi +. +.IP "" 0 +. +.P +The \fBthin\fR gem depends on \fBrack >= 1\.0\fR, while \fBrack\-perftools\-profiler\fR depends on \fBrack ~> 1\.0\fR\. If you run bundle install, you get: +. +.IP "" 4 +. +.nf + +Fetching source index for https://rubygems\.org/ +Installing daemons (1\.1\.0) +Installing eventmachine (0\.12\.10) with native extensions +Installing open4 (1\.0\.1) +Installing perftools\.rb (0\.4\.7) with native extensions +Installing rack (1\.2\.1) +Installing rack\-perftools_profiler (0\.0\.2) +Installing thin (1\.2\.7) with native extensions +Using bundler (1\.0\.0\.rc\.3) +. +.fi +. +.IP "" 0 +. +.P +In this case, the two gems have their own set of dependencies, but they share \fBrack\fR in common\. If you run \fBbundle update thin\fR, bundler will update \fBdaemons\fR, \fBeventmachine\fR and \fBrack\fR, which are dependencies of \fBthin\fR, but not \fBopen4\fR or \fBperftools\.rb\fR, which are dependencies of \fBrack\-perftools_profiler\fR\. Note that \fBbundle update thin\fR will update \fBrack\fR even though it\'s \fIalso\fR a dependency of \fBrack\-perftools_profiler\fR\. +. +.P +In short, by default, when you update a gem using \fBbundle update\fR, bundler will update all dependencies of that gem, including those that are also dependencies of another gem\. +. +.P +To prevent updating shared dependencies, prior to version 1\.14 the only option was the \fBCONSERVATIVE UPDATING\fR behavior in bundle install(1) \fIbundle\-install\.1\.html\fR: +. +.P +In this scenario, updating the \fBthin\fR version manually in the Gemfile(5), and then running bundle install(1) \fIbundle\-install\.1\.html\fR will only update \fBdaemons\fR and \fBeventmachine\fR, but not \fBrack\fR\. For more information, see the \fBCONSERVATIVE UPDATING\fR section of bundle install(1) \fIbundle\-install\.1\.html\fR\. +. +.P +Starting with 1\.14, specifying the \fB\-\-conservative\fR option will also prevent shared dependencies from being updated\. +. +.SH "PATCH LEVEL OPTIONS" +Version 1\.14 introduced 4 patch\-level options that will influence how gem versions are resolved\. One of the following options can be used: \fB\-\-patch\fR, \fB\-\-minor\fR or \fB\-\-major\fR\. \fB\-\-strict\fR can be added to further influence resolution\. +. +.TP +\fB\-\-patch\fR +Prefer updating only to next patch version\. +. +.TP +\fB\-\-minor\fR +Prefer updating only to next minor version\. +. +.TP +\fB\-\-major\fR +Prefer updating to next major version (default)\. +. +.TP +\fB\-\-strict\fR +Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR | \fB\-\-major\fR\. +. +.P +When Bundler is resolving what versions to use to satisfy declared requirements in the Gemfile or in parent gems, it looks up all available versions, filters out any versions that don\'t satisfy the requirement, and then, by default, sorts them from newest to oldest, considering them in that order\. +. +.P +Providing one of the patch level options (e\.g\. \fB\-\-patch\fR) changes the sort order of the satisfying versions, causing Bundler to consider the latest \fB\-\-patch\fR or \fB\-\-minor\fR version available before other versions\. Note that versions outside the stated patch level could still be resolved to if necessary to find a suitable dependency graph\. +. +.P +For example, if gem \'foo\' is locked at 1\.0\.2, with no gem requirement defined in the Gemfile, and versions 1\.0\.3, 1\.0\.4, 1\.1\.0, 1\.1\.1, 2\.0\.0 all exist, the default order of preference by default (\fB\-\-major\fR) will be "2\.0\.0, 1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2"\. +. +.P +If the \fB\-\-patch\fR option is used, the order of preference will change to "1\.0\.4, 1\.0\.3, 1\.0\.2, 1\.1\.1, 1\.1\.0, 2\.0\.0"\. +. +.P +If the \fB\-\-minor\fR option is used, the order of preference will change to "1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2, 2\.0\.0"\. +. +.P +Combining the \fB\-\-strict\fR option with any of the patch level options will remove any versions beyond the scope of the patch level option, to ensure that no gem is updated that far\. +. +.P +To continue the previous example, if both \fB\-\-patch\fR and \fB\-\-strict\fR options are used, the available versions for resolution would be "1\.0\.4, 1\.0\.3, 1\.0\.2"\. If \fB\-\-minor\fR and \fB\-\-strict\fR are used, it would be "1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2"\. +. +.P +Gem requirements as defined in the Gemfile will still be the first determining factor for what versions are available\. If the gem requirement for \fBfoo\fR in the Gemfile is \'~> 1\.0\', that will accomplish the same thing as providing the \fB\-\-minor\fR and \fB\-\-strict\fR options\. +. +.SH "PATCH LEVEL EXAMPLES" +Given the following gem specifications: +. +.IP "" 4 +. +.nf + +foo 1\.4\.3, requires: ~> bar 2\.0 +foo 1\.4\.4, requires: ~> bar 2\.0 +foo 1\.4\.5, requires: ~> bar 2\.1 +foo 1\.5\.0, requires: ~> bar 2\.1 +foo 1\.5\.1, requires: ~> bar 3\.0 +bar with versions 2\.0\.3, 2\.0\.4, 2\.1\.0, 2\.1\.1, 3\.0\.0 +. +.fi +. +.IP "" 0 +. +.P +Gemfile: +. +.IP "" 4 +. +.nf + +gem \'foo\' +. +.fi +. +.IP "" 0 +. +.P +Gemfile\.lock: +. +.IP "" 4 +. +.nf + +foo (1\.4\.3) + bar (~> 2\.0) +bar (2\.0\.3) +. +.fi +. +.IP "" 0 +. +.P +Cases: +. +.IP "" 4 +. +.nf + +# Command Line Result +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- +1 bundle update \-\-patch \'foo 1\.4\.5\', \'bar 2\.1\.1\' +2 bundle update \-\-patch foo \'foo 1\.4\.5\', \'bar 2\.1\.1\' +3 bundle update \-\-minor \'foo 1\.5\.1\', \'bar 3\.0\.0\' +4 bundle update \-\-minor \-\-strict \'foo 1\.5\.0\', \'bar 2\.1\.1\' +5 bundle update \-\-patch \-\-strict \'foo 1\.4\.4\', \'bar 2\.0\.4\' +. +.fi +. +.IP "" 0 +. +.P +In case 1, bar is upgraded to 2\.1\.1, a minor version increase, because the dependency from foo 1\.4\.5 required it\. +. +.P +In case 2, only foo is requested to be unlocked, but bar is also allowed to move because it\'s not a declared dependency in the Gemfile\. +. +.P +In case 3, bar goes up a whole major release, because a minor increase is preferred now for foo, and when it goes to 1\.5\.1, it requires 3\.0\.0 of bar\. +. +.P +In case 4, foo is preferred up to a minor version, but 1\.5\.1 won\'t work because the \-\-strict flag removes bar 3\.0\.0 from consideration since it\'s a major increment\. +. +.P +In case 5, both foo and bar have any minor or major increments removed from consideration because of the \-\-strict flag, so the most they can move is up to 1\.4\.4 and 2\.0\.4\. +. +.SH "RECOMMENDED WORKFLOW" +In general, when working with an application managed with bundler, you should use the following workflow: +. +.IP "\(bu" 4 +After you create your Gemfile(5) for the first time, run +. +.IP +$ bundle install +. +.IP "\(bu" 4 +Check the resulting \fBGemfile\.lock\fR into version control +. +.IP +$ git add Gemfile\.lock +. +.IP "\(bu" 4 +When checking out this repository on another development machine, run +. +.IP +$ bundle install +. +.IP "\(bu" 4 +When checking out this repository on a deployment machine, run +. +.IP +$ bundle install \-\-deployment +. +.IP "\(bu" 4 +After changing the Gemfile(5) to reflect a new or update dependency, run +. +.IP +$ bundle install +. +.IP "\(bu" 4 +Make sure to check the updated \fBGemfile\.lock\fR into version control +. +.IP +$ git add Gemfile\.lock +. +.IP "\(bu" 4 +If bundle install(1) \fIbundle\-install\.1\.html\fR reports a conflict, manually update the specific gems that you changed in the Gemfile(5) +. +.IP +$ bundle update rails thin +. +.IP "\(bu" 4 +If you want to update all the gems to the latest possible versions that still match the gems listed in the Gemfile(5), run +. +.IP +$ bundle update \-\-all +. +.IP "" 0 + diff --git a/man/bundle-update.1.txt b/man/bundle-update.1.txt new file mode 100644 index 00000000000000..573842858d7609 --- /dev/null +++ b/man/bundle-update.1.txt @@ -0,0 +1,390 @@ +BUNDLE-UPDATE(1) BUNDLE-UPDATE(1) + + + +1mNAME0m + 1mbundle-update 22m- Update your gems to the latest available versions + +1mSYNOPSIS0m + 1mbundle update 4m22m*gems24m [--all] [--group=NAME] [--source=NAME] [--local] + [--ruby] [--bundler[=VERSION]] [--full-index] [--jobs=JOBS] [--quiet] + [--patch|--minor|--major] [--redownload] [--strict] [--conservative] + +1mDESCRIPTION0m + Update the gems specified (all gems, if 1m--all 22mflag is used), ignoring + the previously installed gems specified in the 1mGemfile.lock22m. In gen- + eral, you should use bundle install(1) 4mbundle-install.1.html24m to install + the same exact gems and versions across machines. + + You would use 1mbundle update 22mto explicitly update the version of a gem. + +1mOPTIONS0m + 1m--all 22mUpdate all gems specified in Gemfile. + + 1m--group=22m, 1m-g=[]0m + Only update the gems in the specified group. For instance, you + can update all gems in the development group with 1mbundle update0m + 1m--group development22m. You can also call 1mbundle update rails0m + 1m--group test 22mto update the rails gem and all gems in the test + group, for example. + + 1m--source=0m + The name of a 1m:git 22mor 1m:path 22msource used in the Gemfile(5). For + instance, with a 1m:git 22msource of + 1mhttp://github.com/rails/rails.git22m, you would call 1mbundle update0m + 1m--source rails0m + + 1m--local0m + Do not attempt to fetch gems remotely and use the gem cache + instead. + + 1m--ruby 22mUpdate the locked version of Ruby to the current version of + Ruby. + + 1m--bundler0m + Update the locked version of bundler to the invoked bundler ver- + sion. + + 1m--full-index0m + Fall back to using the single-file index of all gems. + + 1m--jobs=[]22m, 1m-j[]0m + Specify the number of jobs to run in parallel. The default is 1m122m. + + 1m--retry=[]0m + Retry failed network or git requests for 4mnumber24m times. + + 1m--quiet0m + Only output warnings and errors. + + 1m--redownload0m + Force downloading every gem. + + 1m--patch0m + Prefer updating only to next patch version. + + 1m--minor0m + Prefer updating only to next minor version. + + 1m--major0m + Prefer updating to next major version (default). + + 1m--strict0m + Do not allow any gem to be updated past latest 1m--patch 22m| 1m--minor0m + | 1m--major22m. + + 1m--conservative0m + Use bundle install conservative update behavior and do not allow + shared dependencies to be updated. + +1mUPDATING ALL GEMS0m + If you run 1mbundle update --all22m, bundler will ignore any previously + installed gems and resolve all dependencies again based on the latest + versions of all gems available in the sources. + + Consider the following Gemfile(5): + + + + source "https://rubygems.org" + + gem "rails", "3.0.0.rc" + gem "nokogiri" + + + + When you run bundle install(1) 4mbundle-install.1.html24m the first time, + bundler will resolve all of the dependencies, all the way down, and + install what you need: + + + + Fetching gem metadata from https://rubygems.org/......... + Resolving dependencies... + Installing builder 2.1.2 + Installing abstract 1.0.0 + Installing rack 1.2.8 + Using bundler 1.7.6 + Installing rake 10.4.0 + Installing polyglot 0.3.5 + Installing mime-types 1.25.1 + Installing i18n 0.4.2 + Installing mini_portile 0.6.1 + Installing tzinfo 0.3.42 + Installing rack-mount 0.6.14 + Installing rack-test 0.5.7 + Installing treetop 1.4.15 + Installing thor 0.14.6 + Installing activesupport 3.0.0.rc + Installing erubis 2.6.6 + Installing activemodel 3.0.0.rc + Installing arel 0.4.0 + Installing mail 2.2.20 + Installing activeresource 3.0.0.rc + Installing actionpack 3.0.0.rc + Installing activerecord 3.0.0.rc + Installing actionmailer 3.0.0.rc + Installing railties 3.0.0.rc + Installing rails 3.0.0.rc + Installing nokogiri 1.6.5 + + Bundle complete! 2 Gemfile dependencies, 26 gems total. + Use `bundle show [gemname]` to see where a bundled gem is installed. + + + + As you can see, even though you have two gems in the Gemfile(5), your + application needs 26 different gems in order to run. Bundler remembers + the exact versions it installed in 1mGemfile.lock22m. The next time you run + bundle install(1) 4mbundle-install.1.html24m, bundler skips the dependency + resolution and installs the same gems as it installed last time. + + After checking in the 1mGemfile.lock 22minto version control and cloning it + on another machine, running bundle install(1) 4mbundle-install.1.html0m + will 4mstill24m install the gems that you installed last time. You don't + need to worry that a new release of 1merubis 22mor 1mmail 22mchanges the gems you + use. + + However, from time to time, you might want to update the gems you are + using to the newest versions that still match the gems in your Gem- + file(5). + + To do this, run 1mbundle update --all22m, which will ignore the 1mGem-0m + 1mfile.lock22m, and resolve all the dependencies again. Keep in mind that + this process can result in a significantly different set of the 25 + gems, based on the requirements of new gems that the gem authors + released since the last time you ran 1mbundle update --all22m. + +1mUPDATING A LIST OF GEMS0m + Sometimes, you want to update a single gem in the Gemfile(5), and leave + the rest of the gems that you specified locked to the versions in the + 1mGemfile.lock22m. + + For instance, in the scenario above, imagine that 1mnokogiri 22mreleases + version 1m1.4.422m, and you want to update it 4mwithout24m updating Rails and all + of its dependencies. To do this, run 1mbundle update nokogiri22m. + + Bundler will update 1mnokogiri 22mand any of its dependencies, but leave + alone Rails and its dependencies. + +1mOVERLAPPING DEPENDENCIES0m + Sometimes, multiple gems declared in your Gemfile(5) are satisfied by + the same second-level dependency. For instance, consider the case of + 1mthin 22mand 1mrack-perftools-profiler22m. + + + + source "https://rubygems.org" + + gem "thin" + gem "rack-perftools-profiler" + + + + The 1mthin 22mgem depends on 1mrack >= 1.022m, while 1mrack-perftools-profiler0m + depends on 1mrack ~> 1.022m. If you run bundle install, you get: + + + + Fetching source index for https://rubygems.org/ + Installing daemons (1.1.0) + Installing eventmachine (0.12.10) with native extensions + Installing open4 (1.0.1) + Installing perftools.rb (0.4.7) with native extensions + Installing rack (1.2.1) + Installing rack-perftools_profiler (0.0.2) + Installing thin (1.2.7) with native extensions + Using bundler (1.0.0.rc.3) + + + + In this case, the two gems have their own set of dependencies, but they + share 1mrack 22min common. If you run 1mbundle update thin22m, bundler will + update 1mdaemons22m, 1meventmachine 22mand 1mrack22m, which are dependencies of 1mthin22m, + but not 1mopen4 22mor 1mperftools.rb22m, which are dependencies of + 1mrack-perftools_profiler22m. Note that 1mbundle update thin 22mwill update 1mrack0m + even though it's 4malso24m a dependency of 1mrack-perftools_profiler22m. + + In short, by default, when you update a gem using 1mbundle update22m, + bundler will update all dependencies of that gem, including those that + are also dependencies of another gem. + + To prevent updating shared dependencies, prior to version 1.14 the only + option was the 1mCONSERVATIVE UPDATING 22mbehavior in bundle install(1) 4mbun-0m + 4mdle-install.1.html24m: + + In this scenario, updating the 1mthin 22mversion manually in the Gemfile(5), + and then running bundle install(1) 4mbundle-install.1.html24m will only + update 1mdaemons 22mand 1meventmachine22m, but not 1mrack22m. For more information, + see the 1mCONSERVATIVE UPDATING 22msection of bundle install(1) 4mbun-0m + 4mdle-install.1.html24m. + + Starting with 1.14, specifying the 1m--conservative 22moption will also pre- + vent shared dependencies from being updated. + +1mPATCH LEVEL OPTIONS0m + Version 1.14 introduced 4 patch-level options that will influence how + gem versions are resolved. One of the following options can be used: + 1m--patch22m, 1m--minor 22mor 1m--major22m. 1m--strict 22mcan be added to further influence + resolution. + + 1m--patch0m + Prefer updating only to next patch version. + + 1m--minor0m + Prefer updating only to next minor version. + + 1m--major0m + Prefer updating to next major version (default). + + 1m--strict0m + Do not allow any gem to be updated past latest 1m--patch 22m| 1m--minor0m + | 1m--major22m. + + When Bundler is resolving what versions to use to satisfy declared + requirements in the Gemfile or in parent gems, it looks up all avail- + able versions, filters out any versions that don't satisfy the require- + ment, and then, by default, sorts them from newest to oldest, consider- + ing them in that order. + + Providing one of the patch level options (e.g. 1m--patch22m) changes the + sort order of the satisfying versions, causing Bundler to consider the + latest 1m--patch 22mor 1m--minor 22mversion available before other versions. Note + that versions outside the stated patch level could still be resolved to + if necessary to find a suitable dependency graph. + + For example, if gem 'foo' is locked at 1.0.2, with no gem requirement + defined in the Gemfile, and versions 1.0.3, 1.0.4, 1.1.0, 1.1.1, 2.0.0 + all exist, the default order of preference by default (1m--major22m) will be + "2.0.0, 1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2". + + If the 1m--patch 22moption is used, the order of preference will change to + "1.0.4, 1.0.3, 1.0.2, 1.1.1, 1.1.0, 2.0.0". + + If the 1m--minor 22moption is used, the order of preference will change to + "1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2, 2.0.0". + + Combining the 1m--strict 22moption with any of the patch level options will + remove any versions beyond the scope of the patch level option, to + ensure that no gem is updated that far. + + To continue the previous example, if both 1m--patch 22mand 1m--strict 22moptions + are used, the available versions for resolution would be "1.0.4, 1.0.3, + 1.0.2". If 1m--minor 22mand 1m--strict 22mare used, it would be "1.1.1, 1.1.0, + 1.0.4, 1.0.3, 1.0.2". + + Gem requirements as defined in the Gemfile will still be the first + determining factor for what versions are available. If the gem require- + ment for 1mfoo 22min the Gemfile is '~> 1.0', that will accomplish the same + thing as providing the 1m--minor 22mand 1m--strict 22moptions. + +1mPATCH LEVEL EXAMPLES0m + Given the following gem specifications: + + + + foo 1.4.3, requires: ~> bar 2.0 + foo 1.4.4, requires: ~> bar 2.0 + foo 1.4.5, requires: ~> bar 2.1 + foo 1.5.0, requires: ~> bar 2.1 + foo 1.5.1, requires: ~> bar 3.0 + bar with versions 2.0.3, 2.0.4, 2.1.0, 2.1.1, 3.0.0 + + + + Gemfile: + + + + gem 'foo' + + + + Gemfile.lock: + + + + foo (1.4.3) + bar (~> 2.0) + bar (2.0.3) + + + + Cases: + + + + # Command Line Result + ------------------------------------------------------------ + 1 bundle update --patch 'foo 1.4.5', 'bar 2.1.1' + 2 bundle update --patch foo 'foo 1.4.5', 'bar 2.1.1' + 3 bundle update --minor 'foo 1.5.1', 'bar 3.0.0' + 4 bundle update --minor --strict 'foo 1.5.0', 'bar 2.1.1' + 5 bundle update --patch --strict 'foo 1.4.4', 'bar 2.0.4' + + + + In case 1, bar is upgraded to 2.1.1, a minor version increase, because + the dependency from foo 1.4.5 required it. + + In case 2, only foo is requested to be unlocked, but bar is also + allowed to move because it's not a declared dependency in the Gemfile. + + In case 3, bar goes up a whole major release, because a minor increase + is preferred now for foo, and when it goes to 1.5.1, it requires 3.0.0 + of bar. + + In case 4, foo is preferred up to a minor version, but 1.5.1 won't work + because the --strict flag removes bar 3.0.0 from consideration since + it's a major increment. + + In case 5, both foo and bar have any minor or major increments removed + from consideration because of the --strict flag, so the most they can + move is up to 1.4.4 and 2.0.4. + +1mRECOMMENDED WORKFLOW0m + In general, when working with an application managed with bundler, you + should use the following workflow: + + o After you create your Gemfile(5) for the first time, run + + $ bundle install + + o Check the resulting 1mGemfile.lock 22minto version control + + $ git add Gemfile.lock + + o When checking out this repository on another development machine, + run + + $ bundle install + + o When checking out this repository on a deployment machine, run + + $ bundle install --deployment + + o After changing the Gemfile(5) to reflect a new or update depen- + dency, run + + $ bundle install + + o Make sure to check the updated 1mGemfile.lock 22minto version control + + $ git add Gemfile.lock + + o If bundle install(1) 4mbundle-install.1.html24m reports a conflict, man- + ually update the specific gems that you changed in the Gemfile(5) + + $ bundle update rails thin + + o If you want to update all the gems to the latest possible versions + that still match the gems listed in the Gemfile(5), run + + $ bundle update --all + + + + + + + November 2018 BUNDLE-UPDATE(1) diff --git a/man/bundle-update.ronn b/man/bundle-update.ronn new file mode 100644 index 00000000000000..481bb5b14ec294 --- /dev/null +++ b/man/bundle-update.ronn @@ -0,0 +1,350 @@ +bundle-update(1) -- Update your gems to the latest available versions +===================================================================== + +## SYNOPSIS + +`bundle update` <*gems> [--all] + [--group=NAME] + [--source=NAME] + [--local] + [--ruby] + [--bundler[=VERSION]] + [--full-index] + [--jobs=JOBS] + [--quiet] + [--force] + [--patch|--minor|--major] + [--strict] + [--conservative] + +## DESCRIPTION + +Update the gems specified (all gems, if `--all` flag is used), ignoring +the previously installed gems specified in the `Gemfile.lock`. In +general, you should use [bundle install(1)](bundle-install.1.html) to install the same exact +gems and versions across machines. + +You would use `bundle update` to explicitly update the version of a +gem. + +## OPTIONS + +* `--all`: + Update all gems specified in Gemfile. + +* `--group=`, `-g=[]`: + Only update the gems in the specified group. For instance, you can update all gems + in the development group with `bundle update --group development`. You can also + call `bundle update rails --group test` to update the rails gem and all gems in + the test group, for example. + +* `--source=`: + The name of a `:git` or `:path` source used in the Gemfile(5). For + instance, with a `:git` source of `http://github.com/rails/rails.git`, + you would call `bundle update --source rails` + +* `--local`: + Do not attempt to fetch gems remotely and use the gem cache instead. + +* `--ruby`: + Update the locked version of Ruby to the current version of Ruby. + +* `--bundler`: + Update the locked version of bundler to the invoked bundler version. + +* `--full-index`: + Fall back to using the single-file index of all gems. + +* `--jobs=[]`, `-j[]`: + Specify the number of jobs to run in parallel. The default is `1`. + +* `--retry=[]`: + Retry failed network or git requests for times. + +* `--quiet`: + Only output warnings and errors. + +* `--force`: + Force downloading every gem. `--redownload` is an alias of this option. + +* `--patch`: + Prefer updating only to next patch version. + +* `--minor`: + Prefer updating only to next minor version. + +* `--major`: + Prefer updating to next major version (default). + +* `--strict`: + Do not allow any gem to be updated past latest `--patch` | `--minor` | `--major`. + +* `--conservative`: + Use bundle install conservative update behavior and do not allow shared dependencies to be updated. + +## UPDATING ALL GEMS + +If you run `bundle update --all`, bundler will ignore +any previously installed gems and resolve all dependencies again +based on the latest versions of all gems available in the sources. + +Consider the following Gemfile(5): + + source "https://rubygems.org" + + gem "rails", "3.0.0.rc" + gem "nokogiri" + +When you run [bundle install(1)](bundle-install.1.html) the first time, bundler will resolve +all of the dependencies, all the way down, and install what you need: + + Fetching gem metadata from https://rubygems.org/......... + Resolving dependencies... + Installing builder 2.1.2 + Installing abstract 1.0.0 + Installing rack 1.2.8 + Using bundler 1.7.6 + Installing rake 10.4.0 + Installing polyglot 0.3.5 + Installing mime-types 1.25.1 + Installing i18n 0.4.2 + Installing mini_portile 0.6.1 + Installing tzinfo 0.3.42 + Installing rack-mount 0.6.14 + Installing rack-test 0.5.7 + Installing treetop 1.4.15 + Installing thor 0.14.6 + Installing activesupport 3.0.0.rc + Installing erubis 2.6.6 + Installing activemodel 3.0.0.rc + Installing arel 0.4.0 + Installing mail 2.2.20 + Installing activeresource 3.0.0.rc + Installing actionpack 3.0.0.rc + Installing activerecord 3.0.0.rc + Installing actionmailer 3.0.0.rc + Installing railties 3.0.0.rc + Installing rails 3.0.0.rc + Installing nokogiri 1.6.5 + + Bundle complete! 2 Gemfile dependencies, 26 gems total. + Use `bundle show [gemname]` to see where a bundled gem is installed. + +As you can see, even though you have two gems in the Gemfile(5), your application +needs 26 different gems in order to run. Bundler remembers the exact versions +it installed in `Gemfile.lock`. The next time you run [bundle install(1)](bundle-install.1.html), bundler skips +the dependency resolution and installs the same gems as it installed last time. + +After checking in the `Gemfile.lock` into version control and cloning it on another +machine, running [bundle install(1)](bundle-install.1.html) will _still_ install the gems that you installed +last time. You don't need to worry that a new release of `erubis` or `mail` changes +the gems you use. + +However, from time to time, you might want to update the gems you are using to the +newest versions that still match the gems in your Gemfile(5). + +To do this, run `bundle update --all`, which will ignore the `Gemfile.lock`, and resolve +all the dependencies again. Keep in mind that this process can result in a significantly +different set of the 25 gems, based on the requirements of new gems that the gem +authors released since the last time you ran `bundle update --all`. + +## UPDATING A LIST OF GEMS + +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the +gems that you specified locked to the versions in the `Gemfile.lock`. + +For instance, in the scenario above, imagine that `nokogiri` releases version `1.4.4`, and +you want to update it _without_ updating Rails and all of its dependencies. To do this, +run `bundle update nokogiri`. + +Bundler will update `nokogiri` and any of its dependencies, but leave alone Rails and +its dependencies. + +## OVERLAPPING DEPENDENCIES + +Sometimes, multiple gems declared in your Gemfile(5) are satisfied by the same +second-level dependency. For instance, consider the case of `thin` and +`rack-perftools-profiler`. + + source "https://rubygems.org" + + gem "thin" + gem "rack-perftools-profiler" + +The `thin` gem depends on `rack >= 1.0`, while `rack-perftools-profiler` depends +on `rack ~> 1.0`. If you run bundle install, you get: + + Fetching source index for https://rubygems.org/ + Installing daemons (1.1.0) + Installing eventmachine (0.12.10) with native extensions + Installing open4 (1.0.1) + Installing perftools.rb (0.4.7) with native extensions + Installing rack (1.2.1) + Installing rack-perftools_profiler (0.0.2) + Installing thin (1.2.7) with native extensions + Using bundler (1.0.0.rc.3) + +In this case, the two gems have their own set of dependencies, but they share +`rack` in common. If you run `bundle update thin`, bundler will update `daemons`, +`eventmachine` and `rack`, which are dependencies of `thin`, but not `open4` or +`perftools.rb`, which are dependencies of `rack-perftools_profiler`. Note that +`bundle update thin` will update `rack` even though it's _also_ a dependency of +`rack-perftools_profiler`. + +In short, by default, when you update a gem using `bundle update`, bundler will +update all dependencies of that gem, including those that are also dependencies +of another gem. + +To prevent updating shared dependencies, prior to version 1.14 the only option +was the `CONSERVATIVE UPDATING` behavior in [bundle install(1)](bundle-install.1.html): + +In this scenario, updating the `thin` version manually in the Gemfile(5), +and then running [bundle install(1)](bundle-install.1.html) will only update `daemons` and `eventmachine`, +but not `rack`. For more information, see the `CONSERVATIVE UPDATING` section +of [bundle install(1)](bundle-install.1.html). + +Starting with 1.14, specifying the `--conservative` option will also prevent shared +dependencies from being updated. + +## PATCH LEVEL OPTIONS + +Version 1.14 introduced 4 patch-level options that will influence how gem +versions are resolved. One of the following options can be used: `--patch`, +`--minor` or `--major`. `--strict` can be added to further influence resolution. + +* `--patch`: + Prefer updating only to next patch version. + +* `--minor`: + Prefer updating only to next minor version. + +* `--major`: + Prefer updating to next major version (default). + +* `--strict`: + Do not allow any gem to be updated past latest `--patch` | `--minor` | `--major`. + +When Bundler is resolving what versions to use to satisfy declared +requirements in the Gemfile or in parent gems, it looks up all +available versions, filters out any versions that don't satisfy +the requirement, and then, by default, sorts them from newest to +oldest, considering them in that order. + +Providing one of the patch level options (e.g. `--patch`) changes the +sort order of the satisfying versions, causing Bundler to consider the +latest `--patch` or `--minor` version available before other versions. +Note that versions outside the stated patch level could still be +resolved to if necessary to find a suitable dependency graph. + +For example, if gem 'foo' is locked at 1.0.2, with no gem requirement +defined in the Gemfile, and versions 1.0.3, 1.0.4, 1.1.0, 1.1.1, 2.0.0 +all exist, the default order of preference by default (`--major`) will +be "2.0.0, 1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2". + +If the `--patch` option is used, the order of preference will change to +"1.0.4, 1.0.3, 1.0.2, 1.1.1, 1.1.0, 2.0.0". + +If the `--minor` option is used, the order of preference will change to +"1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2, 2.0.0". + +Combining the `--strict` option with any of the patch level options +will remove any versions beyond the scope of the patch level option, +to ensure that no gem is updated that far. + +To continue the previous example, if both `--patch` and `--strict` +options are used, the available versions for resolution would be +"1.0.4, 1.0.3, 1.0.2". If `--minor` and `--strict` are used, it would +be "1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2". + +Gem requirements as defined in the Gemfile will still be the first +determining factor for what versions are available. If the gem +requirement for `foo` in the Gemfile is '~> 1.0', that will accomplish +the same thing as providing the `--minor` and `--strict` options. + +## PATCH LEVEL EXAMPLES + +Given the following gem specifications: + + foo 1.4.3, requires: ~> bar 2.0 + foo 1.4.4, requires: ~> bar 2.0 + foo 1.4.5, requires: ~> bar 2.1 + foo 1.5.0, requires: ~> bar 2.1 + foo 1.5.1, requires: ~> bar 3.0 + bar with versions 2.0.3, 2.0.4, 2.1.0, 2.1.1, 3.0.0 + +Gemfile: + + gem 'foo' + +Gemfile.lock: + + foo (1.4.3) + bar (~> 2.0) + bar (2.0.3) + +Cases: + + # Command Line Result + ------------------------------------------------------------ + 1 bundle update --patch 'foo 1.4.5', 'bar 2.1.1' + 2 bundle update --patch foo 'foo 1.4.5', 'bar 2.1.1' + 3 bundle update --minor 'foo 1.5.1', 'bar 3.0.0' + 4 bundle update --minor --strict 'foo 1.5.0', 'bar 2.1.1' + 5 bundle update --patch --strict 'foo 1.4.4', 'bar 2.0.4' + +In case 1, bar is upgraded to 2.1.1, a minor version increase, because +the dependency from foo 1.4.5 required it. + +In case 2, only foo is requested to be unlocked, but bar is also +allowed to move because it's not a declared dependency in the Gemfile. + +In case 3, bar goes up a whole major release, because a minor increase +is preferred now for foo, and when it goes to 1.5.1, it requires 3.0.0 +of bar. + +In case 4, foo is preferred up to a minor version, but 1.5.1 won't work +because the --strict flag removes bar 3.0.0 from consideration since +it's a major increment. + +In case 5, both foo and bar have any minor or major increments removed +from consideration because of the --strict flag, so the most they can +move is up to 1.4.4 and 2.0.4. + +## RECOMMENDED WORKFLOW + +In general, when working with an application managed with bundler, you should +use the following workflow: + +* After you create your Gemfile(5) for the first time, run + + $ bundle install + +* Check the resulting `Gemfile.lock` into version control + + $ git add Gemfile.lock + +* When checking out this repository on another development machine, run + + $ bundle install + +* When checking out this repository on a deployment machine, run + + $ bundle install --deployment + +* After changing the Gemfile(5) to reflect a new or update dependency, run + + $ bundle install + +* Make sure to check the updated `Gemfile.lock` into version control + + $ git add Gemfile.lock + +* If [bundle install(1)](bundle-install.1.html) reports a conflict, manually update the specific + gems that you changed in the Gemfile(5) + + $ bundle update rails thin + +* If you want to update all the gems to the latest possible versions that + still match the gems listed in the Gemfile(5), run + + $ bundle update --all diff --git a/man/bundle-viz.1 b/man/bundle-viz.1 new file mode 100644 index 00000000000000..70ad6835eef2f8 --- /dev/null +++ b/man/bundle-viz.1 @@ -0,0 +1,39 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE\-VIZ" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\-viz\fR \- Generates a visual dependency graph for your Gemfile +. +.SH "SYNOPSIS" +\fBbundle viz\fR [\-\-file=FILE] [\-\-format=FORMAT] [\-\-requirements] [\-\-version] [\-\-without=GROUP GROUP] +. +.SH "DESCRIPTION" +\fBviz\fR generates a PNG file of the current \fBGemfile(5)\fR as a dependency graph\. \fBviz\fR requires the ruby\-graphviz gem (and its dependencies)\. +. +.P +The associated gems must also be installed via \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-file\fR, \fB\-f\fR +The name to use for the generated file\. See \fB\-\-format\fR option +. +.TP +\fB\-\-format\fR, \fB\-F\fR +This is output format option\. Supported format is png, jpg, svg, dot \.\.\. +. +.TP +\fB\-\-requirements\fR, \fB\-R\fR +Set to show the version of each required dependency\. +. +.TP +\fB\-\-version\fR, \fB\-v\fR +Set to show each gem version\. +. +.TP +\fB\-\-without\fR, \fB\-W\fR +Exclude gems that are part of the specified named group\. + diff --git a/man/bundle-viz.1.txt b/man/bundle-viz.1.txt new file mode 100644 index 00000000000000..ada88d89c655dd --- /dev/null +++ b/man/bundle-viz.1.txt @@ -0,0 +1,39 @@ +BUNDLE-VIZ(1) BUNDLE-VIZ(1) + + + +1mNAME0m + 1mbundle-viz 22m- Generates a visual dependency graph for your Gemfile + +1mSYNOPSIS0m + 1mbundle viz 22m[--file=FILE] [--format=FORMAT] [--requirements] [--version] + [--without=GROUP GROUP] + +1mDESCRIPTION0m + 1mviz 22mgenerates a PNG file of the current 1mGemfile(5) 22mas a dependency + graph. 1mviz 22mrequires the ruby-graphviz gem (and its dependencies). + + The associated gems must also be installed via 1mbundle install(1) 4m22mbun-0m + 4mdle-install.1.html24m. + +1mOPTIONS0m + 1m--file22m, 1m-f0m + The name to use for the generated file. See 1m--format 22moption + + 1m--format22m, 1m-F0m + This is output format option. Supported format is png, jpg, svg, + dot ... + + 1m--requirements22m, 1m-R0m + Set to show the version of each required dependency. + + 1m--version22m, 1m-v0m + Set to show each gem version. + + 1m--without22m, 1m-W0m + Exclude gems that are part of the specified named group. + + + + + November 2018 BUNDLE-VIZ(1) diff --git a/man/bundle-viz.ronn b/man/bundle-viz.ronn new file mode 100644 index 00000000000000..701df5415e6c15 --- /dev/null +++ b/man/bundle-viz.ronn @@ -0,0 +1,30 @@ +bundle-viz(1) -- Generates a visual dependency graph for your Gemfile +===================================================================== + +## SYNOPSIS + +`bundle viz` [--file=FILE] + [--format=FORMAT] + [--requirements] + [--version] + [--without=GROUP GROUP] + +## DESCRIPTION + +`viz` generates a PNG file of the current `Gemfile(5)` as a dependency graph. +`viz` requires the ruby-graphviz gem (and its dependencies). + +The associated gems must also be installed via [`bundle install(1)`](bundle-install.1.html). + +## OPTIONS + +* `--file`, `-f`: + The name to use for the generated file. See `--format` option +* `--format`, `-F`: + This is output format option. Supported format is png, jpg, svg, dot ... +* `--requirements`, `-R`: + Set to show the version of each required dependency. +* `--version`, `-v`: + Set to show each gem version. +* `--without`, `-W`: + Exclude gems that are part of the specified named group. diff --git a/man/bundle.1 b/man/bundle.1 new file mode 100644 index 00000000000000..abb90154d91dbc --- /dev/null +++ b/man/bundle.1 @@ -0,0 +1,132 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BUNDLE" "1" "November 2018" "" "" +. +.SH "NAME" +\fBbundle\fR \- Ruby Dependency Management +. +.SH "SYNOPSIS" +\fBbundle\fR COMMAND [\-\-no\-color] [\-\-verbose] [ARGS] +. +.SH "DESCRIPTION" +Bundler manages an \fBapplication\'s dependencies\fR through its entire life across many machines systematically and repeatably\. +. +.P +See the bundler website \fIhttp://bundler\.io\fR for information on getting started, and Gemfile(5) for more information on the \fBGemfile\fR format\. +. +.SH "OPTIONS" +. +.TP +\fB\-\-no\-color\fR +Print all output without color +. +.TP +\fB\-\-retry\fR, \fB\-r\fR +Specify the number of times you wish to attempt network commands +. +.TP +\fB\-\-verbose\fR, \fB\-V\fR +Print out additional logging information +. +.SH "BUNDLE COMMANDS" +We divide \fBbundle\fR subcommands into primary commands and utilities: +. +.SH "PRIMARY COMMANDS" +. +.TP +\fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR +Install the gems specified by the \fBGemfile\fR or \fBGemfile\.lock\fR +. +.TP +\fBbundle update(1)\fR \fIbundle\-update\.1\.html\fR +Update dependencies to their latest versions +. +.TP +\fBbundle package(1)\fR \fIbundle\-package\.1\.html\fR +Package the \.gem files required by your application into the \fBvendor/cache\fR directory +. +.TP +\fBbundle exec(1)\fR \fIbundle\-exec\.1\.html\fR +Execute a script in the current bundle +. +.TP +\fBbundle config(1)\fR \fIbundle\-config\.1\.html\fR +Specify and read configuration options for Bundler +. +.TP +\fBbundle help(1)\fR +Display detailed help for each subcommand +. +.SH "UTILITIES" +. +.TP +\fBbundle add(1)\fR \fIbundle\-add\.1\.html\fR +Add the named gem to the Gemfile and run \fBbundle install\fR +. +.TP +\fBbundle binstubs(1)\fR \fIbundle\-binstubs\.1\.html\fR +Generate binstubs for executables in a gem +. +.TP +\fBbundle check(1)\fR \fIbundle\-check\.1\.html\fR +Determine whether the requirements for your application are installed and available to Bundler +. +.TP +\fBbundle show(1)\fR \fIbundle\-show\.1\.html\fR +Show the source location of a particular gem in the bundle +. +.TP +\fBbundle outdated(1)\fR \fIbundle\-outdated\.1\.html\fR +Show all of the outdated gems in the current bundle +. +.TP +\fBbundle console(1)\fR +Start an IRB session in the current bundle +. +.TP +\fBbundle open(1)\fR \fIbundle\-open\.1\.html\fR +Open an installed gem in the editor +. +.TP +\fBbundle lock(1)\fR \fIbundle\-lock\.1\.hmtl\fR +Generate a lockfile for your dependencies +. +.TP +\fBbundle viz(1)\fR \fIbundle\-viz\.1\.html\fR +Generate a visual representation of your dependencies +. +.TP +\fBbundle init(1)\fR \fIbundle\-init\.1\.html\fR +Generate a simple \fBGemfile\fR, placed in the current directory +. +.TP +\fBbundle gem(1)\fR \fIbundle\-gem\.1\.html\fR +Create a simple gem, suitable for development with Bundler +. +.TP +\fBbundle platform(1)\fR \fIbundle\-platform\.1\.html\fR +Display platform compatibility information +. +.TP +\fBbundle clean(1)\fR \fIbundle\-clean\.1\.html\fR +Clean up unused gems in your Bundler directory +. +.TP +\fBbundle doctor(1)\fR \fIbundle\-doctor\.1\.html\fR +Display warnings about common problems +. +.SH "PLUGINS" +When running a command that isn\'t listed in PRIMARY COMMANDS or UTILITIES, Bundler will try to find an executable on your path named \fBbundler\-\fR and execute it, passing down any extra arguments to it\. +. +.SH "OBSOLETE" +These commands are obsolete and should no longer be used: +. +.IP "\(bu" 4 +\fBbundle cache(1)\fR +. +.IP "\(bu" 4 +\fBbundle show(1)\fR +. +.IP "" 0 + diff --git a/man/bundle.1.txt b/man/bundle.1.txt new file mode 100644 index 00000000000000..0f38628621a15e --- /dev/null +++ b/man/bundle.1.txt @@ -0,0 +1,113 @@ +BUNDLE(1) BUNDLE(1) + + + +1mNAME0m + 1mbundle 22m- Ruby Dependency Management + +1mSYNOPSIS0m + 1mbundle 22mCOMMAND [--no-color] [--verbose] [ARGS] + +1mDESCRIPTION0m + Bundler manages an 1mapplication's dependencies 22mthrough its entire life + across many machines systematically and repeatably. + + See the bundler website 4mhttp://bundler.io24m for information on getting + started, and Gemfile(5) for more information on the 1mGemfile 22mformat. + +1mOPTIONS0m + 1m--no-color0m + Print all output without color + + 1m--retry22m, 1m-r0m + Specify the number of times you wish to attempt network commands + + 1m--verbose22m, 1m-V0m + Print out additional logging information + +1mBUNDLE COMMANDS0m + We divide 1mbundle 22msubcommands into primary commands and utilities: + +1mPRIMARY COMMANDS0m + 1mbundle install(1) 4m22mbundle-install.1.html0m + Install the gems specified by the 1mGemfile 22mor 1mGemfile.lock0m + + 1mbundle update(1) 4m22mbundle-update.1.html0m + Update dependencies to their latest versions + + 1mbundle package(1) 4m22mbundle-package.1.html0m + Package the .gem files required by your application into the + 1mvendor/cache 22mdirectory + + 1mbundle exec(1) 4m22mbundle-exec.1.html0m + Execute a script in the current bundle + + 1mbundle config(1) 4m22mbundle-config.1.html0m + Specify and read configuration options for Bundler + + 1mbundle help(1)0m + Display detailed help for each subcommand + +1mUTILITIES0m + 1mbundle add(1) 4m22mbundle-add.1.html0m + Add the named gem to the Gemfile and run 1mbundle install0m + + 1mbundle binstubs(1) 4m22mbundle-binstubs.1.html0m + Generate binstubs for executables in a gem + + 1mbundle check(1) 4m22mbundle-check.1.html0m + Determine whether the requirements for your application are + installed and available to Bundler + + 1mbundle show(1) 4m22mbundle-show.1.html0m + Show the source location of a particular gem in the bundle + + 1mbundle outdated(1) 4m22mbundle-outdated.1.html0m + Show all of the outdated gems in the current bundle + + 1mbundle console(1)0m + Start an IRB session in the current bundle + + 1mbundle open(1) 4m22mbundle-open.1.html0m + Open an installed gem in the editor + + 1mbundle lock(1) 4m22mbundle-lock.1.hmtl0m + Generate a lockfile for your dependencies + + 1mbundle viz(1) 4m22mbundle-viz.1.html0m + Generate a visual representation of your dependencies + + 1mbundle init(1) 4m22mbundle-init.1.html0m + Generate a simple 1mGemfile22m, placed in the current directory + + 1mbundle gem(1) 4m22mbundle-gem.1.html0m + Create a simple gem, suitable for development with Bundler + + 1mbundle platform(1) 4m22mbundle-platform.1.html0m + Display platform compatibility information + + 1mbundle clean(1) 4m22mbundle-clean.1.html0m + Clean up unused gems in your Bundler directory + + 1mbundle doctor(1) 4m22mbundle-doctor.1.html0m + Display warnings about common problems + +1mPLUGINS0m + When running a command that isn't listed in PRIMARY COMMANDS or UTILI- + TIES, Bundler will try to find an executable on your path named + 1mbundler- 22mand execute it, passing down any extra arguments to + it. + +1mOBSOLETE0m + These commands are obsolete and should no longer be used: + + o 1mbundle cache(1)0m + + o 1mbundle show(1)0m + + + + + + + November 2018 BUNDLE(1) diff --git a/man/bundle.ronn b/man/bundle.ronn new file mode 100644 index 00000000000000..c03201a30cd4ac --- /dev/null +++ b/man/bundle.ronn @@ -0,0 +1,108 @@ +bundle(1) -- Ruby Dependency Management +======================================= + +## SYNOPSIS + +`bundle` COMMAND [--no-color] [--verbose] [ARGS] + +## DESCRIPTION + +Bundler manages an `application's dependencies` through its entire life +across many machines systematically and repeatably. + +See [the bundler website](http://bundler.io) for information on getting +started, and Gemfile(5) for more information on the `Gemfile` format. + +## OPTIONS + +* `--no-color`: + Print all output without color + +* `--retry`, `-r`: + Specify the number of times you wish to attempt network commands + +* `--verbose`, `-V`: + Print out additional logging information + +## BUNDLE COMMANDS + +We divide `bundle` subcommands into primary commands and utilities: + +## PRIMARY COMMANDS + +* [`bundle install(1)`](bundle-install.1.html): + Install the gems specified by the `Gemfile` or `Gemfile.lock` + +* [`bundle update(1)`](bundle-update.1.html): + Update dependencies to their latest versions + +* [`bundle package(1)`](bundle-package.1.html): + Package the .gem files required by your application into the + `vendor/cache` directory + +* [`bundle exec(1)`](bundle-exec.1.html): + Execute a script in the current bundle + +* [`bundle config(1)`](bundle-config.1.html): + Specify and read configuration options for Bundler + +* `bundle help(1)`: + Display detailed help for each subcommand + +## UTILITIES + +* [`bundle add(1)`](bundle-add.1.html): + Add the named gem to the Gemfile and run `bundle install` + +* [`bundle binstubs(1)`](bundle-binstubs.1.html): + Generate binstubs for executables in a gem + +* [`bundle check(1)`](bundle-check.1.html): + Determine whether the requirements for your application are installed + and available to Bundler + +* [`bundle show(1)`](bundle-show.1.html): + Show the source location of a particular gem in the bundle + +* [`bundle outdated(1)`](bundle-outdated.1.html): + Show all of the outdated gems in the current bundle + +* `bundle console(1)`: + Start an IRB session in the current bundle + +* [`bundle open(1)`](bundle-open.1.html): + Open an installed gem in the editor + +* [`bundle lock(1)`](bundle-lock.1.hmtl): + Generate a lockfile for your dependencies + +* [`bundle viz(1)`](bundle-viz.1.html): + Generate a visual representation of your dependencies + +* [`bundle init(1)`](bundle-init.1.html): + Generate a simple `Gemfile`, placed in the current directory + +* [`bundle gem(1)`](bundle-gem.1.html): + Create a simple gem, suitable for development with Bundler + +* [`bundle platform(1)`](bundle-platform.1.html): + Display platform compatibility information + +* [`bundle clean(1)`](bundle-clean.1.html): + Clean up unused gems in your Bundler directory + +* [`bundle doctor(1)`](bundle-doctor.1.html): + Display warnings about common problems + +## PLUGINS + +When running a command that isn't listed in PRIMARY COMMANDS or UTILITIES, +Bundler will try to find an executable on your path named `bundler-` +and execute it, passing down any extra arguments to it. + +## OBSOLETE + +These commands are obsolete and should no longer be used: + +* `bundle cache(1)` +* `bundle show(1)` diff --git a/man/gemfile.5 b/man/gemfile.5 new file mode 100644 index 00000000000000..ccb258f50f8cb2 --- /dev/null +++ b/man/gemfile.5 @@ -0,0 +1,689 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "GEMFILE" "5" "November 2018" "" "" +. +.SH "NAME" +\fBGemfile\fR \- A format for describing gem dependencies for Ruby programs +. +.SH "SYNOPSIS" +A \fBGemfile\fR describes the gem dependencies required to execute associated Ruby code\. +. +.P +Place the \fBGemfile\fR in the root of the directory containing the associated code\. For instance, in a Rails application, place the \fBGemfile\fR in the same directory as the \fBRakefile\fR\. +. +.SH "SYNTAX" +A \fBGemfile\fR is evaluated as Ruby code, in a context which makes available a number of methods used to describe the gem requirements\. +. +.SH "GLOBAL SOURCES" +At the top of the \fBGemfile\fR, add a line for the \fBRubygems\fR source that contains the gems listed in the \fBGemfile\fR\. +. +.IP "" 4 +. +.nf + +source "https://rubygems\.org" +. +.fi +. +.IP "" 0 +. +.P +It is possible, but not recommended as of Bundler 1\.7, to add multiple global \fBsource\fR lines\. Each of these \fBsource\fRs \fBMUST\fR be a valid Rubygems repository\. +. +.P +Sources are checked for gems following the heuristics described in \fISOURCE PRIORITY\fR\. If a gem is found in more than one global source, Bundler will print a warning after installing the gem indicating which source was used, and listing the other sources where the gem is available\. A specific source can be selected for gems that need to use a non\-standard repository, suppressing this warning, by using the \fI\fB:source\fR option\fR or a \fI\fBsource\fR block\fR\. +. +.SS "CREDENTIALS" +Some gem sources require a username and password\. Use bundle config(1) \fIbundle\-config\.1\.html\fR to set the username and password for any of the sources that need it\. The command must be run once on each computer that will install the Gemfile, but this keeps the credentials from being stored in plain text in version control\. +. +.IP "" 4 +. +.nf + +bundle config gems\.example\.com user:password +. +.fi +. +.IP "" 0 +. +.P +For some sources, like a company Gemfury account, it may be easier to include the credentials in the Gemfile as part of the source URL\. +. +.IP "" 4 +. +.nf + +source "https://user:password@gems\.example\.com" +. +.fi +. +.IP "" 0 +. +.P +Credentials in the source URL will take precedence over credentials set using \fBconfig\fR\. +. +.SH "RUBY" +If your application requires a specific Ruby version or engine, specify your requirements using the \fBruby\fR method, with the following arguments\. All parameters are \fBOPTIONAL\fR unless otherwise specified\. +. +.SS "VERSION (required)" +The version of Ruby that your application requires\. If your application requires an alternate Ruby engine, such as JRuby, Rubinius or TruffleRuby, this should be the Ruby version that the engine is compatible with\. +. +.IP "" 4 +. +.nf + +ruby "1\.9\.3" +. +.fi +. +.IP "" 0 +. +.SS "ENGINE" +Each application \fImay\fR specify a Ruby engine\. If an engine is specified, an engine version \fImust\fR also be specified\. +. +.P +What exactly is an Engine? \- A Ruby engine is an implementation of the Ruby language\. +. +.IP "\(bu" 4 +For background: the reference or original implementation of the Ruby programming language is called Matz\'s Ruby Interpreter \fIhttps://en\.wikipedia\.org/wiki/Ruby_MRI\fR, or MRI for short\. This is named after Ruby creator Yukihiro Matsumoto, also known as Matz\. MRI is also known as CRuby, because it is written in C\. MRI is the most widely used Ruby engine\. +. +.IP "\(bu" 4 +Other implementations \fIhttps://www\.ruby\-lang\.org/en/about/\fR of Ruby exist\. Some of the more well\-known implementations include Rubinius \fIhttps://rubinius\.com/\fR, and JRuby \fIhttp://jruby\.org/\fR\. Rubinius is an alternative implementation of Ruby written in Ruby\. JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine\. +. +.IP "" 0 +. +.SS "ENGINE VERSION" +Each application \fImay\fR specify a Ruby engine version\. If an engine version is specified, an engine \fImust\fR also be specified\. If the engine is "ruby" the engine version specified \fImust\fR match the Ruby version\. +. +.IP "" 4 +. +.nf + +ruby "1\.8\.7", :engine => "jruby", :engine_version => "1\.6\.7" +. +.fi +. +.IP "" 0 +. +.SS "PATCHLEVEL" +Each application \fImay\fR specify a Ruby patchlevel\. +. +.IP "" 4 +. +.nf + +ruby "2\.0\.0", :patchlevel => "247" +. +.fi +. +.IP "" 0 +. +.SH "GEMS" +Specify gem requirements using the \fBgem\fR method, with the following arguments\. All parameters are \fBOPTIONAL\fR unless otherwise specified\. +. +.SS "NAME (required)" +For each gem requirement, list a single \fIgem\fR line\. +. +.IP "" 4 +. +.nf + +gem "nokogiri" +. +.fi +. +.IP "" 0 +. +.SS "VERSION" +Each \fIgem\fR \fBMAY\fR have one or more version specifiers\. +. +.IP "" 4 +. +.nf + +gem "nokogiri", ">= 1\.4\.2" +gem "RedCloth", ">= 4\.1\.0", "< 4\.2\.0" +. +.fi +. +.IP "" 0 +. +.SS "REQUIRE AS" +Each \fIgem\fR \fBMAY\fR specify files that should be used when autorequiring via \fBBundler\.require\fR\. You may pass an array with multiple files or \fBtrue\fR if file you want \fBrequired\fR has same name as \fIgem\fR or \fBfalse\fR to prevent any file from being autorequired\. +. +.IP "" 4 +. +.nf + +gem "redis", :require => ["redis/connection/hiredis", "redis"] +gem "webmock", :require => false +gem "debugger", :require => true +. +.fi +. +.IP "" 0 +. +.P +The argument defaults to the name of the gem\. For example, these are identical: +. +.IP "" 4 +. +.nf + +gem "nokogiri" +gem "nokogiri", :require => "nokogiri" +gem "nokogiri", :require => true +. +.fi +. +.IP "" 0 +. +.SS "GROUPS" +Each \fIgem\fR \fBMAY\fR specify membership in one or more groups\. Any \fIgem\fR that does not specify membership in any group is placed in the \fBdefault\fR group\. +. +.IP "" 4 +. +.nf + +gem "rspec", :group => :test +gem "wirble", :groups => [:development, :test] +. +.fi +. +.IP "" 0 +. +.P +The Bundler runtime allows its two main methods, \fBBundler\.setup\fR and \fBBundler\.require\fR, to limit their impact to particular groups\. +. +.IP "" 4 +. +.nf + +# setup adds gems to Ruby\'s load path +Bundler\.setup # defaults to all groups +require "bundler/setup" # same as Bundler\.setup +Bundler\.setup(:default) # only set up the _default_ group +Bundler\.setup(:test) # only set up the _test_ group (but `not` _default_) +Bundler\.setup(:default, :test) # set up the _default_ and _test_ groups, but no others + +# require requires all of the gems in the specified groups +Bundler\.require # defaults to the _default_ group +Bundler\.require(:default) # identical +Bundler\.require(:default, :test) # requires the _default_ and _test_ groups +Bundler\.require(:test) # requires the _test_ group +. +.fi +. +.IP "" 0 +. +.P +The Bundler CLI allows you to specify a list of groups whose gems \fBbundle install\fR should not install with the \fB\-\-without\fR option\. To specify multiple groups to ignore, specify a list of groups separated by spaces\. +. +.IP "" 4 +. +.nf + +bundle install \-\-without test +bundle install \-\-without development test +. +.fi +. +.IP "" 0 +. +.P +After running \fBbundle install \-\-without test\fR, bundler will remember that you excluded the test group in the last installation\. The next time you run \fBbundle install\fR, without any \fB\-\-without option\fR, bundler will recall it\. +. +.P +Also, calling \fBBundler\.setup\fR with no parameters, or calling \fBrequire "bundler/setup"\fR will setup all groups except for the ones you excluded via \fB\-\-without\fR (since they are not available)\. +. +.P +Note that on \fBbundle install\fR, bundler downloads and evaluates all gems, in order to create a single canonical list of all of the required gems and their dependencies\. This means that you cannot list different versions of the same gems in different groups\. For more details, see Understanding Bundler \fIhttp://bundler\.io/rationale\.html\fR\. +. +.SS "PLATFORMS" +If a gem should only be used in a particular platform or set of platforms, you can specify them\. Platforms are essentially identical to groups, except that you do not need to use the \fB\-\-without\fR install\-time flag to exclude groups of gems for other platforms\. +. +.P +There are a number of \fBGemfile\fR platforms: +. +.TP +\fBruby\fR +C Ruby (MRI), Rubinius or TruffleRuby, but \fBNOT\fR Windows +. +.TP +\fBmri\fR +Same as \fIruby\fR, but only C Ruby (MRI) +. +.TP +\fBmingw\fR +Windows 32 bit \'mingw32\' platform (aka RubyInstaller) +. +.TP +\fBx64_mingw\fR +Windows 64 bit \'mingw32\' platform (aka RubyInstaller x64) +. +.TP +\fBrbx\fR +Rubinius +. +.TP +\fBjruby\fR +JRuby +. +.TP +\fBtruffleruby\fR +TruffleRuby +. +.TP +\fBmswin\fR +Windows +. +.P +You can restrict further by platform and version for all platforms \fIexcept\fR for \fBrbx\fR, \fBjruby\fR, \fBtruffleruby\fR and \fBmswin\fR\. +. +.P +To specify a version in addition to a platform, append the version number without the delimiter to the platform\. For example, to specify that a gem should only be used on platforms with Ruby 2\.3, use: +. +.IP "" 4 +. +.nf + +ruby_23 +. +.fi +. +.IP "" 0 +. +.P +The full list of platforms and supported versions includes: +. +.TP +\fBruby\fR +1\.8, 1\.9, 2\.0, 2\.1, 2\.2, 2\.3, 2\.4, 2\.5 +. +.TP +\fBmri\fR +1\.8, 1\.9, 2\.0, 2\.1, 2\.2, 2\.3, 2\.4, 2\.5 +. +.TP +\fBmingw\fR +1\.8, 1\.9, 2\.0, 2\.1, 2\.2, 2\.3, 2\.4, 2\.5 +. +.TP +\fBx64_mingw\fR +2\.0, 2\.1, 2\.2, 2\.3, 2\.4, 2\.5 +. +.P +As with groups, you can specify one or more platforms: +. +.IP "" 4 +. +.nf + +gem "weakling", :platforms => :jruby +gem "ruby\-debug", :platforms => :mri_18 +gem "nokogiri", :platforms => [:mri_18, :jruby] +. +.fi +. +.IP "" 0 +. +.P +All operations involving groups (\fBbundle install\fR \fIbundle\-install\.1\.html\fR, \fBBundler\.setup\fR, \fBBundler\.require\fR) behave exactly the same as if any groups not matching the current platform were explicitly excluded\. +. +.SS "SOURCE" +You can select an alternate Rubygems repository for a gem using the \':source\' option\. +. +.IP "" 4 +. +.nf + +gem "some_internal_gem", :source => "https://gems\.example\.com" +. +.fi +. +.IP "" 0 +. +.P +This forces the gem to be loaded from this source and ignores any global sources declared at the top level of the file\. If the gem does not exist in this source, it will not be installed\. +. +.P +Bundler will search for child dependencies of this gem by first looking in the source selected for the parent, but if they are not found there, it will fall back on global sources using the ordering described in \fISOURCE PRIORITY\fR\. +. +.P +Selecting a specific source repository this way also suppresses the ambiguous gem warning described above in \fIGLOBAL SOURCES (#source)\fR\. +. +.P +Using the \fB:source\fR option for an individual gem will also make that source available as a possible global source for any other gems which do not specify explicit sources\. Thus, when adding gems with explicit sources, it is recommended that you also ensure all other gems in the Gemfile are using explicit sources\. +. +.SS "GIT" +If necessary, you can specify that a gem is located at a particular git repository using the \fB:git\fR parameter\. The repository can be accessed via several protocols: +. +.TP +\fBHTTP(S)\fR +gem "rails", :git => "https://github\.com/rails/rails\.git" +. +.TP +\fBSSH\fR +gem "rails", :git => "git@github\.com:rails/rails\.git" +. +.TP +\fBgit\fR +gem "rails", :git => "git://github\.com/rails/rails\.git" +. +.P +If using SSH, the user that you use to run \fBbundle install\fR \fBMUST\fR have the appropriate keys available in their \fB$HOME/\.ssh\fR\. +. +.P +\fBNOTE\fR: \fBhttp://\fR and \fBgit://\fR URLs should be avoided if at all possible\. These protocols are unauthenticated, so a man\-in\-the\-middle attacker can deliver malicious code and compromise your system\. HTTPS and SSH are strongly preferred\. +. +.P +The \fBgroup\fR, \fBplatforms\fR, and \fBrequire\fR options are available and behave exactly the same as they would for a normal gem\. +. +.P +A git repository \fBSHOULD\fR have at least one file, at the root of the directory containing the gem, with the extension \fB\.gemspec\fR\. This file \fBMUST\fR contain a valid gem specification, as expected by the \fBgem build\fR command\. +. +.P +If a git repository does not have a \fB\.gemspec\fR, bundler will attempt to create one, but it will not contain any dependencies, executables, or C extension compilation instructions\. As a result, it may fail to properly integrate into your application\. +. +.P +If a git repository does have a \fB\.gemspec\fR for the gem you attached it to, a version specifier, if provided, means that the git repository is only valid if the \fB\.gemspec\fR specifies a version matching the version specifier\. If not, bundler will print a warning\. +. +.IP "" 4 +. +.nf + +gem "rails", "2\.3\.8", :git => "https://github\.com/rails/rails\.git" +# bundle install will fail, because the \.gemspec in the rails +# repository\'s master branch specifies version 3\.0\.0 +. +.fi +. +.IP "" 0 +. +.P +If a git repository does \fBnot\fR have a \fB\.gemspec\fR for the gem you attached it to, a version specifier \fBMUST\fR be provided\. Bundler will use this version in the simple \fB\.gemspec\fR it creates\. +. +.P +Git repositories support a number of additional options\. +. +.TP +\fBbranch\fR, \fBtag\fR, and \fBref\fR +You \fBMUST\fR only specify at most one of these options\. The default is \fB:branch => "master"\fR +. +.TP +For example: +. +.IP +git "https://github\.com/rails/rails\.git", :branch => "5\-0\-stable" do +. +.IP +git "https://github\.com/rails/rails\.git", :tag => "v5\.0\.0" do +. +.IP +git "https://github\.com/rails/rails\.git", :ref => "4aded" do +. +.TP +\fBsubmodules\fR +For reference, a git submodule \fIhttps://git\-scm\.com/book/en/v2/Git\-Tools\-Submodules\fR lets you have another git repository within a subfolder of your repository\. Specify \fB:submodules => true\fR to cause bundler to expand any submodules included in the git repository +. +.P +If a git repository contains multiple \fB\.gemspecs\fR, each \fB\.gemspec\fR represents a gem located at the same place in the file system as the \fB\.gemspec\fR\. +. +.IP "" 4 +. +.nf + +|~rails [git root] +| |\-rails\.gemspec [rails gem located here] +|~actionpack +| |\-actionpack\.gemspec [actionpack gem located here] +|~activesupport +| |\-activesupport\.gemspec [activesupport gem located here] +|\.\.\. +. +.fi +. +.IP "" 0 +. +.P +To install a gem located in a git repository, bundler changes to the directory containing the gemspec, runs \fBgem build name\.gemspec\fR and then installs the resulting gem\. The \fBgem build\fR command, which comes standard with Rubygems, evaluates the \fB\.gemspec\fR in the context of the directory in which it is located\. +. +.SS "GIT SOURCE" +A custom git source can be defined via the \fBgit_source\fR method\. Provide the source\'s name as an argument, and a block which receives a single argument and interpolates it into a string to return the full repo address: +. +.IP "" 4 +. +.nf + +git_source(:stash){ |repo_name| "https://stash\.corp\.acme\.pl/#{repo_name}\.git" } +gem \'rails\', :stash => \'forks/rails\' +. +.fi +. +.IP "" 0 +. +.P +In addition, if you wish to choose a specific branch: +. +.IP "" 4 +. +.nf + +gem "rails", :stash => "forks/rails", :branch => "branch_name" +. +.fi +. +.IP "" 0 +. +.SS "GITHUB" +\fBNOTE\fR: This shorthand should be avoided until Bundler 2\.0, since it currently expands to an insecure \fBgit://\fR URL\. This allows a man\-in\-the\-middle attacker to compromise your system\. +. +.P +If the git repository you want to use is hosted on GitHub and is public, you can use the :github shorthand to specify the github username and repository name (without the trailing "\.git"), separated by a slash\. If both the username and repository name are the same, you can omit one\. +. +.IP "" 4 +. +.nf + +gem "rails", :github => "rails/rails" +gem "rails", :github => "rails" +. +.fi +. +.IP "" 0 +. +.P +Are both equivalent to +. +.IP "" 4 +. +.nf + +gem "rails", :git => "git://github\.com/rails/rails\.git" +. +.fi +. +.IP "" 0 +. +.P +Since the \fBgithub\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\. +. +.SS "GIST" +If the git repository you want to use is hosted as a Github Gist and is public, you can use the :gist shorthand to specify the gist identifier (without the trailing "\.git")\. +. +.IP "" 4 +. +.nf + +gem "the_hatch", :gist => "4815162342" +. +.fi +. +.IP "" 0 +. +.P +Is equivalent to: +. +.IP "" 4 +. +.nf + +gem "the_hatch", :git => "https://gist\.github\.com/4815162342\.git" +. +.fi +. +.IP "" 0 +. +.P +Since the \fBgist\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\. +. +.SS "BITBUCKET" +If the git repository you want to use is hosted on Bitbucket and is public, you can use the :bitbucket shorthand to specify the bitbucket username and repository name (without the trailing "\.git"), separated by a slash\. If both the username and repository name are the same, you can omit one\. +. +.IP "" 4 +. +.nf + +gem "rails", :bitbucket => "rails/rails" +gem "rails", :bitbucket => "rails" +. +.fi +. +.IP "" 0 +. +.P +Are both equivalent to +. +.IP "" 4 +. +.nf + +gem "rails", :git => "https://rails@bitbucket\.org/rails/rails\.git" +. +.fi +. +.IP "" 0 +. +.P +Since the \fBbitbucket\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\. +. +.SS "PATH" +You can specify that a gem is located in a particular location on the file system\. Relative paths are resolved relative to the directory containing the \fBGemfile\fR\. +. +.P +Similar to the semantics of the \fB:git\fR option, the \fB:path\fR option requires that the directory in question either contains a \fB\.gemspec\fR for the gem, or that you specify an explicit version that bundler should use\. +. +.P +Unlike \fB:git\fR, bundler does not compile C extensions for gems specified as paths\. +. +.IP "" 4 +. +.nf + +gem "rails", :path => "vendor/rails" +. +.fi +. +.IP "" 0 +. +.P +If you would like to use multiple local gems directly from the filesystem, you can set a global \fBpath\fR option to the path containing the gem\'s files\. This will automatically load gemspec files from subdirectories\. +. +.IP "" 4 +. +.nf + +path \'components\' do + gem \'admin_ui\' + gem \'public_ui\' +end +. +.fi +. +.IP "" 0 +. +.SH "BLOCK FORM OF SOURCE, GIT, PATH, GROUP and PLATFORMS" +The \fB:source\fR, \fB:git\fR, \fB:path\fR, \fB:group\fR, and \fB:platforms\fR options may be applied to a group of gems by using block form\. +. +.IP "" 4 +. +.nf + +source "https://gems\.example\.com" do + gem "some_internal_gem" + gem "another_internal_gem" +end + +git "https://github\.com/rails/rails\.git" do + gem "activesupport" + gem "actionpack" +end + +platforms :ruby do + gem "ruby\-debug" + gem "sqlite3" +end + +group :development, :optional => true do + gem "wirble" + gem "faker" +end +. +.fi +. +.IP "" 0 +. +.P +In the case of the group block form the :optional option can be given to prevent a group from being installed unless listed in the \fB\-\-with\fR option given to the \fBbundle install\fR command\. +. +.P +In the case of the \fBgit\fR block form, the \fB:ref\fR, \fB:branch\fR, \fB:tag\fR, and \fB:submodules\fR options may be passed to the \fBgit\fR method, and all gems in the block will inherit those options\. +. +.P +The presence of a \fBsource\fR block in a Gemfile also makes that source available as a possible global source for any other gems which do not specify explicit sources\. Thus, when defining source blocks, it is recommended that you also ensure all other gems in the Gemfile are using explicit sources, either via source blocks or \fB:source\fR directives on individual gems\. +. +.SH "INSTALL_IF" +The \fBinstall_if\fR method allows gems to be installed based on a proc or lambda\. This is especially useful for optional gems that can only be used if certain software is installed or some other conditions are met\. +. +.IP "" 4 +. +.nf + +install_if \-> { RUBY_PLATFORM =~ /darwin/ } do + gem "pasteboard" +end +. +.fi +. +.IP "" 0 +. +.SH "GEMSPEC" +The \fB\.gemspec\fR \fIhttp://guides\.rubygems\.org/specification\-reference/\fR file is where you provide metadata about your gem to Rubygems\. Some required Gemspec attributes include the name, description, and homepage of your gem\. This is also where you specify the dependencies your gem needs to run\. +. +.P +If you wish to use Bundler to help install dependencies for a gem while it is being developed, use the \fBgemspec\fR method to pull in the dependencies listed in the \fB\.gemspec\fR file\. +. +.P +The \fBgemspec\fR method adds any runtime dependencies as gem requirements in the default group\. It also adds development dependencies as gem requirements in the \fBdevelopment\fR group\. Finally, it adds a gem requirement on your project (\fB:path => \'\.\'\fR)\. In conjunction with \fBBundler\.setup\fR, this allows you to require project files in your test code as you would if the project were installed as a gem; you need not manipulate the load path manually or require project files via relative paths\. +. +.P +The \fBgemspec\fR method supports optional \fB:path\fR, \fB:glob\fR, \fB:name\fR, and \fB:development_group\fR options, which control where bundler looks for the \fB\.gemspec\fR, the glob it uses to look for the gemspec (defaults to: "{,\fI,\fR/*}\.gemspec"), what named \fB\.gemspec\fR it uses (if more than one is present), and which group development dependencies are included in\. +. +.P +When a \fBgemspec\fR dependency encounters version conflicts during resolution, the local version under development will always be selected \-\- even if there are remote versions that better match other requirements for the \fBgemspec\fR gem\. +. +.SH "SOURCE PRIORITY" +When attempting to locate a gem to satisfy a gem requirement, bundler uses the following priority order: +. +.IP "1." 4 +The source explicitly attached to the gem (using \fB:source\fR, \fB:path\fR, or \fB:git\fR) +. +.IP "2." 4 +For implicit gems (dependencies of explicit gems), any source, git, or path repository declared on the parent\. This results in bundler prioritizing the ActiveSupport gem from the Rails git repository over ones from \fBrubygems\.org\fR +. +.IP "3." 4 +The sources specified via global \fBsource\fR lines, searching each source in your \fBGemfile\fR from last added to first added\. +. +.IP "" 0 + diff --git a/man/gemfile.5.ronn b/man/gemfile.5.ronn new file mode 100644 index 00000000000000..f4772f6d8d6e37 --- /dev/null +++ b/man/gemfile.5.ronn @@ -0,0 +1,521 @@ +Gemfile(5) -- A format for describing gem dependencies for Ruby programs +======================================================================== + +## SYNOPSIS + +A `Gemfile` describes the gem dependencies required to execute associated +Ruby code. + +Place the `Gemfile` in the root of the directory containing the associated +code. For instance, in a Rails application, place the `Gemfile` in the same +directory as the `Rakefile`. + +## SYNTAX + +A `Gemfile` is evaluated as Ruby code, in a context which makes available +a number of methods used to describe the gem requirements. + +## GLOBAL SOURCES + +At the top of the `Gemfile`, add a line for the `Rubygems` source that contains +the gems listed in the `Gemfile`. + + source "https://rubygems.org" + +It is possible, but not recommended as of Bundler 1.7, to add multiple global +`source` lines. Each of these `source`s `MUST` be a valid Rubygems repository. + +Sources are checked for gems following the heuristics described in +[SOURCE PRIORITY][]. If a gem is found in more than one global source, Bundler +will print a warning after installing the gem indicating which source was used, +and listing the other sources where the gem is available. A specific source can +be selected for gems that need to use a non-standard repository, suppressing +this warning, by using the [`:source` option](#SOURCE) or a +[`source` block](#BLOCK-FORM-OF-SOURCE-GIT-PATH-GROUP-and-PLATFORMS). + +### CREDENTIALS + +Some gem sources require a username and password. Use [bundle config(1)](bundle-config.1.html) to set +the username and password for any of the sources that need it. The command must +be run once on each computer that will install the Gemfile, but this keeps the +credentials from being stored in plain text in version control. + + bundle config gems.example.com user:password + +For some sources, like a company Gemfury account, it may be easier to +include the credentials in the Gemfile as part of the source URL. + + source "https://user:password@gems.example.com" + +Credentials in the source URL will take precedence over credentials set using +`config`. + +## RUBY + +If your application requires a specific Ruby version or engine, specify your +requirements using the `ruby` method, with the following arguments. +All parameters are `OPTIONAL` unless otherwise specified. + +### VERSION (required) + +The version of Ruby that your application requires. If your application +requires an alternate Ruby engine, such as JRuby, Rubinius or TruffleRuby, this +should be the Ruby version that the engine is compatible with. + + ruby "1.9.3" + +### ENGINE + +Each application _may_ specify a Ruby engine. If an engine is specified, an +engine version _must_ also be specified. + +What exactly is an Engine? + - A Ruby engine is an implementation of the Ruby language. + + - For background: the reference or original implementation of the Ruby + programming language is called + [Matz's Ruby Interpreter](https://en.wikipedia.org/wiki/Ruby_MRI), or MRI + for short. This is named after Ruby creator Yukihiro Matsumoto, + also known as Matz. MRI is also known as CRuby, because it is written in C. + MRI is the most widely used Ruby engine. + + - [Other implementations](https://www.ruby-lang.org/en/about/) of Ruby exist. + Some of the more well-known implementations include + [Rubinius](https://rubinius.com/), and [JRuby](http://jruby.org/). + Rubinius is an alternative implementation of Ruby written in Ruby. + JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine. + +### ENGINE VERSION + +Each application _may_ specify a Ruby engine version. If an engine version is +specified, an engine _must_ also be specified. If the engine is "ruby" the +engine version specified _must_ match the Ruby version. + + ruby "1.8.7", :engine => "jruby", :engine_version => "1.6.7" + +### PATCHLEVEL + +Each application _may_ specify a Ruby patchlevel. + + ruby "2.0.0", :patchlevel => "247" + +## GEMS + +Specify gem requirements using the `gem` method, with the following arguments. +All parameters are `OPTIONAL` unless otherwise specified. + +### NAME (required) + +For each gem requirement, list a single _gem_ line. + + gem "nokogiri" + +### VERSION + +Each _gem_ `MAY` have one or more version specifiers. + + gem "nokogiri", ">= 1.4.2" + gem "RedCloth", ">= 4.1.0", "< 4.2.0" + +### REQUIRE AS + +Each _gem_ `MAY` specify files that should be used when autorequiring via +`Bundler.require`. You may pass an array with multiple files or `true` if file +you want `required` has same name as _gem_ or `false` to +prevent any file from being autorequired. + + gem "redis", :require => ["redis/connection/hiredis", "redis"] + gem "webmock", :require => false + gem "debugger", :require => true + +The argument defaults to the name of the gem. For example, these are identical: + + gem "nokogiri" + gem "nokogiri", :require => "nokogiri" + gem "nokogiri", :require => true + +### GROUPS + +Each _gem_ `MAY` specify membership in one or more groups. Any _gem_ that does +not specify membership in any group is placed in the `default` group. + + gem "rspec", :group => :test + gem "wirble", :groups => [:development, :test] + +The Bundler runtime allows its two main methods, `Bundler.setup` and +`Bundler.require`, to limit their impact to particular groups. + + # setup adds gems to Ruby's load path + Bundler.setup # defaults to all groups + require "bundler/setup" # same as Bundler.setup + Bundler.setup(:default) # only set up the _default_ group + Bundler.setup(:test) # only set up the _test_ group (but `not` _default_) + Bundler.setup(:default, :test) # set up the _default_ and _test_ groups, but no others + + # require requires all of the gems in the specified groups + Bundler.require # defaults to the _default_ group + Bundler.require(:default) # identical + Bundler.require(:default, :test) # requires the _default_ and _test_ groups + Bundler.require(:test) # requires the _test_ group + +The Bundler CLI allows you to specify a list of groups whose gems `bundle install` should +not install with the `--without` option. To specify multiple groups to ignore, specify a +list of groups separated by spaces. + + bundle install --without test + bundle install --without development test + +After running `bundle install --without test`, bundler will remember that you excluded +the test group in the last installation. The next time you run `bundle install`, +without any `--without option`, bundler will recall it. + +Also, calling `Bundler.setup` with no parameters, or calling `require "bundler/setup"` +will setup all groups except for the ones you excluded via `--without` (since they +are not available). + +Note that on `bundle install`, bundler downloads and evaluates all gems, in order to +create a single canonical list of all of the required gems and their dependencies. +This means that you cannot list different versions of the same gems in different +groups. For more details, see [Understanding Bundler](http://bundler.io/rationale.html). + +### PLATFORMS + +If a gem should only be used in a particular platform or set of platforms, you can +specify them. Platforms are essentially identical to groups, except that you do not +need to use the `--without` install-time flag to exclude groups of gems for other +platforms. + +There are a number of `Gemfile` platforms: + + * `ruby`: + C Ruby (MRI), Rubinius or TruffleRuby, but `NOT` Windows + * `mri`: + Same as _ruby_, but only C Ruby (MRI) + * `mingw`: + Windows 32 bit 'mingw32' platform (aka RubyInstaller) + * `x64_mingw`: + Windows 64 bit 'mingw32' platform (aka RubyInstaller x64) + * `rbx`: + Rubinius + * `jruby`: + JRuby + * `truffleruby`: + TruffleRuby + * `mswin`: + Windows + +You can restrict further by platform and version for all platforms *except* for +`rbx`, `jruby`, `truffleruby` and `mswin`. + +To specify a version in addition to a platform, append the version number without +the delimiter to the platform. For example, to specify that a gem should only be +used on platforms with Ruby 2.3, use: + + ruby_23 + +The full list of platforms and supported versions includes: + + * `ruby`: + 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + * `mri`: + 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + * `mingw`: + 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + * `x64_mingw`: + 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + +As with groups, you can specify one or more platforms: + + gem "weakling", :platforms => :jruby + gem "ruby-debug", :platforms => :mri_18 + gem "nokogiri", :platforms => [:mri_18, :jruby] + +All operations involving groups ([`bundle install`](bundle-install.1.html), `Bundler.setup`, +`Bundler.require`) behave exactly the same as if any groups not +matching the current platform were explicitly excluded. + +### SOURCE + +You can select an alternate Rubygems repository for a gem using the ':source' +option. + + gem "some_internal_gem", :source => "https://gems.example.com" + +This forces the gem to be loaded from this source and ignores any global sources +declared at the top level of the file. If the gem does not exist in this source, +it will not be installed. + +Bundler will search for child dependencies of this gem by first looking in the +source selected for the parent, but if they are not found there, it will fall +back on global sources using the ordering described in [SOURCE PRIORITY][]. + +Selecting a specific source repository this way also suppresses the ambiguous +gem warning described above in +[GLOBAL SOURCES (#source)](#GLOBAL-SOURCES). + +Using the `:source` option for an individual gem will also make that source +available as a possible global source for any other gems which do not specify +explicit sources. Thus, when adding gems with explicit sources, it is +recommended that you also ensure all other gems in the Gemfile are using +explicit sources. + +### GIT + +If necessary, you can specify that a gem is located at a particular +git repository using the `:git` parameter. The repository can be accessed via +several protocols: + + * `HTTP(S)`: + gem "rails", :git => "https://github.com/rails/rails.git" + * `SSH`: + gem "rails", :git => "git@github.com:rails/rails.git" + * `git`: + gem "rails", :git => "git://github.com/rails/rails.git" + +If using SSH, the user that you use to run `bundle install` `MUST` have the +appropriate keys available in their `$HOME/.ssh`. + +`NOTE`: `http://` and `git://` URLs should be avoided if at all possible. These +protocols are unauthenticated, so a man-in-the-middle attacker can deliver +malicious code and compromise your system. HTTPS and SSH are strongly +preferred. + +The `group`, `platforms`, and `require` options are available and behave +exactly the same as they would for a normal gem. + +A git repository `SHOULD` have at least one file, at the root of the +directory containing the gem, with the extension `.gemspec`. This file +`MUST` contain a valid gem specification, as expected by the `gem build` +command. + +If a git repository does not have a `.gemspec`, bundler will attempt to +create one, but it will not contain any dependencies, executables, or +C extension compilation instructions. As a result, it may fail to properly +integrate into your application. + +If a git repository does have a `.gemspec` for the gem you attached it +to, a version specifier, if provided, means that the git repository is +only valid if the `.gemspec` specifies a version matching the version +specifier. If not, bundler will print a warning. + + gem "rails", "2.3.8", :git => "https://github.com/rails/rails.git" + # bundle install will fail, because the .gemspec in the rails + # repository's master branch specifies version 3.0.0 + +If a git repository does `not` have a `.gemspec` for the gem you attached +it to, a version specifier `MUST` be provided. Bundler will use this +version in the simple `.gemspec` it creates. + +Git repositories support a number of additional options. + + * `branch`, `tag`, and `ref`: + You `MUST` only specify at most one of these options. The default + is `:branch => "master"` + * For example: + + git "https://github.com/rails/rails.git", :branch => "5-0-stable" do + + git "https://github.com/rails/rails.git", :tag => "v5.0.0" do + + git "https://github.com/rails/rails.git", :ref => "4aded" do + + * `submodules`: + For reference, a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) + lets you have another git repository within a subfolder of your repository. + Specify `:submodules => true` to cause bundler to expand any + submodules included in the git repository + +If a git repository contains multiple `.gemspecs`, each `.gemspec` +represents a gem located at the same place in the file system as +the `.gemspec`. + + |~rails [git root] + | |-rails.gemspec [rails gem located here] + |~actionpack + | |-actionpack.gemspec [actionpack gem located here] + |~activesupport + | |-activesupport.gemspec [activesupport gem located here] + |... + +To install a gem located in a git repository, bundler changes to +the directory containing the gemspec, runs `gem build name.gemspec` +and then installs the resulting gem. The `gem build` command, +which comes standard with Rubygems, evaluates the `.gemspec` in +the context of the directory in which it is located. + +### GIT SOURCE + +A custom git source can be defined via the `git_source` method. Provide the source's name +as an argument, and a block which receives a single argument and interpolates it into a +string to return the full repo address: + + git_source(:stash){ |repo_name| "https://stash.corp.acme.pl/#{repo_name}.git" } + gem 'rails', :stash => 'forks/rails' + +In addition, if you wish to choose a specific branch: + + gem "rails", :stash => "forks/rails", :branch => "branch_name" + +### GITHUB + +`NOTE`: This shorthand should be avoided until Bundler 2.0, since it +currently expands to an insecure `git://` URL. This allows a +man-in-the-middle attacker to compromise your system. + +If the git repository you want to use is hosted on GitHub and is public, you can use the +:github shorthand to specify the github username and repository name (without the +trailing ".git"), separated by a slash. If both the username and repository name are the +same, you can omit one. + + gem "rails", :github => "rails/rails" + gem "rails", :github => "rails" + +Are both equivalent to + + gem "rails", :git => "git://github.com/rails/rails.git" + +Since the `github` method is a specialization of `git_source`, it accepts a `:branch` named argument. + +### GIST + +If the git repository you want to use is hosted as a Github Gist and is public, you can use +the :gist shorthand to specify the gist identifier (without the trailing ".git"). + + gem "the_hatch", :gist => "4815162342" + +Is equivalent to: + + gem "the_hatch", :git => "https://gist.github.com/4815162342.git" + +Since the `gist` method is a specialization of `git_source`, it accepts a `:branch` named argument. + +### BITBUCKET + +If the git repository you want to use is hosted on Bitbucket and is public, you can use the +:bitbucket shorthand to specify the bitbucket username and repository name (without the +trailing ".git"), separated by a slash. If both the username and repository name are the +same, you can omit one. + + gem "rails", :bitbucket => "rails/rails" + gem "rails", :bitbucket => "rails" + +Are both equivalent to + + gem "rails", :git => "https://rails@bitbucket.org/rails/rails.git" + +Since the `bitbucket` method is a specialization of `git_source`, it accepts a `:branch` named argument. + +### PATH + +You can specify that a gem is located in a particular location +on the file system. Relative paths are resolved relative to the +directory containing the `Gemfile`. + +Similar to the semantics of the `:git` option, the `:path` +option requires that the directory in question either contains +a `.gemspec` for the gem, or that you specify an explicit +version that bundler should use. + +Unlike `:git`, bundler does not compile C extensions for +gems specified as paths. + + gem "rails", :path => "vendor/rails" + +If you would like to use multiple local gems directly from the filesystem, you can set a global `path` option to the path containing the gem's files. This will automatically load gemspec files from subdirectories. + + path 'components' do + gem 'admin_ui' + gem 'public_ui' + end + +## BLOCK FORM OF SOURCE, GIT, PATH, GROUP and PLATFORMS + +The `:source`, `:git`, `:path`, `:group`, and `:platforms` options may be +applied to a group of gems by using block form. + + source "https://gems.example.com" do + gem "some_internal_gem" + gem "another_internal_gem" + end + + git "https://github.com/rails/rails.git" do + gem "activesupport" + gem "actionpack" + end + + platforms :ruby do + gem "ruby-debug" + gem "sqlite3" + end + + group :development, :optional => true do + gem "wirble" + gem "faker" + end + +In the case of the group block form the :optional option can be given +to prevent a group from being installed unless listed in the `--with` +option given to the `bundle install` command. + +In the case of the `git` block form, the `:ref`, `:branch`, `:tag`, +and `:submodules` options may be passed to the `git` method, and +all gems in the block will inherit those options. + +The presence of a `source` block in a Gemfile also makes that source +available as a possible global source for any other gems which do not specify +explicit sources. Thus, when defining source blocks, it is +recommended that you also ensure all other gems in the Gemfile are using +explicit sources, either via source blocks or `:source` directives on +individual gems. + +## INSTALL_IF + +The `install_if` method allows gems to be installed based on a proc or lambda. +This is especially useful for optional gems that can only be used if certain +software is installed or some other conditions are met. + + install_if -> { RUBY_PLATFORM =~ /darwin/ } do + gem "pasteboard" + end + +## GEMSPEC + +The [`.gemspec`](http://guides.rubygems.org/specification-reference/) file is where + you provide metadata about your gem to Rubygems. Some required Gemspec + attributes include the name, description, and homepage of your gem. This is + also where you specify the dependencies your gem needs to run. + +If you wish to use Bundler to help install dependencies for a gem while it is +being developed, use the `gemspec` method to pull in the dependencies listed in +the `.gemspec` file. + +The `gemspec` method adds any runtime dependencies as gem requirements in the +default group. It also adds development dependencies as gem requirements in the +`development` group. Finally, it adds a gem requirement on your project (`:path +=> '.'`). In conjunction with `Bundler.setup`, this allows you to require project +files in your test code as you would if the project were installed as a gem; you +need not manipulate the load path manually or require project files via relative +paths. + +The `gemspec` method supports optional `:path`, `:glob`, `:name`, and `:development_group` +options, which control where bundler looks for the `.gemspec`, the glob it uses to look +for the gemspec (defaults to: "{,*,*/*}.gemspec"), what named `.gemspec` it uses +(if more than one is present), and which group development dependencies are included in. + +When a `gemspec` dependency encounters version conflicts during resolution, the +local version under development will always be selected -- even if there are +remote versions that better match other requirements for the `gemspec` gem. + +## SOURCE PRIORITY + +When attempting to locate a gem to satisfy a gem requirement, +bundler uses the following priority order: + + 1. The source explicitly attached to the gem (using `:source`, `:path`, or + `:git`) + 2. For implicit gems (dependencies of explicit gems), any source, git, or path + repository declared on the parent. This results in bundler prioritizing the + ActiveSupport gem from the Rails git repository over ones from + `rubygems.org` + 3. The sources specified via global `source` lines, searching each source in + your `Gemfile` from last added to first added. diff --git a/man/gemfile.5.txt b/man/gemfile.5.txt new file mode 100644 index 00000000000000..85ef7135d9f661 --- /dev/null +++ b/man/gemfile.5.txt @@ -0,0 +1,653 @@ +GEMFILE(5) GEMFILE(5) + + + +1mNAME0m + 1mGemfile 22m- A format for describing gem dependencies for Ruby programs + +1mSYNOPSIS0m + A 1mGemfile 22mdescribes the gem dependencies required to execute associated + Ruby code. + + Place the 1mGemfile 22min the root of the directory containing the associ- + ated code. For instance, in a Rails application, place the 1mGemfile 22min + the same directory as the 1mRakefile22m. + +1mSYNTAX0m + A 1mGemfile 22mis evaluated as Ruby code, in a context which makes available + a number of methods used to describe the gem requirements. + +1mGLOBAL SOURCES0m + At the top of the 1mGemfile22m, add a line for the 1mRubygems 22msource that con- + tains the gems listed in the 1mGemfile22m. + + + + source "https://rubygems.org" + + + + It is possible, but not recommended as of Bundler 1.7, to add multiple + global 1msource 22mlines. Each of these 1msource22ms 1mMUST 22mbe a valid Rubygems + repository. + + Sources are checked for gems following the heuristics described in + 4mSOURCE24m 4mPRIORITY24m. If a gem is found in more than one global source, + Bundler will print a warning after installing the gem indicating which + source was used, and listing the other sources where the gem is avail- + able. A specific source can be selected for gems that need to use a + non-standard repository, suppressing this warning, by using the 1m:source0m + option or a 1msource 22mblock. + + 1mCREDENTIALS0m + Some gem sources require a username and password. Use bundle config(1) + 4mbundle-config.1.html24m to set the username and password for any of the + sources that need it. The command must be run once on each computer + that will install the Gemfile, but this keeps the credentials from + being stored in plain text in version control. + + + + bundle config gems.example.com user:password + + + + For some sources, like a company Gemfury account, it may be easier to + include the credentials in the Gemfile as part of the source URL. + + + + source "https://user:password@gems.example.com" + + + + Credentials in the source URL will take precedence over credentials set + using 1mconfig22m. + +1mRUBY0m + If your application requires a specific Ruby version or engine, specify + your requirements using the 1mruby 22mmethod, with the following arguments. + All parameters are 1mOPTIONAL 22munless otherwise specified. + + 1mVERSION (required)0m + The version of Ruby that your application requires. If your application + requires an alternate Ruby engine, such as JRuby, Rubinius or Truf- + fleRuby, this should be the Ruby version that the engine is compatible + with. + + + + ruby "1.9.3" + + + + 1mENGINE0m + Each application 4mmay24m specify a Ruby engine. If an engine is specified, + an engine version 4mmust24m also be specified. + + What exactly is an Engine? - A Ruby engine is an implementation of the + Ruby language. + + o For background: the reference or original implementation of the + Ruby programming language is called Matz's Ruby Interpreter + 4mhttps://en.wikipedia.org/wiki/Ruby_MRI24m, or MRI for short. This is + named after Ruby creator Yukihiro Matsumoto, also known as Matz. + MRI is also known as CRuby, because it is written in C. MRI is the + most widely used Ruby engine. + + o Other implementations 4mhttps://www.ruby-lang.org/en/about/24m of Ruby + exist. Some of the more well-known implementations include Rubinius + 4mhttps://rubinius.com/24m, and JRuby 4mhttp://jruby.org/24m. Rubinius is an + alternative implementation of Ruby written in Ruby. JRuby is an + implementation of Ruby on the JVM, short for Java Virtual Machine. + + + + 1mENGINE VERSION0m + Each application 4mmay24m specify a Ruby engine version. If an engine ver- + sion is specified, an engine 4mmust24m also be specified. If the engine is + "ruby" the engine version specified 4mmust24m match the Ruby version. + + + + ruby "1.8.7", :engine => "jruby", :engine_version => "1.6.7" + + + + 1mPATCHLEVEL0m + Each application 4mmay24m specify a Ruby patchlevel. + + + + ruby "2.0.0", :patchlevel => "247" + + + +1mGEMS0m + Specify gem requirements using the 1mgem 22mmethod, with the following argu- + ments. All parameters are 1mOPTIONAL 22munless otherwise specified. + + 1mNAME (required)0m + For each gem requirement, list a single 4mgem24m line. + + + + gem "nokogiri" + + + + 1mVERSION0m + Each 4mgem24m 1mMAY 22mhave one or more version specifiers. + + + + gem "nokogiri", ">= 1.4.2" + gem "RedCloth", ">= 4.1.0", "< 4.2.0" + + + + 1mREQUIRE AS0m + Each 4mgem24m 1mMAY 22mspecify files that should be used when autorequiring via + 1mBundler.require22m. You may pass an array with multiple files or 1mtrue 22mif + file you want 1mrequired 22mhas same name as 4mgem24m or 1mfalse 22mto prevent any + file from being autorequired. + + + + gem "redis", :require => ["redis/connection/hiredis", "redis"] + gem "webmock", :require => false + gem "debugger", :require => true + + + + The argument defaults to the name of the gem. For example, these are + identical: + + + + gem "nokogiri" + gem "nokogiri", :require => "nokogiri" + gem "nokogiri", :require => true + + + + 1mGROUPS0m + Each 4mgem24m 1mMAY 22mspecify membership in one or more groups. Any 4mgem24m that + does not specify membership in any group is placed in the 1mdefault0m + group. + + + + gem "rspec", :group => :test + gem "wirble", :groups => [:development, :test] + + + + The Bundler runtime allows its two main methods, 1mBundler.setup 22mand + 1mBundler.require22m, to limit their impact to particular groups. + + + + # setup adds gems to Ruby's load path + Bundler.setup # defaults to all groups + require "bundler/setup" # same as Bundler.setup + Bundler.setup(:default) # only set up the _default_ group + Bundler.setup(:test) # only set up the _test_ group (but `not` _default_) + Bundler.setup(:default, :test) # set up the _default_ and _test_ groups, but no others + + # require requires all of the gems in the specified groups + Bundler.require # defaults to the _default_ group + Bundler.require(:default) # identical + Bundler.require(:default, :test) # requires the _default_ and _test_ groups + Bundler.require(:test) # requires the _test_ group + + + + The Bundler CLI allows you to specify a list of groups whose gems 1mbun-0m + 1mdle install 22mshould not install with the 1m--without 22moption. To specify + multiple groups to ignore, specify a list of groups separated by spa- + ces. + + + + bundle install --without test + bundle install --without development test + + + + After running 1mbundle install --without test22m, bundler will remember that + you excluded the test group in the last installation. The next time you + run 1mbundle install22m, without any 1m--without option22m, bundler will recall + it. + + Also, calling 1mBundler.setup 22mwith no parameters, or calling 1mrequire0m + 1m"bundler/setup" 22mwill setup all groups except for the ones you excluded + via 1m--without 22m(since they are not available). + + Note that on 1mbundle install22m, bundler downloads and evaluates all gems, + in order to create a single canonical list of all of the required gems + and their dependencies. This means that you cannot list different ver- + sions of the same gems in different groups. For more details, see + Understanding Bundler 4mhttp://bundler.io/rationale.html24m. + + 1mPLATFORMS0m + If a gem should only be used in a particular platform or set of plat- + forms, you can specify them. Platforms are essentially identical to + groups, except that you do not need to use the 1m--without 22minstall-time + flag to exclude groups of gems for other platforms. + + There are a number of 1mGemfile 22mplatforms: + + 1mruby 22mC Ruby (MRI), Rubinius or TruffleRuby, but 1mNOT 22mWindows + + 1mmri 22mSame as 4mruby24m, but only C Ruby (MRI) + + 1mmingw 22mWindows 32 bit 'mingw32' platform (aka RubyInstaller) + + 1mx64_mingw0m + Windows 64 bit 'mingw32' platform (aka RubyInstaller x64) + + 1mrbx 22mRubinius + + 1mjruby 22mJRuby + + 1mtruffleruby0m + TruffleRuby + + 1mmswin 22mWindows + + You can restrict further by platform and version for all platforms + 4mexcept24m for 1mrbx22m, 1mjruby22m, 1mtruffleruby 22mand 1mmswin22m. + + To specify a version in addition to a platform, append the version num- + ber without the delimiter to the platform. For example, to specify that + a gem should only be used on platforms with Ruby 2.3, use: + + + + ruby_23 + + + + The full list of platforms and supported versions includes: + + 1mruby 22m1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + + 1mmri 22m1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + + 1mmingw 22m1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + + 1mx64_mingw0m + 2.0, 2.1, 2.2, 2.3, 2.4, 2.5 + + As with groups, you can specify one or more platforms: + + + + gem "weakling", :platforms => :jruby + gem "ruby-debug", :platforms => :mri_18 + gem "nokogiri", :platforms => [:mri_18, :jruby] + + + + All operations involving groups (1mbundle install 4m22mbundle-install.1.html24m, + 1mBundler.setup22m, 1mBundler.require22m) behave exactly the same as if any + groups not matching the current platform were explicitly excluded. + + 1mSOURCE0m + You can select an alternate Rubygems repository for a gem using the + ':source' option. + + + + gem "some_internal_gem", :source => "https://gems.example.com" + + + + This forces the gem to be loaded from this source and ignores any + global sources declared at the top level of the file. If the gem does + not exist in this source, it will not be installed. + + Bundler will search for child dependencies of this gem by first looking + in the source selected for the parent, but if they are not found there, + it will fall back on global sources using the ordering described in + 4mSOURCE24m 4mPRIORITY24m. + + Selecting a specific source repository this way also suppresses the + ambiguous gem warning described above in 4mGLOBAL24m 4mSOURCES24m 4m(#source)24m. + + Using the 1m:source 22moption for an individual gem will also make that + source available as a possible global source for any other gems which + do not specify explicit sources. Thus, when adding gems with explicit + sources, it is recommended that you also ensure all other gems in the + Gemfile are using explicit sources. + + 1mGIT0m + If necessary, you can specify that a gem is located at a particular git + repository using the 1m:git 22mparameter. The repository can be accessed via + several protocols: + + 1mHTTP(S)0m + gem "rails", :git => "https://github.com/rails/rails.git" + + 1mSSH 22mgem "rails", :git => "git@github.com:rails/rails.git" + + 1mgit 22mgem "rails", :git => "git://github.com/rails/rails.git" + + If using SSH, the user that you use to run 1mbundle install MUST 22mhave the + appropriate keys available in their 1m$HOME/.ssh22m. + + 1mNOTE22m: 1mhttp:// 22mand 1mgit:// 22mURLs should be avoided if at all possible. + These protocols are unauthenticated, so a man-in-the-middle attacker + can deliver malicious code and compromise your system. HTTPS and SSH + are strongly preferred. + + The 1mgroup22m, 1mplatforms22m, and 1mrequire 22moptions are available and behave + exactly the same as they would for a normal gem. + + A git repository 1mSHOULD 22mhave at least one file, at the root of the + directory containing the gem, with the extension 1m.gemspec22m. This file + 1mMUST 22mcontain a valid gem specification, as expected by the 1mgem build0m + command. + + If a git repository does not have a 1m.gemspec22m, bundler will attempt to + create one, but it will not contain any dependencies, executables, or C + extension compilation instructions. As a result, it may fail to prop- + erly integrate into your application. + + If a git repository does have a 1m.gemspec 22mfor the gem you attached it + to, a version specifier, if provided, means that the git repository is + only valid if the 1m.gemspec 22mspecifies a version matching the version + specifier. If not, bundler will print a warning. + + + + gem "rails", "2.3.8", :git => "https://github.com/rails/rails.git" + # bundle install will fail, because the .gemspec in the rails + # repository's master branch specifies version 3.0.0 + + + + If a git repository does 1mnot 22mhave a 1m.gemspec 22mfor the gem you attached + it to, a version specifier 1mMUST 22mbe provided. Bundler will use this ver- + sion in the simple 1m.gemspec 22mit creates. + + Git repositories support a number of additional options. + + 1mbranch22m, 1mtag22m, and 1mref0m + You 1mMUST 22monly specify at most one of these options. The default + is 1m:branch => "master"0m + + For example: + + git "https://github.com/rails/rails.git", :branch => "5-0-sta- + ble" do + + git "https://github.com/rails/rails.git", :tag => "v5.0.0" do + + git "https://github.com/rails/rails.git", :ref => "4aded" do + + 1msubmodules0m + For reference, a git submodule + 4mhttps://git-scm.com/book/en/v2/Git-Tools-Submodules24m lets you + have another git repository within a subfolder of your reposi- + tory. Specify 1m:submodules => true 22mto cause bundler to expand any + submodules included in the git repository + + If a git repository contains multiple 1m.gemspecs22m, each 1m.gemspec 22mrepre- + sents a gem located at the same place in the file system as the 1m.gem-0m + 1mspec22m. + + + + |~rails [git root] + | |-rails.gemspec [rails gem located here] + |~actionpack + | |-actionpack.gemspec [actionpack gem located here] + |~activesupport + | |-activesupport.gemspec [activesupport gem located here] + |... + + + + To install a gem located in a git repository, bundler changes to the + directory containing the gemspec, runs 1mgem build name.gemspec 22mand then + installs the resulting gem. The 1mgem build 22mcommand, which comes standard + with Rubygems, evaluates the 1m.gemspec 22min the context of the directory + in which it is located. + + 1mGIT SOURCE0m + A custom git source can be defined via the 1mgit_source 22mmethod. Provide + the source's name as an argument, and a block which receives a single + argument and interpolates it into a string to return the full repo + address: + + + + git_source(:stash){ |repo_name| "https://stash.corp.acme.pl/#{repo_name}.git" } + gem 'rails', :stash => 'forks/rails' + + + + In addition, if you wish to choose a specific branch: + + + + gem "rails", :stash => "forks/rails", :branch => "branch_name" + + + + 1mGITHUB0m + 1mNOTE22m: This shorthand should be avoided until Bundler 2.0, since it cur- + rently expands to an insecure 1mgit:// 22mURL. This allows a man-in-the-mid- + dle attacker to compromise your system. + + If the git repository you want to use is hosted on GitHub and is pub- + lic, you can use the :github shorthand to specify the github username + and repository name (without the trailing ".git"), separated by a + slash. If both the username and repository name are the same, you can + omit one. + + + + gem "rails", :github => "rails/rails" + gem "rails", :github => "rails" + + + + Are both equivalent to + + + + gem "rails", :git => "git://github.com/rails/rails.git" + + + + Since the 1mgithub 22mmethod is a specialization of 1mgit_source22m, it accepts a + 1m:branch 22mnamed argument. + + 1mGIST0m + If the git repository you want to use is hosted as a Github Gist and is + public, you can use the :gist shorthand to specify the gist identifier + (without the trailing ".git"). + + + + gem "the_hatch", :gist => "4815162342" + + + + Is equivalent to: + + + + gem "the_hatch", :git => "https://gist.github.com/4815162342.git" + + + + Since the 1mgist 22mmethod is a specialization of 1mgit_source22m, it accepts a + 1m:branch 22mnamed argument. + + 1mBITBUCKET0m + If the git repository you want to use is hosted on Bitbucket and is + public, you can use the :bitbucket shorthand to specify the bitbucket + username and repository name (without the trailing ".git"), separated + by a slash. If both the username and repository name are the same, you + can omit one. + + + + gem "rails", :bitbucket => "rails/rails" + gem "rails", :bitbucket => "rails" + + + + Are both equivalent to + + + + gem "rails", :git => "https://rails@bitbucket.org/rails/rails.git" + + + + Since the 1mbitbucket 22mmethod is a specialization of 1mgit_source22m, it + accepts a 1m:branch 22mnamed argument. + + 1mPATH0m + You can specify that a gem is located in a particular location on the + file system. Relative paths are resolved relative to the directory con- + taining the 1mGemfile22m. + + Similar to the semantics of the 1m:git 22moption, the 1m:path 22moption requires + that the directory in question either contains a 1m.gemspec 22mfor the gem, + or that you specify an explicit version that bundler should use. + + Unlike 1m:git22m, bundler does not compile C extensions for gems specified + as paths. + + + + gem "rails", :path => "vendor/rails" + + + + If you would like to use multiple local gems directly from the filesys- + tem, you can set a global 1mpath 22moption to the path containing the gem's + files. This will automatically load gemspec files from subdirectories. + + + + path 'components' do + gem 'admin_ui' + gem 'public_ui' + end + + + +1mBLOCK FORM OF SOURCE, GIT, PATH, GROUP and PLATFORMS0m + The 1m:source22m, 1m:git22m, 1m:path22m, 1m:group22m, and 1m:platforms 22moptions may be applied + to a group of gems by using block form. + + + + source "https://gems.example.com" do + gem "some_internal_gem" + gem "another_internal_gem" + end + + git "https://github.com/rails/rails.git" do + gem "activesupport" + gem "actionpack" + end + + platforms :ruby do + gem "ruby-debug" + gem "sqlite3" + end + + group :development, :optional => true do + gem "wirble" + gem "faker" + end + + + + In the case of the group block form the :optional option can be given + to prevent a group from being installed unless listed in the 1m--with0m + option given to the 1mbundle install 22mcommand. + + In the case of the 1mgit 22mblock form, the 1m:ref22m, 1m:branch22m, 1m:tag22m, and 1m:sub-0m + 1mmodules 22moptions may be passed to the 1mgit 22mmethod, and all gems in the + block will inherit those options. + + The presence of a 1msource 22mblock in a Gemfile also makes that source + available as a possible global source for any other gems which do not + specify explicit sources. Thus, when defining source blocks, it is rec- + ommended that you also ensure all other gems in the Gemfile are using + explicit sources, either via source blocks or 1m:source 22mdirectives on + individual gems. + +1mINSTALL_IF0m + The 1minstall_if 22mmethod allows gems to be installed based on a proc or + lambda. This is especially useful for optional gems that can only be + used if certain software is installed or some other conditions are met. + + + + install_if -> { RUBY_PLATFORM =~ /darwin/ } do + gem "pasteboard" + end + + + +1mGEMSPEC0m + The 1m.gemspec 4m22mhttp://guides.rubygems.org/specification-reference/24m file + is where you provide metadata about your gem to Rubygems. Some required + Gemspec attributes include the name, description, and homepage of your + gem. This is also where you specify the dependencies your gem needs to + run. + + If you wish to use Bundler to help install dependencies for a gem while + it is being developed, use the 1mgemspec 22mmethod to pull in the dependen- + cies listed in the 1m.gemspec 22mfile. + + The 1mgemspec 22mmethod adds any runtime dependencies as gem requirements in + the default group. It also adds development dependencies as gem + requirements in the 1mdevelopment 22mgroup. Finally, it adds a gem require- + ment on your project (1m:path => '.'22m). In conjunction with 1mBundler.setup22m, + this allows you to require project files in your test code as you would + if the project were installed as a gem; you need not manipulate the + load path manually or require project files via relative paths. + + The 1mgemspec 22mmethod supports optional 1m:path22m, 1m:glob22m, 1m:name22m, and 1m:develop-0m + 1mment_group 22moptions, which control where bundler looks for the 1m.gemspec22m, + the glob it uses to look for the gemspec (defaults to: "{,4m,24m/*}.gem- + spec"), what named 1m.gemspec 22mit uses (if more than one is present), and + which group development dependencies are included in. + + When a 1mgemspec 22mdependency encounters version conflicts during resolu- + tion, the local version under development will always be selected -- + even if there are remote versions that better match other requirements + for the 1mgemspec 22mgem. + +1mSOURCE PRIORITY0m + When attempting to locate a gem to satisfy a gem requirement, bundler + uses the following priority order: + + 1. The source explicitly attached to the gem (using 1m:source22m, 1m:path22m, or + 1m:git22m) + + 2. For implicit gems (dependencies of explicit gems), any source, git, + or path repository declared on the parent. This results in bundler + prioritizing the ActiveSupport gem from the Rails git repository + over ones from 1mrubygems.org0m + + 3. The sources specified via global 1msource 22mlines, searching each + source in your 1mGemfile 22mfrom last added to first added. + + + + + + + November 2018 GEMFILE(5) diff --git a/marshal.c b/marshal.c index 06184001146804..73d36e2a32988d 100644 --- a/marshal.c +++ b/marshal.c @@ -401,8 +401,8 @@ w_float(double d, struct dump_arg *arg) w_cstr("nan", arg); } else if (d == 0.0) { - if (1.0/d < 0) w_cstr("-0", arg); - else w_cstr("0", arg); + if (signbit(d)) w_cstr("-0", arg); + else w_cstr("0", arg); } else { int decpt, sign, digs, len = 0; @@ -817,6 +817,7 @@ w_object(VALUE obj, struct dump_arg *arg, int limit) char sign = BIGNUM_SIGN(obj) ? '+' : '-'; size_t len = BIGNUM_LEN(obj); size_t slen; + size_t j; BDIGIT *d = BIGNUM_DIGITS(obj); slen = SHORTLEN(len); @@ -826,7 +827,7 @@ w_object(VALUE obj, struct dump_arg *arg, int limit) w_byte(sign, arg); w_long((long)slen, arg); - while (len--) { + for (j = 0; j < len; j++) { #if SIZEOF_BDIGIT > SIZEOF_SHORT BDIGIT num = *d; int i; @@ -834,7 +835,7 @@ w_object(VALUE obj, struct dump_arg *arg, int limit) for (i=0; i.*/ruby" nil))) + (error)) + nil) (c-set-style "ruby"))) (provide 'ruby-style) diff --git a/mjit.c b/mjit.c index 3cb8f1daf1e563..1f9140b7a838ab 100644 --- a/mjit.c +++ b/mjit.c @@ -24,26 +24,33 @@ static void mjit_copy_job_handler(void *data) { - struct mjit_copy_job *job; - if (stop_worker_p) { - /* `copy_cache_from_main_thread()` stops to wait for this job. Then job - data which is allocated by `alloca()` could be expired and we might - not be able to access that. */ + mjit_copy_job_t *job = data; + const struct rb_iseq_constant_body *body; + if (stop_worker_p) { /* check if mutex is still alive, before calling CRITICAL_SECTION_START. */ return; } - job = (struct mjit_copy_job *)data; + CRITICAL_SECTION_START(3, "in mjit_copy_job_handler"); + /* Make sure that this job is never executed when: + 1. job is being modified + 2. alloca memory inside job is expired + 3. ISeq is GC-ed */ + if (job->finish_p || job->unit->iseq == NULL) { + CRITICAL_SECTION_FINISH(3, "in mjit_copy_job_handler"); + return; + } + + body = job->unit->iseq->body; if (job->cc_entries) { - memcpy(job->cc_entries, job->body->cc_entries, sizeof(struct rb_call_cache) * (job->body->ci_size + job->body->ci_kw_size)); + memcpy(job->cc_entries, body->cc_entries, sizeof(struct rb_call_cache) * (body->ci_size + body->ci_kw_size)); } if (job->is_entries) { - memcpy(job->is_entries, job->body->is_entries, sizeof(union iseq_inline_storage_entry) * job->body->is_size); + memcpy(job->is_entries, body->is_entries, sizeof(union iseq_inline_storage_entry) * body->is_size); } - CRITICAL_SECTION_START(3, "in MJIT copy job wait"); job->finish_p = TRUE; rb_native_cond_broadcast(&mjit_worker_wakeup); - CRITICAL_SECTION_FINISH(3, "in MJIT copy job wait"); + CRITICAL_SECTION_FINISH(3, "in mjit_copy_job_handler"); } extern int rb_thread_create_mjit_thread(void (*worker_func)(void)); @@ -119,7 +126,7 @@ mjit_free_iseq(const rb_iseq_t *iseq) static void init_list(struct rb_mjit_unit_list *list) { - list->head = NULL; + list_head_init(&list->head); list->length = 0; } @@ -127,13 +134,14 @@ init_list(struct rb_mjit_unit_list *list) because node of unit_queue and one of active_units may have the same unit during proceeding unit. */ static void -free_list(struct rb_mjit_unit_list *list) +free_list(struct rb_mjit_unit_list *list, int close_handle_p) { - struct rb_mjit_unit_node *node, *next; - for (node = list->head; node != NULL; node = next) { - next = node->next; - free_unit(node->unit); - xfree(node); + struct rb_mjit_unit *unit = 0, *next; + + list_for_each_safe(&list->head, unit, next, unode) { + list_del(&unit->unode); + if (!close_handle_p) unit->handle = NULL; /* Skip dlclose in free_unit() */ + free_unit(unit); } list->length = 0; } @@ -248,24 +256,23 @@ unload_units(void) { rb_vm_t *vm = GET_THREAD()->vm; rb_thread_t *th = NULL; - struct rb_mjit_unit_node *node, *next, *worst_node; + struct rb_mjit_unit *unit = 0, *next, *worst; struct mjit_cont *cont; int delete_num, units_num = active_units.length; /* For now, we don't unload units when ISeq is GCed. We should unload such ISeqs first here. */ - for (node = active_units.head; node != NULL; node = next) { - next = node->next; - if (node->unit->iseq == NULL) { /* ISeq is GCed. */ - free_unit(node->unit); - remove_from_list(node, &active_units); + list_for_each_safe(&active_units.head, unit, next, unode) { + if (unit->iseq == NULL) { /* ISeq is GCed. */ + remove_from_list(unit, &active_units); + free_unit(unit); } } /* Detect units which are in use and can't be unloaded. */ - for (node = active_units.head; node != NULL; node = node->next) { - assert(node->unit != NULL && node->unit->iseq != NULL && node->unit->handle != NULL); - node->unit->used_code_p = FALSE; + list_for_each(&active_units.head, unit, unode) { + assert(unit->iseq != NULL && unit->handle != NULL); + unit->used_code_p = FALSE; } list_for_each(&vm->living_threads, th, vmlt_node) { mark_ec_units(th->ec); @@ -280,23 +287,23 @@ unload_units(void) delete_num = active_units.length / 10; for (; active_units.length > mjit_opts.max_cache_size - delete_num;) { /* Find one unit that has the minimum total_calls. */ - worst_node = NULL; - for (node = active_units.head; node != NULL; node = node->next) { - if (node->unit->used_code_p) /* We can't unload code on stack. */ + worst = NULL; + list_for_each(&active_units.head, unit, unode) { + if (unit->used_code_p) /* We can't unload code on stack. */ continue; - if (worst_node == NULL || worst_node->unit->iseq->body->total_calls > node->unit->iseq->body->total_calls) { - worst_node = node; + if (worst == NULL || worst->iseq->body->total_calls > unit->iseq->body->total_calls) { + worst = unit; } } - if (worst_node == NULL) + if (worst == NULL) break; /* Unload the worst node. */ - verbose(2, "Unloading unit %d (calls=%lu)", worst_node->unit->id, worst_node->unit->iseq->body->total_calls); - assert(worst_node->unit->handle != NULL); - free_unit(worst_node->unit); - remove_from_list(worst_node, &active_units); + verbose(2, "Unloading unit %d (calls=%lu)", worst->id, worst->iseq->body->total_calls); + assert(worst->handle != NULL); + remove_from_list(worst, &active_units); + free_unit(worst); } verbose(1, "Too many JIT code -- %d units unloaded", units_num - active_units.length); } @@ -306,8 +313,6 @@ unload_units(void) void mjit_add_iseq_to_process(const rb_iseq_t *iseq) { - struct rb_mjit_unit_node *node; - if (!mjit_enabled || pch_status == PCH_FAILED) return; @@ -317,14 +322,8 @@ mjit_add_iseq_to_process(const rb_iseq_t *iseq) /* Failure in creating the unit. */ return; - node = create_list_node(iseq->body->jit_unit); - if (node == NULL) { - mjit_warning("failed to allocate a node to be added to unit_queue"); - return; - } - CRITICAL_SECTION_START(3, "in add_iseq_to_process"); - add_to_list(node, &unit_queue); + add_to_list(iseq->body->jit_unit, &unit_queue); if (active_units.length >= mjit_opts.max_cache_size) { unload_units(); } @@ -389,6 +388,10 @@ init_header_filename(void) ; const size_t libpathflag_len = sizeof(libpathflag) - 1; #endif +#ifndef LOAD_RELATIVE + const char *build_dir = 0; + struct stat st; +#endif basedir_val = ruby_prefix_path; basedir = StringValuePtr(basedir_val); @@ -399,8 +402,31 @@ init_header_filename(void) /* This path is not intended to be used on production, but using build directory's header file here because people want to run `make test-all` without running `make install`. Don't use $MJIT_SEARCH_BUILD_DIR except for test-all. */ - basedir = MJIT_BUILD_DIR; - baselen = strlen(basedir); + + build_dir = dlsym(RTLD_DEFAULT, "MJIT_BUILD_DIR"); + if (!build_dir) { + verbose(1, "No mjit_build_directory"); + } + else if (build_dir[0] != '/') { + verbose(1, "Non-absolute path MJIT_BUILD_DIR: %s", build_dir); + } + else if (stat(build_dir, &st) || !S_ISDIR(st.st_mode)) { + verbose(1, "Non-directory path MJIT_BUILD_DIR: %s", build_dir); + } + else if (!rb_path_check(build_dir)) { + verbose(1, "Unsafe MJIT_BUILD_DIR: %s", build_dir); + return FALSE; + } + else { + /* Do not pass PRELOADENV to child processes, on + * multi-arch environment */ + verbose(3, "PRELOADENV("PRELOADENV")=%s", getenv(PRELOADENV)); + /* assume no other PRELOADENV in test-all */ + unsetenv(PRELOADENV); + verbose(3, "MJIT_BUILD_DIR: %s", build_dir); + basedir = build_dir; + baselen = strlen(build_dir); + } } #endif @@ -419,6 +445,21 @@ init_header_filename(void) header_file = NULL; return FALSE; } +#ifndef LOAD_RELATIVE + if (basedir == build_dir) { + memset(&st, 0, sizeof(st)); + if (fstat(fd, &st) || + (st.st_uid != getuid()) || + (st.st_mode & 022)) { + (void)close(fd); + verbose(1, "Unsafe header file: uid=%ld mode=%#o %s", + (long)st.st_uid, (unsigned)st.st_mode, header_file); + xfree(header_file); + header_file = NULL; + return FALSE; + } + } +#endif (void)close(fd); } @@ -456,18 +497,6 @@ init_header_filename(void) return TRUE; } -/* This is called after each fork in the child in to switch off MJIT - engine in the child as it does not inherit MJIT threads. */ -void -mjit_child_after_fork(void) -{ - if (mjit_enabled) { - verbose(3, "Switching off MJIT in a forked child"); - mjit_enabled = FALSE; - } - /* TODO: Should we initiate MJIT in the forked Ruby. */ -} - static enum rb_id_table_iterator_result valid_class_serials_add_i(ID key, VALUE v, void *unused) { @@ -621,6 +650,7 @@ mjit_init(struct mjit_options *opts) verbose(1, "Failure in MJIT header file name initialization\n"); return; } + pch_owner_pid = getpid(); init_list(&unit_queue); init_list(&active_units); @@ -652,10 +682,10 @@ stop_worker(void) { rb_execution_context_t *ec = GET_EC(); - stop_worker_p = TRUE; while (!worker_stopped) { verbose(3, "Sending cancel signal to worker"); CRITICAL_SECTION_START(3, "in stop_worker"); + stop_worker_p = TRUE; /* Setting this inside loop because RUBY_VM_CHECK_INTS may make this FALSE. */ rb_native_cond_broadcast(&mjit_worker_wakeup); CRITICAL_SECTION_FINISH(3, "in stop_worker"); RUBY_VM_CHECK_INTS(ec); @@ -708,11 +738,62 @@ mjit_resume(void) return Qtrue; } +/* Skip calling `clean_object_files` for units which currently exist in the list. */ +static void +skip_cleaning_object_files(struct rb_mjit_unit_list *list) +{ + struct rb_mjit_unit *unit = NULL, *next; + + /* No mutex for list, assuming MJIT worker does not exist yet since it's immediately after fork. */ + list_for_each_safe(&list->head, unit, next, unode) { +#ifndef _MSC_VER /* Actually mswin does not reach here since it doesn't have fork */ + if (unit->o_file) unit->o_file_inherited_p = TRUE; +#endif + +#if defined(_WIN32) /* mswin doesn't reach here either. This is for MinGW. */ + if (unit->so_file) unit->so_file = NULL; +#endif + } +} + +/* This is called after fork initiated by Ruby's method to launch MJIT worker thread + for child Ruby process. + + In multi-process Ruby applications, child Ruby processes do most of the jobs. + Thus we want child Ruby processes to enqueue ISeqs to MJIT worker's queue and + call the JIT-ed code. + + But unfortunately current MJIT-generated code is process-specific. After the fork, + JIT-ed code created by parent Ruby process cannot be used in child Ruby process + because the code could rely on inline cache values (ivar's IC, send's CC) which + may vary between processes after fork or embed some process-specific addresses. + + So child Ruby process can't request parent process to JIT an ISeq and use the code. + Instead of that, MJIT worker thread is created for all child Ruby processes, even + while child processes would end up with compiling the same ISeqs. + */ +void +mjit_child_after_fork(void) +{ + if (!mjit_enabled) + return; + + /* Let parent process delete the already-compiled object files. + This must be done before starting MJIT worker on child process. */ + skip_cleaning_object_files(&active_units); + + /* MJIT worker thread is not inherited on fork. Start it for this child process. */ + start_worker(); +} + /* Finish the threads processing units and creating PCH, finalize and free MJIT data. It should be called last during MJIT - life. */ + life. + + If close_handle_p is TRUE, it calls dlclose() for JIT-ed code. So it should be FALSE + if the code can still be on stack. ...But it means to leak JIT-ed handle forever (FIXME). */ void -mjit_finish(void) +mjit_finish(int close_handle_p) { if (!mjit_enabled) return; @@ -741,7 +822,7 @@ mjit_finish(void) rb_native_cond_destroy(&mjit_gc_wakeup); #ifndef _MSC_VER /* mswin has prebuilt precompiled header */ - if (!mjit_opts.save_temps) + if (!mjit_opts.save_temps && getpid() == pch_owner_pid) remove_file(pch_file); xfree(header_file); header_file = NULL; @@ -750,9 +831,9 @@ mjit_finish(void) xfree(pch_file); pch_file = NULL; mjit_call_p = FALSE; - free_list(&unit_queue); - free_list(&active_units); - free_list(&compact_units); + free_list(&unit_queue, close_handle_p); + free_list(&active_units, close_handle_p); + free_list(&compact_units, close_handle_p); finish_conts(); mjit_enabled = FALSE; @@ -762,14 +843,14 @@ mjit_finish(void) void mjit_mark(void) { - struct rb_mjit_unit_node *node; + struct rb_mjit_unit *unit = 0; if (!mjit_enabled) return; RUBY_MARK_ENTER("mjit"); CRITICAL_SECTION_START(4, "mjit_mark"); - for (node = unit_queue.head; node != NULL; node = node->next) { - if (node->unit->iseq) { /* ISeq is still not GCed */ - VALUE iseq = (VALUE)node->unit->iseq; + list_for_each(&unit_queue.head, unit, unode) { + if (unit->iseq) { /* ISeq is still not GCed */ + VALUE iseq = (VALUE)unit->iseq; CRITICAL_SECTION_FINISH(4, "mjit_mark rb_gc_mark"); /* Don't wrap critical section with this. This may trigger GC, diff --git a/mjit.h b/mjit.h index 325fe555a7a178..3aabf514ac9060 100644 --- a/mjit.h +++ b/mjit.h @@ -66,7 +66,8 @@ RUBY_SYMBOL_EXPORT_END extern int mjit_compile(FILE *f, const struct rb_iseq_constant_body *body, const char *funcname, struct rb_call_cache *cc_entries, union iseq_inline_storage_entry *is_entries); extern void mjit_init(struct mjit_options *opts); -extern void mjit_finish(void); +extern void mjit_postponed_job_register_start_hook(void); +extern void mjit_postponed_job_register_finish_hook(void); extern void mjit_gc_start_hook(void); extern void mjit_gc_finish_hook(void); extern void mjit_free_iseq(const rb_iseq_t *iseq); @@ -132,7 +133,8 @@ void mjit_child_after_fork(void); #else /* USE_MJIT */ static inline struct mjit_cont *mjit_cont_new(rb_execution_context_t *ec){return NULL;} static inline void mjit_cont_free(struct mjit_cont *cont){} -static inline void mjit_finish(void){} +static inline void mjit_postponed_job_register_start_hook(void){} +static inline void mjit_postponed_job_register_finish_hook(void){} static inline void mjit_gc_start_hook(void){} static inline void mjit_gc_finish_hook(void){} static inline void mjit_free_iseq(const rb_iseq_t *iseq){} diff --git a/mjit_worker.c b/mjit_worker.c index 18957cf373c9a5..90aaf9d9a9ccbd 100644 --- a/mjit_worker.c +++ b/mjit_worker.c @@ -132,6 +132,10 @@ struct rb_mjit_unit { #ifndef _MSC_VER /* This value is always set for `compact_all_jit_code`. Also used for lazy deletion. */ char *o_file; + /* TRUE if it's inherited from parent Ruby process and lazy deletion should be skipped. + `o_file = NULL` can't be used to skip lazy deletion because `o_file` could be used + by child for `compact_all_jit_code`. */ + int o_file_inherited_p; #endif #if defined(_WIN32) /* DLL cannot be removed while loaded on Windows. If this is set, it'll be lazily deleted. */ @@ -139,18 +143,12 @@ struct rb_mjit_unit { #endif /* Only used by unload_units. Flag to check this unit is currently on stack or not. */ char used_code_p; -}; - -/* Node of linked list in struct rb_mjit_unit_list. - TODO: use ccan/list for this */ -struct rb_mjit_unit_node { - struct rb_mjit_unit *unit; - struct rb_mjit_unit_node *next, *prev; + struct list_node unode; }; /* Linked list of struct rb_mjit_unit. */ struct rb_mjit_unit_list { - struct rb_mjit_unit_node *head; + struct list_head head; int length; /* the list length */ }; @@ -175,17 +173,17 @@ struct mjit_options mjit_opts; /* TRUE if MJIT is enabled. */ int mjit_enabled = FALSE; -/* TRUE if JIT-ed code should be called. When `ruby_vm_event_enabled_flags & ISEQ_TRACE_EVENTS` +/* TRUE if JIT-ed code should be called. When `ruby_vm_event_enabled_global_flags & ISEQ_TRACE_EVENTS` and `mjit_call_p == FALSE`, any JIT-ed code execution is cancelled as soon as possible. */ int mjit_call_p = FALSE; /* Priority queue of iseqs waiting for JIT compilation. This variable is a pointer to head unit of the queue. */ -static struct rb_mjit_unit_list unit_queue; +static struct rb_mjit_unit_list unit_queue = { LIST_HEAD_INIT(unit_queue.head) }; /* List of units which are successfully compiled. */ -static struct rb_mjit_unit_list active_units; -/* List of compacted so files which will be deleted in `mjit_finish()`. */ -static struct rb_mjit_unit_list compact_units; +static struct rb_mjit_unit_list active_units = { LIST_HEAD_INIT(active_units.head) }; +/* List of compacted so files which will be cleaned up by `free_list()` in `mjit_finish()`. */ +static struct rb_mjit_unit_list compact_units = { LIST_HEAD_INIT(compact_units.head) }; /* The number of so far processed ISEQs, used to generate unique id. */ static int current_unit_num; /* A mutex for conitionals and critical sections. */ @@ -219,6 +217,8 @@ static VALUE valid_class_serials; static const char *cc_path; /* Name of the precompiled header file. */ static char *pch_file; +/* The process id which should delete the pch_file on mjit_finish. */ +static rb_pid_t pch_owner_pid; /* Status of the precompiled header creation. The status is shared by the workers and the pch thread. */ static enum {PCH_NOT_READY, PCH_FAILED, PCH_SUCCESS} pch_status; @@ -318,57 +318,20 @@ mjit_warning(const char *format, ...) } } -/* Allocate struct rb_mjit_unit_node and return it. This MUST NOT be - called inside critical section because that causes deadlock. ZALLOC - may fire GC and GC hooks mjit_gc_start_hook that starts critical section. */ -static struct rb_mjit_unit_node * -create_list_node(struct rb_mjit_unit *unit) -{ - struct rb_mjit_unit_node *node = calloc(1, sizeof(struct rb_mjit_unit_node)); /* To prevent GC, don't use ZALLOC */ - if (node == NULL) return NULL; - node->unit = unit; - return node; -} - /* Add unit node to the tail of doubly linked LIST. It should be not in the list before. */ static void -add_to_list(struct rb_mjit_unit_node *node, struct rb_mjit_unit_list *list) +add_to_list(struct rb_mjit_unit *unit, struct rb_mjit_unit_list *list) { - /* Append iseq to list */ - if (list->head == NULL) { - list->head = node; - } - else { - struct rb_mjit_unit_node *tail = list->head; - while (tail->next != NULL) { - tail = tail->next; - } - tail->next = node; - node->prev = tail; - } + list_add_tail(&list->head, &unit->unode); list->length++; } static void -remove_from_list(struct rb_mjit_unit_node *node, struct rb_mjit_unit_list *list) +remove_from_list(struct rb_mjit_unit *unit, struct rb_mjit_unit_list *list) { - if (node->prev && node->next) { - node->prev->next = node->next; - node->next->prev = node->prev; - } - else if (node->prev == NULL && node->next) { - list->head = node->next; - node->next->prev = NULL; - } - else if (node->prev && node->next == NULL) { - node->prev->next = NULL; - } - else { - list->head = NULL; - } + list_del(&unit->unode); list->length--; - free(node); } static void @@ -390,7 +353,7 @@ clean_object_files(struct rb_mjit_unit *unit) unit->o_file = NULL; /* For compaction, unit->o_file is always set when compilation succeeds. So save_temps needs to be checked here. */ - if (!mjit_opts.save_temps) + if (!mjit_opts.save_temps && !unit->o_file_inherited_p) remove_file(o_file); free(o_file); } @@ -489,7 +452,7 @@ mjit_valid_class_serial_p(rb_serial_t class_serial) int found_p; CRITICAL_SECTION_START(3, "in valid_class_serial_p"); - found_p = st_lookup(RHASH_TBL_RAW(valid_class_serials), LONG2FIX(class_serial), NULL); + found_p = rb_hash_stlike_lookup(valid_class_serials, LONG2FIX(class_serial), NULL); CRITICAL_SECTION_FINISH(3, "in valid_class_serial_p"); return found_p; } @@ -497,28 +460,26 @@ mjit_valid_class_serial_p(rb_serial_t class_serial) /* Return the best unit from list. The best is the first high priority unit or the unit whose iseq has the biggest number of calls so far. */ -static struct rb_mjit_unit_node * +static struct rb_mjit_unit * get_from_list(struct rb_mjit_unit_list *list) { - struct rb_mjit_unit_node *node, *next, *best = NULL; - - if (list->head == NULL) - return NULL; + struct rb_mjit_unit *unit = NULL, *next, *best = NULL; /* Find iseq with max total_calls */ - for (node = list->head; node != NULL; node = next) { - next = node->next; - if (node->unit->iseq == NULL) { /* ISeq is GCed. */ - free_unit(node->unit); - remove_from_list(node, list); + list_for_each_safe(&list->head, unit, next, unode) { + if (unit->iseq == NULL) { /* ISeq is GCed. */ + remove_from_list(unit, list); + free_unit(unit); continue; } - if (best == NULL || best->unit->iseq->body->total_calls < node->unit->iseq->body->total_calls) { - best = node; + if (best == NULL || best->iseq->body->total_calls < unit->iseq->body->total_calls) { + best = unit; } } - + if (best) { + remove_from_list(best, list); + } return best; } @@ -566,11 +527,10 @@ COMPILER_WARNING_PUSH #ifdef __GNUC__ COMPILER_WARNING_IGNORED(-Wdeprecated-declarations) #endif -/* Start an OS process of executable PATH with arguments ARGV. Return - PID of the process. - TODO: Use the same function in process.c */ +/* Start an OS process of absolute executable path with arguments ARGV. + Return PID of the process. */ static pid_t -start_process(const char *path, char *const *argv) +start_process(const char *abspath, char *const *argv) { pid_t pid; /* @@ -578,18 +538,12 @@ start_process(const char *path, char *const *argv) * and execv for safety */ int dev_null = rb_cloexec_open(ruby_null_device, O_WRONLY, 0); - char fbuf[MAXPATHLEN]; - const char *abspath = dln_find_exe_r(path, 0, fbuf, sizeof(fbuf)); - if (!abspath) { - verbose(1, "MJIT: failed to find `%s' in PATH", path); - return -1; - } if (mjit_opts.verbose >= 2) { int i; const char *arg; - fprintf(stderr, "Starting process: %s", path); + fprintf(stderr, "Starting process: %s", abspath); for (i = 0; (arg = argv[i]) != NULL; i++) fprintf(stderr, " %s", arg); fprintf(stderr, "\n"); @@ -612,7 +566,7 @@ start_process(const char *path, char *const *argv) } } #else - if ((pid = vfork()) == 0) { + if ((pid = vfork()) == 0) { /* TODO: reuse some function in process.c */ umask(0077); if (mjit_opts.verbose == 0) { /* CC can be started in a thread using a file which has been @@ -881,8 +835,7 @@ static void compact_all_jit_code(void) { # ifndef _WIN32 /* This requires header transformation but we don't transform header on Windows for now */ - struct rb_mjit_unit *unit; - struct rb_mjit_unit_node *node; + struct rb_mjit_unit *unit, *cur = 0; double start_time, end_time; static const char so_ext[] = DLEXT; char so_file[MAXPATHLEN]; @@ -899,8 +852,8 @@ compact_all_jit_code(void) o_files = alloca(sizeof(char *) * (active_units.length + 1)); o_files[active_units.length] = NULL; CRITICAL_SECTION_START(3, "in compact_all_jit_code to keep .o files"); - for (node = active_units.head; node != NULL; node = node->next) { - o_files[i] = node->unit->o_file; + list_for_each(&active_units.head, cur, unode) { + o_files[i] = cur->o_file; i++; } @@ -924,27 +877,25 @@ compact_all_jit_code(void) unit->handle = handle; /* lazily dlclose handle (and .so file for win32) on `mjit_finish()`. */ - node = calloc(1, sizeof(struct rb_mjit_unit_node)); /* To prevent GC, don't use ZALLOC */ - node->unit = unit; - add_to_list(node, &compact_units); + add_to_list(unit, &compact_units); if (!mjit_opts.save_temps) remove_so_file(so_file, unit); CRITICAL_SECTION_START(3, "in compact_all_jit_code to read list"); - for (node = active_units.head; node != NULL; node = node->next) { + list_for_each(&active_units.head, cur, unode) { void *func; char funcname[35]; /* TODO: reconsider `35` */ - sprintf(funcname, "_mjit%d", node->unit->id); + sprintf(funcname, "_mjit%d", cur->id); if ((func = dlsym(handle, funcname)) == NULL) { mjit_warning("skipping to reload '%s' from '%s': %s", funcname, so_file, dlerror()); continue; } - if (node->unit->iseq) { /* Check whether GCed or not */ + if (cur->iseq) { /* Check whether GCed or not */ /* Usage of jit_code might be not in a critical section. */ - MJIT_ATOMIC_SET(node->unit->iseq->body->jit_func, (mjit_func_t)func); + MJIT_ATOMIC_SET(cur->iseq->body->jit_func, (mjit_func_t)func); } } CRITICAL_SECTION_FINISH(3, "in compact_all_jit_code to read list"); @@ -1086,8 +1037,22 @@ convert_unit_to_func(struct rb_mjit_unit *unit, struct rb_call_cache *cc_entries verbose(3, "Waiting wakeup from GC"); rb_native_cond_wait(&mjit_gc_wakeup, &mjit_engine_mutex); } - in_jit = TRUE; + + /* We need to check again here because we could've waited on GC above */ + if (unit->iseq == NULL) { + fclose(f); + if (!mjit_opts.save_temps) + remove_file(c_file); + free_unit(unit); + in_jit = FALSE; /* just being explicit for return */ + } + else { + in_jit = TRUE; + } CRITICAL_SECTION_FINISH(3, "before mjit_compile to wait GC finish"); + if (!in_jit) { + return (mjit_func_t)NOT_COMPILED_JIT_ISEQ_FUNC; + } { VALUE s = rb_iseq_path(unit->iseq); @@ -1124,7 +1089,7 @@ convert_unit_to_func(struct rb_mjit_unit *unit, struct rb_call_cache *cc_entries o_files[0] = o_file; success = link_o_to_so(o_files, so_file); - /* Alwasy set o_file for compaction. The value is also used for lazy deletion. */ + /* Always set o_file for compaction. The value is also used for lazy deletion. */ unit->o_file = strdup(o_file); if (unit->o_file == NULL) { mjit_warning("failed to allocate memory to remember '%s' (%s), removing it...", o_file, strerror(errno)); @@ -1146,14 +1111,8 @@ convert_unit_to_func(struct rb_mjit_unit *unit, struct rb_call_cache *cc_entries remove_so_file(so_file, unit); if ((uintptr_t)func > (uintptr_t)LAST_JIT_ISEQ_FUNC) { - struct rb_mjit_unit_node *node = create_list_node(unit); - if (node == NULL) { - mjit_warning("failed to allocate a node to be added to active_units"); - return (mjit_func_t)NOT_COMPILED_JIT_ISEQ_FUNC; - } - CRITICAL_SECTION_START(3, "end of jit"); - add_to_list(node, &active_units); + add_to_list(unit, &active_units); if (unit->iseq) print_jit_result("success", unit, end_time - start_time, c_file); CRITICAL_SECTION_FINISH(3, "end of jit"); @@ -1161,26 +1120,39 @@ convert_unit_to_func(struct rb_mjit_unit *unit, struct rb_call_cache *cc_entries return (mjit_func_t)func; } -struct mjit_copy_job { - const struct rb_iseq_constant_body *body; +typedef struct { + struct rb_mjit_unit *unit; struct rb_call_cache *cc_entries; union iseq_inline_storage_entry *is_entries; int finish_p; -}; +} mjit_copy_job_t; + +/* Singleton MJIT copy job. This is made global since it needs to be durable even when MJIT worker thread is stopped. + (ex: register job -> MJIT pause -> MJIT resume -> dispatch job. Actually this should be just cancelled by finish_p check) */ +static mjit_copy_job_t mjit_copy_job; static void mjit_copy_job_handler(void *data); +/* vm_trace.c */ +int rb_workqueue_register(unsigned flags, rb_postponed_job_func_t , void *); + /* We're lazily copying cache values from main thread because these cache values could be different between ones on enqueue timing and ones on dequeue timing. Return TRUE if copy succeeds. */ static int -copy_cache_from_main_thread(struct mjit_copy_job *job) +copy_cache_from_main_thread(mjit_copy_job_t *job) { - job->finish_p = FALSE; + CRITICAL_SECTION_START(3, "in copy_cache_from_main_thread"); + job->finish_p = FALSE; /* allow dispatching this job in mjit_copy_job_handler */ + CRITICAL_SECTION_FINISH(3, "in copy_cache_from_main_thread"); - if (!rb_postponed_job_register(0, mjit_copy_job_handler, (void *)job)) - return FALSE; + if (UNLIKELY(mjit_opts.wait)) { + mjit_copy_job_handler((void *)job); + return job->finish_p; + } + if (!rb_workqueue_register(0, mjit_copy_job_handler, (void *)job)) + return FALSE; CRITICAL_SECTION_START(3, "in MJIT copy job wait"); /* checking `stop_worker_p` too because `RUBY_VM_CHECK_INTS(ec)` may not lush mjit_copy_job_handler when EC_EXEC_TAG() is not TAG_NONE, and then @@ -1199,6 +1171,8 @@ copy_cache_from_main_thread(struct mjit_copy_job *job) void mjit_worker(void) { + mjit_copy_job_t *job = &mjit_copy_job; /* just a shorthand */ + #ifndef _MSC_VER if (pch_status == PCH_NOT_READY) { make_pch(); @@ -1216,48 +1190,49 @@ mjit_worker(void) /* main worker loop */ while (!stop_worker_p) { - struct rb_mjit_unit_node *node; + struct rb_mjit_unit *unit; /* wait until unit is available */ CRITICAL_SECTION_START(3, "in worker dequeue"); - while ((unit_queue.head == NULL || active_units.length >= mjit_opts.max_cache_size) && !stop_worker_p) { + while ((list_empty(&unit_queue.head) || active_units.length >= mjit_opts.max_cache_size) && !stop_worker_p) { rb_native_cond_wait(&mjit_worker_wakeup, &mjit_engine_mutex); verbose(3, "Getting wakeup from client"); } - node = get_from_list(&unit_queue); + unit = get_from_list(&unit_queue); + job->finish_p = TRUE; /* disable dispatching this job in mjit_copy_job_handler while it's being modified */ CRITICAL_SECTION_FINISH(3, "in worker dequeue"); - if (node) { + if (unit) { mjit_func_t func; - struct mjit_copy_job job; + const struct rb_iseq_constant_body *body = unit->iseq->body; - job.body = node->unit->iseq->body; - job.cc_entries = NULL; - if (job.body->ci_size > 0 || job.body->ci_kw_size > 0) - job.cc_entries = alloca(sizeof(struct rb_call_cache) * (job.body->ci_size + job.body->ci_kw_size)); - job.is_entries = NULL; - if (job.body->is_size > 0) - job.is_entries = alloca(sizeof(union iseq_inline_storage_entry) * job.body->is_size); + job->unit = unit; + job->cc_entries = NULL; + if (body->ci_size > 0 || body->ci_kw_size > 0) + job->cc_entries = alloca(sizeof(struct rb_call_cache) * (body->ci_size + body->ci_kw_size)); + job->is_entries = NULL; + if (body->is_size > 0) + job->is_entries = alloca(sizeof(union iseq_inline_storage_entry) * body->is_size); /* Copy ISeq's inline caches values to avoid race condition. */ - if (job.cc_entries != NULL || job.is_entries != NULL) { - if (UNLIKELY(mjit_opts.wait)) { - mjit_copy_job_handler((void *)&job); /* main thread is waiting in mjit_wait_call() and doesn't race */ - } - else if (copy_cache_from_main_thread(&job) == FALSE) { + if (job->cc_entries != NULL || job->is_entries != NULL) { + if (copy_cache_from_main_thread(job) == FALSE) { continue; /* retry postponed_job failure, or stop worker */ } } /* JIT compile */ - func = convert_unit_to_func(node->unit, job.cc_entries, job.is_entries); + func = convert_unit_to_func(unit, job->cc_entries, job->is_entries); CRITICAL_SECTION_START(3, "in jit func replace"); - if (node->unit->iseq) { /* Check whether GCed or not */ + while (in_gc) { /* Make sure we're not GC-ing when touching ISeq */ + verbose(3, "Waiting wakeup from GC"); + rb_native_cond_wait(&mjit_gc_wakeup, &mjit_engine_mutex); + } + if (unit->iseq) { /* Check whether GCed or not */ /* Usage of jit_code might be not in a critical section. */ - MJIT_ATOMIC_SET(node->unit->iseq->body->jit_func, func); + MJIT_ATOMIC_SET(unit->iseq->body->jit_func, func); } - remove_from_list(node, &unit_queue); CRITICAL_SECTION_FINISH(3, "in jit func replace"); #ifndef _MSC_VER @@ -1270,6 +1245,10 @@ mjit_worker(void) } } + /* Disable dispatching this job in mjit_copy_job_handler while memory allocated by alloca + could be expired after finishing this function. */ + job->finish_p = TRUE; + /* To keep mutex unlocked when it is destroyed by mjit_finish, don't wrap CRITICAL_SECTION here. */ worker_stopped = TRUE; } diff --git a/node.h b/node.h index f725a38df42486..0f5a2a923a1fc3 100644 --- a/node.h +++ b/node.h @@ -164,6 +164,7 @@ typedef struct RNode { VALUE value; } u3; rb_code_location_t nd_loc; + int node_id; } NODE; #define RNODE(obj) (R_CAST(RNode)(obj)) @@ -201,6 +202,8 @@ typedef struct RNode { #define nd_set_last_lineno(n, v) ((n)->nd_loc.end_pos.lineno = (v)) #define nd_last_loc(n) ((n)->nd_loc.end_pos) #define nd_set_last_loc(n, v) (nd_last_loc(n) = (v)) +#define nd_node_id(n) ((n)->node_id) +#define nd_set_node_id(n,id) ((n)->node_id = (id)) #define nd_head u1.node #define nd_alen u2.argc @@ -399,6 +402,7 @@ rb_ast_t *rb_parser_compile_string(VALUE, const char*, VALUE, int); rb_ast_t *rb_parser_compile_file(VALUE, const char*, VALUE, int); rb_ast_t *rb_parser_compile_string_path(VALUE vparser, VALUE fname, VALUE src, int line); rb_ast_t *rb_parser_compile_file_path(VALUE vparser, VALUE fname, VALUE input, int line); +rb_ast_t *rb_parser_compile_generic(VALUE vparser, VALUE (*lex_gets)(VALUE, int), VALUE fname, VALUE input, int line); rb_ast_t *rb_compile_cstr(const char*, const char*, int, int); rb_ast_t *rb_compile_string(const char*, VALUE, int); diff --git a/numeric.c b/numeric.c index d698fcedeafde9..8f0a805c689cc9 100644 --- a/numeric.c +++ b/numeric.c @@ -1083,6 +1083,36 @@ flo_mul(VALUE x, VALUE y) } } +static bool +flo_iszero(VALUE f) +{ + return RFLOAT_VALUE(f) == 0.0; +} + +static double +double_div_double(double x, double y) +{ + if (LIKELY(y != 0.0)) { + return x / y; + } + else if (x == 0.0) { + return nan(""); + } + else { + double z = signbit(y) ? -1.0 : 1.0; + return x * z * HUGE_VAL; + } +} + +MJIT_FUNC_EXPORTED VALUE +rb_flo_div_flo(VALUE x, VALUE y) +{ + double num = RFLOAT_VALUE(x); + double den = RFLOAT_VALUE(y); + double ret = double_div_double(num, den); + return DBL2NUM(ret); +} + /* * call-seq: * float / other -> float @@ -1093,23 +1123,25 @@ flo_mul(VALUE x, VALUE y) static VALUE flo_div(VALUE x, VALUE y) { - long f_y; - double d; + double num = RFLOAT_VALUE(x); + double den; + double ret; if (RB_TYPE_P(y, T_FIXNUM)) { - f_y = FIX2LONG(y); - return DBL2NUM(RFLOAT_VALUE(x) / (double)f_y); + den = FIX2LONG(y); } else if (RB_TYPE_P(y, T_BIGNUM)) { - d = rb_big2dbl(y); - return DBL2NUM(RFLOAT_VALUE(x) / d); + den = rb_big2dbl(y); } else if (RB_TYPE_P(y, T_FLOAT)) { - return DBL2NUM(RFLOAT_VALUE(x) / RFLOAT_VALUE(y)); + den = RFLOAT_VALUE(y); } else { return rb_num_coerce_bin(x, y, '/'); } + + ret = double_div_double(num, den); + return DBL2NUM(ret); } /* @@ -1276,7 +1308,7 @@ rb_float_pow(VALUE x, VALUE y) dx = RFLOAT_VALUE(x); dy = RFLOAT_VALUE(y); if (dx < 0 && dy != round(dy)) - return rb_dbl_complex_polar(pow(-dx, dy), dy); + return rb_dbl_complex_polar_pi(pow(-dx, dy), dy); } else { return rb_num_coerce_bin(x, y, idPow); @@ -1676,10 +1708,7 @@ rb_float_abs(VALUE flt) static VALUE flo_zero_p(VALUE num) { - if (RFLOAT_VALUE(num) == 0.0) { - return Qtrue; - } - return Qfalse; + return flo_iszero(num) ? Qtrue : Qfalse; } /* @@ -2456,15 +2485,16 @@ static double ruby_float_step_size(double beg, double end, double unit, int excl) { const double epsilon = DBL_EPSILON; - double n = (end - beg)/unit; - double err = (fabs(beg) + fabs(end) + fabs(end-beg)) / fabs(unit) * epsilon; + double n, err; + if (unit == 0) { + return HUGE_VAL; + } + n= (end - beg)/unit; + err = (fabs(beg) + fabs(end) + fabs(end-beg)) / fabs(unit) * epsilon; if (isinf(unit)) { return unit > 0 ? beg <= end : beg >= end; } - if (unit == 0) { - return HUGE_VAL; - } if (err>0.5) err=0.5; if (excl) { if (n<=0) return 0; @@ -3672,13 +3702,13 @@ static double fix_fdiv_double(VALUE x, VALUE y) { if (FIXNUM_P(y)) { - return (double)FIX2LONG(x) / (double)FIX2LONG(y); + return double_div_double(FIX2LONG(x), FIX2LONG(y)); } else if (RB_TYPE_P(y, T_BIGNUM)) { return rb_big_fdiv_double(rb_int2big(FIX2LONG(x)), y); } else if (RB_TYPE_P(y, T_FLOAT)) { - return (double)FIX2LONG(x) / RFLOAT_VALUE(y); + return double_div_double(FIX2LONG(x), RFLOAT_VALUE(y)); } else { return NUM2DBL(rb_num_coerce_bin(x, y, rb_intern("fdiv"))); @@ -3748,19 +3778,16 @@ fix_divide(VALUE x, VALUE y, ID op) return rb_big_div(x, y); } else if (RB_TYPE_P(y, T_FLOAT)) { - { - double div; - if (op == '/') { - div = (double)FIX2LONG(x) / RFLOAT_VALUE(y); - return DBL2NUM(div); + double d = FIX2LONG(x); + return rb_flo_div_flo(DBL2NUM(d), y); } else { + VALUE v; if (RFLOAT_VALUE(y) == 0) rb_num_zerodiv(); - div = (double)FIX2LONG(x) / RFLOAT_VALUE(y); - return rb_dbl2big(floor(div)); + v = fix_divide(x, y, '/'); + return flo_floor(0, 0, v); } - } } else { if (RB_TYPE_P(y, T_RATIONAL) && @@ -3875,7 +3902,7 @@ rb_int_modulo(VALUE x, VALUE y) * See Numeric#divmod. */ -VALUE +static VALUE int_remainder(VALUE x, VALUE y) { if (FIXNUM_P(x)) { @@ -4056,7 +4083,7 @@ fix_pow(VALUE x, VALUE y) if (a == 1) return DBL2NUM(1.0); { if (a < 0 && dy != round(dy)) - return rb_dbl_complex_polar(pow(-(double)a, dy), dy); + return rb_dbl_complex_polar_pi(pow(-(double)a, dy), dy); return DBL2NUM(pow((double)a, dy)); } } diff --git a/object.c b/object.c index cc1c75e639e7b5..0160151d3ef7f6 100644 --- a/object.c +++ b/object.c @@ -1479,6 +1479,19 @@ nil_inspect(VALUE obj) return rb_usascii_str_new2("nil"); } +/* + * call-seq: + * nil =~ other -> nil + * + * Dummy pattern matching -- always returns nil. + */ + +static VALUE +nil_match(VALUE obj1, VALUE obj2) +{ + return Qnil; +} + /*********************************************************************** * Document-class: TrueClass * @@ -1673,6 +1686,7 @@ rb_false(VALUE obj) static VALUE rb_obj_match(VALUE obj1, VALUE obj2) { + rb_warning("Object#=~ is deprecated; it always returns nil"); return Qnil; } @@ -3055,7 +3069,8 @@ rb_check_convert_type_with_id(VALUE val, int type, const char *tname, ID method) #define try_to_int(val, mid, raise) \ convert_type_with_id(val, "Integer", mid, raise, -1) -static VALUE +ALWAYS_INLINE(static VALUE rb_to_integer(VALUE val, const char *method, ID mid)); +static inline VALUE rb_to_integer(VALUE val, const char *method, ID mid) { VALUE v; @@ -4148,6 +4163,7 @@ InitVM_Object(void) rb_define_method(rb_cNilClass, "to_a", nil_to_a, 0); rb_define_method(rb_cNilClass, "to_h", nil_to_h, 0); rb_define_method(rb_cNilClass, "inspect", nil_inspect, 0); + rb_define_method(rb_cNilClass, "=~", nil_match, 1); rb_define_method(rb_cNilClass, "&", false_and, 1); rb_define_method(rb_cNilClass, "|", false_or, 1); rb_define_method(rb_cNilClass, "^", false_xor, 1); diff --git a/pack.c b/pack.c index c9e952181d97a8..b1efef54f1d417 100644 --- a/pack.c +++ b/pack.c @@ -14,6 +14,7 @@ #include #include #include +#include /* * It is intentional that the condition for natstr is HAVE_TRUE_LONG_LONG @@ -42,20 +43,20 @@ static const char endstr[] = "sSiIlLqQjJ"; #endif #ifdef DYNAMIC_ENDIAN - /* for universal binary of NEXTSTEP and MacOS X */ - /* useless since autoconf 2.63? */ - static int - is_bigendian(void) - { - static int init = 0; - static int endian_value; - char *p; - - if (init) return endian_value; - init = 1; - p = (char*)&init; - return endian_value = p[0]?0:1; - } +/* for universal binary of NEXTSTEP and MacOS X */ +/* useless since autoconf 2.63? */ +static int +is_bigendian(void) +{ + static int init = 0; + static int endian_value; + char *p; + + if (init) return endian_value; + init = 1; + p = (char*)&init; + return endian_value = p[0]?0:1; +} # define BIGENDIAN_P() (is_bigendian()) #elif defined(WORDS_BIGENDIAN) # define BIGENDIAN_P() 1 @@ -127,6 +128,47 @@ str_associated(VALUE str) return rb_ivar_lookup(str, id_associated, Qfalse); } +static void +unknown_directive(const char *mode, char type, VALUE fmt) +{ + VALUE f; + char unknown[5]; + + if (ISPRINT(type)) { + unknown[0] = type; + unknown[1] = '\0'; + } + else { + snprintf(unknown, sizeof(unknown), "\\x%.2x", type & 0xff); + } + f = rb_str_quote_unprintable(fmt); + if (f != fmt) { + fmt = rb_str_subseq(f, 1, RSTRING_LEN(f) - 2); + } + rb_warning("unknown %s directive '%s' in '%"PRIsVALUE"'", + mode, unknown, fmt); +} + +static float +VALUE_to_float(VALUE obj) +{ + VALUE v = rb_to_float(obj); + double d = RFLOAT_VALUE(v); + + if (isnan(d)) { + return NAN; + } + else if (d < -FLT_MAX) { + return -INFINITY; + } + else if (d <= FLT_MAX) { + return d; + } + else { + return INFINITY; + } +} + /* * call-seq: * arr.pack( aTemplateString ) -> aBinaryString @@ -642,7 +684,7 @@ pack_pack(int argc, VALUE *argv, VALUE ary) float f; from = NEXTFROM; - f = (float)RFLOAT_VALUE(rb_to_float(from)); + f = VALUE_to_float(from); rb_str_buf_cat(res, (char*)&f, sizeof(float)); } break; @@ -652,7 +694,7 @@ pack_pack(int argc, VALUE *argv, VALUE ary) FLOAT_CONVWITH(tmp); from = NEXTFROM; - tmp.f = (float)RFLOAT_VALUE(rb_to_float(from)); + tmp.f = VALUE_to_float(from); HTOVF(tmp); rb_str_buf_cat(res, tmp.buf, sizeof(float)); } @@ -683,7 +725,7 @@ pack_pack(int argc, VALUE *argv, VALUE ary) while (len-- > 0) { FLOAT_CONVWITH(tmp); from = NEXTFROM; - tmp.f = (float)RFLOAT_VALUE(rb_to_float(from)); + tmp.f = VALUE_to_float(from); HTONF(tmp); rb_str_buf_cat(res, tmp.buf, sizeof(float)); } @@ -839,9 +881,9 @@ pack_pack(int argc, VALUE *argv, VALUE ary) cp = RSTRING_PTR(buf); while (1 < numbytes) { - *cp |= 0x80; - cp++; - numbytes--; + *cp |= 0x80; + cp++; + numbytes--; } rb_str_buf_cat(res, RSTRING_PTR(buf), RSTRING_LEN(buf)); @@ -849,16 +891,7 @@ pack_pack(int argc, VALUE *argv, VALUE ary) break; default: { - char unknown[5]; - if (ISPRINT(type)) { - unknown[0] = type; - unknown[1] = '\0'; - } - else { - snprintf(unknown, sizeof(unknown), "\\x%.2x", type & 0xff); - } - rb_warning("unknown pack directive '%s' in '% "PRIsVALUE"'", - unknown, fmt); + unknown_directive("pack", type, fmt); break; } } @@ -1005,7 +1038,7 @@ hex2num(char c) tmp_len = 0; \ if (len > (long)((send-s)/(sz))) { \ if (!star) { \ - tmp_len = len-(send-s)/(sz); \ + tmp_len = len-(send-s)/(sz); \ } \ len = (send-s)/(sz); \ } \ @@ -1748,8 +1781,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, int mode) break; default: - rb_warning("unknown unpack directive '%c' in '%s'", - type, RSTRING_PTR(fmt)); + unknown_directive("unpack", type, fmt); break; } } diff --git a/parse.y b/parse.y index 606daab7d8d2a0..278c5b0296503b 100644 --- a/parse.y +++ b/parse.y @@ -201,7 +201,10 @@ struct parser_params { const char *pcur; const char *pend; const char *ptok; - long gets_ptr; + union { + long ptr; + VALUE (*call)(VALUE, int); + } gets_; enum lex_state_e state; /* track the nest level of any parens "()[]{}" */ int paren_nest; @@ -222,7 +225,7 @@ struct parser_params { struct local_vars *lvtbl; int line_count; int ruby_sourceline; /* current line no. */ - char *ruby_sourcefile; /* current source file */ + const char *ruby_sourcefile; /* current source file */ VALUE ruby_sourcefile_string; rb_encoding *enc; token_info *token_info; @@ -234,6 +237,7 @@ struct parser_params { ID cur_arg; rb_ast_t *ast; + int node_id; unsigned int command_start:1; unsigned int eofp: 1; @@ -339,6 +343,14 @@ static NODE* node_newnode(struct parser_params *, enum node_type, VALUE, VALUE, static NODE *nd_set_loc(NODE *nd, const YYLTYPE *loc); +static int +parser_get_node_id(struct parser_params *p) +{ + int node_id = p->node_id; + p->node_id++; + return node_id; +} + #ifndef RIPPER static inline void set_line_body(NODE *body, int line) @@ -764,55 +776,55 @@ static void token_info_warn(struct parser_params *p, const char *token, token_in } %token - keyword_class - keyword_module - keyword_def - keyword_undef - keyword_begin - keyword_rescue - keyword_ensure - keyword_end - keyword_if - keyword_unless - keyword_then - keyword_elsif - keyword_else - keyword_case - keyword_when - keyword_while - keyword_until - keyword_for - keyword_break - keyword_next - keyword_redo - keyword_retry - keyword_in - keyword_do - keyword_do_cond - keyword_do_block - keyword_do_LAMBDA - keyword_return - keyword_yield - keyword_super - keyword_self - keyword_nil - keyword_true - keyword_false - keyword_and - keyword_or - keyword_not - modifier_if - modifier_unless - modifier_while - modifier_until - modifier_rescue - keyword_alias - keyword_defined - keyword_BEGIN - keyword_END - keyword__LINE__ - keyword__FILE__ - keyword__ENCODING__ + keyword_class "class" + keyword_module "module" + keyword_def "def" + keyword_undef "undef" + keyword_begin "begin" + keyword_rescue "rescue" + keyword_ensure "ensure" + keyword_end "end" + keyword_if "if" + keyword_unless "unless" + keyword_then "then" + keyword_elsif "elsif" + keyword_else "else" + keyword_case "case" + keyword_when "when" + keyword_while "while" + keyword_until "until" + keyword_for "for" + keyword_break "break" + keyword_next "next" + keyword_redo "redo" + keyword_retry "retry" + keyword_in "in" + keyword_do "do" + keyword_do_cond "do (for condition)" + keyword_do_block "do (for block)" + keyword_do_LAMBDA "do (for lambda)" + keyword_return "return" + keyword_yield "yield" + keyword_super "super" + keyword_self "self" + keyword_nil "nil" + keyword_true "true" + keyword_false "false" + keyword_and "and" + keyword_or "or" + keyword_not "not" + modifier_if "if (modifier)" + modifier_unless "unless (modifier)" + modifier_while "while (modifier)" + modifier_until "until (modifier)" + modifier_rescue "rescue (modifier)" + keyword_alias "alias" + keyword_defined "defined?" + keyword_BEGIN "BEGIN" + keyword_END "END" + keyword__LINE__ "__LINE__" + keyword__FILE__ "__FILE__" + keyword__ENCODING__ "__ENCODING__" %token tIDENTIFIER tFID tGVAR tIVAR tCONSTANT tCVAR tLABEL %token tINTEGER tFLOAT tRATIONAL tIMAGINARY tSTRING_CONTENT tCHAR @@ -1351,7 +1363,7 @@ command : fcall command_args %prec tLOWEST { /*%%%*/ $1->nd_args = $2; - nd_set_last_loc($1, nd_last_loc($2)); + nd_set_last_loc($1, @2.end_pos); $$ = $1; /*% %*/ /*% ripper: command!($1, $2) %*/ @@ -1363,7 +1375,7 @@ command : fcall command_args %prec tLOWEST $1->nd_args = $2; $$ = method_add_block(p, $1, $3, &@$); fixpos($$, $1); - nd_set_last_loc($1, nd_last_loc($2)); + nd_set_last_loc($1, @2.end_pos); /*% %*/ /*% ripper: method_add_block!(command!($1, $2), $3) %*/ } @@ -1772,7 +1784,7 @@ undef_list : fitem | undef_list ',' {SET_LEX_STATE(EXPR_FNAME|EXPR_FITEM);} fitem { /*%%%*/ - NODE *undef = NEW_UNDEF($4, &@$); + NODE *undef = NEW_UNDEF($4, &@4); $$ = block_append(p, $1, undef); /*% %*/ /*% ripper: rb_ary_push($1, get_value($4)) %*/ @@ -4864,7 +4876,7 @@ yycompile0(VALUE arg) struct parser_params *p = (struct parser_params *)arg; VALUE cov = Qfalse; - if (!compile_for_eval && rb_safe_level() == 0) { + if (!compile_for_eval && rb_safe_level() == 0 && !NIL_P(p->ruby_sourcefile_string)) { p->debug_lines = debug_lines(p->ruby_sourcefile_string); if (p->debug_lines && p->ruby_sourceline > 0) { VALUE str = STR_NEW0(); @@ -4924,8 +4936,14 @@ static rb_ast_t * yycompile(VALUE vparser, struct parser_params *p, VALUE fname, int line) { rb_ast_t *ast; - p->ruby_sourcefile_string = rb_str_new_frozen(fname); - p->ruby_sourcefile = StringValueCStr(fname); + if (NIL_P(fname)) { + p->ruby_sourcefile_string = Qnil; + p->ruby_sourcefile = "(none)"; + } + else { + p->ruby_sourcefile_string = rb_str_new_frozen(fname); + p->ruby_sourcefile = StringValueCStr(fname); + } p->ruby_sourceline = line - 1; p->ast = ast = rb_ast_new(); @@ -4956,14 +4974,14 @@ lex_get_str(struct parser_params *p, VALUE s) beg = RSTRING_PTR(s); len = RSTRING_LEN(s); start = beg; - if (p->lex.gets_ptr) { - if (len == p->lex.gets_ptr) return Qnil; - beg += p->lex.gets_ptr; - len -= p->lex.gets_ptr; + if (p->lex.gets_.ptr) { + if (len == p->lex.gets_.ptr) return Qnil; + beg += p->lex.gets_.ptr; + len -= p->lex.gets_.ptr; } end = memchr(beg, '\n', len); if (end) len = ++end - beg; - p->lex.gets_ptr += len; + p->lex.gets_.ptr += len; return rb_str_subseq(s, beg - start, len); } @@ -4994,7 +5012,7 @@ parser_compile_string(VALUE vparser, VALUE fname, VALUE s, int line) TypedData_Get_Struct(vparser, struct parser_params, &parser_data_type, p); p->lex.gets = lex_get_str; - p->lex.gets_ptr = 0; + p->lex.gets_.ptr = 0; p->lex.input = rb_str_new_frozen(s); p->lex.pbeg = p->lex.pcur = p->lex.pend = 0; @@ -5070,6 +5088,27 @@ rb_parser_compile_file_path(VALUE vparser, VALUE fname, VALUE file, int start) return yycompile(vparser, p, fname, start); } + +static VALUE +lex_generic_gets(struct parser_params *p, VALUE input) +{ + return (*p->lex.gets_.call)(input, p->line_count); +} + +rb_ast_t* +rb_parser_compile_generic(VALUE vparser, VALUE (*lex_gets)(VALUE, int), VALUE fname, VALUE input, int start) +{ + struct parser_params *p; + + TypedData_Get_Struct(vparser, struct parser_params, &parser_data_type, p); + + p->lex.gets = lex_generic_gets; + p->lex.gets_.call = lex_gets; + p->lex.input = input; + p->lex.pbeg = p->lex.pcur = p->lex.pend = 0; + + return yycompile(vparser, p, fname, start); +} #endif /* !RIPPER */ #define STR_FUNC_ESCAPE 0x01 @@ -6134,12 +6173,14 @@ heredoc_dedent(struct parser_params *p, NODE *root) return 0; } else { + NODE *end = node->nd_end; node = prev_node->nd_next = node->nd_next; if (!node) { if (nd_type(prev_node) == NODE_DSTR) nd_set_type(prev_node, NODE_STR); break; } + node->nd_end = end; goto next_str; } @@ -8292,6 +8333,7 @@ node_newnode(struct parser_params *p, enum node_type type, VALUE a0, VALUE a1, V rb_node_init(n, type, a0, a1, a2); nd_set_loc(n, loc); + nd_set_node_id(n, parser_get_node_id(p)); return n; } @@ -8704,7 +8746,14 @@ gettable(struct parser_params *p, ID id, const YYLTYPE *loc) return NEW_FALSE(loc); case keyword__FILE__: WARN_LOCATION("__FILE__"); - node = NEW_STR(add_mark_object(p, rb_str_dup(p->ruby_sourcefile_string)), loc); + { + VALUE file = p->ruby_sourcefile_string; + if (NIL_P(file)) + file = rb_str_new(0, 0); + else + file = rb_str_dup(file); + node = NEW_STR(add_mark_object(p, file), loc); + } return node; case keyword__LINE__: WARN_LOCATION("__LINE__"); @@ -10772,6 +10821,7 @@ parser_initialize(struct parser_params *p) p->command_start = TRUE; p->ruby_sourcefile_string = Qnil; p->lex.lpar_beg = -1; /* make lambda_beginning_p() == FALSE at first */ + p->node_id = 0; #ifdef RIPPER p->delayed = Qnil; p->result = Qnil; diff --git a/prelude.rb b/prelude.rb index 6ebaabc758ecfa..1d62c132198368 100644 --- a/prelude.rb +++ b/prelude.rb @@ -132,6 +132,12 @@ def write_nonblock(buf, exception: true) end end +class TracePoint + def enable target: nil, target_line: nil, &blk + self.__enable target, target_line, &blk + end +end + class Binding # :nodoc: def irb diff --git a/proc.c b/proc.c index bf5921e9ce49ed..50e09d76e75367 100644 --- a/proc.c +++ b/proc.c @@ -2342,7 +2342,7 @@ rb_method_entry_min_max_arity(const rb_method_entry_t *me, int *max) def = def->body.alias.original_me->def; goto again; case VM_METHOD_TYPE_BMETHOD: - return rb_proc_min_max_arity(def->body.proc, max); + return rb_proc_min_max_arity(def->body.bmethod.proc, max); case VM_METHOD_TYPE_ISEQ: return rb_iseq_min_max_arity(rb_iseq_check(def->body.iseq.iseqptr), max); case VM_METHOD_TYPE_UNDEF: @@ -2478,8 +2478,8 @@ rb_obj_method_arity(VALUE obj, ID id) return rb_mod_method_arity(CLASS_OF(obj), id); } -static inline const rb_method_definition_t * -method_def(VALUE method) +const rb_method_definition_t * +rb_method_def(VALUE method) { const struct METHOD *data; @@ -2494,7 +2494,7 @@ method_def_iseq(const rb_method_definition_t *def) case VM_METHOD_TYPE_ISEQ: return rb_iseq_check(def->body.iseq.iseqptr); case VM_METHOD_TYPE_BMETHOD: - return rb_proc_get_iseq(def->body.proc, 0); + return rb_proc_get_iseq(def->body.bmethod.proc, 0); case VM_METHOD_TYPE_ALIAS: return method_def_iseq(def->body.alias.original_me->def); case VM_METHOD_TYPE_CFUNC: @@ -2514,13 +2514,13 @@ method_def_iseq(const rb_method_definition_t *def) const rb_iseq_t * rb_method_iseq(VALUE method) { - return method_def_iseq(method_def(method)); + return method_def_iseq(rb_method_def(method)); } static const rb_cref_t * method_cref(VALUE method) { - const rb_method_definition_t *def = method_def(method); + const rb_method_definition_t *def = rb_method_def(method); again: switch (def->type) { @@ -2576,7 +2576,7 @@ rb_obj_method_location(VALUE obj, ID id) VALUE rb_method_location(VALUE method) { - return method_def_location(method_def(method)); + return method_def_location(rb_method_def(method)); } /* @@ -3046,6 +3046,136 @@ rb_method_curry(int argc, const VALUE *argv, VALUE self) return proc_curry(argc, argv, proc); } +static VALUE +compose(VALUE dummy, VALUE args, int argc, VALUE *argv, VALUE passed_proc) +{ + VALUE f, g, fargs; + f = RARRAY_AREF(args, 0); + g = RARRAY_AREF(args, 1); + + if (rb_obj_is_proc(g)) + fargs = rb_proc_call_with_block(g, argc, argv, passed_proc); + else + fargs = rb_funcall_with_block(g, idCall, argc, argv, passed_proc); + + if (rb_obj_is_proc(f)) + return rb_proc_call(f, rb_ary_new3(1, fargs)); + else + return rb_funcallv(f, idCall, 1, &fargs); +} + +/* + * call-seq: + * prc << g -> a_proc + * + * Returns a proc that is the composition of this proc and the given g. + * The returned proc takes a variable number of arguments, calls g with them + * then calls this proc with the result. + * + * f = proc {|x| x * x } + * g = proc {|x| x + x } + * p (f << g).call(2) #=> 16 + */ +static VALUE +proc_compose_to_left(VALUE self, VALUE g) +{ + VALUE proc, args, procs[2]; + rb_proc_t *procp; + int is_lambda; + + procs[0] = self; + procs[1] = g; + args = rb_ary_tmp_new_from_values(0, 2, procs); + + GetProcPtr(self, procp); + is_lambda = procp->is_lambda; + + proc = rb_proc_new(compose, args); + GetProcPtr(proc, procp); + procp->is_lambda = is_lambda; + + return proc; +} + +/* + * call-seq: + * prc >> g -> a_proc + * + * Returns a proc that is the composition of this proc and the given g. + * The returned proc takes a variable number of arguments, calls g with them + * then calls this proc with the result. + * + * f = proc {|x| x * x } + * g = proc {|x| x + x } + * p (f >> g).call(2) #=> 8 + */ +static VALUE +proc_compose_to_right(VALUE self, VALUE g) +{ + VALUE proc, args, procs[2]; + rb_proc_t *procp; + int is_lambda; + + procs[0] = g; + procs[1] = self; + args = rb_ary_tmp_new_from_values(0, 2, procs); + + GetProcPtr(self, procp); + is_lambda = procp->is_lambda; + + proc = rb_proc_new(compose, args); + GetProcPtr(proc, procp); + procp->is_lambda = is_lambda; + + return proc; +} + +/* + * call-seq: + * meth << g -> a_proc + * + * Returns a proc that is the composition of this method and the given g. + * The returned proc takes a variable number of arguments, calls g with them + * then calls this method with the result. + * + * def f(x) + * x * x + * end + * + * f = self.method(:f) + * g = proc {|x| x + x } + * p (f << g).call(2) #=> 16 + */ +static VALUE +rb_method_compose_to_left(VALUE self, VALUE g) +{ + VALUE proc = method_to_proc(self); + return proc_compose_to_left(proc, g); +} + +/* + * call-seq: + * meth >> g -> a_proc + * + * Returns a proc that is the composition of this method and the given g. + * The returned proc takes a variable number of arguments, calls g with them + * then calls this method with the result. + * + * def f(x) + * x * x + * end + * + * f = self.method(:f) + * g = proc {|x| x + x } + * p (f >> g).call(2) #=> 8 + */ +static VALUE +rb_method_compose_to_right(VALUE self, VALUE g) +{ + VALUE proc = method_to_proc(self); + return proc_compose_to_right(proc, g); +} + /* * Document-class: LocalJumpError * @@ -3142,6 +3272,8 @@ Init_Proc(void) rb_define_method(rb_cProc, "lambda?", rb_proc_lambda_p, 0); rb_define_method(rb_cProc, "binding", proc_binding, 0); rb_define_method(rb_cProc, "curry", proc_curry, -1); + rb_define_method(rb_cProc, "<<", proc_compose_to_left, 1); + rb_define_method(rb_cProc, ">>", proc_compose_to_right, 1); rb_define_method(rb_cProc, "source_location", rb_proc_location, 0); rb_define_method(rb_cProc, "parameters", rb_proc_parameters, 0); @@ -3168,6 +3300,8 @@ Init_Proc(void) rb_define_method(rb_cMethod, "call", rb_method_call, -1); rb_define_method(rb_cMethod, "===", rb_method_call, -1); rb_define_method(rb_cMethod, "curry", rb_method_curry, -1); + rb_define_method(rb_cMethod, "<<", rb_method_compose_to_left, 1); + rb_define_method(rb_cMethod, ">>", rb_method_compose_to_right, 1); rb_define_method(rb_cMethod, "[]", rb_method_call, -1); rb_define_method(rb_cMethod, "arity", method_arity_m, 0); rb_define_method(rb_cMethod, "inspect", method_inspect, 0); diff --git a/process.c b/process.c index 13af9d182cee02..56a90e770b842e 100644 --- a/process.c +++ b/process.c @@ -1474,6 +1474,39 @@ before_exec_non_async_signal_safe(void) rb_thread_stop_timer_thread(); } +#define WRITE_CONST(fd, str) (void)(write((fd),(str),sizeof(str)-1)<0) +#ifdef _WIN32 +int rb_w32_set_nonblock2(int fd, int nonblock); +#endif + +static int +set_blocking(int fd) +{ +#ifdef _WIN32 + return rb_w32_set_nonblock2(fd, 0); +#elif defined(F_GETFL) && defined(F_SETFL) + int fl = fcntl(fd, F_GETFL); /* async-signal-safe */ + + /* EBADF ought to be possible */ + if (fl == -1) return fl; + if (fl & O_NONBLOCK) { + fl &= ~O_NONBLOCK; + return fcntl(fd, F_SETFL, fl); + } + return 0; +#endif +} + +static void +stdfd_clear_nonblock(void) +{ + /* many programs cannot deal with non-blocking stdin/stdout/stderr */ + int fd; + for (fd = 0; fd < 3; fd++) { + (void)set_blocking(fd); /* can't do much about errors anyhow */ + } +} + static void before_exec(void) { @@ -2214,7 +2247,7 @@ rb_check_exec_options(VALUE opthash, VALUE execarg_obj) { if (RHASH_EMPTY_P(opthash)) return; - st_foreach(rb_hash_tbl_raw(opthash), check_exec_options_i, (st_data_t)execarg_obj); + rb_hash_stlike_foreach(opthash, check_exec_options_i, (st_data_t)execarg_obj); } VALUE @@ -2225,7 +2258,7 @@ rb_execarg_extract_options(VALUE execarg_obj, VALUE opthash) return Qnil; args[0] = execarg_obj; args[1] = Qnil; - st_foreach(rb_hash_tbl_raw(opthash), check_exec_options_i_extract, (st_data_t)args); + rb_hash_stlike_foreach(opthash, check_exec_options_i_extract, (st_data_t)args); return args[1]; } @@ -2269,7 +2302,7 @@ rb_check_exec_env(VALUE hash, VALUE *path) env[0] = hide_obj(rb_ary_new()); env[1] = Qfalse; - st_foreach(rb_hash_tbl_raw(hash), check_exec_env_i, (st_data_t)env); + rb_hash_stlike_foreach(hash, check_exec_env_i, (st_data_t)env); *path = env[1]; return env[0]; @@ -2733,7 +2766,7 @@ rb_execarg_parent_start1(VALUE execarg_obj) } envp_buf = rb_str_buf_new(0); hide_obj(envp_buf); - st_foreach(RHASH_TBL_RAW(envtbl), fill_envp_buf_i, (st_data_t)envp_buf); + rb_hash_stlike_foreach(envtbl, fill_envp_buf_i, (st_data_t)envp_buf); envp_str = rb_str_buf_new(sizeof(char*) * (RHASH_SIZE(envtbl) + 1)); hide_obj(envp_str); p = RSTRING_PTR(envp_buf); @@ -2911,7 +2944,7 @@ rb_f_exec(int argc, const VALUE *argv) execarg_obj = rb_execarg_new(argc, argv, TRUE, FALSE); eargp = rb_execarg_get(execarg_obj); - if (mjit_enabled) mjit_pause(FALSE); /* do not leak children */ + if (mjit_enabled) mjit_finish(FALSE); /* avoid leaking resources, and do not leave files. XXX: JIT-ed handle can leak after exec error is rescued. */ before_exec(); /* stop timer thread before redirects */ rb_execarg_parent_start(execarg_obj); fail_str = eargp->use_shell ? eargp->invoke.sh.shell_script : eargp->invoke.cmd.command_name; @@ -3445,6 +3478,11 @@ rb_execarg_run_options(const struct rb_execarg *eargp, struct rb_execarg *sargp, rb_execarg_allocate_dup2_tmpbuf(sargp, RARRAY_LEN(ary)); } } + { + int preserve = errno; + stdfd_clear_nonblock(); + errno = preserve; + } return 0; } @@ -3645,6 +3683,12 @@ read_retry(int fd, void *buf, size_t len) { ssize_t r; + if (set_blocking(fd) != 0) { +#ifndef _WIN32 + rb_async_bug_errno("set_blocking failed reading child error", errno); +#endif + } + do { r = read(fd, buf, len); } while (r < 0 && errno == EINTR); @@ -3993,12 +4037,14 @@ rb_fork_ruby(int *status) while (1) { prefork(); + if (mjit_enabled) mjit_pause(FALSE); /* Don't leave locked mutex to child. Note: child_handler must be enabled to pause MJIT. */ disable_child_handler_before_fork(&old); before_fork_ruby(); pid = fork(); err = errno; - after_fork_ruby(); + after_fork_ruby(); disable_child_handler_fork_parent(&old); /* yes, bad name */ + if (mjit_enabled && pid > 0) mjit_resume(); /* child (pid == 0) is cared by rb_thread_atfork */ if (pid >= 0) /* fork succeed */ return pid; /* fork failed */ @@ -6420,10 +6466,11 @@ rb_daemon(int nochdir, int noclose) { int err = 0; #ifdef HAVE_DAEMON + if (mjit_enabled) mjit_pause(FALSE); /* Don't leave locked mutex to child. */ before_fork_ruby(); err = daemon(nochdir, noclose); after_fork_ruby(); - rb_thread_atfork(); + rb_thread_atfork(); /* calls mjit_resume() */ #else int n; diff --git a/random.c b/random.c index cd16a57b39cedf..4f6d51dffa3e0d 100644 --- a/random.c +++ b/random.c @@ -112,6 +112,9 @@ struct MT { #define genrand_initialized(mt) ((mt)->next != 0) #define uninit_genrand(mt) ((mt)->next = 0) +NO_SANITIZE("unsigned-integer-overflow", static void init_genrand(struct MT *mt, unsigned int s)); +NO_SANITIZE("unsigned-integer-overflow", static void init_by_array(struct MT *mt, const uint32_t init_key[], int key_length)); + /* initializes state[N] with a seed */ static void init_genrand(struct MT *mt, unsigned int s) @@ -1570,6 +1573,7 @@ init_seed(struct MT *mt) seed.u32[i] = genrand_int32(mt); } +NO_SANITIZE("unsigned-integer-overflow", extern st_index_t rb_hash_start(st_index_t h)); st_index_t rb_hash_start(st_index_t h) { diff --git a/rational.c b/rational.c index 9712ce72bbd3bf..47e67f128da6f0 100644 --- a/rational.c +++ b/rational.c @@ -41,6 +41,8 @@ static ID id_abs, id_idiv, id_integer_p, #define f_inspect rb_inspect #define f_to_s rb_obj_as_string +static VALUE nurat_to_f(VALUE self); + #define binop(n,op) \ inline static VALUE \ f_##n(VALUE x, VALUE y)\ @@ -929,8 +931,10 @@ nurat_div(VALUE self, VALUE other) other, ONE, '/'); } } - else if (RB_FLOAT_TYPE_P(other)) - return DBL2NUM(nurat_to_double(self) / RFLOAT_VALUE(other)); + else if (RB_FLOAT_TYPE_P(other)) { + VALUE v = nurat_to_f(self); + return rb_flo_div_flo(v, other); + } else if (RB_TYPE_P(other, T_RATIONAL)) { if (f_zero_p(other)) rb_num_zerodiv(); @@ -951,8 +955,6 @@ nurat_div(VALUE self, VALUE other) } } -static VALUE nurat_to_f(VALUE self); - /* * call-seq: * rat.fdiv(numeric) -> float @@ -968,7 +970,7 @@ nurat_fdiv(VALUE self, VALUE other) { VALUE div; if (f_zero_p(other)) - return DBL2NUM(nurat_to_double(self) / 0.0); + return nurat_div(self, rb_float_new(0.0)); if (FIXNUM_P(other) && other == LONG2FIX(1)) return nurat_to_f(self); div = nurat_div(self, other); @@ -2336,13 +2338,13 @@ negate_num(VALUE num) } static int -read_num(const char **s, const char *const end, VALUE *num, VALUE *div) +read_num(const char **s, const char *const end, VALUE *num, VALUE *nexp) { VALUE fp = ONE, exp, fn = ZERO, n = ZERO; int expsign = 0, ok = 0; char *e; - *div = ONE; + *nexp = ZERO; *num = ZERO; if (*s < end && **s != '.') { n = rb_int_parse_cstr(*s, end-*s, &e, NULL, @@ -2364,10 +2366,9 @@ read_num(const char **s, const char *const end, VALUE *num, VALUE *div) return 1; *s = e; { - VALUE l = f_expt10(SIZET2NUM(count)); + VALUE l = f_expt10(*nexp = SIZET2NUM(count)); n = n == ZERO ? fp : rb_int_plus(rb_int_mul(*num, l), fp); *num = n; - *div = l; fn = SIZET2NUM(count); } ok = 1; @@ -2384,18 +2385,12 @@ read_num(const char **s, const char *const end, VALUE *num, VALUE *div) if (exp != ZERO) { if (expsign == '-') { if (fn != ZERO) exp = rb_int_plus(exp, fn); - *div = f_expt10(exp); } else { if (fn != ZERO) exp = rb_int_minus(exp, fn); - if (INT_NEGATIVE_P(exp)) { - *div = f_expt10(negate_num(exp)); - } - else { - if (exp != ZERO) *num = rb_int_mul(n, f_expt10(exp)); - *div = ONE; - } + exp = negate_num(exp); } + *nexp = exp; } } @@ -2414,22 +2409,21 @@ static VALUE parse_rat(const char *s, const char *const e, int strict, int raise) { int sign; - VALUE num, den, ndiv, ddiv; + VALUE num, den, nexp, dexp; s = skip_ws(s, e); sign = read_sign(&s, e); - if (!read_num(&s, e, &num, &ndiv)) { + if (!read_num(&s, e, &num, &nexp)) { if (strict) return Qnil; return canonicalization ? ZERO : nurat_s_alloc(rb_cRational); } - nurat_reduce(&num, &ndiv); - den = ndiv; + den = ONE; if (s < e && *s == '/') { s++; - if (!read_num(&s, e, &den, &ddiv)) { + if (!read_num(&s, e, &den, &dexp)) { if (strict) return Qnil; - den = ndiv; + den = ONE; } else if (den == ZERO) { if (!raise) return Qnil; @@ -2439,17 +2433,38 @@ parse_rat(const char *s, const char *const e, int strict, int raise) return Qnil; } else { - nurat_reduce(&den, &ddiv); + nexp = rb_int_minus(nexp, dexp); nurat_reduce(&num, &den); - nurat_reduce(&ndiv, &ddiv); - if (ndiv != ONE) den = rb_int_mul(den, ndiv); - if (ddiv != ONE) num = rb_int_mul(num, ddiv); } } else if (strict && skip_ws(s, e) != e) { return Qnil; } + if (nexp != ZERO) { + if (INT_NEGATIVE_P(nexp)) { + VALUE mul; + if (!FIXNUM_P(nexp)) { + overflow: + return sign == '-' ? DBL2NUM(-HUGE_VAL) : DBL2NUM(HUGE_VAL); + } + mul = f_expt10(LONG2NUM(-FIX2LONG(nexp))); + if (RB_FLOAT_TYPE_P(mul)) goto overflow; + num = rb_int_mul(num, mul); + } + else { + VALUE div; + if (!FIXNUM_P(nexp)) { + underflow: + return sign == '-' ? DBL2NUM(-0.0) : DBL2NUM(+0.0); + } + div = f_expt10(nexp); + if (RB_FLOAT_TYPE_P(div)) goto underflow; + den = rb_int_mul(den, div); + } + nurat_reduce(&num, &den); + } + if (sign == '-') { num = negate_num(num); } @@ -2459,6 +2474,8 @@ parse_rat(const char *s, const char *const e, int strict, int raise) return num; } +#define FLOAT_ZERO_P(x) (rb_float_value(x) == 0.0) + static VALUE string_to_r_strict(VALUE self, int raise) { @@ -2473,7 +2490,7 @@ string_to_r_strict(VALUE self, int raise) self); } - if (RB_FLOAT_TYPE_P(num)) { + if (RB_FLOAT_TYPE_P(num) && !FLOAT_ZERO_P(num)) { if (!raise) return Qnil; rb_raise(rb_eFloatDomainError, "Infinity"); } @@ -2517,7 +2534,7 @@ string_to_r(VALUE self) num = parse_rat(RSTRING_PTR(self), RSTRING_END(self), 0, TRUE); - if (RB_FLOAT_TYPE_P(num)) + if (RB_FLOAT_TYPE_P(num) && !FLOAT_ZERO_P(num)) rb_raise(rb_eFloatDomainError, "Infinity"); return num; } @@ -2529,7 +2546,7 @@ rb_cstr_to_rat(const char *s, int strict) /* for complex's internal */ num = parse_rat(s, s + strlen(s), strict, TRUE); - if (RB_FLOAT_TYPE_P(num)) + if (RB_FLOAT_TYPE_P(num) && !FLOAT_ZERO_P(num)) rb_raise(rb_eFloatDomainError, "Infinity"); return num; } diff --git a/re.c b/re.c index 394fc461295c79..84027707e05c70 100644 --- a/re.c +++ b/re.c @@ -2037,7 +2037,7 @@ match_aref(int argc, VALUE *argv, VALUE match) /* * call-seq: * - * mtch.values_at([index]*) -> array + * mtch.values_at(index, ...) -> array * * Uses each index to access the matching values, returning an array of * the corresponding matches. @@ -2389,7 +2389,8 @@ unescape_escaped_nonascii(const char **pp, const char *end, rb_encoding *enc, { const char *p = *pp; int chmaxlen = rb_enc_mbmaxlen(enc); - char *chbuf = ALLOCA_N(char, chmaxlen); + unsigned char *area = ALLOCA_N(unsigned char, chmaxlen); + char *chbuf = (char *)area; int chlen = 0; int byte; int l; @@ -2401,14 +2402,14 @@ unescape_escaped_nonascii(const char **pp, const char *end, rb_encoding *enc, return -1; } - chbuf[chlen++] = byte; + area[chlen++] = byte; while (chlen < chmaxlen && MBCLEN_NEEDMORE_P(rb_enc_precise_mbclen(chbuf, chbuf+chlen, enc))) { byte = read_escaped_byte(&p, end, err); if (byte == -1) { return -1; } - chbuf[chlen++] = byte; + area[chlen++] = byte; } l = rb_enc_precise_mbclen(chbuf, chbuf+chlen, enc); @@ -2416,7 +2417,7 @@ unescape_escaped_nonascii(const char **pp, const char *end, rb_encoding *enc, errcpy(err, "invalid multibyte escape"); return -1; } - if (1 < chlen || (chbuf[0] & 0x80)) { + if (1 < chlen || (area[0] & 0x80)) { rb_str_buf_cat(buf, chbuf, chlen); if (*encp == 0) @@ -2428,7 +2429,7 @@ unescape_escaped_nonascii(const char **pp, const char *end, rb_encoding *enc, } else { char escbuf[5]; - snprintf(escbuf, sizeof(escbuf), "\\x%02X", chbuf[0]&0xff); + snprintf(escbuf, sizeof(escbuf), "\\x%02X", area[0]&0xff); rb_str_buf_cat(buf, escbuf, 4); } *pp = p; @@ -2538,7 +2539,7 @@ unescape_nonascii(const char *p, const char *end, rb_encoding *enc, VALUE buf, rb_encoding **encp, int *has_property, onig_errmsg_buffer err) { - char c; + unsigned char c; char smallbuf[2]; while (p < end) { @@ -2601,8 +2602,9 @@ unescape_nonascii(const char *p, const char *end, rb_encoding *enc, p = p-2; if (enc == rb_usascii_encoding()) { const char *pbeg = p; - c = read_escaped_byte(&p, end, err); - if (c == (char)-1) return -1; + int byte = read_escaped_byte(&p, end, err); + if (byte == -1) return -1; + c = byte; rb_str_buf_cat(buf, pbeg, p-pbeg); } else { @@ -2651,7 +2653,7 @@ unescape_nonascii(const char *p, const char *end, rb_encoding *enc, break; default: - rb_str_buf_cat(buf, &c, 1); + rb_str_buf_cat(buf, (char *)&c, 1); break; } } diff --git a/regparse.c b/regparse.c index af19f23bf45376..362352d15bc1a8 100644 --- a/regparse.c +++ b/regparse.c @@ -5722,6 +5722,86 @@ extern const OnigCodePoint onigenc_unicode_GCB_ranges_GAZ[]; extern const OnigCodePoint onigenc_unicode_GCB_ranges_E_Base[]; extern const OnigCodePoint onigenc_unicode_GCB_ranges_Emoji[]; +/* + * helper methods for node_extended_grapheme_cluster (/\X/) + */ +static int +create_property_node(Node **np, ScanEnv* env, const char* propname) +{ + int r; + CClassNode* cc; + + *np = node_new_cclass(); + if (IS_NULL(*np)) return ONIGERR_MEMORY; + cc = NCCLASS(*np); + r = add_property_to_cc(cc, propname, 0, env); + if (r != 0) + onig_node_free(*np); + return r; +} + +static int +quantify_node(Node **np, int lower, int upper) +{ + Node* tmp = node_new_quantifier(lower, upper, 0); + if (IS_NULL(tmp)) return ONIGERR_MEMORY; + NQTFR(tmp)->target = *np; + *np = tmp; + return 0; +} + +static int +quantify_property_node(Node **np, ScanEnv* env, const char* propname, char repetitions) +{ + int r; + int lower = 0; + int upper = REPEAT_INFINITE; + + r = create_property_node(np, env, propname); + if (r != 0) return r; + switch (repetitions) { + case '?': + upper = 1; + break; + case '+': + lower = 1; + break; + case '*': + break; + case '2': + lower = upper = 2; + break; + default: + return ONIGERR_PARSER_BUG; + } + return quantify_node(np, lower, upper); +} + +/* IMPORTANT: Make sure node_array ends with NULL_NODE */ +static int +create_sequence_node(Node **np, Node **node_array) +{ + Node* tmp = NULL_NODE; + int i = 0; + + while (node_array[i] != NULL_NODE) i++; + while (--i >= 0) { + *np = node_new_list(node_array[i], tmp); + if (IS_NULL(*np)) { + while (i >= 0) { + onig_node_free(node_array[i]); + node_array[i--] = NULL_NODE; + } + onig_node_free(tmp); + return ONIGERR_MEMORY; + } + tmp = *np; + } + return 0; +} + +#define R_ERR(call) r=(call);if(r!=0)goto err + static int node_extended_grapheme_cluster(Node** np, ScanEnv* env) { @@ -5742,46 +5822,43 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) /* UTF-8, UTF-16BE/LE, UTF-32BE/LE */ CClassNode* cc; OnigCodePoint sb_out = (ONIGENC_MBC_MINLEN(env->enc) > 1) ? 0x00 : 0x80; - int extend = propname2ctype(env, "Grapheme_Cluster_Break=Extend"); - if (extend < 0) goto err; - /* Prepend* + if (propname2ctype(env, "Grapheme_Cluster_Break=Extend") < 0) goto err; + /* main comment: The order of the code is mostly in reverse of the order + * the various expressions appear in the grammar */ + /* Unicode 10.0.0 */ + /* CRLF + * | Prepend* * ( RI-sequence | Hangul-Syllable | !Control ) - * ( Grapheme_Extend | SpacingMark )* */ + * ( Grapheme_Extend | SpacingMark )* + * | . */ + /* Unicode 10.0.0 */ /* ( Grapheme_Extend | SpacingMark )* */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; + R_ERR(create_property_node(&np1, env, "Grapheme_Cluster_Break=Extend")); + cc = NCCLASS(np1); - r = add_ctype_to_cc(cc, extend, 0, 0, env); - if (r != 0) goto err; - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=SpacingMark", 0, env); - if (r != 0) goto err; - r = add_code_range(&(cc->mbuf), env, 0x200D, 0x200D); - if (r != 0) goto err; + R_ERR(add_property_to_cc(cc, "Grapheme_Cluster_Break=SpacingMark", 0, env)); + R_ERR(add_code_range(&(cc->mbuf), env, 0x200D, 0x200D)); - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_node(&np1, 0, REPEAT_INFINITE)); tmp = node_new_list(np1, NULL_NODE); if (IS_NULL(tmp)) goto err; list = tmp; np1 = NULL; + /* Unicode 10.0.0 */ /* ( RI-sequence | Hangul-Syllable | !Control ) */ /* !Control */ np1 = node_new_cclass(); if (IS_NULL(np1)) goto err; cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=Control", 1, env); - if (r != 0) goto err; + R_ERR(add_property_to_cc(cc, "Grapheme_Cluster_Break=Control", 1, env)); if (ONIGENC_MBC_MINLEN(env->enc) > 1) { BBuf *pbuf2 = NULL; - r = add_code_range(&pbuf1, env, 0x0a, 0x0a); - if (r != 0) goto err; - r = add_code_range(&pbuf1, env, 0x0d, 0x0d); + R_ERR(add_code_range(&pbuf1, env, 0x0a, 0x0a)); + R_ERR(add_code_range(&pbuf1, env, 0x0d, 0x0d)); if (r != 0) goto err; r = and_code_range_buf(cc->mbuf, 0, pbuf1, 1, &pbuf2, env); if (r != 0) { @@ -5803,24 +5880,22 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) alt = tmp; np1 = NULL; + /* Unicode 10.0.0 */ /* Hangul-Syllable * := L* V+ T* * | L* LV V* T* * | L* LVT T* * | L+ * | T+ */ + /* Unicode 11.0.0 */ + /* Hangul-Syllable + * := L* (V+ | LV V* | LVT) T* + * | L+ + * | T+ */ + /* these are equivalent, so we leave things as is for the moment */ /* T+ */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=T", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(1, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(&np1, env, "Grapheme_Cluster_Break=T", '+')); tmp = onig_node_new_alt(np1, alt); if (IS_NULL(tmp)) goto err; @@ -5828,16 +5903,7 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) np1 = NULL; /* L+ */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=L", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(1, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(&np1, env, "Grapheme_Cluster_Break=L", '+')); tmp = onig_node_new_alt(np1, alt); if (IS_NULL(tmp)) goto err; @@ -5845,48 +5911,16 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) np1 = NULL; /* L* LVT T* */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=T", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=LVT", 0, env); - if (r != 0) goto err; - - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=L", 0, env); - if (r != 0) goto err; + { + Node* seq[4]; - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(seq+0, env, "Grapheme_Cluster_Break=L", '*')); + R_ERR(create_property_node(seq+1, env, "Grapheme_Cluster_Break=LVT")); + R_ERR(quantify_property_node(seq+2, env, "Grapheme_Cluster_Break=T", '*')); - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + seq[3] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); + } tmp = onig_node_new_alt(list2, alt); if (IS_NULL(tmp)) goto err; @@ -5894,64 +5928,17 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) list2 = NULL; /* L* LV V* T* */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=T", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=V", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=LV", 0, env); - if (r != 0) goto err; - - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=L", 0, env); - if (r != 0) goto err; + { + Node* seq[5]; - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(seq+0, env, "Grapheme_Cluster_Break=L", '*')); + R_ERR(create_property_node(seq+1, env, "Grapheme_Cluster_Break=LV")); + R_ERR(quantify_property_node(seq+2, env, "Grapheme_Cluster_Break=V", '*')); + R_ERR(quantify_property_node(seq+3, env, "Grapheme_Cluster_Break=T", '*')); - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + seq[4] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); + } tmp = onig_node_new_alt(list2, alt); if (IS_NULL(tmp)) goto err; @@ -5959,181 +5946,88 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) list2 = NULL; /* L* V+ T* */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=T", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=V", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(1, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=L", 0, env); - if (r != 0) goto err; + { + Node* seq[4]; - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(seq+0, env, "Grapheme_Cluster_Break=L", '*')); + R_ERR(quantify_property_node(seq+1, env, "Grapheme_Cluster_Break=V", '+')); + R_ERR(quantify_property_node(seq+2, env, "Grapheme_Cluster_Break=T", '*')); - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + seq[3] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); + } tmp = onig_node_new_alt(list2, alt); if (IS_NULL(tmp)) goto err; alt = tmp; list2 = NULL; + /* end of Hangul-Syllable */ + /* Unicode 10.0.0 */ /* Emoji sequence := (E_Base | EBG) Extend* E_Modifier? * (ZWJ (Glue_After_Zwj | EBG Extend* E_Modifier?) )* */ - /* ZWJ (Glue_After_Zwj | E_Base_GAZ Extend* E_Modifier?) */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Modifier", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, 1, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_ctype_to_cc(cc, extend, 0, 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Base_GAZ", 0, env); - if (r != 0) goto err; + /* E_Base_GAZ Extend* E_Modifier? */ + { + Node* seq[4]; + R_ERR(create_property_node(seq+0, env, "Grapheme_Cluster_Break=E_Base_GAZ")); + R_ERR(quantify_property_node(seq+1, env, "Grapheme_Cluster_Break=Extend", '*')); + R_ERR(quantify_property_node(seq+2, env, "Grapheme_Cluster_Break=E_Modifier", '?')); - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + seq[3] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); + } tmp = onig_node_new_alt(list2, NULL_NODE); if (IS_NULL(tmp)) goto err; alt2 = tmp; list2 = NULL; + /* Unicode 10.0.0 */ /* Glue_After_Zwj */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_ctype_to_cc(cc, extend, 0, 0, env); - if (r != 0) goto err; + { + Node* seq[3]; - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + seq[0] = node_new_cclass(); + if (IS_NULL(seq[0])) goto err; + cc = NCCLASS(seq[0]); + R_ERR(add_ctype_to_cc_by_range(cc, -1, 0, env, sb_out, onigenc_unicode_GCB_ranges_GAZ)); + R_ERR(add_property_to_cc(cc, "Grapheme_Cluster_Break=Glue_After_Zwj", 0, env)); - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + R_ERR(quantify_property_node(seq+1, env, "Grapheme_Cluster_Break=Extend", '*')); - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - { - const OnigCodePoint *ranges = onigenc_unicode_GCB_ranges_GAZ; - r = add_ctype_to_cc_by_range(cc, -1, 0, env, sb_out, ranges); - if (r != 0) goto err; + seq[2] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); } - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=Glue_After_Zwj", 0, env); - if (r != 0) goto err; - - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; tmp = onig_node_new_alt(list2, alt2); if (IS_NULL(tmp)) goto err; alt2 = tmp; list2 = NULL; + /* Unicode 10.0.0 */ /* Emoji variation sequence * http://unicode.org/Public/emoji/4.0/emoji-zwj-sequences.txt */ - r = ONIGENC_CODE_TO_MBC(env->enc, 0xfe0f, buf); - if (r < 0) goto err; - np1 = node_new_str_raw(buf, buf + r); - if (IS_NULL(np1)) goto err; + { + Node* seq[3]; - tmp = node_new_quantifier(0, 1, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + seq[0] = node_new_cclass(); + if (IS_NULL(seq[0])) goto err; + cc = NCCLASS(seq[0]); + R_ERR(add_ctype_to_cc_by_range(cc, -1, 0, env, sb_out, onigenc_unicode_GCB_ranges_Emoji)); - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + r = ONIGENC_CODE_TO_MBC(env->enc, 0xfe0f, buf); /* VARIATION SELECTOR-16 */ + if (r < 0) goto err; + seq[1] = node_new_str_raw(buf, buf + r); + if (IS_NULL(seq[1])) goto err; + R_ERR(quantify_node(seq+1, 0, 1)); - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - { - const OnigCodePoint *ranges = onigenc_unicode_GCB_ranges_Emoji; - r = add_ctype_to_cc_by_range(cc, -1, 0, env, sb_out, ranges); - if (r != 0) goto err; + seq[2] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); } - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - tmp = onig_node_new_alt(list2, alt2); if (IS_NULL(tmp)) goto err; alt2 = tmp; @@ -6145,7 +6039,7 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) alt2 = NULL; /* ZWJ */ - r = ONIGENC_CODE_TO_MBC(env->enc, 0x200D, buf); + r = ONIGENC_CODE_TO_MBC(env->enc, 0x200D, buf); /* ZERO WIDTH JOINER (ZWJ) */ if (r < 0) goto err; np1 = node_new_str_raw(buf, buf + r); if (IS_NULL(np1)) goto err; @@ -6155,10 +6049,8 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) list2 = tmp; np1 = NULL; - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = list2; - np1 = tmp; + R_ERR(quantify_node(&list2, 0, REPEAT_INFINITE)); + np1 = list2; list2 = NULL; tmp = node_new_list(np1, NULL_NODE); @@ -6167,16 +6059,7 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) np1 = NULL; /* E_Modifier? */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Modifier", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, 1, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(&np1, env, "Grapheme_Cluster_Break=E_Modifier", '?')); tmp = node_new_list(np1, list2); if (IS_NULL(tmp)) goto err; @@ -6184,16 +6067,7 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) np1 = NULL; /* Extend* */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_ctype_to_cc(cc, extend, 0, 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(&np1, env, "Grapheme_Cluster_Break=Extend", '*')); tmp = node_new_list(np1, list2); if (IS_NULL(tmp)) goto err; @@ -6204,15 +6078,9 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) np1 = node_new_cclass(); if (IS_NULL(np1)) goto err; cc = NCCLASS(np1); - { - const OnigCodePoint *ranges = onigenc_unicode_GCB_ranges_E_Base; - r = add_ctype_to_cc_by_range(cc, -1, 0, env, sb_out, ranges); - if (r != 0) goto err; - } - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Base", 0, env); - if (r != 0) goto err; - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Base_GAZ", 0, env); - if (r != 0) goto err; + R_ERR(add_ctype_to_cc_by_range(cc, -1, 0, env, sb_out, onigenc_unicode_GCB_ranges_E_Base)); + R_ERR(add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Base", 0, env)); + R_ERR(add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Base_GAZ", 0, env)); tmp = node_new_list(np1, list2); if (IS_NULL(tmp)) goto err; @@ -6224,68 +6092,44 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) alt = tmp; list2 = NULL; - /* ZWJ (E_Base_GAZ | Glue_After_Zwj) E_Modifier? */ + /* Unicode 10.0.0 */ /* a sequence starting with ZWJ seems artificial, but GraphemeBreakTest * has such examples. * http://www.unicode.org/Public/9.0.0/ucd/auxiliary/GraphemeBreakTest.html */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Modifier", 0, env); - if (r != 0) goto err; + /* ZWJ (E_Base_GAZ | Glue_After_Zwj) E_Modifier? */ + { + Node* seq[4]; - tmp = node_new_quantifier(0, 1, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + r = ONIGENC_CODE_TO_MBC(env->enc, 0x200D, buf); /* ZERO WIDTH JOINER (ZWJ) */ + if (r < 0) goto err; + seq[0] = node_new_str_raw(buf, buf + r); + if (IS_NULL(seq[0])) goto err; - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + seq[1] = node_new_cclass(); + if (IS_NULL(seq[1])) goto err; + cc = NCCLASS(seq[1]); + R_ERR(add_property_to_cc(cc, "Grapheme_Cluster_Break=Glue_After_Zwj", 0, env)); + R_ERR(add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Base_GAZ", 0, env)); - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=Glue_After_Zwj", 0, env); - if (r != 0) goto err; - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=E_Base_GAZ", 0, env); - if (r != 0) goto err; + R_ERR(quantify_property_node(seq+2, env, "Grapheme_Cluster_Break=E_Modifier", '?')); - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; - - r = ONIGENC_CODE_TO_MBC(env->enc, 0x200D, buf); - if (r < 0) goto err; - np1 = node_new_str_raw(buf, buf + r); - if (IS_NULL(np1)) goto err; - - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + seq[3] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); + } tmp = onig_node_new_alt(list2, alt); if (IS_NULL(tmp)) goto err; alt = tmp; list2 = NULL; + /* Unicode 10.0.0/11.0.0 */ + /* this is actually Regional_Indicator+ in Unicode 10.0.0, + * but it is Regional_Indicator{2} in Unicode 11.0.0, so no need to fix */ /* RI-Sequence := Regional_Indicator{2} */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_code_range(&(cc->mbuf), env, 0x1F1E6, 0x1F1FF); - if (r != 0) goto err; + R_ERR(quantify_property_node(&np1, env, "Regional_Indicator", '2')); - tmp = node_new_quantifier(2, 2, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, list2); + tmp = node_new_list(np1, list2); /* here, list2 should be guaranteed to be NULL */ if (IS_NULL(tmp)) goto err; list2 = tmp; np1 = NULL; @@ -6301,16 +6145,7 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) alt = NULL; /* Prepend* */ - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=Prepend", 0, env); - if (r != 0) goto err; - - tmp = node_new_quantifier(0, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + R_ERR(quantify_property_node(&np1, env, "Grapheme_Cluster_Break=Prepend", '*')); tmp = node_new_list(np1, list); if (IS_NULL(tmp)) goto err; @@ -6334,36 +6169,20 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) np1 = NULL; /* Prepend+ */ - r = ONIGENC_CODE_TO_MBC(env->enc, 0x200D, buf); - if (r < 0) goto err; - np1 = node_new_str_raw(buf, buf + r); - if (IS_NULL(np1)) goto err; - - tmp = node_new_quantifier(0, 1, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; - - tmp = node_new_list(np1, NULL_NODE); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + { + Node* seq[3]; - np1 = node_new_cclass(); - if (IS_NULL(np1)) goto err; - cc = NCCLASS(np1); - r = add_property_to_cc(cc, "Grapheme_Cluster_Break=Prepend", 0, env); - if (r != 0) goto err; + R_ERR(quantify_property_node(seq+0, env, "Grapheme_Cluster_Break=Prepend", '+')); - tmp = node_new_quantifier(1, REPEAT_INFINITE, 0); - if (IS_NULL(tmp)) goto err; - NQTFR(tmp)->target = np1; - np1 = tmp; + r = ONIGENC_CODE_TO_MBC(env->enc, 0x200D, buf); /* does this belong to Prepend?? */ + if (r < 0) goto err; + seq[1] = node_new_str_raw(buf, buf + r); + if (IS_NULL(seq[1])) goto err; + R_ERR(quantify_node(seq+1, 0, 1)); - tmp = node_new_list(np1, list2); - if (IS_NULL(tmp)) goto err; - list2 = tmp; - np1 = NULL; + seq[2] = NULL_NODE; + R_ERR(create_sequence_node(&list2, seq)); + } tmp = onig_node_new_alt(list2, alt); if (IS_NULL(tmp)) goto err; @@ -6439,6 +6258,7 @@ node_extended_grapheme_cluster(Node** np, ScanEnv* env) bbuf_free(pbuf1); return (r == 0) ? ONIGERR_MEMORY : r; } +#undef R_ERR static int countbits(unsigned int bits) diff --git a/ruby-runner.c b/ruby-runner.c index 3d59ebb76073f9..0ad2561aa4720f 100644 --- a/ruby-runner.c +++ b/ruby-runner.c @@ -3,8 +3,11 @@ #include #include #include +#include +#include #include "ruby-runner.h" +#include "ruby/config.h" #define STRINGIZE(expr) STRINGIZE0(expr) #define STRINGIZE0(expr) #expr @@ -57,6 +60,10 @@ main(int argc, char **argv) PATH_SEPARATOR EXTOUT_DIR"/"ARCH ; +#ifndef LOAD_RELATIVE + static const char mjit_build_dir[] = BUILDDIR"/mjit_build_dir."SOEXT; + struct stat stbuf; +#endif const size_t dirsize = sizeof(builddir); const size_t namesize = sizeof(rubypath) - dirsize; const char *rubyname = rubypath + dirsize; @@ -64,6 +71,12 @@ main(int argc, char **argv) insert_env_path(LIBPATHENV, builddir, dirsize, 1); insert_env_path("RUBYLIB", rubylib, sizeof(rubylib), 0); +#ifndef LOAD_RELATIVE + if (PRELOADENV[0] && stat(mjit_build_dir, &stbuf) == 0) { + insert_env_path(PRELOADENV, mjit_build_dir, sizeof(mjit_build_dir), 1); + setenv("MJIT_SEARCH_BUILD_DIR", "true", 0); + } +#endif if (!(p = strrchr(arg0, '/'))) p = arg0; else p++; if (strlen(p) < namesize - 1) { diff --git a/ruby.c b/ruby.c index a791371a3abfe6..93298cbc31b92e 100644 --- a/ruby.c +++ b/ruby.c @@ -252,8 +252,8 @@ usage(const char *name, int help) M("-w", "", "turn warnings on for your script"), M("-W[level=2]", "", "set warning level; 0=silence, 1=medium, 2=verbose"), M("-x[directory]", "", "strip off text before #!ruby line and perhaps cd to directory"), - M("--jit", "", "enable MJIT with default options (experimental)"), - M("--jit-[option]","", "enable MJIT with an option (experimental)"), + M("--jit", "", "enable JIT with default options (experimental)"), + M("--jit-[option]","", "enable JIT with an option (experimental)"), M("-h", "", "show this message, --help for more info"), }; static const struct message help_msg[] = { @@ -279,14 +279,14 @@ usage(const char *name, int help) M("did_you_mean", "", "did_you_mean (default: "DEFAULT_RUBYGEMS_ENABLED")"), M("rubyopt", "", "RUBYOPT environment variable (default: enabled)"), M("frozen-string-literal", "", "freeze all string literals (default: disabled)"), - M("jit", "", "MJIT (default: disabled)"), + M("jit", "", "JIT compiler (default: disabled)"), }; static const struct message mjit_options[] = { - M("--jit-warnings", "", "Enable printing MJIT warnings"), - M("--jit-debug", "", "Enable MJIT debugging (very slow)"), + M("--jit-warnings", "", "Enable printing JIT warnings"), + M("--jit-debug", "", "Enable JIT debugging (very slow)"), M("--jit-wait", "", "Wait until JIT compilation is finished everytime (for testing)"), - M("--jit-save-temps", "", "Save MJIT temporary files in $TMP or /tmp (for testing)"), - M("--jit-verbose=num", "", "Print MJIT logs of level num or less to stderr (default: 0)"), + M("--jit-save-temps", "", "Save JIT temporary files in $TMP or /tmp (for testing)"), + M("--jit-verbose=num", "", "Print JIT logs of level num or less to stderr (default: 0)"), M("--jit-max-cache=num", "", "Max number of methods to be JIT-ed in a cache (default: 1000)"), M("--jit-min-calls=num", "", "Number of calls to trigger JIT (for testing, default: 5)"), }; @@ -308,7 +308,7 @@ usage(const char *name, int help) puts("Features:"); for (i = 0; i < numberof(features); ++i) SHOW(features[i]); - puts("MJIT options (experimental):"); + puts("JIT options (experimental):"); for (i = 0; i < numberof(mjit_options); ++i) SHOW(mjit_options[i]); } @@ -1429,6 +1429,7 @@ opt_enc_index(VALUE enc_name) #define rb_progname (GET_VM()->progname) #define rb_orig_progname (GET_VM()->orig_progname) VALUE rb_argv0; +VALUE rb_e_script; static VALUE false_value(void) @@ -1856,6 +1857,10 @@ process_options(int argc, char **argv, ruby_cmdline_options_t *opt) rb_define_readonly_boolean("$-l", opt->do_line); rb_define_readonly_boolean("$-a", opt->do_split); + if ((rb_e_script = opt->e_script) != 0) { + rb_gc_register_mark_object(opt->e_script); + } + rb_set_safe_level(opt->safe_level); return (VALUE)iseq; diff --git a/siphash.h b/siphash.h index 2e7553f2084177..f49bc511b15caf 100644 --- a/siphash.h +++ b/siphash.h @@ -43,6 +43,6 @@ int sip_hash_digest_integer(sip_hash *h, const uint8_t *data, size_t data_len, u void sip_hash_free(sip_hash *h); void sip_hash_dump(sip_hash *h); -uint64_t sip_hash13(const uint8_t key[16], const uint8_t *data, size_t len); +NO_SANITIZE("unsigned-integer-overflow", uint64_t sip_hash13(const uint8_t key[16], const uint8_t *data, size_t len)); #endif diff --git a/spec/README.md b/spec/README.md index 7f2b827a1f15a8..59c2c605c54649 100644 --- a/spec/README.md +++ b/spec/README.md @@ -1,3 +1,14 @@ +# spec/bundler + +spec/bundler is rspec examples for bundler library(lib/bundler.rb, lib/bundler/*). + +## Running spec/bundler + +To run rspec for bundler: +```bash +make test-bundler +``` + # spec/ruby ruby/spec (https://github.com/ruby/spec/) is diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb new file mode 100644 index 00000000000000..194d6752b2ca5f --- /dev/null +++ b/spec/bundler/bundler/bundler_spec.rb @@ -0,0 +1,490 @@ +# encoding: utf-8 +# frozen_string_literal: true + +require "bundler" +require "tmpdir" + +RSpec.describe Bundler do + describe "#load_gemspec_uncached" do + let(:app_gemspec_path) { tmp("test.gemspec") } + subject { Bundler.load_gemspec_uncached(app_gemspec_path) } + + context "with incorrect YAML file" do + before do + File.open(app_gemspec_path, "wb") do |f| + f.write strip_whitespace(<<-GEMSPEC) + --- + {:!00 ao=gu\g1= 7~f + GEMSPEC + end + end + + it "catches YAML syntax errors" do + expect { subject }.to raise_error(Bundler::GemspecError, /error while loading `test.gemspec`/) + end + + context "on Rubies with a settable YAML engine", :if => defined?(YAML::ENGINE) do + context "with Syck as YAML::Engine" do + it "raises a GemspecError after YAML load throws ArgumentError" do + orig_yamler = YAML::ENGINE.yamler + YAML::ENGINE.yamler = "syck" + + expect { subject }.to raise_error(Bundler::GemspecError) + + YAML::ENGINE.yamler = orig_yamler + end + end + + context "with Psych as YAML::Engine" do + it "raises a GemspecError after YAML load throws Psych::SyntaxError" do + orig_yamler = YAML::ENGINE.yamler + YAML::ENGINE.yamler = "psych" + + expect { subject }.to raise_error(Bundler::GemspecError) + + YAML::ENGINE.yamler = orig_yamler + end + end + end + end + + context "with correct YAML file", :if => defined?(Encoding) do + it "can load a gemspec with unicode characters with default ruby encoding" do + # spec_helper forces the external encoding to UTF-8 but that's not the + # default until Ruby 2.0 + verbose = $VERBOSE + $VERBOSE = false + encoding = Encoding.default_external + Encoding.default_external = "ASCII" + $VERBOSE = verbose + + File.open(app_gemspec_path, "wb") do |file| + file.puts <<-GEMSPEC.gsub(/^\s+/, "") + # -*- encoding: utf-8 -*- + Gem::Specification.new do |gem| + gem.author = "André the Giant" + end + GEMSPEC + end + + expect(subject.author).to eq("André the Giant") + + verbose = $VERBOSE + $VERBOSE = false + Encoding.default_external = encoding + $VERBOSE = verbose + end + end + + it "sets loaded_from" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC + end + + expect(subject.loaded_from).to eq(app_gemspec_path.expand_path.to_s) + end + + context "validate is true" do + subject { Bundler.load_gemspec_uncached(app_gemspec_path, true) } + + it "validates the specification" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC + end + expect(Bundler.rubygems).to receive(:validate).with have_attributes(:name => "validated") + subject + end + end + + context "with gemspec containing local variables" do + before do + File.open(app_gemspec_path, "wb") do |f| + f.write strip_whitespace(<<-GEMSPEC) + must_not_leak = true + Gem::Specification.new do |gem| + gem.name = "leak check" + end + GEMSPEC + end + end + + it "should not pollute the TOPLEVEL_BINDING" do + subject + expect(TOPLEVEL_BINDING.eval("local_variables")).to_not include(:must_not_leak) + end + end + end + + describe "#which" do + let(:executable) { "executable" } + let(:path) { %w[/a /b c ../d /e] } + let(:expected) { "executable" } + + before do + ENV["PATH"] = path.join(File::PATH_SEPARATOR) + + allow(File).to receive(:file?).and_return(false) + allow(File).to receive(:executable?).and_return(false) + if expected + expect(File).to receive(:file?).with(expected).and_return(true) + expect(File).to receive(:executable?).with(expected).and_return(true) + end + end + + subject { described_class.which(executable) } + + shared_examples_for "it returns the correct executable" do + it "returns the expected file" do + expect(subject).to eq(expected) + end + end + + it_behaves_like "it returns the correct executable" + + context "when the executable in inside a quoted path" do + let(:expected) { "/e/executable" } + it_behaves_like "it returns the correct executable" + end + + context "when the executable is not found" do + let(:expected) { nil } + it_behaves_like "it returns the correct executable" + end + end + + describe "configuration" do + context "disable_shared_gems" do + it "should unset GEM_PATH with empty string" do + env = {} + expect(Bundler).to receive(:use_system_gems?).and_return(false) + Bundler.send(:configure_gem_path, env) + expect(env.keys).to include("GEM_PATH") + expect(env["GEM_PATH"]).to eq "" + end + end + end + + describe "#rm_rf" do + context "the directory is world writable" do + let(:bundler_ui) { Bundler.ui } + it "should raise a friendly error" do + allow(File).to receive(:exist?).and_return(true) + allow(bundler_fileutils).to receive(:remove_entry_secure).and_raise(ArgumentError) + allow(File).to receive(:world_writable?).and_return(true) + message = < true) + end + end + end + + describe "#user_home" do + context "home directory is set" do + it "should return the user home" do + path = "/home/oggy" + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).with(path).and_return true + expect(Bundler.user_home).to eq(Pathname(path)) + end + + context "is not a directory" do + it "should issue a warning and return a temporary user home" do + path = "/home/oggy" + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return false + allow(Etc).to receive(:getlogin).and_return("USER") + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(true) + expect(FileUtils).to receive(:mkpath).with("/TMP/bundler/home/USER") + message = <" do + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| + ruby = ENV["BUNDLE_RUBY"] || "/usr/bin/env ruby" + f.puts "#!#{ruby}\nputs 'Hello, world'\n" + end + + with_path_added(tmp) do + bundle "testtasks" + end + + expect(exitstatus).to be_zero if exitstatus + expect(out).to eq("Hello, world") + end + + context "with no arguments" do + it "prints a concise help message", :bundler => "3" do + bundle! "" + expect(last_command.stderr).to be_empty + expect(last_command.stdout).to include("Bundler version #{Bundler::VERSION}"). + and include("\n\nBundler commands:\n\n"). + and include("\n\n Primary commands:\n"). + and include("\n\n Utilities:\n"). + and include("\n\nOptions:\n") + end + end + + context "when ENV['BUNDLE_GEMFILE'] is set to an empty string" do + it "ignores it" do + gemfile bundled_app("Gemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle :install, :env => { "BUNDLE_GEMFILE" => "" } + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "when ENV['RUBYGEMS_GEMDEPS'] is set" do + it "displays a warning" do + gemfile bundled_app("Gemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle :install, :env => { "RUBYGEMS_GEMDEPS" => "foo" } + expect(out).to include("RUBYGEMS_GEMDEPS") + expect(out).to include("conflict with Bundler") + + bundle :install, :env => { "RUBYGEMS_GEMDEPS" => "" } + expect(out).not_to include("RUBYGEMS_GEMDEPS") + end + end + + context "with --verbose" do + it "prints the running command" do + gemfile "" + bundle! "info bundler", :verbose => true + expect(last_command.stdout).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION}") + end + + it "doesn't print defaults" do + install_gemfile! "", :verbose => true + expect(last_command.stdout).to start_with("Running `bundle install --retry 0 --verbose` with bundler #{Bundler::VERSION}") + end + + it "doesn't print defaults" do + install_gemfile! "", :verbose => true + expect(last_command.stdout).to start_with("Running `bundle install --retry 0 --verbose` with bundler #{Bundler::VERSION}") + end + end + + describe "printing the outdated warning" do + shared_examples_for "no warning" do + it "prints no warning" do + bundle "fail" + expect(last_command.stdboth).to eq("Could not find command \"fail\".") + end + end + + let(:bundler_version) { "1.1" } + let(:latest_version) { nil } + before do + bundle! "config --global disable_version_check false" + + simulate_bundler_version(bundler_version) + if latest_version + info_path = home(".bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/bundler") + info_path.parent.mkpath + info_path.open("w") {|f| f.write "#{latest_version}\n" } + end + end + + context "when there is no latest version" do + include_examples "no warning" + end + + context "when the latest version is equal to the current version" do + let(:latest_version) { bundler_version } + include_examples "no warning" + end + + context "when the latest version is less than the current version" do + let(:latest_version) { "0.9" } + include_examples "no warning" + end + + context "when the latest version is greater than the current version" do + let(:latest_version) { "222.0" } + it "prints the version warning" do + bundle "fail" + expect(last_command.stdout).to start_with(<<-EOS.strip) +The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. +To install the latest version, run `gem install bundler` + EOS + end + + context "and disable_version_check is set" do + before { bundle! "config disable_version_check true" } + include_examples "no warning" + end + + context "running a parseable command" do + it "prints no warning" do + bundle! "config --parseable foo" + expect(last_command.stdboth).to eq "" + + bundle "platform --ruby" + expect(last_command.stdboth).to eq "Could not locate Gemfile" + end + end + + context "and is a pre-release" do + let(:latest_version) { "222.0.0.pre.4" } + it "prints the version warning" do + bundle "fail" + expect(last_command.stdout).to start_with(<<-EOS.strip) +The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. +To install the latest version, run `gem install bundler --pre` + EOS + end + end + end + end +end + +RSpec.describe "bundler executable" do + it "shows the bundler version just as the `bundle` executable does", :bundler => "< 3" do + bundler "--version" + expect(out).to eq("Bundler version #{Bundler::VERSION}") + end + + it "shows the bundler version just as the `bundle` executable does", :bundler => "3" do + bundler "--version" + expect(out).to eq(Bundler::VERSION) + end +end diff --git a/spec/bundler/bundler/compact_index_client/updater_spec.rb b/spec/bundler/bundler/compact_index_client/updater_spec.rb new file mode 100644 index 00000000000000..fd554a7b0dbe24 --- /dev/null +++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "net/http" +require "bundler/compact_index_client" +require "bundler/compact_index_client/updater" + +RSpec.describe Bundler::CompactIndexClient::Updater do + let(:fetcher) { double(:fetcher) } + let(:local_path) { Pathname("/tmp/localpath") } + let(:remote_path) { double(:remote_path) } + + subject(:updater) { described_class.new(fetcher) } + + context "when the ETag header is missing" do + # Regression test for https://github.com/bundler/bundler/issues/5463 + + let(:response) { double(:response, :body => "") } + + it "MisMatchedChecksumError is raised" do + # Twice: #update retries on failure + expect(response).to receive(:[]).with("Content-Encoding").twice { "" } + expect(response).to receive(:[]).with("ETag").twice { nil } + expect(fetcher).to receive(:call).twice { response } + + expect do + updater.update(local_path, remote_path) + end.to raise_error(Bundler::CompactIndexClient::Updater::MisMatchedChecksumError) + end + end + + context "when the download is corrupt" do + let(:response) { double(:response, :body => "") } + + it "raises HTTPError" do + expect(response).to receive(:[]).with("Content-Encoding") { "gzip" } + expect(fetcher).to receive(:call) { response } + + expect do + updater.update(local_path, remote_path) + end.to raise_error(Bundler::HTTPError) + end + end + + context "when bundler doesn't have permissions on Dir.tmpdir" do + let(:response) { double(:response, :body => "") } + + it "Errno::EACCES is raised" do + allow(Dir).to receive(:mktmpdir) { raise Errno::EACCES } + + expect do + updater.update(local_path, remote_path) + end.to raise_error(Bundler::PermissionError) + end + end +end diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb new file mode 100644 index 00000000000000..a38c0b05b02fb4 --- /dev/null +++ b/spec/bundler/bundler/definition_spec.rb @@ -0,0 +1,358 @@ +# frozen_string_literal: true + +require "bundler/definition" + +RSpec.describe Bundler::Definition do + describe "#lock" do + before do + allow(Bundler).to receive(:settings) { Bundler::Settings.new(".") } + allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") } + allow(Bundler).to receive(:ui) { double("UI", :info => "", :debug => "") } + end + context "when it's not possible to write to the file" do + subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } + + it "raises an PermissionError with explanation" do + expect(File).to receive(:open).with("Gemfile.lock", "wb"). + and_raise(Errno::EACCES) + expect { subject.lock("Gemfile.lock") }. + to raise_error(Bundler::PermissionError, /Gemfile\.lock/) + end + end + context "when a temporary resource access issue occurs" do + subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } + + it "raises a TemporaryResourceError with explanation" do + expect(File).to receive(:open).with("Gemfile.lock", "wb"). + and_raise(Errno::EAGAIN) + expect { subject.lock("Gemfile.lock") }. + to raise_error(Bundler::TemporaryResourceError, /temporarily unavailable/) + end + end + end + + describe "detects changes" do + it "for a path gem with changes", :bundler => "< 3" do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + bundle :install, :env => { "DEBUG" => 1 } + + expect(out).to match(/re-resolving dependencies/) + lockfile_should_be <<-G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a path gem with changes", :bundler => "3" do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + bundle :install, :env => { "DEBUG" => 1 } + + expect(out).to match(/re-resolving dependencies/) + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a path gem with deps and no changes", :bundler => "< 3" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + s.add_development_dependency "net-ssh", "1.0" + end + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle :check, :env => { "DEBUG" => 1 } + + expect(out).to match(/using resolution from the lockfile/) + lockfile_should_be <<-G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a path gem with deps and no changes", :bundler => "3" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + s.add_development_dependency "net-ssh", "1.0" + end + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle :check, :env => { "DEBUG" => 1 } + + expect(out).to match(/using resolution from the lockfile/) + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a rubygems gem" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo" + G + + bundle :check, :env => { "DEBUG" => 1 } + + expect(out).to match(/using resolution from the lockfile/) + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + end + + describe "initialize" do + context "gem version promoter" do + context "with lockfile" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo" + G + end + + it "should get a locked specs list when updating all" do + definition = Bundler::Definition.new(bundled_app("Gemfile.lock"), [], Bundler::SourceList.new, true) + locked_specs = definition.gem_version_promoter.locked_specs + expect(locked_specs.to_a.map(&:name)).to eq ["foo"] + expect(definition.instance_variable_get("@locked_specs").empty?).to eq true + end + end + + context "without gemfile or lockfile" do + it "should not attempt to parse empty lockfile contents" do + definition = Bundler::Definition.new(nil, [], mock_source_list, true) + expect(definition.gem_version_promoter.locked_specs.to_a).to eq [] + end + end + + context "eager unlock" do + let(:source_list) do + Bundler::SourceList.new.tap do |source_list| + source_list.global_rubygems_source = "file://#{gem_repo4}" + end + end + + before do + gemfile <<-G + source "file://#{gem_repo4}" + gem 'isolated_owner' + + gem 'shared_owner_a' + gem 'shared_owner_b' + G + + lockfile <<-L + GEM + remote: file://#{gem_repo4} + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.1) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.1) + shared_dep (~> 5.0) + shared_owner_b (4.0.1) + shared_dep (~> 5.0) + + PLATFORMS + ruby + + DEPENDENCIES + shared_owner_a + shared_owner_b + isolated_owner + + BUNDLED WITH + 1.13.0 + L + end + + it "should not eagerly unlock shared dependency with bundle install conservative updating behavior" do + updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"), + Bundler::Dependency.new("shared_owner_a", "3.0.2"), + Bundler::Dependency.new("shared_owner_b", ">= 0")] + unlock_hash_for_bundle_install = {} + definition = Bundler::Definition.new( + bundled_app("Gemfile.lock"), + updated_deps_in_gemfile, + source_list, + unlock_hash_for_bundle_install + ) + locked = definition.send(:converge_locked_specs).map(&:name) + expect(locked).to include "shared_dep" + end + + it "should not eagerly unlock shared dependency with bundle update conservative updating behavior" do + updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"), + Bundler::Dependency.new("shared_owner_a", ">= 0"), + Bundler::Dependency.new("shared_owner_b", ">= 0")] + definition = Bundler::Definition.new( + bundled_app("Gemfile.lock"), + updated_deps_in_gemfile, + source_list, + :gems => ["shared_owner_a"], :lock_shared_dependencies => true + ) + locked = definition.send(:converge_locked_specs).map(&:name) + expect(locked).to eq %w[isolated_dep isolated_owner shared_dep shared_owner_b] + expect(locked.include?("shared_dep")).to be_truthy + end + end + end + end + + describe "find_resolved_spec" do + it "with no platform set in SpecSet" do + ss = Bundler::SpecSet.new([build_stub_spec("a", "1.0"), build_stub_spec("b", "1.0")]) + dfn = Bundler::Definition.new(nil, [], mock_source_list, true) + dfn.instance_variable_set("@specs", ss) + found = dfn.find_resolved_spec(build_spec("a", "0.9", "ruby").first) + expect(found.name).to eq "a" + expect(found.version.to_s).to eq "1.0" + end + end + + describe "find_indexed_specs" do + it "with no platform set in indexed specs" do + index = Bundler::Index.new + %w[1.0.0 1.0.1 1.1.0].each {|v| index << build_stub_spec("foo", v) } + + dfn = Bundler::Definition.new(nil, [], mock_source_list, true) + dfn.instance_variable_set("@index", index) + found = dfn.find_indexed_specs(build_spec("foo", "0.9", "ruby").first) + expect(found.length).to eq 3 + end + end + + def build_stub_spec(name, version) + Bundler::StubSpecification.new(name, version, nil, nil) + end + + def mock_source_list + Class.new do + def all_sources + [] + end + + def path_sources + [] + end + + def rubygems_remotes + [] + end + + def replace_sources!(arg) + nil + end + end.new + end +end diff --git a/spec/bundler/bundler/dep_proxy_spec.rb b/spec/bundler/bundler/dep_proxy_spec.rb new file mode 100644 index 00000000000000..0f8d6b1076af6b --- /dev/null +++ b/spec/bundler/bundler/dep_proxy_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::DepProxy do + let(:dep) { Bundler::Dependency.new("rake", ">= 0") } + subject { described_class.new(dep, Gem::Platform::RUBY) } + let(:same) { subject } + let(:other) { subject.dup } + let(:different) { described_class.new(dep, Gem::Platform::JAVA) } + + describe "#eql?" do + it { expect(subject.eql?(same)).to be true } + it { expect(subject.eql?(other)).to be true } + it { expect(subject.eql?(different)).to be false } + it { expect(subject.eql?(nil)).to be false } + it { expect(subject.eql?("foobar")).to be false } + end + + describe "#hash" do + it { expect(subject.hash).to eq(same.hash) } + it { expect(subject.hash).to eq(other.hash) } + end +end diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb new file mode 100644 index 00000000000000..94d54ad8774a7e --- /dev/null +++ b/spec/bundler/bundler/dsl_spec.rb @@ -0,0 +1,305 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Dsl do + before do + @rubygems = double("rubygems") + allow(Bundler::Source::Rubygems).to receive(:new) { @rubygems } + end + + describe "#git_source" do + it "registers custom hosts" do + subject.git_source(:example) {|repo_name| "git@git.example.com:#{repo_name}.git" } + subject.git_source(:foobar) {|repo_name| "git@foobar.com:#{repo_name}.git" } + subject.gem("dobry-pies", :example => "strzalek/dobry-pies") + example_uri = "git@git.example.com:strzalek/dobry-pies.git" + expect(subject.dependencies.first.source.uri).to eq(example_uri) + end + + it "raises exception on invalid hostname" do + expect do + subject.git_source(:group) {|repo_name| "git@git.example.com:#{repo_name}.git" } + end.to raise_error(Bundler::InvalidOption) + end + + it "expects block passed" do + expect { subject.git_source(:example) }.to raise_error(Bundler::InvalidOption) + end + + context "github_https feature flag" do + it "is true when github.https is true" do + bundle "config github.https true" + expect(Bundler.feature_flag.github_https?).to eq "true" + end + end + + context "default hosts (git, gist)", :bundler => "< 3" do + context "when github.https config is true" do + before { bundle "config github.https true" } + it "converts :github to :git using https" do + subject.gem("sparks", :github => "indirect/sparks") + github_uri = "https://github.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + end + + it "converts :github to :git" do + subject.gem("sparks", :github => "indirect/sparks") + github_uri = "git://github.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + + it "converts numeric :gist to :git" do + subject.gem("not-really-a-gem", :gist => 2_859_988) + github_uri = "https://gist.github.com/2859988.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + + it "converts :gist to :git" do + subject.gem("not-really-a-gem", :gist => "2859988") + github_uri = "https://gist.github.com/2859988.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + + it "converts 'rails' to 'rails/rails'" do + subject.gem("rails", :github => "rails") + github_uri = "git://github.com/rails/rails.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + + it "converts :bitbucket to :git" do + subject.gem("not-really-a-gem", :bitbucket => "mcorp/flatlab-rails") + bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/flatlab-rails.git" + expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri) + end + + it "converts 'mcorp' to 'mcorp/mcorp'" do + subject.gem("not-really-a-gem", :bitbucket => "mcorp") + bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/mcorp.git" + expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri) + end + end + + context "default git sources", :bundler => "3" do + it "has none" do + expect(subject.instance_variable_get(:@git_sources)).to eq({}) + end + end + end + + describe "#method_missing" do + it "raises an error for unknown DSL methods" do + expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s). + and_return("unknown") + + error_msg = "There was an error parsing `Gemfile`: Undefined local variable or method `unknown' for Gemfile. Bundler cannot continue." + expect { subject.eval_gemfile("Gemfile") }. + to raise_error(Bundler::GemfileError, Regexp.new(error_msg)) + end + end + + describe "#eval_gemfile" do + it "handles syntax errors with a useful message" do + expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s).and_return("}") + expect { subject.eval_gemfile("Gemfile") }. + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '\}'). Bundler cannot continue./) + end + + it "distinguishes syntax errors from evaluation errors" do + expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s).and_return( + "ruby '2.1.5', :engine => 'ruby', :engine_version => '1.2.4'" + ) + expect { subject.eval_gemfile("Gemfile") }. + to raise_error(Bundler::GemfileError, /There was an error evaluating `Gemfile`: ruby_version must match the :engine_version for MRI/) + end + end + + describe "#gem" do + [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :mri, :mri_18, :mri_19, + :mri_20, :mri_21, :mri_22, :mri_23, :mri_24, :mri_25, :jruby, :rbx, :truffleruby].each do |platform| + it "allows #{platform} as a valid platform" do + subject.gem("foo", :platform => platform) + end + end + + it "rejects invalid platforms" do + expect { subject.gem("foo", :platform => :bogus) }. + to raise_error(Bundler::GemfileError, /is not a valid platform/) + end + + it "rejects empty gem name" do + expect { subject.gem("") }. + to raise_error(Bundler::GemfileError, /an empty gem name is not valid/) + end + + it "rejects with a leading space in the name" do + expect { subject.gem(" foo") }. + to raise_error(Bundler::GemfileError, /' foo' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a trailing space in the name" do + expect { subject.gem("foo ") }. + to raise_error(Bundler::GemfileError, /'foo ' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a space in the gem name" do + expect { subject.gem("fo o") }. + to raise_error(Bundler::GemfileError, /'fo o' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a tab in the gem name" do + expect { subject.gem("fo\to") }. + to raise_error(Bundler::GemfileError, /'fo\to' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a newline in the gem name" do + expect { subject.gem("fo\no") }. + to raise_error(Bundler::GemfileError, /'fo\no' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a carriage return in the gem name" do + expect { subject.gem("fo\ro") }. + to raise_error(Bundler::GemfileError, /'fo\ro' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a form feed in the gem name" do + expect { subject.gem("fo\fo") }. + to raise_error(Bundler::GemfileError, /'fo\fo' is not a valid gem name because it contains whitespace/) + end + + it "rejects symbols as gem name" do + expect { subject.gem(:foo) }. + to raise_error(Bundler::GemfileError, /You need to specify gem names as Strings. Use 'gem "foo"' instead/) + end + + it "rejects branch option on non-git gems" do + expect { subject.gem("foo", :branch => "test") }. + to raise_error(Bundler::GemfileError, /The `branch` option for `gem 'foo'` is not allowed. Only gems with a git source can specify a branch/) + end + + it "allows specifying a branch on git gems" do + subject.gem("foo", :branch => "test", :git => "http://mytestrepo") + dep = subject.dependencies.last + expect(dep.name).to eq "foo" + end + + it "allows specifying a branch on git gems with a git_source" do + subject.git_source(:test_source) {|n| "https://github.com/#{n}" } + subject.gem("foo", :branch => "test", :test_source => "bundler/bundler") + dep = subject.dependencies.last + expect(dep.name).to eq "foo" + end + end + + describe "#gemspec" do + let(:spec) do + Gem::Specification.new do |gem| + gem.name = "example" + gem.platform = platform + end + end + + before do + allow(Dir).to receive(:[]).and_return(["spec_path"]) + allow(Bundler).to receive(:load_gemspec).with("spec_path").and_return(spec) + allow(Bundler).to receive(:default_gemfile).and_return(Pathname.new("./Gemfile")) + end + + context "with a ruby platform" do + let(:platform) { "ruby" } + + it "keeps track of the ruby platforms in the dependency" do + subject.gemspec + expect(subject.dependencies.last.platforms).to eq(Bundler::Dependency::REVERSE_PLATFORM_MAP[Gem::Platform::RUBY]) + end + end + + context "with a jruby platform" do + let(:platform) { "java" } + + it "keeps track of the jruby platforms in the dependency" do + allow(Gem::Platform).to receive(:local).and_return(java) + subject.gemspec + expect(subject.dependencies.last.platforms).to eq(Bundler::Dependency::REVERSE_PLATFORM_MAP[Gem::Platform::JAVA]) + end + end + end + + context "can bundle groups of gems with" do + # git "https://github.com/rails/rails.git" do + # gem "railties" + # gem "action_pack" + # gem "active_model" + # end + describe "#git" do + it "from a single repo" do + rails_gems = %w[railties action_pack active_model] + subject.git "https://github.com/rails/rails.git" do + rails_gems.each {|rails_gem| subject.send :gem, rails_gem } + end + expect(subject.dependencies.map(&:name)).to match_array rails_gems + end + end + + # github 'spree' do + # gem 'spree_core' + # gem 'spree_api' + # gem 'spree_backend' + # end + describe "#github", :bundler => "< 3" do + it "from github" do + spree_gems = %w[spree_core spree_api spree_backend] + subject.github "spree" do + spree_gems.each {|spree_gem| subject.send :gem, spree_gem } + end + + subject.dependencies.each do |d| + expect(d.source.uri).to eq("git://github.com/spree/spree.git") + end + end + end + + describe "#github", :bundler => "3" do + it "from github" do + expect do + spree_gems = %w[spree_core spree_api spree_backend] + subject.github "spree" do + spree_gems.each {|spree_gem| subject.send :gem, spree_gem } + end + end.to raise_error(Bundler::DeprecatedError, /github method has been removed/) + end + end + end + + describe "syntax errors" do + it "will raise a Bundler::GemfileError" do + gemfile "gem 'foo', :path => /unquoted/string/syntax/error" + expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`:( compile error -)? unknown regexp options - trg. Bundler cannot continue./) + end + end + + describe "Runtime errors", :unless => Bundler.current_ruby.on_18? do + it "will raise a Bundler::GemfileError" do + gemfile "s = 'foo'.freeze; s.strip!" + expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: can't modify frozen String. Bundler cannot continue./i) + end + end + + describe "#with_source" do + context "if there was a rubygem source already defined" do + it "restores it after it's done" do + other_source = double("other-source") + allow(Bundler::Source::Rubygems).to receive(:new).and_return(other_source) + allow(Bundler).to receive(:default_gemfile).and_return(Pathname.new("./Gemfile")) + + subject.source("https://other-source.org") do + subject.gem("dobry-pies", :path => "foo") + subject.gem("foo") + end + + expect(subject.dependencies.last.source).to eq(other_source) + end + end + end +end diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb new file mode 100644 index 00000000000000..a9371f6617aa3e --- /dev/null +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::EndpointSpecification do + let(:name) { "foo" } + let(:version) { "1.0.0" } + let(:platform) { Gem::Platform::RUBY } + let(:dependencies) { [] } + let(:metadata) { nil } + + subject(:spec) { described_class.new(name, version, platform, dependencies, metadata) } + + describe "#build_dependency" do + let(:name) { "foo" } + let(:requirement1) { "~> 1.1" } + let(:requirement2) { ">= 1.1.7" } + + it "should return a Gem::Dependency" do + expect(subject.send(:build_dependency, name, [requirement1, requirement2])). + to eq(Gem::Dependency.new(name, requirement1, requirement2)) + end + + context "when an ArgumentError occurs" do + before do + allow(Gem::Dependency).to receive(:new).with(name, [requirement1, requirement2]) { + raise ArgumentError.new("Some error occurred") + } + end + + it "should raise the original error" do + expect { subject.send(:build_dependency, name, [requirement1, requirement2]) }.to raise_error( + ArgumentError, "Some error occurred" + ) + end + end + + context "when there is an ill formed requirement" do + before do + allow(Gem::Dependency).to receive(:new).with(name, [requirement1, requirement2]) { + raise ArgumentError.new("Ill-formed requirement [\"# ">\n" } } + it "raises a helpful error message" do + expect { subject }.to raise_error( + Bundler::GemspecError, + a_string_including("There was an error parsing the metadata for the gem foo (1.0.0)"). + and(a_string_including('The metadata was {"rubygems"=>">\n"}')) + ) + end + end + end + + it "supports equality comparison" do + other_spec = described_class.new("bar", version, platform, dependencies, metadata) + expect(spec).to eql(spec) + expect(spec).to_not eql(other_spec) + end +end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb new file mode 100644 index 00000000000000..20bd38b0214bf1 --- /dev/null +++ b/spec/bundler/bundler/env_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require "bundler/settings" + +RSpec.describe Bundler::Env do + let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil, nil) } + + describe "#report" do + it "prints the environment" do + out = described_class.report + + expect(out).to include("Environment") + expect(out).to include(Bundler::VERSION) + expect(out).to include(Gem::VERSION) + expect(out).to include(described_class.send(:ruby_version)) + expect(out).to include(described_class.send(:git_version)) + expect(out).to include(OpenSSL::OPENSSL_VERSION) + end + + context "when there is a Gemfile and a lockfile and print_gemfile is true" do + before do + gemfile "gem 'rack', '1.0.0'" + + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + L + end + + let(:output) { described_class.report(:print_gemfile => true) } + + it "prints the Gemfile" do + expect(output).to include("Gemfile") + expect(output).to include("'rack', '1.0.0'") + end + + it "prints the lockfile" do + expect(output).to include("Gemfile.lock") + expect(output).to include("rack (1.0.0)") + end + end + + context "when there no Gemfile and print_gemfile is true" do + let(:output) { described_class.report(:print_gemfile => true) } + + it "prints the environment" do + expect(output).to start_with("## Environment") + end + end + + context "when Gemfile contains a gemspec and print_gemspecs is true" do + let(:gemspec) do + strip_whitespace(<<-GEMSPEC) + Gem::Specification.new do |gem| + gem.name = "foo" + gem.author = "Fumofu" + end + GEMSPEC + end + + before do + gemfile("gemspec") + + File.open(bundled_app.join("foo.gemspec"), "wb") do |f| + f.write(gemspec) + end + end + + it "prints the gemspec" do + output = described_class.report(:print_gemspecs => true) + + expect(output).to include("foo.gemspec") + expect(output).to include(gemspec) + end + end + + context "when eval_gemfile is used" do + it "prints all gemfiles" do + create_file "other/Gemfile-other", "gem 'rack'" + create_file "other/Gemfile", "eval_gemfile 'Gemfile-other'" + create_file "Gemfile-alt", <<-G + source "file:#{gem_repo1}" + eval_gemfile "other/Gemfile" + G + gemfile "eval_gemfile #{File.expand_path("Gemfile-alt").dump}" + + output = described_class.report(:print_gemspecs => true) + expect(output).to include(strip_whitespace(<<-ENV)) + ## Gemfile + + ### Gemfile + + ```ruby + eval_gemfile #{File.expand_path("Gemfile-alt").dump} + ``` + + ### Gemfile-alt + + ```ruby + source "file:#{gem_repo1}" + eval_gemfile "other/Gemfile" + ``` + + ### other/Gemfile + + ```ruby + eval_gemfile 'Gemfile-other' + ``` + + ### other/Gemfile-other + + ```ruby + gem 'rack' + ``` + + ### Gemfile.lock + + ``` + + ``` + ENV + end + end + + context "when the git version is OS specific" do + it "includes OS specific information with the version number" do + expect(git_proxy_stub).to receive(:git).with("--version"). + and_return("git version 1.2.3 (Apple Git-BS)") + expect(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) + + expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)") + end + end + end + + describe ".version_of", :ruby_repo do + let(:parsed_version) { described_class.send(:version_of, "ruby") } + + it "strips version of new line characters" do + expect(parsed_version).to_not include("\n") + end + end +end diff --git a/spec/bundler/bundler/environment_preserver_spec.rb b/spec/bundler/bundler/environment_preserver_spec.rb new file mode 100644 index 00000000000000..530ca6f8356d70 --- /dev/null +++ b/spec/bundler/bundler/environment_preserver_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::EnvironmentPreserver do + let(:preserver) { described_class.new(env, ["foo"]) } + + describe "#backup" do + let(:env) { { "foo" => "my-foo", "bar" => "my-bar" } } + subject { preserver.backup } + + it "should create backup entries" do + expect(subject["BUNDLER_ORIG_foo"]).to eq("my-foo") + end + + it "should keep the original entry" do + expect(subject["foo"]).to eq("my-foo") + end + + it "should not create backup entries for unspecified keys" do + expect(subject.key?("BUNDLER_ORIG_bar")).to eq(false) + end + + it "should not affect the original env" do + subject + expect(env.keys.sort).to eq(%w[bar foo]) + end + + context "when a key is empty" do + let(:env) { { "foo" => "" } } + + it "should not create backup entries" do + expect(subject).not_to have_key "BUNDLER_ORIG_foo" + end + end + + context "when an original key is set" do + let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } } + + it "should keep the original value in the BUNDLER_ORIG_ variable" do + expect(subject["BUNDLER_ORIG_foo"]).to eq("orig-foo") + end + + it "should keep the variable" do + expect(subject["foo"]).to eq("my-foo") + end + end + end + + describe "#restore" do + subject { preserver.restore } + + context "when an original key is set" do + let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } } + + it "should restore the original value" do + expect(subject["foo"]).to eq("orig-foo") + end + + it "should delete the backup value" do + expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false) + end + end + + context "when no original key is set" do + let(:env) { { "foo" => "my-foo" } } + + it "should keep the current value" do + expect(subject["foo"]).to eq("my-foo") + end + end + + context "when the original key is empty" do + let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "" } } + + it "should keep the current value" do + expect(subject["foo"]).to eq("my-foo") + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/base_spec.rb b/spec/bundler/bundler/fetcher/base_spec.rb new file mode 100644 index 00000000000000..df1245d44d07f5 --- /dev/null +++ b/spec/bundler/bundler/fetcher/base_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Base do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote) } + let(:display_uri) { "http://sample_uri.com" } + + class TestClass < described_class; end + + subject { TestClass.new(downloader, remote, display_uri) } + + describe "#initialize" do + context "with the abstract Base class" do + it "should raise an error" do + expect { described_class.new(downloader, remote, display_uri) }.to raise_error(RuntimeError, "Abstract class") + end + end + + context "with a class that inherits the Base class" do + it "should set the passed attributes" do + expect(subject.downloader).to eq(downloader) + expect(subject.remote).to eq(remote) + expect(subject.display_uri).to eq("http://sample_uri.com") + end + end + end + + describe "#remote_uri" do + let(:remote_uri_obj) { double(:remote_uri_obj) } + + before { allow(remote).to receive(:uri).and_return(remote_uri_obj) } + + it "should return the remote's uri" do + expect(subject.remote_uri).to eq(remote_uri_obj) + end + end + + describe "#fetch_uri" do + let(:remote_uri_obj) { URI("http://rubygems.org") } + + before { allow(subject).to receive(:remote_uri).and_return(remote_uri_obj) } + + context "when the remote uri's host is rubygems.org" do + it "should create a copy of the remote uri with index.rubygems.org as the host" do + fetched_uri = subject.fetch_uri + expect(fetched_uri.host).to eq("index.rubygems.org") + expect(fetched_uri).to_not be(remote_uri_obj) + end + end + + context "when the remote uri's host is not rubygems.org" do + let(:remote_uri_obj) { URI("http://otherhost.org") } + + it "should return the remote uri" do + expect(subject.fetch_uri).to eq(URI("http://otherhost.org")) + end + end + + it "memoizes the fetched uri" do + expect(remote_uri_obj).to receive(:host).once + 2.times { subject.fetch_uri } + end + end + + describe "#available?" do + it "should return whether the api is available" do + expect(subject.available?).to be_truthy + end + end + + describe "#api_fetcher?" do + it "should return false" do + expect(subject.api_fetcher?).to be_falsey + end + end +end diff --git a/spec/bundler/bundler/fetcher/compact_index_spec.rb b/spec/bundler/bundler/fetcher/compact_index_spec.rb new file mode 100644 index 00000000000000..e0f58766eab7ec --- /dev/null +++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::CompactIndex do + let(:downloader) { double(:downloader) } + let(:display_uri) { URI("http://sampleuri.com") } + let(:remote) { double(:remote, :cache_slug => "lsjdf", :uri => display_uri) } + let(:compact_index) { described_class.new(downloader, remote, display_uri) } + + before do + allow(compact_index).to receive(:log_specs) {} + end + + describe "#specs_for_names" do + it "has only one thread open at the end of the run" do + compact_index.specs_for_names(["lskdjf"]) + + thread_count = Thread.list.count {|thread| thread.status == "run" } + expect(thread_count).to eq 1 + end + + it "calls worker#stop during the run" do + expect_any_instance_of(Bundler::Worker).to receive(:stop).at_least(:once) + + compact_index.specs_for_names(["lskdjf"]) + end + + describe "#available?" do + before do + allow(compact_index).to receive(:compact_index_client). + and_return(double(:compact_index_client, :update_and_parse_checksums! => true)) + end + + it "returns true" do + expect(compact_index).to be_available + end + + context "when OpenSSL is not available" do + before do + allow(compact_index).to receive(:require).with("openssl").and_raise(LoadError) + end + + it "returns true" do + expect(compact_index).to be_available + end + end + + context "when OpenSSL is FIPS-enabled", :ruby => ">= 2.0.0" do + def remove_cached_md5_availability + return unless Bundler::SharedHelpers.instance_variable_defined?(:@md5_available) + Bundler::SharedHelpers.remove_instance_variable(:@md5_available) + end + + before do + remove_cached_md5_availability + stub_const("OpenSSL::OPENSSL_FIPS", true) + end + + after { remove_cached_md5_availability } + + context "when FIPS-mode is active" do + before do + allow(OpenSSL::Digest::MD5).to receive(:digest). + and_raise(OpenSSL::Digest::DigestError) + end + + it "returns false" do + expect(compact_index).to_not be_available + end + end + + it "returns true" do + expect(compact_index).to be_available + end + end + end + + context "logging" do + before { allow(compact_index).to receive(:log_specs).and_call_original } + + context "with debug on" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true) + end + + it "should log at info level" do + expect(Bundler).to receive_message_chain(:ui, :debug).with('Looking up gems ["lskdjf"]') + compact_index.specs_for_names(["lskdjf"]) + end + end + + context "with debug off" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) + end + + it "should log at info level" do + expect(Bundler).to receive_message_chain(:ui, :info).with(".", false) + compact_index.specs_for_names(["lskdjf"]) + end + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/dependency_spec.rb b/spec/bundler/bundler/fetcher/dependency_spec.rb new file mode 100644 index 00000000000000..081fdff34de446 --- /dev/null +++ b/spec/bundler/bundler/fetcher/dependency_spec.rb @@ -0,0 +1,287 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Dependency do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote, :uri => URI("http://localhost:5000")) } + let(:display_uri) { "http://sample_uri.com" } + + subject { described_class.new(downloader, remote, display_uri) } + + describe "#available?" do + let(:dependency_api_uri) { double(:dependency_api_uri) } + let(:fetched_spec) { double(:fetched_spec) } + + before do + allow(subject).to receive(:dependency_api_uri).and_return(dependency_api_uri) + allow(downloader).to receive(:fetch).with(dependency_api_uri).and_return(fetched_spec) + end + + it "should be truthy" do + expect(subject.available?).to be_truthy + end + + context "when there is no network access" do + before do + allow(downloader).to receive(:fetch).with(dependency_api_uri) { + raise Bundler::Fetcher::NetworkDownError.new("Network Down Message") + } + end + + it "should raise an HTTPError with the original message" do + expect { subject.available? }.to raise_error(Bundler::HTTPError, "Network Down Message") + end + end + + context "when authentication is required" do + let(:remote_uri) { "http://remote_uri.org" } + + before do + allow(downloader).to receive(:fetch).with(dependency_api_uri) { + raise Bundler::Fetcher::AuthenticationRequiredError.new(remote_uri) + } + end + + it "should raise the original error" do + expect { subject.available? }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, + %r{Authentication is required for http://remote_uri.org}) + end + end + + context "when there is an http error" do + before { allow(downloader).to receive(:fetch).with(dependency_api_uri) { raise Bundler::HTTPError.new } } + + it "should be falsey" do + expect(subject.available?).to be_falsey + end + end + end + + describe "#api_fetcher?" do + it "should return true" do + expect(subject.api_fetcher?).to be_truthy + end + end + + describe "#specs" do + let(:gem_names) { %w[foo bar] } + let(:full_dependency_list) { ["bar"] } + let(:last_spec_list) { [["boulder", gem_version1, "ruby", resque]] } + let(:fail_errors) { double(:fail_errors) } + let(:bundler_retry) { double(:bundler_retry) } + let(:gem_version1) { double(:gem_version1) } + let(:resque) { double(:resque) } + let(:remote_uri) { "http://remote-uri.org" } + + before do + stub_const("Bundler::Fetcher::FAIL_ERRORS", fail_errors) + allow(Bundler::Retry).to receive(:new).with("dependency api", fail_errors).and_return(bundler_retry) + allow(bundler_retry).to receive(:attempts) {|&block| block.call } + allow(subject).to receive(:log_specs) {} + allow(subject).to receive(:remote_uri).and_return(remote_uri) + allow(Bundler).to receive_message_chain(:ui, :debug?) + allow(Bundler).to receive_message_chain(:ui, :info) + allow(Bundler).to receive_message_chain(:ui, :debug) + end + + context "when there are given gem names that are not in the full dependency list" do + let(:spec_list) { [["top", gem_version2, "ruby", faraday]] } + let(:deps_list) { [] } + let(:dependency_specs) { [spec_list, deps_list] } + let(:gem_version2) { double(:gem_version2) } + let(:faraday) { double(:faraday) } + + before { allow(subject).to receive(:dependency_specs).with(["foo"]).and_return(dependency_specs) } + + it "should return a hash with the remote_uri and the list of specs" do + expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq([ + ["top", gem_version2, "ruby", faraday], + ["boulder", gem_version1, "ruby", resque], + ]) + end + end + + context "when all given gem names are in the full dependency list" do + let(:gem_names) { ["foo"] } + let(:full_dependency_list) { %w[foo bar] } + let(:last_spec_list) { ["boulder"] } + + it "should return a hash with the remote_uri and the last spec list" do + expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq(["boulder"]) + end + end + + context "logging" do + before { allow(subject).to receive(:log_specs).and_call_original } + + context "with debug on" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true) + allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []]) + end + + it "should log the query list at debug level" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: [\"foo\"]") + expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: []") + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + + context "with debug off" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) + allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []]) + end + + it "should log at info level" do + expect(Bundler).to receive_message_chain(:ui, :info).with(".", false) + expect(Bundler).to receive_message_chain(:ui, :info).with(".", false) + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + end + + shared_examples_for "the error is properly handled" do + it "should return nil" do + expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to be_nil + end + + context "debug logging is not on" do + before { allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) } + + it "should log a new line to info" do + expect(Bundler).to receive_message_chain(:ui, :info).with("") + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + end + + shared_examples_for "the error suggests retrying with the full index" do + it "should log the inability to fetch from API at debug level" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API\nit's suggested to retry using the full index via `bundle install --full-index`") + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + + context "when an HTTPError occurs" do + before { allow(subject).to receive(:dependency_specs) { raise Bundler::HTTPError.new } } + + it_behaves_like "the error is properly handled" + it_behaves_like "the error suggests retrying with the full index" + end + + context "when a GemspecError occurs" do + before { allow(subject).to receive(:dependency_specs) { raise Bundler::GemspecError.new } } + + it_behaves_like "the error is properly handled" + it_behaves_like "the error suggests retrying with the full index" + end + + context "when a MarshalError occurs" do + before { allow(subject).to receive(:dependency_specs) { raise Bundler::MarshalError.new } } + + it_behaves_like "the error is properly handled" + + it "should log the inability to fetch from API and mention retrying" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API, trying the full index") + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + end + + describe "#dependency_specs" do + let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] } + let(:gem_list) { double(:gem_list) } + let(:formatted_specs_and_deps) { double(:formatted_specs_and_deps) } + + before do + allow(subject).to receive(:unmarshalled_dep_gems).with(gem_names).and_return(gem_list) + allow(subject).to receive(:get_formatted_specs_and_deps).with(gem_list).and_return(formatted_specs_and_deps) + end + + it "should log the query list at debug level" do + expect(Bundler).to receive_message_chain(:ui, :debug).with( + "Query Gemcutter Dependency Endpoint API: foo,bar,bundler,rubocop" + ) + subject.dependency_specs(gem_names) + end + + it "should return formatted specs and a unique list of dependencies" do + expect(subject.dependency_specs(gem_names)).to eq(formatted_specs_and_deps) + end + end + + describe "#unmarshalled_dep_gems" do + let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] } + let(:dep_api_uri) { double(:dep_api_uri) } + let(:unmarshalled_gems) { double(:unmarshalled_gems) } + let(:fetch_response) { double(:fetch_response, :body => double(:body)) } + let(:rubygems_limit) { 50 } + + before { allow(subject).to receive(:dependency_api_uri).with(gem_names).and_return(dep_api_uri) } + + it "should fetch dependencies from RubyGems and unmarshal them" do + expect(gem_names).to receive(:each_slice).with(rubygems_limit).and_call_original + expect(downloader).to receive(:fetch).with(dep_api_uri).and_return(fetch_response) + expect(Bundler).to receive(:load_marshal).with(fetch_response.body).and_return([unmarshalled_gems]) + expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems]) + end + end + + describe "#get_formatted_specs_and_deps" do + let(:gem_list) do + [ + { + :dependencies => { + "resque" => "req3,req4", + }, + :name => "typhoeus", + :number => "1.0.1", + :platform => "ruby", + }, + { + :dependencies => { + "faraday" => "req1,req2", + }, + :name => "grape", + :number => "2.0.2", + :platform => "jruby", + }, + ] + end + + it "should return formatted specs and a unique list of dependencies" do + spec_list, deps_list = subject.get_formatted_specs_and_deps(gem_list) + expect(spec_list).to eq([["typhoeus", "1.0.1", "ruby", [["resque", ["req3,req4"]]]], + ["grape", "2.0.2", "jruby", [["faraday", ["req1,req2"]]]]]) + expect(deps_list).to eq(%w[resque faraday]) + end + end + + describe "#dependency_api_uri" do + let(:uri) { URI("http://gem-api.com") } + + context "with gem names" do + let(:gem_names) { %w[foo bar bundler rubocop] } + + before { allow(subject).to receive(:fetch_uri).and_return(uri) } + + it "should return an api calling uri with the gems in the query" do + expect(subject.dependency_api_uri(gem_names).to_s).to eq( + "http://gem-api.com/api/v1/dependencies?gems=bar%2Cbundler%2Cfoo%2Crubocop" + ) + end + end + + context "with no gem names" do + let(:gem_names) { [] } + + before { allow(subject).to receive(:fetch_uri).and_return(uri) } + + it "should return an api calling uri with no query" do + expect(subject.dependency_api_uri(gem_names).to_s).to eq( + "http://gem-api.com/api/v1/dependencies" + ) + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb new file mode 100644 index 00000000000000..c9b4fa662a32ec --- /dev/null +++ b/spec/bundler/bundler/fetcher/downloader_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Downloader do + let(:connection) { double(:connection) } + let(:redirect_limit) { 5 } + let(:uri) { URI("http://www.uri-to-fetch.com/api/v2/endpoint") } + let(:options) { double(:options) } + + subject { described_class.new(connection, redirect_limit) } + + describe "fetch" do + let(:counter) { 0 } + let(:httpv) { "1.1" } + let(:http_response) { double(:response) } + + before do + allow(subject).to receive(:request).with(uri, options).and_return(http_response) + allow(http_response).to receive(:body).and_return("Body with info") + end + + context "when the # requests counter is greater than the redirect limit" do + let(:counter) { redirect_limit + 1 } + + it "should raise a Bundler::HTTPError specifying too many redirects" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Too many redirects") + end + end + + context "logging" do + let(:http_response) { Net::HTTPSuccess.new("1.1", 200, "Success") } + + it "should log the HTTP response code and message to debug" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP 200 Success #{uri}") + subject.fetch(uri, options, counter) + end + end + + context "when the request response is a Net::HTTPRedirection" do + let(:http_response) { Net::HTTPRedirection.new(httpv, 308, "Moved") } + + before { http_response["location"] = "http://www.redirect-uri.com/api/v2/endpoint" } + + it "should try to fetch the redirect uri and iterate the # requests counter" do + expect(subject).to receive(:fetch).with(URI("http://www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(URI("http://www.redirect-uri.com/api/v2/endpoint"), options, 1) + subject.fetch(uri, options, counter) + end + + context "when the redirect uri and original uri are the same" do + let(:uri) { URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + + before { http_response["location"] = "ssh://www.uri-to-fetch.com/api/v1/endpoint" } + + it "should set the same user and password for the redirect uri" do + expect(subject).to receive(:fetch).with(URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(URI("ssh://username:password@www.uri-to-fetch.com/api/v1/endpoint"), options, 1) + subject.fetch(uri, options, counter) + end + end + end + + context "when the request response is a Net::HTTPSuccess" do + let(:http_response) { Net::HTTPSuccess.new("1.1", 200, "Success") } + + it "should return the response body" do + expect(subject.fetch(uri, options, counter)).to eq(http_response) + end + end + + context "when the request response is a Net::HTTPRequestEntityTooLarge" do + let(:http_response) { Net::HTTPRequestEntityTooLarge.new("1.1", 413, "Too Big") } + + it "should raise a Bundler::Fetcher::FallbackError with the response body" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::FallbackError, "Body with info") + end + end + + context "when the request response is a Net::HTTPUnauthorized" do + let(:http_response) { Net::HTTPUnauthorized.new("1.1", 401, "Unauthorized") } + + it "should raise a Bundler::Fetcher::AuthenticationRequiredError with the uri host" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, + /Authentication is required for www.uri-to-fetch.com/) + end + end + + context "when the request response is a Net::HTTPNotFound" do + let(:http_response) { Net::HTTPNotFound.new("1.1", 404, "Not Found") } + + it "should raise a Bundler::Fetcher::FallbackError with Net::HTTPNotFound" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::FallbackError, "Net::HTTPNotFound") + end + end + + context "when the request response is some other type" do + let(:http_response) { Net::HTTPBadGateway.new("1.1", 500, "Fatal Error") } + + it "should raise a Bundler::HTTPError with the response class and body" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Net::HTTPBadGateway: Body with info") + end + end + end + + describe "request" do + let(:net_http_get) { double(:net_http_get) } + let(:response) { double(:response) } + + before do + allow(Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get) + allow(connection).to receive(:request).with(uri, net_http_get).and_return(response) + end + + it "should log the HTTP GET request to debug" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint") + subject.request(uri, options) + end + + context "when there is a user provided in the request" do + context "and there is also a password provided" do + context "that contains cgi escaped characters" do + let(:uri) { URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") } + + it "should request basic authentication with the username and password" do + expect(net_http_get).to receive(:basic_auth).with("username", "password$") + subject.request(uri, options) + end + end + + context "that is all unescaped characters" do + let(:uri) { URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + it "should request basic authentication with the username and proper cgi compliant password" do + expect(net_http_get).to receive(:basic_auth).with("username", "password") + subject.request(uri, options) + end + end + end + + context "and there is no password provided" do + let(:uri) { URI("http://username@www.uri-to-fetch.com/api/v2/endpoint") } + + it "should request basic authentication with just the user" do + expect(net_http_get).to receive(:basic_auth).with("username", nil) + subject.request(uri, options) + end + end + + context "that contains cgi escaped characters" do + let(:uri) { URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") } + + it "should request basic authentication with the proper cgi compliant password user" do + expect(net_http_get).to receive(:basic_auth).with("username$", nil) + subject.request(uri, options) + end + end + end + + context "when the request response causes a NoMethodError" do + before { allow(connection).to receive(:request).with(uri, net_http_get) { raise NoMethodError.new(message) } } + + context "and the error message is about use_ssl=" do + let(:message) { "undefined method 'use_ssl='" } + + it "should raise a LoadError about openssl" do + expect { subject.request(uri, options) }.to raise_error(LoadError, "cannot load such file -- openssl") + end + end + + context "and the error message is not about use_ssl=" do + let(:message) { "undefined method 'undefined_method_call'" } + + it "should raise the original NoMethodError" do + expect { subject.request(uri, options) }.to raise_error(NoMethodError, "undefined method 'undefined_method_call'") + end + end + end + + context "when the request response causes a OpenSSL::SSL::SSLError" do + before { allow(connection).to receive(:request).with(uri, net_http_get) { raise OpenSSL::SSL::SSLError.new } } + + it "should raise a LoadError about openssl" do + expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::CertificateFailureError, + %r{Could not verify the SSL certificate for http://www.uri-to-fetch.com/api/v2/endpoint}) + end + end + + context "when the request response causes an error included in HTTP_ERRORS" do + let(:message) { nil } + let(:error) { RuntimeError.new(message) } + + before do + stub_const("Bundler::Fetcher::HTTP_ERRORS", [RuntimeError]) + allow(connection).to receive(:request).with(uri, net_http_get) { raise error } + end + + it "should trace log the error" do + allow(Bundler).to receive_message_chain(:ui, :debug) + expect(Bundler).to receive_message_chain(:ui, :trace).with(error) + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError) + end + + context "when error message is about the host being down" do + let(:message) { "host down: http://www.uri-to-fetch.com" } + + it "should raise a Bundler::Fetcher::NetworkDownError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError, + /Could not reach host www.uri-to-fetch.com/) + end + end + + context "when error message is about getaddrinfo issues" do + let(:message) { "getaddrinfo: nodename nor servname provided for http://www.uri-to-fetch.com" } + + it "should raise a Bundler::Fetcher::NetworkDownError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError, + /Could not reach host www.uri-to-fetch.com/) + end + end + + context "when error message is about neither host down or getaddrinfo" do + let(:message) { "other error about network" } + + it "should raise a Bundler::HTTPError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError, + "Network error while fetching http://www.uri-to-fetch.com/api/v2/endpoint (other error about network)") + end + + context "when the there are credentials provided in the request" do + let(:uri) { URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + before do + allow(net_http_get).to receive(:basic_auth).with("username", "password") + end + + it "should raise a Bundler::HTTPError that doesn't contain the password" do + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError, + "Network error while fetching http://username@www.uri-to-fetch.com/api/v2/endpoint (other error about network)") + end + end + end + + context "when error message is about no route to host" do + let(:message) { "Failed to open TCP connection to www.uri-to-fetch.com:443 " } + + it "should raise a Bundler::Fetcher::HTTPError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError, + "Network error while fetching http://www.uri-to-fetch.com/api/v2/endpoint (#{message})") + end + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/index_spec.rb b/spec/bundler/bundler/fetcher/index_spec.rb new file mode 100644 index 00000000000000..0cf0ae764ec814 --- /dev/null +++ b/spec/bundler/bundler/fetcher/index_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Index do + let(:downloader) { nil } + let(:remote) { nil } + let(:display_uri) { "http://sample_uri.com" } + let(:rubygems) { double(:rubygems) } + let(:gem_names) { %w[foo bar] } + + subject { described_class.new(downloader, remote, display_uri) } + + before { allow(Bundler).to receive(:rubygems).and_return(rubygems) } + + it "fetches and returns the list of remote specs" do + expect(rubygems).to receive(:fetch_all_remote_specs) { nil } + subject.specs(gem_names) + end + + context "error handling" do + shared_examples_for "the error is properly handled" do + let(:remote_uri) { URI("http://remote-uri.org") } + before do + allow(subject).to receive(:remote_uri).and_return(remote_uri) + end + + context "when certificate verify failed" do + let(:error_message) { "certificate verify failed" } + + it "should raise a Bundler::Fetcher::CertificateFailureError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::CertificateFailureError, + %r{Could not verify the SSL certificate for http://sample_uri.com}) + end + end + + context "when a 401 response occurs" do + let(:error_message) { "401" } + + it "should raise a Bundler::Fetcher::AuthenticationRequiredError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, + %r{Authentication is required for http://remote-uri.org}) + end + end + + context "when a 403 response occurs" do + let(:error_message) { "403" } + + before do + allow(remote_uri).to receive(:userinfo).and_return(userinfo) + end + + context "and there was userinfo" do + let(:userinfo) { double(:userinfo) } + + it "should raise a Bundler::Fetcher::BadAuthenticationError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::BadAuthenticationError, + %r{Bad username or password for http://remote-uri.org}) + end + end + + context "and there was no userinfo" do + let(:userinfo) { nil } + + it "should raise a Bundler::Fetcher::AuthenticationRequiredError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, + %r{Authentication is required for http://remote-uri.org}) + end + end + end + + context "any other message is returned" do + let(:error_message) { "You get an error, you get an error!" } + + before { allow(Bundler).to receive(:ui).and_return(double(:trace => nil)) } + + it "should raise a Bundler::HTTPError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::HTTPError, "Could not fetch specs from http://sample_uri.com") + end + end + end + + context "when a Gem::RemoteFetcher::FetchError occurs" do + before { allow(rubygems).to receive(:fetch_all_remote_specs) { raise Gem::RemoteFetcher::FetchError.new(error_message, nil) } } + + it_behaves_like "the error is properly handled" + end + + context "when a OpenSSL::SSL::SSLError occurs" do + before { allow(rubygems).to receive(:fetch_all_remote_specs) { raise OpenSSL::SSL::SSLError.new(error_message) } } + + it_behaves_like "the error is properly handled" + end + + context "when a Net::HTTPFatalError occurs" do + before { allow(rubygems).to receive(:fetch_all_remote_specs) { raise Net::HTTPFatalError.new(error_message, 404) } } + + it_behaves_like "the error is properly handled" + end + end +end diff --git a/spec/bundler/bundler/fetcher_spec.rb b/spec/bundler/bundler/fetcher_spec.rb new file mode 100644 index 00000000000000..184b9efa646f37 --- /dev/null +++ b/spec/bundler/bundler/fetcher_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require "bundler/fetcher" + +RSpec.describe Bundler::Fetcher do + let(:uri) { URI("https://example.com") } + let(:remote) { double("remote", :uri => uri, :original_uri => nil) } + + subject(:fetcher) { Bundler::Fetcher.new(remote) } + + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + describe "#connection" do + context "when Gem.configuration doesn't specify http_proxy" do + it "specify no http_proxy" do + expect(fetcher.http_proxy).to be_nil + end + it "consider environment vars when determine proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do + expect(fetcher.http_proxy).to match("http://proxy-example.com") + end + end + end + context "when Gem.configuration specifies http_proxy " do + let(:proxy) { "http://proxy-example2.com" } + before do + allow(Bundler.rubygems.configuration).to receive(:[]).with(:http_proxy).and_return(proxy) + end + it "consider Gem.configuration when determine proxy" do + expect(fetcher.http_proxy).to match("http://proxy-example2.com") + end + it "consider Gem.configuration when determine proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do + expect(fetcher.http_proxy).to match("http://proxy-example2.com") + end + end + context "when the proxy is :no_proxy" do + let(:proxy) { :no_proxy } + it "does not set a proxy" do + expect(fetcher.http_proxy).to be_nil + end + end + end + + context "when a rubygems source mirror is set" do + let(:orig_uri) { URI("http://zombo.com") } + let(:remote_with_mirror) do + double("remote", :uri => uri, :original_uri => orig_uri, :anonymized_uri => uri) + end + + let(:fetcher) { Bundler::Fetcher.new(remote_with_mirror) } + + it "sets the 'X-Gemfile-Source' header containing the original source" do + expect( + fetcher.send(:connection).override_headers["X-Gemfile-Source"] + ).to eq("http://zombo.com") + end + end + + context "when there is no rubygems source mirror set" do + let(:remote_no_mirror) do + double("remote", :uri => uri, :original_uri => nil, :anonymized_uri => uri) + end + + let(:fetcher) { Bundler::Fetcher.new(remote_no_mirror) } + + it "does not set the 'X-Gemfile-Source' header" do + expect(fetcher.send(:connection).override_headers["X-Gemfile-Source"]).to be_nil + end + end + + context "when there are proxy environment variable(s) set" do + it "consider http_proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example3.com") do + expect(fetcher.http_proxy).to match("http://proxy-example3.com") + end + end + it "consider no_proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example4.com", "NO_PROXY" => ".example.com,.example.net") do + expect( + fetcher.send(:connection).no_proxy + ).to eq([".example.com", ".example.net"]) + end + end + end + + context "when no ssl configuration is set" do + it "no cert" do + expect(fetcher.send(:connection).cert).to be_nil + expect(fetcher.send(:connection).key).to be_nil + end + end + + context "when bunder ssl ssl configuration is set" do + before do + cert = File.join(Spec::Path.tmpdir, "cert") + File.open(cert, "w") {|f| f.write "PEM" } + allow(Bundler.settings).to receive(:[]).and_return(nil) + allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(cert) + expect(OpenSSL::X509::Certificate).to receive(:new).with("PEM").and_return("cert") + expect(OpenSSL::PKey::RSA).to receive(:new).with("PEM").and_return("key") + end + after do + FileUtils.rm File.join(Spec::Path.tmpdir, "cert") + end + it "use bundler configuration" do + expect(fetcher.send(:connection).cert).to eq("cert") + expect(fetcher.send(:connection).key).to eq("key") + end + end + + context "when gem ssl configuration is set" do + before do + allow(Bundler.rubygems.configuration).to receive_messages( + :http_proxy => nil, + :ssl_client_cert => "cert", + :ssl_ca_cert => "ca" + ) + expect(File).to receive(:read).and_return("") + expect(OpenSSL::X509::Certificate).to receive(:new).and_return("cert") + expect(OpenSSL::PKey::RSA).to receive(:new).and_return("key") + store = double("ca store") + expect(store).to receive(:add_file) + expect(OpenSSL::X509::Store).to receive(:new).and_return(store) + end + it "use gem configuration" do + expect(fetcher.send(:connection).cert).to eq("cert") + expect(fetcher.send(:connection).key).to eq("key") + end + end + end + + describe "#user_agent" do + it "builds user_agent with current ruby version and Bundler settings" do + allow(Bundler.settings).to receive(:all).and_return(%w[foo bar]) + expect(fetcher.user_agent).to match(%r{bundler/(\d.)}) + expect(fetcher.user_agent).to match(%r{rubygems/(\d.)}) + expect(fetcher.user_agent).to match(%r{ruby/(\d.)}) + expect(fetcher.user_agent).to match(%r{options/foo,bar}) + end + + describe "include CI information" do + it "from one CI" do + with_env_vars("JENKINS_URL" => "foo") do + ci_part = fetcher.user_agent.split(" ").find {|x| x.match(%r{\Aci/}) } + expect(ci_part).to match("jenkins") + end + end + + it "from many CI" do + with_env_vars("TRAVIS" => "foo", "CI_NAME" => "my_ci") do + ci_part = fetcher.user_agent.split(" ").find {|x| x.match(%r{\Aci/}) } + expect(ci_part).to match("travis") + expect(ci_part).to match("my_ci") + end + end + end + end +end diff --git a/spec/bundler/bundler/friendly_errors_spec.rb b/spec/bundler/bundler/friendly_errors_spec.rb new file mode 100644 index 00000000000000..2a1be491efab34 --- /dev/null +++ b/spec/bundler/bundler/friendly_errors_spec.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +require "bundler" +require "bundler/friendly_errors" +require "cgi" + +RSpec.describe Bundler, "friendly errors" do + context "with invalid YAML in .gemrc" do + before do + File.open(Gem.configuration.config_file_name, "w") do |f| + f.write "invalid: yaml: hah" + end + end + + after do + FileUtils.rm(Gem.configuration.config_file_name) + end + + it "reports a relevant friendly error message", :ruby => ">= 1.9", :rubygems => "< 2.5.0" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, :env => { "DEBUG" => true } + + expect(out).to include("Your RubyGems configuration") + expect(out).to include("invalid YAML syntax") + expect(out).to include("Psych::SyntaxError") + expect(out).not_to include("ERROR REPORT TEMPLATE") + expect(exitstatus).to eq(25) if exitstatus + end + + it "reports a relevant friendly error message", :ruby => ">= 1.9", :rubygems => ">= 2.5.0" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, :env => { "DEBUG" => true } + + expect(last_command.stderr).to include("Failed to load #{home(".gemrc")}") + expect(exitstatus).to eq(0) if exitstatus + end + end + + it "calls log_error in case of exception" do + exception = Exception.new + expect(Bundler::FriendlyErrors).to receive(:exit_status).with(exception).and_return(1) + expect do + Bundler.with_friendly_errors do + raise exception + end + end.to raise_error(SystemExit) + end + + it "calls exit_status on exception" do + exception = Exception.new + expect(Bundler::FriendlyErrors).to receive(:log_error).with(exception) + expect do + Bundler.with_friendly_errors do + raise exception + end + end.to raise_error(SystemExit) + end + + describe "#log_error" do + shared_examples "Bundler.ui receive error" do |error, message| + it "" do + expect(Bundler.ui).to receive(:error).with(message || error.message) + Bundler::FriendlyErrors.log_error(error) + end + end + + shared_examples "Bundler.ui receive trace" do |error| + it "" do + expect(Bundler.ui).to receive(:trace).with(error) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "YamlSyntaxError" do + it_behaves_like "Bundler.ui receive error", Bundler::YamlSyntaxError.new(StandardError.new, "sample_message") + + it "Bundler.ui receive trace" do + std_error = StandardError.new + exception = Bundler::YamlSyntaxError.new(std_error, "sample_message") + expect(Bundler.ui).to receive(:trace).with(std_error) + Bundler::FriendlyErrors.log_error(exception) + end + end + + context "Dsl::DSLError, GemspecError" do + it_behaves_like "Bundler.ui receive error", Bundler::Dsl::DSLError.new("description", "dsl_path", "backtrace") + it_behaves_like "Bundler.ui receive error", Bundler::GemspecError.new + end + + context "GemRequireError" do + let(:orig_error) { StandardError.new } + let(:error) { Bundler::GemRequireError.new(orig_error, "sample_message") } + + before do + allow(orig_error).to receive(:backtrace).and_return([]) + end + + it "Bundler.ui receive error" do + expect(Bundler.ui).to receive(:error).with(error.message) + Bundler::FriendlyErrors.log_error(error) + end + + it "writes to Bundler.ui.trace" do + expect(Bundler.ui).to receive(:trace).with(orig_error, nil, true) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "BundlerError" do + it "Bundler.ui receive error" do + error = Bundler::BundlerError.new + expect(Bundler.ui).to receive(:error).with(error.message, :wrap => true) + Bundler::FriendlyErrors.log_error(error) + end + it_behaves_like "Bundler.ui receive trace", Bundler::BundlerError.new + end + + context "Thor::Error" do + it_behaves_like "Bundler.ui receive error", Bundler::Thor::Error.new + end + + context "LoadError" do + let(:error) { LoadError.new("cannot load such file -- openssl") } + + it "Bundler.ui receive error" do + expect(Bundler.ui).to receive(:error).with("\nCould not load OpenSSL.") + Bundler::FriendlyErrors.log_error(error) + end + + it "Bundler.ui receive warn" do + expect(Bundler.ui).to receive(:warn).with(any_args, :wrap => true) + Bundler::FriendlyErrors.log_error(error) + end + + it "Bundler.ui receive trace" do + expect(Bundler.ui).to receive(:trace).with(error) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "Interrupt" do + it "Bundler.ui receive error" do + expect(Bundler.ui).to receive(:error).with("\nQuitting...") + Bundler::FriendlyErrors.log_error(Interrupt.new) + end + it_behaves_like "Bundler.ui receive trace", Interrupt.new + end + + context "Gem::InvalidSpecificationException" do + it "Bundler.ui receive error" do + error = Gem::InvalidSpecificationException.new + expect(Bundler.ui).to receive(:error).with(error.message, :wrap => true) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "SystemExit" do + # Does nothing + end + + context "Java::JavaLang::OutOfMemoryError" do + module Java + module JavaLang + class OutOfMemoryError < StandardError; end + end + end + + it "Bundler.ui receive error" do + error = Java::JavaLang::OutOfMemoryError.new + expect(Bundler.ui).to receive(:error).with(/JVM has run out of memory/) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "unexpected error" do + it "calls request_issue_report_for with error" do + error = StandardError.new + expect(Bundler::FriendlyErrors).to receive(:request_issue_report_for).with(error) + Bundler::FriendlyErrors.log_error(error) + end + end + end + + describe "#exit_status" do + it "calls status_code for BundlerError" do + error = Bundler::BundlerError.new + expect(error).to receive(:status_code).and_return("sample_status_code") + expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status_code") + end + + it "returns 15 for Thor::Error" do + error = Bundler::Thor::Error.new + expect(Bundler::FriendlyErrors.exit_status(error)).to eq(15) + end + + it "calls status for SystemExit" do + error = SystemExit.new + expect(error).to receive(:status).and_return("sample_status") + expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status") + end + + it "returns 1 in other cases" do + error = StandardError.new + expect(Bundler::FriendlyErrors.exit_status(error)).to eq(1) + end + end + + describe "#request_issue_report_for" do + it "calls relevant methods for Bundler.ui" do + expect(Bundler.ui).to receive(:info) + expect(Bundler.ui).to receive(:error) + expect(Bundler.ui).to receive(:warn) + Bundler::FriendlyErrors.request_issue_report_for(StandardError.new) + end + + it "includes error class, message and backlog" do + error = StandardError.new + allow(Bundler::FriendlyErrors).to receive(:issues_url).and_return("") + + expect(error).to receive(:class).at_least(:once) + expect(error).to receive(:message).at_least(:once) + expect(error).to receive(:backtrace).at_least(:once) + Bundler::FriendlyErrors.request_issue_report_for(error) + end + end + + describe "#issues_url" do + it "generates a search URL for the exception message" do + exception = Exception.new("Exception message") + + expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/bundler/bundler/search?q=Exception+message&type=Issues") + end + + it "generates a search URL for only the first line of a multi-line exception message" do + exception = Exception.new(< "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + bundle "gem #{app_name}" + end + + context "determining gemspec" do + subject { Bundler::GemHelper.new(app_path) } + + context "fails" do + it "when there is no gemspec" do + FileUtils.rm app_gemspec_path + expect { subject }.to raise_error(/Unable to determine name/) + end + + it "when there are two gemspecs and the name isn't specified" do + FileUtils.touch app_path.join("#{app_name}-2.gemspec") + expect { subject }.to raise_error(/Unable to determine name/) + end + end + + context "interpolates the name" do + before do + # Remove exception that prevents public pushes on older RubyGems versions + if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + content = File.read(app_gemspec_path) + content.sub!(/raise "RubyGems 2\.0 or newer.*/, "") + File.open(app_gemspec_path, "w") {|f| f.write(content) } + end + end + + it "when there is only one gemspec" do + expect(subject.gemspec.name).to eq(app_name) + end + + it "for a hidden gemspec" do + FileUtils.mv app_gemspec_path, app_path.join(".gemspec") + expect(subject.gemspec.name).to eq(app_name) + end + end + + it "handles namespaces and converts them to CamelCase" do + bundle "gem #{app_name}-foo_bar" + underscore_path = bundled_app "#{app_name}-foo_bar" + + lib = underscore_path.join("lib/#{app_name}/foo_bar.rb").read + expect(lib).to include("module LoremIpsum") + expect(lib).to include("module FooBar") + end + end + + context "gem management" do + def mock_confirm_message(message) + expect(Bundler.ui).to receive(:confirm).with(message) + end + + def mock_build_message(name, version) + message = "#{name} #{version} built to pkg/#{name}-#{version}.gem." + mock_confirm_message message + end + + subject! { Bundler::GemHelper.new(app_path) } + let(:app_version) { "0.1.0" } + let(:app_gem_dir) { app_path.join("pkg") } + let(:app_gem_path) { app_gem_dir.join("#{app_name}-#{app_version}.gem") } + let(:app_gemspec_content) { remove_push_guard(File.read(app_gemspec_path)) } + + before(:each) do + content = app_gemspec_content.gsub("TODO: ", "") + content.sub!(/homepage\s+= ".*"/, 'homepage = ""') + content.gsub!(/spec\.metadata.+\n/, "") + File.open(app_gemspec_path, "w") {|file| file << content } + end + + def remove_push_guard(gemspec_content) + # Remove exception that prevents public pushes on older RubyGems versions + if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + gemspec_content.sub!(/raise "RubyGems 2\.0 or newer.*/, "") + end + gemspec_content + end + + it "uses a shell UI for output" do + expect(Bundler.ui).to be_a(Bundler::UI::Shell) + end + + describe "#install" do + let!(:rake_application) { Rake.application } + + before(:each) do + Rake.application = Rake::Application.new + end + + after(:each) do + Rake.application = rake_application + end + + context "defines Rake tasks" do + let(:task_names) do + %w[build install release release:guard_clean + release:source_control_push release:rubygem_push] + end + + context "before installation" do + it "raises an error with appropriate message" do + task_names.each do |name| + expect { Rake.application[name] }. + to raise_error(/^Don't know how to build task '#{name}'/) + end + end + end + + context "after installation" do + before do + subject.install + end + + it "adds Rake tasks successfully" do + task_names.each do |name| + expect { Rake.application[name] }.not_to raise_error + expect(Rake.application[name]).to be_instance_of Rake::Task + end + end + + it "provides a way to access the gemspec object" do + expect(subject.gemspec.name).to eq(app_name) + end + end + end + end + + describe "#build_gem" do + context "when build failed" do + it "raises an error with appropriate message" do + # break the gemspec by adding back the TODOs + File.open(app_gemspec_path, "w") {|file| file << app_gemspec_content } + expect { subject.build_gem }.to raise_error(/TODO/) + end + end + + context "when build was successful" do + it "creates .gem file" do + mock_build_message app_name, app_version + subject.build_gem + expect(app_gem_path).to exist + end + end + end + + describe "#install_gem" do + context "when installation was successful" do + it "gem is installed" do + mock_build_message app_name, app_version + mock_confirm_message "#{app_name} (#{app_version}) installed." + subject.install_gem(nil, :local) + expect(app_gem_path).to exist + gem_command! :list + expect(out).to include("#{app_name} (#{app_version})") + end + end + + context "when installation fails" do + it "raises an error with appropriate message" do + # create empty gem file in order to simulate install failure + allow(subject).to receive(:build_gem) do + FileUtils.mkdir_p(app_gem_dir) + FileUtils.touch app_gem_path + app_gem_path + end + expect { subject.install_gem }.to raise_error(/Couldn't install gem/) + end + end + end + + describe "rake release" do + let!(:rake_application) { Rake.application } + + before(:each) do + Rake.application = Rake::Application.new + subject.install + end + + after(:each) do + Rake.application = rake_application + end + + before do + Dir.chdir(app_path) do + `git init` + `git config user.email "you@example.com"` + `git config user.name "name"` + `git config push.default simple` + end + + # silence messages + allow(Bundler.ui).to receive(:confirm) + allow(Bundler.ui).to receive(:error) + end + + context "fails" do + it "when there are unstaged files" do + expect { Rake.application["release"].invoke }. + to raise_error("There are files that need to be committed first.") + end + + it "when there are uncommitted files" do + Dir.chdir(app_path) { `git add .` } + expect { Rake.application["release"].invoke }. + to raise_error("There are files that need to be committed first.") + end + + it "when there is no git remote" do + Dir.chdir(app_path) { `git commit -a -m "initial commit"` } + expect { Rake.application["release"].invoke }.to raise_error(RuntimeError) + end + end + + context "succeeds" do + before do + Dir.chdir(gem_repo1) { `git init --bare` } + Dir.chdir(app_path) do + `git remote add origin file://#{gem_repo1}` + `git commit -a -m "initial commit"` + end + end + + it "on releasing" do + mock_build_message app_name, app_version + mock_confirm_message "Tagged v#{app_version}." + mock_confirm_message "Pushed git commits and tags." + expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) + + Dir.chdir(app_path) { sys_exec("git push -u origin master") } + + Rake.application["release"].invoke + end + + it "even if tag already exists" do + mock_build_message app_name, app_version + mock_confirm_message "Tag v#{app_version} has already been created." + expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) + + Dir.chdir(app_path) do + `git tag -a -m \"Version #{app_version}\" v#{app_version}` + end + + Rake.application["release"].invoke + end + end + end + + describe "release:rubygem_push" do + let!(:rake_application) { Rake.application } + + before(:each) do + Rake.application = Rake::Application.new + subject.install + allow(subject).to receive(:sh) + end + + after(:each) do + Rake.application = rake_application + end + + before do + Dir.chdir(app_path) do + `git init` + `git config user.email "you@example.com"` + `git config user.name "name"` + `git config push.default simple` + end + + # silence messages + allow(Bundler.ui).to receive(:confirm) + allow(Bundler.ui).to receive(:error) + + credentials = double("credentials", "file?" => true) + allow(Bundler.user_home).to receive(:join). + with(".gem/credentials").and_return(credentials) + end + + describe "success messaging" do + context "No allowed_push_host set" do + before do + allow(subject).to receive(:allowed_push_host).and_return(nil) + end + + around do |example| + orig_host = ENV["RUBYGEMS_HOST"] + ENV["RUBYGEMS_HOST"] = rubygems_host_env + + example.run + + ENV["RUBYGEMS_HOST"] = orig_host + end + + context "RUBYGEMS_HOST env var is set" do + let(:rubygems_host_env) { "https://custom.env.gemhost.com" } + + it "should report successful push to the host from the environment" do + mock_confirm_message "Pushed #{app_name} #{app_version} to #{rubygems_host_env}" + + Rake.application["release:rubygem_push"].invoke + end + end + + context "RUBYGEMS_HOST env var is not set" do + let(:rubygems_host_env) { nil } + + it "should report successful push to rubygems.org" do + mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org" + + Rake.application["release:rubygem_push"].invoke + end + end + + context "RUBYGEMS_HOST env var is an empty string" do + let(:rubygems_host_env) { "" } + + it "should report successful push to rubygems.org" do + mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org" + + Rake.application["release:rubygem_push"].invoke + end + end + end + + context "allowed_push_host set in gemspec" do + before do + allow(subject).to receive(:allowed_push_host).and_return("https://my.gemhost.com") + end + + it "should report successful push to the allowed gem host" do + mock_confirm_message "Pushed #{app_name} #{app_version} to https://my.gemhost.com" + + Rake.application["release:rubygem_push"].invoke + end + end + end + end + end +end diff --git a/spec/bundler/bundler/gem_version_promoter_spec.rb b/spec/bundler/bundler/gem_version_promoter_spec.rb new file mode 100644 index 00000000000000..01e0232fbab83b --- /dev/null +++ b/spec/bundler/bundler/gem_version_promoter_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::GemVersionPromoter do + context "conservative resolver" do + def versions(result) + result.flatten.map(&:version).map(&:to_s) + end + + def make_instance(*args) + @gvp = Bundler::GemVersionPromoter.new(*args).tap do |gvp| + gvp.class.class_eval { public :filter_dep_specs, :sort_dep_specs } + end + end + + def unlocking(options) + make_instance(Bundler::SpecSet.new([]), ["foo"]).tap do |p| + p.level = options[:level] if options[:level] + p.strict = options[:strict] if options[:strict] + end + end + + def keep_locked(options) + make_instance(Bundler::SpecSet.new([]), ["bar"]).tap do |p| + p.level = options[:level] if options[:level] + p.strict = options[:strict] if options[:strict] + end + end + + def build_spec_groups(name, versions) + versions.map do |v| + Bundler::Resolver::SpecGroup.new(build_spec(name, v)) + end + end + + # Rightmost (highest array index) in result is most preferred. + # Leftmost (lowest array index) in result is least preferred. + # `build_spec_groups` has all versions of gem in index. + # `build_spec` is the version currently in the .lock file. + # + # In default (not strict) mode, all versions in the index will + # be returned, allowing Bundler the best chance to resolve all + # dependencies, but sometimes resulting in upgrades that some + # would not consider conservative. + context "filter specs (strict) level patch" do + it "when keeping build_spec, keep current, next release" do + keep_locked(:level => :patch) + res = @gvp.filter_dep_specs( + build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]), + build_spec("foo", "1.7.8").first + ) + expect(versions(res)).to eq %w[1.7.9 1.7.8] + end + + it "when unlocking prefer next release first" do + unlocking(:level => :patch) + res = @gvp.filter_dep_specs( + build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]), + build_spec("foo", "1.7.8").first + ) + expect(versions(res)).to eq %w[1.7.8 1.7.9] + end + + it "when unlocking keep current when already at latest release" do + unlocking(:level => :patch) + res = @gvp.filter_dep_specs( + build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]), + build_spec("foo", "1.7.9").first + ) + expect(versions(res)).to eq %w[1.7.9] + end + end + + context "filter specs (strict) level minor" do + it "when unlocking favor next releases, remove minor and major increases" do + unlocking(:level => :minor) + res = @gvp.filter_dep_specs( + build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), + build_spec("foo", "0.2.0").first + ) + expect(versions(res)).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0] + end + + it "when keep locked, keep current, then favor next release, remove minor and major increases" do + keep_locked(:level => :minor) + res = @gvp.filter_dep_specs( + build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), + build_spec("foo", "0.2.0").first + ) + expect(versions(res)).to eq %w[0.3.0 0.3.1 0.9.0 0.2.0] + end + end + + context "sort specs (not strict) level patch" do + it "when not unlocking, same order but make sure build_spec version is most preferred to stay put" do + keep_locked(:level => :patch) + res = @gvp.sort_dep_specs( + build_spec_groups("foo", %w[1.5.4 1.6.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 2.0.0 2.0.1]), + build_spec("foo", "1.7.7").first + ) + expect(versions(res)).to eq %w[1.5.4 1.6.5 1.7.6 2.0.0 2.0.1 1.8.0 1.8.1 1.7.8 1.7.9 1.7.7] + end + + it "when unlocking favor next release, then current over minor increase" do + unlocking(:level => :patch) + res = @gvp.sort_dep_specs( + build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.8.0]), + build_spec("foo", "1.7.8").first + ) + expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9] + end + + it "when unlocking do proper integer comparison, not string" do + unlocking(:level => :patch) + res = @gvp.sort_dep_specs( + build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0]), + build_spec("foo", "1.7.8").first + ) + expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9 1.7.15] + end + + it "leave current when unlocking but already at latest release" do + unlocking(:level => :patch) + res = @gvp.sort_dep_specs( + build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]), + build_spec("foo", "1.7.9").first + ) + expect(versions(res)).to eq %w[2.0.0 1.8.0 1.7.9] + end + end + + context "sort specs (not strict) level minor" do + it "when unlocking favor next release, then minor increase over current" do + unlocking(:level => :minor) + res = @gvp.sort_dep_specs( + build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), + build_spec("foo", "0.2.0").first + ) + expect(versions(res)).to eq %w[2.0.0 2.0.1 1.0.0 0.2.0 0.3.0 0.3.1 0.9.0] + end + end + + context "level error handling" do + subject { Bundler::GemVersionPromoter.new } + + it "should raise if not major, minor or patch is passed" do + expect { subject.level = :minjor }.to raise_error ArgumentError + end + + it "should raise if invalid classes passed" do + [123, nil].each do |value| + expect { subject.level = value }.to raise_error ArgumentError + end + end + + it "should accept major, minor patch symbols" do + [:major, :minor, :patch].each do |value| + subject.level = value + expect(subject.level).to eq value + end + end + + it "should accept major, minor patch strings" do + %w[major minor patch].each do |value| + subject.level = value + expect(subject.level).to eq value.to_sym + end + end + end + + context "debug output" do + it "should not kerblooie on its own debug output" do + gvp = unlocking(:level => :patch) + dep = Bundler::DepProxy.new(dep("foo", "1.2.0").first, "ruby") + result = gvp.send(:debug_format_result, dep, build_spec_groups("foo", %w[1.2.0 1.3.0])) + expect(result.class).to eq Array + end + end + end +end diff --git a/spec/bundler/bundler/index_spec.rb b/spec/bundler/bundler/index_spec.rb new file mode 100644 index 00000000000000..0f3f6e49445c39 --- /dev/null +++ b/spec/bundler/bundler/index_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Index do + let(:specs) { [] } + subject { described_class.build {|i| i.use(specs) } } + + context "specs with a nil platform" do + let(:spec) do + Gem::Specification.new do |s| + s.name = "json" + s.version = "1.8.3" + allow(s).to receive(:platform).and_return(nil) + end + end + let(:specs) { [spec] } + + describe "#search_by_spec" do + it "finds the spec when a nil platform is specified" do + expect(subject.search(spec)).to eq([spec]) + end + + it "finds the spec when a ruby platform is specified" do + query = spec.dup.tap {|s| s.platform = "ruby" } + expect(subject.search(query)).to eq([spec]) + end + end + end + + context "with specs that include development dependencies" do + let(:specs) { [*build_spec("a", "1.0.0") {|s| s.development("b", "~> 1.0") }] } + + it "does not include b in #dependency_names" do + expect(subject.dependency_names).not_to include("b") + end + end +end diff --git a/spec/bundler/bundler/installer/gem_installer_spec.rb b/spec/bundler/bundler/installer/gem_installer_spec.rb new file mode 100644 index 00000000000000..7340a3acc0aefe --- /dev/null +++ b/spec/bundler/bundler/installer/gem_installer_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "bundler/installer/gem_installer" + +RSpec.describe Bundler::GemInstaller do + let(:installer) { instance_double("Installer") } + let(:spec_source) { instance_double("SpecSource") } + let(:spec) { instance_double("Specification", :name => "dummy", :version => "0.0.1", :loaded_from => "dummy", :source => spec_source) } + + subject { described_class.new(spec, installer) } + + context "spec_settings is nil" do + it "invokes install method with empty build_args", :rubygems => ">= 2" do + allow(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => []) + subject.install_from_spec + end + end + + context "spec_settings is build option" do + it "invokes install method with build_args", :rubygems => ">= 2" do + allow(Bundler.settings).to receive(:[]).with(:bin) + allow(Bundler.settings).to receive(:[]).with(:inline) + allow(Bundler.settings).to receive(:[]).with(:forget_cli_options) + allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy") + expect(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => ["--with-dummy-config=dummy"]) + subject.install_from_spec + end + end +end diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb new file mode 100644 index 00000000000000..ace5c1a23a9a8c --- /dev/null +++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "bundler/installer/parallel_installer" + +RSpec.describe Bundler::ParallelInstaller do + let(:installer) { instance_double("Installer") } + let(:all_specs) { [] } + let(:size) { 1 } + let(:standalone) { false } + let(:force) { false } + + subject { described_class.new(installer, all_specs, size, standalone, force) } + + context "when dependencies that are not on the overall installation list are the only ones not installed" do + let(:all_specs) do + [ + build_spec("alpha", "1.0") {|s| s.runtime "a", "1" }, + ].flatten + end + + it "prints a warning" do + expect(Bundler.ui).to receive(:warn).with(<<-W.strip) +Your lockfile was created by an old Bundler that left some things out. +You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile. +The missing gems are: +* a depended upon by alpha + W + subject.check_for_corrupt_lockfile + end + + context "when size > 1" do + let(:size) { 500 } + + it "prints a warning and sets size to 1" do + expect(Bundler.ui).to receive(:warn).with(<<-W.strip) +Your lockfile was created by an old Bundler that left some things out. +Because of the missing DEPENDENCIES, we can only install gems one at a time, instead of installing 500 at a time. +You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile. +The missing gems are: +* a depended upon by alpha + W + subject.check_for_corrupt_lockfile + expect(subject.size).to eq(1) + end + end + end +end diff --git a/spec/bundler/bundler/installer/spec_installation_spec.rb b/spec/bundler/bundler/installer/spec_installation_spec.rb new file mode 100644 index 00000000000000..a9cf09a372968c --- /dev/null +++ b/spec/bundler/bundler/installer/spec_installation_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "bundler/installer/parallel_installer" + +RSpec.describe Bundler::ParallelInstaller::SpecInstallation do + let!(:dep) do + a_spec = Object.new + def a_spec.name + "I like tests" + end + a_spec + end + + describe "#ready_to_enqueue?" do + context "when in enqueued state" do + it "is falsey" do + spec = described_class.new(dep) + spec.state = :enqueued + expect(spec.ready_to_enqueue?).to be_falsey + end + end + + context "when in installed state" do + it "returns falsey" do + spec = described_class.new(dep) + spec.state = :installed + expect(spec.ready_to_enqueue?).to be_falsey + end + end + + it "returns truthy" do + spec = described_class.new(dep) + expect(spec.ready_to_enqueue?).to be_truthy + end + end + + describe "#dependencies_installed?" do + context "when all dependencies are installed" do + it "returns true" do + dependencies = [] + dependencies << instance_double("SpecInstallation", :spec => "alpha", :name => "alpha", :installed? => true, :all_dependencies => [], :type => :production) + dependencies << instance_double("SpecInstallation", :spec => "beta", :name => "beta", :installed? => true, :all_dependencies => [], :type => :production) + all_specs = dependencies + [instance_double("SpecInstallation", :spec => "gamma", :name => "gamma", :installed? => false, :all_dependencies => [], :type => :production)] + spec = described_class.new(dep) + allow(spec).to receive(:all_dependencies).and_return(dependencies) + expect(spec.dependencies_installed?(all_specs)).to be_truthy + end + end + + context "when all dependencies are not installed" do + it "returns false" do + dependencies = [] + dependencies << instance_double("SpecInstallation", :spec => "alpha", :name => "alpha", :installed? => false, :all_dependencies => [], :type => :production) + dependencies << instance_double("SpecInstallation", :spec => "beta", :name => "beta", :installed? => true, :all_dependencies => [], :type => :production) + all_specs = dependencies + [instance_double("SpecInstallation", :spec => "gamma", :name => "gamma", :installed? => false, :all_dependencies => [], :type => :production)] + spec = described_class.new(dep) + allow(spec).to receive(:all_dependencies).and_return(dependencies) + expect(spec.dependencies_installed?(all_specs)).to be_falsey + end + end + end +end diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb new file mode 100644 index 00000000000000..3a6d61336ff30c --- /dev/null +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require "bundler/lockfile_parser" + +RSpec.describe Bundler::LockfileParser do + let(:lockfile_contents) { strip_whitespace(<<-L) } + GIT + remote: https://github.com/alloy/peiji-san.git + revision: eca485d8dc95f12aaec1a434b49d295c7e91844b + specs: + peiji-san (1.2.0) + + GEM + remote: https://rubygems.org/ + specs: + rake (10.3.2) + + PLATFORMS + ruby + + DEPENDENCIES + peiji-san! + rake + + RUBY VERSION + ruby 2.1.3p242 + + BUNDLED WITH + 1.12.0.rc.2 + L + + describe ".sections_in_lockfile" do + it "returns the attributes" do + attributes = described_class.sections_in_lockfile(lockfile_contents) + expect(attributes).to contain_exactly( + "BUNDLED WITH", "DEPENDENCIES", "GEM", "GIT", "PLATFORMS", "RUBY VERSION" + ) + end + end + + describe ".unknown_sections_in_lockfile" do + let(:lockfile_contents) { strip_whitespace(<<-L) } + UNKNOWN ATTR + + UNKNOWN ATTR 2 + random contents + L + + it "returns the unknown attributes" do + attributes = described_class.unknown_sections_in_lockfile(lockfile_contents) + expect(attributes).to contain_exactly("UNKNOWN ATTR", "UNKNOWN ATTR 2") + end + end + + describe ".sections_to_ignore" do + subject { described_class.sections_to_ignore(base_version) } + + context "with a nil base version" do + let(:base_version) { nil } + + it "returns the same as > 1.0" do + expect(subject).to contain_exactly( + described_class::BUNDLED, described_class::RUBY, described_class::PLUGIN + ) + end + end + + context "with a prerelease base version" do + let(:base_version) { Gem::Version.create("1.11.0.rc.1") } + + it "returns the same as for the release version" do + expect(subject).to contain_exactly( + described_class::RUBY, described_class::PLUGIN + ) + end + end + + context "with a current version" do + let(:base_version) { Gem::Version.create(Bundler::VERSION) } + + it "returns an empty array" do + expect(subject).to eq([]) + end + end + + context "with a future version" do + let(:base_version) { Gem::Version.create("5.5.5") } + + it "returns an empty array" do + expect(subject).to eq([]) + end + end + end + + describe "#initialize" do + before { allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app("gems.rb")) } + subject { described_class.new(lockfile_contents) } + + let(:sources) do + [Bundler::Source::Git.new("uri" => "https://github.com/alloy/peiji-san.git", "revision" => "eca485d8dc95f12aaec1a434b49d295c7e91844b"), + Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"])] + end + let(:dependencies) do + { + "peiji-san" => Bundler::Dependency.new("peiji-san", ">= 0"), + "rake" => Bundler::Dependency.new("rake", ">= 0"), + } + end + let(:specs) do + [ + Bundler::LazySpecification.new("peiji-san", v("1.2.0"), rb), + Bundler::LazySpecification.new("rake", v("10.3.2"), rb), + ] + end + let(:platforms) { [rb] } + let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") } + let(:ruby_version) { "ruby 2.1.3p242" } + + shared_examples_for "parsing" do + it "parses correctly" do + expect(subject.sources).to eq sources + expect(subject.dependencies).to eq dependencies + expect(subject.specs).to eq specs + expect(Hash[subject.specs.map {|s| [s, s.dependencies] }]).to eq Hash[subject.specs.map {|s| [s, s.dependencies] }] + expect(subject.platforms).to eq platforms + expect(subject.bundler_version).to eq bundler_version + expect(subject.ruby_version).to eq ruby_version + end + end + + include_examples "parsing" + + context "when an extra section is at the end" do + let(:lockfile_contents) { super() + "\n\nFOO BAR\n baz\n baa\n qux\n" } + include_examples "parsing" + end + + context "when an extra section is at the start" do + let(:lockfile_contents) { "FOO BAR\n baz\n baa\n qux\n\n" + super() } + include_examples "parsing" + end + + context "when an extra section is in the middle" do + let(:lockfile_contents) { super().split(/(?=GEM)/).insert(1, "FOO BAR\n baz\n baa\n qux\n\n").join } + include_examples "parsing" + end + + context "when a dependency has options" do + let(:lockfile_contents) { super().sub("peiji-san!", "peiji-san!\n foo: bar") } + include_examples "parsing" + end + end +end diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb new file mode 100644 index 00000000000000..acd0895f2fcde5 --- /dev/null +++ b/spec/bundler/bundler/mirror_spec.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true + +require "bundler/mirror" + +RSpec.describe Bundler::Settings::Mirror do + let(:mirror) { Bundler::Settings::Mirror.new } + + it "returns zero when fallback_timeout is not set" do + expect(mirror.fallback_timeout).to eq(0) + end + + it "takes a number as a fallback_timeout" do + mirror.fallback_timeout = 1 + expect(mirror.fallback_timeout).to eq(1) + end + + it "takes truthy as a default fallback timeout" do + mirror.fallback_timeout = true + expect(mirror.fallback_timeout).to eq(0.1) + end + + it "takes falsey as a zero fallback timeout" do + mirror.fallback_timeout = false + expect(mirror.fallback_timeout).to eq(0) + end + + it "takes a string with 'true' as a default fallback timeout" do + mirror.fallback_timeout = "true" + expect(mirror.fallback_timeout).to eq(0.1) + end + + it "takes a string with 'false' as a zero fallback timeout" do + mirror.fallback_timeout = "false" + expect(mirror.fallback_timeout).to eq(0) + end + + it "takes a string for the uri but returns an uri object" do + mirror.uri = "http://localhost:9292" + expect(mirror.uri).to eq(URI("http://localhost:9292")) + end + + it "takes an uri object for the uri" do + mirror.uri = URI("http://localhost:9293") + expect(mirror.uri).to eq(URI("http://localhost:9293")) + end + + context "without a uri" do + it "invalidates the mirror" do + mirror.validate! + expect(mirror.valid?).to be_falsey + end + end + + context "with an uri" do + before { mirror.uri = "http://localhost:9292" } + + context "without a fallback timeout" do + it "is not valid by default" do + expect(mirror.valid?).to be_falsey + end + + context "when probed" do + let(:probe) { double } + + context "with a replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(true) + mirror.validate!(probe) + end + + it "is valid" do + expect(mirror.valid?).to be_truthy + end + end + + context "with a non replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(false) + mirror.validate!(probe) + end + + it "is still valid" do + expect(mirror.valid?).to be_truthy + end + end + end + end + + context "with a fallback timeout" do + before { mirror.fallback_timeout = 1 } + + it "is not valid by default" do + expect(mirror.valid?).to be_falsey + end + + context "when probed" do + let(:probe) { double } + + context "with a replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(true) + mirror.validate!(probe) + end + + it "is valid" do + expect(mirror.valid?).to be_truthy + end + + it "is validated only once" do + allow(probe).to receive(:replies?).and_raise("Only once!") + mirror.validate!(probe) + expect(mirror.valid?).to be_truthy + end + end + + context "with a non replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(false) + mirror.validate!(probe) + end + + it "is not valid" do + expect(mirror.valid?).to be_falsey + end + + it "is validated only once" do + allow(probe).to receive(:replies?).and_raise("Only once!") + mirror.validate!(probe) + expect(mirror.valid?).to be_falsey + end + end + end + end + + describe "#==" do + it "returns true if uri and fallback timeout are the same" do + uri = "https://ruby.taobao.org" + mirror = Bundler::Settings::Mirror.new(uri, 1) + another_mirror = Bundler::Settings::Mirror.new(uri, 1) + + expect(mirror == another_mirror).to be true + end + end + end +end + +RSpec.describe Bundler::Settings::Mirrors do + let(:localhost_uri) { URI("http://localhost:9292") } + + context "with a just created mirror" do + let(:mirrors) do + probe = double + allow(probe).to receive(:replies?).and_return(true) + Bundler::Settings::Mirrors.new(probe) + end + + it "returns a mirror that contains the source uri for an unknown uri" do + mirror = mirrors.for("http://rubygems.org/") + expect(mirror).to eq(Bundler::Settings::Mirror.new("http://rubygems.org/")) + end + + it "parses a mirror key and returns a mirror for the parsed uri" do + mirrors.parse("mirror.http://rubygems.org/", localhost_uri) + expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri) + end + + it "parses a relative mirror key and returns a mirror for the parsed http uri" do + mirrors.parse("mirror.rubygems.org", localhost_uri) + expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri) + end + + it "parses a relative mirror key and returns a mirror for the parsed https uri" do + mirrors.parse("mirror.rubygems.org", localhost_uri) + expect(mirrors.for("https://rubygems.org/").uri).to eq(localhost_uri) + end + + context "with a uri parsed already" do + before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) } + + it "takes a mirror fallback_timeout and assigns the timeout" do + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "2") + expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(2) + end + + it "parses a 'true' fallback timeout and sets the default timeout" do + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true") + expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0.1) + end + + it "parses a 'false' fallback timeout and sets it to zero" do + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "false") + expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0) + end + end + end + + context "with a mirror prober that replies on time" do + let(:mirrors) do + probe = double + allow(probe).to receive(:replies?).and_return(true) + Bundler::Settings::Mirrors.new(probe) + end + + context "with a default fallback_timeout for rubygems.org" do + before do + mirrors.parse("mirror.http://rubygems.org/", localhost_uri) + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true") + end + + it "returns localhost" do + expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri) + end + end + + context "with a mirror for all" do + before do + mirrors.parse("mirror.all", localhost_uri) + end + + context "without a fallback timeout" do + it "returns localhost uri for rubygems" do + expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri) + end + + it "returns localhost for any other url" do + expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri) + end + end + context "with a fallback timeout" do + before { mirrors.parse("mirror.all.fallback_timeout", "1") } + + it "returns localhost uri for rubygems" do + expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri) + end + + it "returns localhost for any other url" do + expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri) + end + end + end + end + + context "with a mirror prober that does not reply on time" do + let(:mirrors) do + probe = double + allow(probe).to receive(:replies?).and_return(false) + Bundler::Settings::Mirrors.new(probe) + end + + context "with a localhost mirror for all" do + before { mirrors.parse("mirror.all", localhost_uri) } + + context "without a fallback timeout" do + it "returns localhost" do + expect(mirrors.for("http://whatever.com").uri).to eq(localhost_uri) + end + end + + context "with a fallback timeout" do + before { mirrors.parse("mirror.all.fallback_timeout", "true") } + + it "returns the source uri, not localhost" do + expect(mirrors.for("http://whatever.com").uri).to eq(URI("http://whatever.com/")) + end + end + end + + context "with localhost as a mirror for rubygems.org" do + before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) } + + context "without a fallback timeout" do + it "returns the uri that is not mirrored" do + expect(mirrors.for("http://whatever.com").uri).to eq(URI("http://whatever.com/")) + end + + it "returns localhost for rubygems.org" do + expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri) + end + end + + context "with a fallback timeout" do + before { mirrors.parse("mirror.http://rubygems.org/.fallback_timeout", "true") } + + it "returns the uri that is not mirrored" do + expect(mirrors.for("http://whatever.com").uri).to eq(URI("http://whatever.com/")) + end + + it "returns rubygems.org for rubygems.org" do + expect(mirrors.for("http://rubygems.org/").uri).to eq(URI("http://rubygems.org/")) + end + end + end + end +end + +RSpec.describe Bundler::Settings::TCPSocketProbe do + let(:probe) { Bundler::Settings::TCPSocketProbe.new } + + context "with a listening TCP Server" do + def with_server_and_mirror + server = TCPServer.new("127.0.0.1", 0) + mirror = Bundler::Settings::Mirror.new("http://localhost:#{server.addr[1]}", 1) + yield server, mirror + server.close unless server.closed? + end + + it "probes the server correctly", :ruby_repo do + with_server_and_mirror do |server, mirror| + expect(server.closed?).to be_falsey + expect(probe.replies?(mirror)).to be_truthy + end + end + + it "probes falsey when the server is down" do + with_server_and_mirror do |server, mirror| + server.close + expect(probe.replies?(mirror)).to be_falsey + end + end + end + + context "with an invalid mirror" do + let(:mirror) { Bundler::Settings::Mirror.new("http://127.0.0.127:9292", true) } + + it "fails with a timeout when there is nothing to tcp handshake" do + expect(probe.replies?(mirror)).to be_falsey + end + end +end diff --git a/spec/bundler/bundler/plugin/api/source_spec.rb b/spec/bundler/bundler/plugin/api/source_spec.rb new file mode 100644 index 00000000000000..2c50ff56a43a3c --- /dev/null +++ b/spec/bundler/bundler/plugin/api/source_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::API::Source do + let(:uri) { "uri://to/test" } + let(:type) { "spec_type" } + + subject(:source) do + klass = Class.new + klass.send :include, Bundler::Plugin::API::Source + klass.new("uri" => uri, "type" => type) + end + + describe "attributes" do + it "allows access to uri" do + expect(source.uri).to eq("uri://to/test") + end + + it "allows access to name" do + expect(source.name).to eq("spec_type at uri://to/test") + end + end + + context "post_install" do + let(:installer) { double(:installer) } + + before do + allow(Bundler::Source::Path::Installer).to receive(:new) { installer } + end + + it "calls Path::Installer's post_install" do + expect(installer).to receive(:post_install).once + + source.post_install(double(:spec)) + end + end + + context "install_path" do + let(:uri) { "uri://to/a/repository-name" } + let(:hash) { Digest(:SHA1).hexdigest(uri) } + let(:install_path) { Pathname.new "/bundler/install/path" } + + before do + allow(Bundler).to receive(:install_path) { install_path } + end + + it "returns basename with uri_hash" do + expected = Pathname.new "#{install_path}/repository-name-#{hash[0..11]}" + expect(source.install_path).to eq(expected) + end + end + + context "to_lock" do + it "returns the string with remote and type" do + expected = strip_whitespace <<-L + PLUGIN SOURCE + remote: #{uri} + type: #{type} + specs: + L + + expect(source.to_lock).to eq(expected) + end + + context "with additional options to lock" do + before do + allow(source).to receive(:options_to_lock) { { "first" => "option" } } + end + + it "includes them" do + expected = strip_whitespace <<-L + PLUGIN SOURCE + remote: #{uri} + type: #{type} + first: option + specs: + L + + expect(source.to_lock).to eq(expected) + end + end + end +end diff --git a/spec/bundler/bundler/plugin/api_spec.rb b/spec/bundler/bundler/plugin/api_spec.rb new file mode 100644 index 00000000000000..58fb908572d74f --- /dev/null +++ b/spec/bundler/bundler/plugin/api_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::API do + context "plugin declarations" do + before do + stub_const "UserPluginClass", Class.new(Bundler::Plugin::API) + end + + describe "#command" do + it "declares a command plugin with same class as handler" do + expect(Bundler::Plugin). + to receive(:add_command).with("meh", UserPluginClass).once + + UserPluginClass.command "meh" + end + + it "accepts another class as argument that handles the command" do + stub_const "NewClass", Class.new + expect(Bundler::Plugin).to receive(:add_command).with("meh", NewClass).once + + UserPluginClass.command "meh", NewClass + end + end + + describe "#source" do + it "declares a source plugin with same class as handler" do + expect(Bundler::Plugin). + to receive(:add_source).with("a_source", UserPluginClass).once + + UserPluginClass.source "a_source" + end + + it "accepts another class as argument that handles the command" do + stub_const "NewClass", Class.new + expect(Bundler::Plugin).to receive(:add_source).with("a_source", NewClass).once + + UserPluginClass.source "a_source", NewClass + end + end + + describe "#hook" do + it "accepts a block and passes it to Plugin module" do + foo = double("tester") + expect(foo).to receive(:called) + + expect(Bundler::Plugin).to receive(:add_hook).with("post-foo").and_yield + + Bundler::Plugin::API.hook("post-foo") { foo.called } + end + end + end + + context "bundler interfaces provided" do + before do + stub_const "UserPluginClass", Class.new(Bundler::Plugin::API) + end + + subject(:api) { UserPluginClass.new } + + # A test of delegation + it "provides the Bundler's functions" do + expect(Bundler).to receive(:an_unknown_function).once + + api.an_unknown_function + end + + it "includes Bundler::SharedHelpers' functions" do + expect(Bundler::SharedHelpers).to receive(:an_unknown_helper).once + + api.an_unknown_helper + end + + context "#tmp" do + it "provides a tmp dir" do + expect(api.tmp("mytmp")).to be_directory + end + + it "accepts multiple names for suffix" do + expect(api.tmp("myplugin", "download")).to be_directory + end + end + end +end diff --git a/spec/bundler/bundler/plugin/dsl_spec.rb b/spec/bundler/bundler/plugin/dsl_spec.rb new file mode 100644 index 00000000000000..be23db3bbacd02 --- /dev/null +++ b/spec/bundler/bundler/plugin/dsl_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::DSL do + DSL = Bundler::Plugin::DSL + + subject(:dsl) { Bundler::Plugin::DSL.new } + + before do + allow(Bundler).to receive(:root) { Pathname.new "/" } + end + + describe "it ignores only the methods defined in Bundler::Dsl" do + it "doesn't raises error for Dsl methods" do + expect { dsl.install_if }.not_to raise_error + end + + it "raises error for other methods" do + expect { dsl.no_method }.to raise_error(DSL::PluginGemfileError) + end + end + + describe "source block" do + it "adds #source with :type to list and also inferred_plugins list" do + expect(dsl).to receive(:plugin).with("bundler-source-news").once + + dsl.source("some_random_url", :type => "news") {} + + expect(dsl.inferred_plugins).to eq(["bundler-source-news"]) + end + + it "registers a source type plugin only once for multiple declataions" do + expect(dsl).to receive(:plugin).with("bundler-source-news").and_call_original.once + + dsl.source("some_random_url", :type => "news") {} + dsl.source("another_random_url", :type => "news") {} + end + end +end diff --git a/spec/bundler/bundler/plugin/events_spec.rb b/spec/bundler/bundler/plugin/events_spec.rb new file mode 100644 index 00000000000000..b09e9156824922 --- /dev/null +++ b/spec/bundler/bundler/plugin/events_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::Events do + context "plugin events" do + describe "#define" do + it "raises when redefining a constant" do + expect do + Bundler::Plugin::Events.send(:define, :GEM_BEFORE_INSTALL_ALL, "another-value") + end.to raise_error(ArgumentError) + end + + it "can define a new constant" do + Bundler::Plugin::Events.send(:define, :NEW_CONSTANT, "value") + expect(Bundler::Plugin::Events::NEW_CONSTANT).to eq("value") + end + end + end +end diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb new file mode 100644 index 00000000000000..ca3476ea2aa7ae --- /dev/null +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::Index do + Index = Bundler::Plugin::Index + + before do + gemfile "" + path = lib_path(plugin_name) + index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], commands, sources, hooks) + end + + let(:plugin_name) { "new-plugin" } + let(:commands) { [] } + let(:sources) { [] } + let(:hooks) { [] } + + subject(:index) { Index.new } + + describe "#register plugin" do + it "is available for retrieval" do + expect(index.plugin_path(plugin_name)).to eq(lib_path(plugin_name)) + end + + it "load_paths is available for retrival" do + expect(index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s]) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.plugin_path(plugin_name)).to eq(lib_path(plugin_name)) + end + + it "load_paths are persistent" do + new_index = Index.new + expect(new_index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s]) + end + end + + describe "commands" do + let(:commands) { ["newco"] } + + it "returns the plugins name on query" do + expect(index.command_plugin("newco")).to eq(plugin_name) + end + + it "raises error on conflict" do + expect do + index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, ["newco"], [], []) + end.to raise_error(Index::CommandConflict) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.command_plugin("newco")).to eq(plugin_name) + end + end + + describe "source" do + let(:sources) { ["new_source"] } + + it "returns the plugins name on query" do + expect(index.source_plugin("new_source")).to eq(plugin_name) + end + + it "raises error on conflict" do + expect do + index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], ["new_source"], []) + end.to raise_error(Index::SourceConflict) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.source_plugin("new_source")).to eq(plugin_name) + end + end + + describe "hook" do + let(:hooks) { ["after-bar"] } + + it "returns the plugins name on query" do + expect(index.hook_plugins("after-bar")).to include(plugin_name) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.hook_plugins("after-bar")).to eq([plugin_name]) + end + + context "that are not registered", :focused do + let(:file) { double("index-file") } + + before do + index.hook_plugins("not-there") + allow(File).to receive(:open).and_yield(file) + end + + it "should not save it with next registered hook" do + expect(file).to receive(:puts) do |content| + expect(content).not_to include("not-there") + end + + index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], [], []) + end + end + end + + describe "global index" do + before do + Dir.chdir(tmp) do + Bundler::Plugin.reset! + path = lib_path("gplugin") + index.register_plugin("gplugin", path.to_s, [path.join("lib").to_s], [], ["glb_source"], []) + end + end + + it "skips sources" do + new_index = Index.new + expect(new_index.source_plugin("glb_source")).to be_falsy + end + end + + describe "after conflict" do + let(:commands) { ["foo"] } + let(:sources) { ["bar"] } + let(:hooks) { ["hoook"] } + + shared_examples "it cleans up" do + it "the path" do + expect(index.installed?("cplugin")).to be_falsy + end + + it "the command" do + expect(index.command_plugin("xfoo")).to be_falsy + end + + it "the source" do + expect(index.source_plugin("xbar")).to be_falsy + end + + it "the hook" do + expect(index.hook_plugins("xhoook")).to be_empty + end + end + + context "on command conflict it cleans up" do + before do + expect do + path = lib_path("cplugin") + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xhoook"]) + end.to raise_error(Index::CommandConflict) + end + + include_examples "it cleans up" + end + + context "on source conflict it cleans up" do + before do + expect do + path = lib_path("cplugin") + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xhoook"]) + end.to raise_error(Index::SourceConflict) + end + + include_examples "it cleans up" + end + + context "on command and source conflict it cleans up" do + before do + expect do + path = lib_path("cplugin") + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xhoook"]) + end.to raise_error(Index::CommandConflict) + end + + include_examples "it cleans up" + end + end + + describe "readonly disk without home" do + it "ignores being unable to create temp home dir" do + expect_any_instance_of(Bundler::Plugin::Index).to receive(:global_index_file). + and_raise(Bundler::GenericSystemCallError.new("foo", "bar")) + Bundler::Plugin::Index.new + end + end +end diff --git a/spec/bundler/bundler/plugin/installer_spec.rb b/spec/bundler/bundler/plugin/installer_spec.rb new file mode 100644 index 00000000000000..f8bf8450c93f09 --- /dev/null +++ b/spec/bundler/bundler/plugin/installer_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::Installer do + subject(:installer) { Bundler::Plugin::Installer.new } + + before do + # allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(Pathname.new("/Gemfile")) + end + + describe "cli install" do + it "uses Gem.sources when non of the source is provided" do + sources = double(:sources) + Bundler.settings # initialize it before we have to touch rubygems.ext_lock + allow(Bundler).to receive_message_chain("rubygems.sources") { sources } + + allow(installer).to receive(:install_rubygems). + with("new-plugin", [">= 0"], sources).once + + installer.install("new-plugin", {}) + end + + describe "with mocked installers" do + let(:spec) { double(:spec) } + it "returns the installed spec after installing git plugins" do + allow(installer).to receive(:install_git). + and_return("new-plugin" => spec) + + expect(installer.install(["new-plugin"], :git => "https://some.ran/dom")). + to eq("new-plugin" => spec) + end + + it "returns the installed spec after installing rubygems plugins" do + allow(installer).to receive(:install_rubygems). + and_return("new-plugin" => spec) + + expect(installer.install(["new-plugin"], :source => "https://some.ran/dom")). + to eq("new-plugin" => spec) + end + end + + describe "with actual installers" do + before do + build_repo2 do + build_plugin "re-plugin" + build_plugin "ma-plugin" + end + end + + context "git plugins" do + before do + build_git "ga-plugin", :path => lib_path("ga-plugin") do |s| + s.write "plugins.rb" + end + end + + let(:result) do + installer.install(["ga-plugin"], :git => "file://#{lib_path("ga-plugin")}") + end + + it "returns the installed spec after installing" do + spec = result["ga-plugin"] + expect(spec.full_name).to eq "ga-plugin-1.0" + end + + it "has expected full gem path" do + rev = revision_for(lib_path("ga-plugin")) + expect(result["ga-plugin"].full_gem_path). + to eq(Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s) + end + end + + context "rubygems plugins" do + let(:result) do + installer.install(["re-plugin"], :source => "file://#{gem_repo2}") + end + + it "returns the installed spec after installing " do + expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification) + end + + it "has expected full_gem)path" do + expect(result["re-plugin"].full_gem_path). + to eq(global_plugin_gem("re-plugin-1.0").to_s) + end + end + + context "multiple plugins" do + let(:result) do + installer.install(["re-plugin", "ma-plugin"], :source => "file://#{gem_repo2}") + end + + it "returns the installed spec after installing " do + expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification) + expect(result["ma-plugin"]).to be_kind_of(Bundler::RemoteSpecification) + end + + it "has expected full_gem)path" do + expect(result["re-plugin"].full_gem_path).to eq(global_plugin_gem("re-plugin-1.0").to_s) + expect(result["ma-plugin"].full_gem_path).to eq(global_plugin_gem("ma-plugin-1.0").to_s) + end + end + end + end +end diff --git a/spec/bundler/bundler/plugin/source_list_spec.rb b/spec/bundler/bundler/plugin/source_list_spec.rb new file mode 100644 index 00000000000000..64a1233dd151ad --- /dev/null +++ b/spec/bundler/bundler/plugin/source_list_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::SourceList do + SourceList = Bundler::Plugin::SourceList + + before do + allow(Bundler).to receive(:root) { Pathname.new "/" } + end + + subject(:source_list) { SourceList.new } + + describe "adding sources uses classes for plugin" do + it "uses Plugin::Installer::Rubygems for rubygems sources" do + source = source_list. + add_rubygems_source("remotes" => ["https://existing-rubygems.org"]) + expect(source).to be_instance_of(Bundler::Plugin::Installer::Rubygems) + end + + it "uses Plugin::Installer::Git for git sources" do + source = source_list. + add_git_source("uri" => "git://existing-git.org/path.git") + expect(source).to be_instance_of(Bundler::Plugin::Installer::Git) + end + end +end diff --git a/spec/bundler/bundler/plugin_spec.rb b/spec/bundler/bundler/plugin_spec.rb new file mode 100644 index 00000000000000..9266fad1eb07c8 --- /dev/null +++ b/spec/bundler/bundler/plugin_spec.rb @@ -0,0 +1,309 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin do + Plugin = Bundler::Plugin + + let(:installer) { double(:installer) } + let(:index) { double(:index) } + let(:spec) { double(:spec) } + let(:spec2) { double(:spec2) } + + before do + build_lib "new-plugin", :path => lib_path("new-plugin") do |s| + s.write "plugins.rb" + end + + build_lib "another-plugin", :path => lib_path("another-plugin") do |s| + s.write "plugins.rb" + end + + allow(spec).to receive(:full_gem_path). + and_return(lib_path("new-plugin").to_s) + allow(spec).to receive(:load_paths). + and_return([lib_path("new-plugin").join("lib").to_s]) + + allow(spec2).to receive(:full_gem_path). + and_return(lib_path("another-plugin").to_s) + allow(spec2).to receive(:load_paths). + and_return([lib_path("another-plugin").join("lib").to_s]) + + allow(Plugin::Installer).to receive(:new) { installer } + allow(Plugin).to receive(:index) { index } + allow(index).to receive(:register_plugin) + end + + describe "install command" do + let(:opts) { { "version" => "~> 1.0", "source" => "foo" } } + + before do + allow(installer).to receive(:install).with(["new-plugin"], opts) do + { "new-plugin" => spec } + end + end + + it "passes the name and options to installer" do + allow(installer).to receive(:install).with(["new-plugin"], opts) do + { "new-plugin" => spec } + end.once + + subject.install ["new-plugin"], opts + end + + it "validates the installed plugin" do + allow(subject). + to receive(:validate_plugin!).with(lib_path("new-plugin")).once + + subject.install ["new-plugin"], opts + end + + it "registers the plugin with index" do + allow(index).to receive(:register_plugin). + with("new-plugin", lib_path("new-plugin").to_s, [lib_path("new-plugin").join("lib").to_s], []).once + subject.install ["new-plugin"], opts + end + + context "multiple plugins" do + it do + allow(installer).to receive(:install). + with(["new-plugin", "another-plugin"], opts) do + { + "new-plugin" => spec, + "another-plugin" => spec2, + } + end.once + + allow(subject).to receive(:validate_plugin!).twice + allow(index).to receive(:register_plugin).twice + subject.install ["new-plugin", "another-plugin"], opts + end + end + end + + describe "evaluate gemfile for plugins" do + let(:definition) { double("definition") } + let(:builder) { double("builder") } + let(:gemfile) { bundled_app("Gemfile") } + + before do + allow(Plugin::DSL).to receive(:new) { builder } + allow(builder).to receive(:eval_gemfile).with(gemfile) + allow(builder).to receive(:to_definition) { definition } + allow(builder).to receive(:inferred_plugins) { [] } + end + + it "doesn't calls installer without any plugins" do + allow(definition).to receive(:dependencies) { [] } + allow(installer).to receive(:install_definition).never + + subject.gemfile_install(gemfile) + end + + context "with dependencies" do + let(:plugin_specs) do + { + "new-plugin" => spec, + "another-plugin" => spec2, + } + end + + before do + allow(index).to receive(:installed?) { nil } + allow(definition).to receive(:dependencies) { [Bundler::Dependency.new("new-plugin", ">=0"), Bundler::Dependency.new("another-plugin", ">=0")] } + allow(installer).to receive(:install_definition) { plugin_specs } + end + + it "should validate and register the plugins" do + expect(subject).to receive(:validate_plugin!).twice + expect(subject).to receive(:register_plugin).twice + + subject.gemfile_install(gemfile) + end + + it "should pass the optional plugins to #register_plugin" do + allow(builder).to receive(:inferred_plugins) { ["another-plugin"] } + + expect(subject).to receive(:register_plugin). + with("new-plugin", spec, false).once + + expect(subject).to receive(:register_plugin). + with("another-plugin", spec2, true).once + + subject.gemfile_install(gemfile) + end + end + end + + describe "#command?" do + it "returns true value for commands in index" do + allow(index). + to receive(:command_plugin).with("newcommand") { "my-plugin" } + result = subject.command? "newcommand" + expect(result).to be_truthy + end + + it "returns false value for commands not in index" do + allow(index).to receive(:command_plugin).with("newcommand") { nil } + result = subject.command? "newcommand" + expect(result).to be_falsy + end + end + + describe "#exec_command" do + it "raises UndefinedCommandError when command is not found" do + allow(index).to receive(:command_plugin).with("newcommand") { nil } + expect { subject.exec_command("newcommand", []) }. + to raise_error(Plugin::UndefinedCommandError) + end + end + + describe "#source?" do + it "returns true value for sources in index" do + allow(index). + to receive(:command_plugin).with("foo-source") { "my-plugin" } + result = subject.command? "foo-source" + expect(result).to be_truthy + end + + it "returns false value for source not in index" do + allow(index).to receive(:command_plugin).with("foo-source") { nil } + result = subject.command? "foo-source" + expect(result).to be_falsy + end + end + + describe "#source" do + it "raises UnknownSourceError when source is not found" do + allow(index).to receive(:source_plugin).with("bar") { nil } + expect { subject.source("bar") }. + to raise_error(Plugin::UnknownSourceError) + end + + it "loads the plugin, if not loaded" do + allow(index).to receive(:source_plugin).with("foo-bar") { "plugin_name" } + + expect(subject).to receive(:load_plugin).with("plugin_name") + subject.source("foo-bar") + end + + it "returns the class registered with #add_source" do + allow(index).to receive(:source_plugin).with("foo") { "plugin_name" } + stub_const "NewClass", Class.new + + subject.add_source("foo", NewClass) + expect(subject.source("foo")).to be(NewClass) + end + end + + describe "#source_from_lock" do + it "returns instance of registered class initialized with locked opts" do + opts = { "type" => "l_source", "remote" => "xyz", "other" => "random" } + allow(index).to receive(:source_plugin).with("l_source") { "plugin_name" } + + stub_const "SClass", Class.new + s_instance = double(:s_instance) + subject.add_source("l_source", SClass) + + expect(SClass).to receive(:new). + with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance } + expect(subject.source_from_lock(opts)).to be(s_instance) + end + end + + describe "#root" do + context "in app dir" do + before do + gemfile "" + end + + it "returns plugin dir in app .bundle path" do + expect(subject.root).to eq(bundled_app.join(".bundle/plugin")) + end + end + + context "outside app dir" do + it "returns plugin dir in global bundle path" do + Dir.chdir tmp + expect(subject.root).to eq(home.join(".bundle/plugin")) + end + end + end + + describe "#add_hook" do + it "raises an ArgumentError on an unregistered event" do + ran = false + expect do + Plugin.add_hook("unregistered-hook") { ran = true } + end.to raise_error(ArgumentError) + expect(ran).to be(false) + end + end + + describe "#hook" do + before do + path = lib_path("foo-plugin") + build_lib "foo-plugin", :path => path do |s| + s.write "plugins.rb", code + end + + Bundler::Plugin::Events.send(:reset) + Bundler::Plugin::Events.send(:define, :EVENT_1, "event-1") + Bundler::Plugin::Events.send(:define, :EVENT_2, "event-2") + + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_1). + and_return(["foo-plugin"]) + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_2). + and_return(["foo-plugin"]) + allow(index).to receive(:plugin_path).with("foo-plugin").and_return(path) + allow(index).to receive(:load_paths).with("foo-plugin").and_return([]) + end + + let(:code) { <<-RUBY } + Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" } + RUBY + + it "raises an ArgumentError on an unregistered event" do + expect do + Plugin.hook("unregistered-hook") + end.to raise_error(ArgumentError) + end + + it "executes the hook" do + out = capture(:stdout) do + Plugin.hook(Bundler::Plugin::Events::EVENT_1) + end.strip + + expect(out).to eq("hook for event 1") + end + + context "single plugin declaring more than one hook" do + let(:code) { <<-RUBY } + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_1) {} + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_2) {} + puts "loaded" + RUBY + + it "evals plugins.rb once" do + out = capture(:stdout) do + Plugin.hook(Bundler::Plugin::Events::EVENT_1) + Plugin.hook(Bundler::Plugin::Events::EVENT_2) + end.strip + + expect(out).to eq("loaded") + end + end + + context "a block is passed" do + let(:code) { <<-RUBY } + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_1) { |&blk| blk.call } + RUBY + + it "is passed to the hook" do + out = capture(:stdout) do + Plugin.hook(Bundler::Plugin::Events::EVENT_1) { puts "win" } + end.strip + + expect(out).to eq("win") + end + end + end +end diff --git a/spec/bundler/bundler/psyched_yaml_spec.rb b/spec/bundler/bundler/psyched_yaml_spec.rb new file mode 100644 index 00000000000000..d5d68c5cc3c55d --- /dev/null +++ b/spec/bundler/bundler/psyched_yaml_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "bundler/psyched_yaml" + +RSpec.describe "Bundler::YamlLibrarySyntaxError" do + it "is raised on YAML parse errors" do + expect { YAML.parse "{foo" }.to raise_error(Bundler::YamlLibrarySyntaxError) + end +end diff --git a/spec/bundler/bundler/remote_specification_spec.rb b/spec/bundler/bundler/remote_specification_spec.rb new file mode 100644 index 00000000000000..8115e026d8f588 --- /dev/null +++ b/spec/bundler/bundler/remote_specification_spec.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::RemoteSpecification do + let(:name) { "foo" } + let(:version) { Gem::Version.new("1.0.0") } + let(:platform) { Gem::Platform::RUBY } + let(:spec_fetcher) { double(:spec_fetcher) } + + subject { described_class.new(name, version, platform, spec_fetcher) } + + it "is Comparable" do + expect(described_class.ancestors).to include(Comparable) + end + + it "can match platforms" do + expect(described_class.ancestors).to include(Bundler::MatchPlatform) + end + + describe "#fetch_platform" do + let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + + before { allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) } + + it "should return the spec platform" do + expect(subject.fetch_platform).to eq("jruby") + end + end + + describe "#full_name" do + context "when platform is ruby" do + it "should return the spec name and version" do + expect(subject.full_name).to eq("foo-1.0.0") + end + end + + context "when platform is nil" do + let(:platform) { nil } + + it "should return the spec name and version" do + expect(subject.full_name).to eq("foo-1.0.0") + end + end + + context "when platform is a non-ruby platform" do + let(:platform) { "jruby" } + + it "should return the spec name, version, and platform" do + expect(subject.full_name).to eq("foo-1.0.0-jruby") + end + end + end + + describe "#<=>" do + let(:other_name) { name } + let(:other_version) { version } + let(:other_platform) { platform } + let(:other_spec_fetcher) { spec_fetcher } + + shared_examples_for "a comparison" do + context "which exactly matches" do + it "returns 0" do + expect(subject <=> other).to eq(0) + end + end + + context "which is different by name" do + let(:other_name) { "a" } + it "returns 1" do + expect(subject <=> other).to eq(1) + end + end + + context "which has a lower version" do + let(:other_version) { Gem::Version.new("0.9.0") } + it "returns 1" do + expect(subject <=> other).to eq(1) + end + end + + context "which has a higher version" do + let(:other_version) { Gem::Version.new("1.1.0") } + it "returns -1" do + expect(subject <=> other).to eq(-1) + end + end + + context "which has a different platform" do + let(:other_platform) { Gem::Platform.new("x86-mswin32") } + it "returns -1" do + expect(subject <=> other).to eq(-1) + end + end + end + + context "comparing another Bundler::RemoteSpecification" do + let(:other) do + Bundler::RemoteSpecification.new(other_name, other_version, + other_platform, nil) + end + + it_should_behave_like "a comparison" + end + + context "comparing a Gem::Specification" do + let(:other) do + Gem::Specification.new(other_name, other_version).tap do |s| + s.platform = other_platform + end + end + + it_should_behave_like "a comparison" + end + + context "comparing a non sortable object" do + let(:other) { Object.new } + let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + + before do + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + allow(remote_spec).to receive(:<=>).and_return(nil) + end + + it "should use default object comparison" do + expect(subject <=> other).to eq(nil) + end + end + end + + describe "#__swap__" do + let(:spec) { double(:spec, :dependencies => []) } + let(:new_spec) { double(:new_spec, :dependencies => [], :runtime_dependencies => []) } + + before { subject.instance_variable_set(:@_remote_specification, spec) } + + it "should replace remote specification with the passed spec" do + expect(subject.instance_variable_get(:@_remote_specification)).to be(spec) + subject.__swap__(new_spec) + expect(subject.instance_variable_get(:@_remote_specification)).to be(new_spec) + end + end + + describe "#sort_obj" do + context "when platform is ruby" do + it "should return a sorting delegate array with name, version, and -1" do + expect(subject.sort_obj).to match_array(["foo", version, -1]) + end + end + + context "when platform is not ruby" do + let(:platform) { "jruby" } + + it "should return a sorting delegate array with name, version, and 1" do + expect(subject.sort_obj).to match_array(["foo", version, 1]) + end + end + end + + describe "method missing" do + context "and is present in Gem::Specification" do + let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + + before do + allow(subject).to receive(:_remote_specification).and_return(remote_spec) + expect(subject.methods.map(&:to_sym)).not_to include(:authors) + end + + it "should send through to Gem::Specification" do + expect(subject.authors).to eq("abcd") + end + end + end + + describe "respond to missing?" do + context "and is present in Gem::Specification" do + let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + + before do + allow(subject).to receive(:_remote_specification).and_return(remote_spec) + expect(subject.methods.map(&:to_sym)).not_to include(:authors) + end + + it "should send through to Gem::Specification" do + expect(subject.respond_to?(:authors)).to be_truthy + end + end + end +end diff --git a/spec/bundler/bundler/retry_spec.rb b/spec/bundler/bundler/retry_spec.rb new file mode 100644 index 00000000000000..b893580d72ceec --- /dev/null +++ b/spec/bundler/bundler/retry_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Retry do + it "return successful result if no errors" do + attempts = 0 + result = Bundler::Retry.new(nil, nil, 3).attempt do + attempts += 1 + :success + end + expect(result).to eq(:success) + expect(attempts).to eq(1) + end + + it "returns the first valid result" do + jobs = [proc { raise "foo" }, proc { :bar }, proc { raise "foo" }] + attempts = 0 + result = Bundler::Retry.new(nil, nil, 3).attempt do + attempts += 1 + jobs.shift.call + end + expect(result).to eq(:bar) + expect(attempts).to eq(2) + end + + it "raises the last error" do + errors = [StandardError, StandardError, StandardError, Bundler::GemfileNotFound] + attempts = 0 + expect do + Bundler::Retry.new(nil, nil, 3).attempt do + attempts += 1 + raise errors.shift + end + end.to raise_error(Bundler::GemfileNotFound) + expect(attempts).to eq(4) + end + + it "raises exceptions" do + error = Bundler::GemfileNotFound + attempts = 0 + expect do + Bundler::Retry.new(nil, error).attempt do + attempts += 1 + raise error + end + end.to raise_error(error) + expect(attempts).to eq(1) + end + + context "logging" do + let(:error) { Bundler::GemfileNotFound } + let(:failure_message) { "Retrying test due to error (2/2): #{error} #{error}" } + + context "with debugging on" do + it "print error message with newline" do + allow(Bundler.ui).to receive(:debug?).and_return(true) + expect(Bundler.ui).to_not receive(:info) + expect(Bundler.ui).to receive(:warn).with(failure_message, true) + + expect do + Bundler::Retry.new("test", [], 1).attempt do + raise error + end + end.to raise_error(error) + end + end + + context "with debugging off" do + it "print error message with newlines" do + allow(Bundler.ui).to receive(:debug?).and_return(false) + expect(Bundler.ui).to receive(:info).with("").twice + expect(Bundler.ui).to receive(:warn).with(failure_message, false) + + expect do + Bundler::Retry.new("test", [], 1).attempt do + raise error + end + end.to raise_error(error) + end + end + end +end diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb new file mode 100644 index 00000000000000..bc1ca98457b77a --- /dev/null +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "bundler/ruby_dsl" + +RSpec.describe Bundler::RubyDsl do + class MockDSL + include Bundler::RubyDsl + + attr_reader :ruby_version + end + + let(:dsl) { MockDSL.new } + let(:ruby_version) { "2.0.0" } + let(:version) { "2.0.0" } + let(:engine) { "jruby" } + let(:engine_version) { "9000" } + let(:patchlevel) { "100" } + let(:options) do + { :patchlevel => patchlevel, + :engine => engine, + :engine_version => engine_version } + end + + let(:invoke) do + proc do + args = Array(ruby_version) + [options] + dsl.ruby(*args) + end + end + + subject do + invoke.call + dsl.ruby_version + end + + describe "#ruby_version" do + shared_examples_for "it stores the ruby version" do + it "stores the version" do + expect(subject.versions).to eq(Array(ruby_version)) + expect(subject.gem_version.version).to eq(version) + end + + it "stores the engine details" do + expect(subject.engine).to eq(engine) + expect(subject.engine_versions).to eq(Array(engine_version)) + end + + it "stores the patchlevel" do + expect(subject.patchlevel).to eq(patchlevel) + end + end + + context "with a plain version" do + it_behaves_like "it stores the ruby version" + end + + context "with a single requirement" do + let(:ruby_version) { ">= 2.0.0" } + it_behaves_like "it stores the ruby version" + end + + context "with two requirements in the same string" do + let(:ruby_version) { ">= 2.0.0, < 3.0" } + it "raises an error" do + expect { subject }.to raise_error(ArgumentError) + end + end + + context "with two requirements" do + let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] } + it_behaves_like "it stores the ruby version" + end + + context "with multiple engine versions" do + let(:engine_version) { ["> 200", "< 300"] } + it_behaves_like "it stores the ruby version" + end + + context "with no options hash" do + let(:invoke) { proc { dsl.ruby(ruby_version) } } + + let(:patchlevel) { nil } + let(:engine) { "ruby" } + let(:engine_version) { version } + + it_behaves_like "it stores the ruby version" + + context "and with multiple requirements" do + let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] } + let(:engine_version) { ruby_version } + it_behaves_like "it stores the ruby version" + end + end + end +end diff --git a/spec/bundler/bundler/ruby_version_spec.rb b/spec/bundler/bundler/ruby_version_spec.rb new file mode 100644 index 00000000000000..46a1b2918b5983 --- /dev/null +++ b/spec/bundler/bundler/ruby_version_spec.rb @@ -0,0 +1,524 @@ +# frozen_string_literal: true + +require "bundler/ruby_version" + +RSpec.describe "Bundler::RubyVersion and its subclasses" do + let(:version) { "2.0.0" } + let(:patchlevel) { "645" } + let(:engine) { "jruby" } + let(:engine_version) { "2.0.1" } + + describe Bundler::RubyVersion do + subject { Bundler::RubyVersion.new(version, patchlevel, engine, engine_version) } + + let(:ruby_version) { subject } + let(:other_version) { version } + let(:other_patchlevel) { patchlevel } + let(:other_engine) { engine } + let(:other_engine_version) { engine_version } + let(:other_ruby_version) { Bundler::RubyVersion.new(other_version, other_patchlevel, other_engine, other_engine_version) } + + describe "#initialize" do + context "no engine is passed" do + let(:engine) { nil } + + it "should set ruby as the engine" do + expect(subject.engine).to eq("ruby") + end + end + + context "no engine_version is passed" do + let(:engine_version) { nil } + + it "should set engine version as the passed version" do + expect(subject.engine_versions).to eq(["2.0.0"]) + end + end + + context "with engine in symbol" do + let(:engine) { :jruby } + + it "should coerce engine to string" do + expect(subject.engine).to eq("jruby") + end + end + + context "is called with multiple requirements" do + let(:version) { ["<= 2.0.0", "> 1.9.3"] } + let(:engine_version) { nil } + + it "sets the versions" do + expect(subject.versions).to eq(version) + end + + it "sets the engine versions" do + expect(subject.engine_versions).to eq(version) + end + end + + context "is called with multiple engine requirements" do + let(:engine_version) { [">= 2.0", "< 2.3"] } + + it "sets the engine versions" do + expect(subject.engine_versions).to eq(engine_version) + end + end + end + + describe ".from_string" do + shared_examples_for "returning" do + it "returns the original RubyVersion" do + expect(described_class.from_string(subject.to_s)).to eq(subject) + end + end + + include_examples "returning" + + context "no patchlevel" do + let(:patchlevel) { nil } + + include_examples "returning" + end + + context "engine is ruby" do + let(:engine) { "ruby" } + let(:engine_version) { version } + + include_examples "returning" + end + + context "with multiple requirements" do + let(:engine_version) { ["> 9", "< 11"] } + let(:version) { ["> 8", "< 10"] } + let(:patchlevel) { nil } + + it "returns nil" do + expect(described_class.from_string(subject.to_s)).to be_nil + end + end + end + + describe "#to_s" do + it "should return info string with the ruby version, patchlevel, engine, and engine version" do + expect(subject.to_s).to eq("ruby 2.0.0p645 (jruby 2.0.1)") + end + + context "no patchlevel" do + let(:patchlevel) { nil } + + it "should return info string with the version, engine, and engine version" do + expect(subject.to_s).to eq("ruby 2.0.0 (jruby 2.0.1)") + end + end + + context "engine is ruby" do + let(:engine) { "ruby" } + + it "should return info string with the ruby version and patchlevel" do + expect(subject.to_s).to eq("ruby 2.0.0p645") + end + end + + context "with multiple requirements" do + let(:engine_version) { ["> 9", "< 11"] } + let(:version) { ["> 8", "< 10"] } + let(:patchlevel) { nil } + + it "should return info string with all requirements" do + expect(subject.to_s).to eq("ruby > 8, < 10 (jruby > 9, < 11)") + end + end + end + + describe "#==" do + shared_examples_for "two ruby versions are not equal" do + it "should return false" do + expect(subject).to_not eq(other_ruby_version) + end + end + + context "the versions, pathlevels, engines, and engine_versions match" do + it "should return true" do + expect(subject).to eq(other_ruby_version) + end + end + + context "the versions do not match" do + let(:other_version) { "1.21.6" } + + it_behaves_like "two ruby versions are not equal" + end + + context "the patchlevels do not match" do + let(:other_patchlevel) { "21" } + + it_behaves_like "two ruby versions are not equal" + end + + context "the engines do not match" do + let(:other_engine) { "ruby" } + + it_behaves_like "two ruby versions are not equal" + end + + context "the engine versions do not match" do + let(:other_engine_version) { "1.11.2" } + + it_behaves_like "two ruby versions are not equal" + end + end + + describe "#host" do + before do + allow(RbConfig::CONFIG).to receive(:[]).with("host_cpu").and_return("x86_64") + allow(RbConfig::CONFIG).to receive(:[]).with("host_vendor").and_return("apple") + allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return("darwin14.5.0") + end + + it "should return an info string with the host cpu, vendor, and os" do + expect(subject.host).to eq("x86_64-apple-darwin14.5.0") + end + + it "memoizes the info string with the host cpu, vendor, and os" do + expect(RbConfig::CONFIG).to receive(:[]).with("host_cpu").once.and_call_original + expect(RbConfig::CONFIG).to receive(:[]).with("host_vendor").once.and_call_original + expect(RbConfig::CONFIG).to receive(:[]).with("host_os").once.and_call_original + 2.times { ruby_version.host } + end + end + + describe "#gem_version" do + let(:gem_version) { "2.0.0" } + let(:gem_version_obj) { Gem::Version.new(gem_version) } + + shared_examples_for "it parses the version from the requirement string" do |version| + let(:version) { version } + it "should return the underlying version" do + expect(ruby_version.gem_version).to eq(gem_version_obj) + expect(ruby_version.gem_version.version).to eq(gem_version) + end + end + + it_behaves_like "it parses the version from the requirement string", "2.0.0" + it_behaves_like "it parses the version from the requirement string", ">= 2.0.0" + it_behaves_like "it parses the version from the requirement string", "~> 2.0.0" + it_behaves_like "it parses the version from the requirement string", "< 2.0.0" + it_behaves_like "it parses the version from the requirement string", "= 2.0.0" + it_behaves_like "it parses the version from the requirement string", ["> 2.0.0", "< 2.4.5"] + end + + describe "#diff" do + let(:engine) { "ruby" } + + shared_examples_for "there is a difference in the engines" do + it "should return a tuple with :engine and the two different engines" do + expect(ruby_version.diff(other_ruby_version)).to eq([:engine, engine, other_engine]) + end + end + + shared_examples_for "there is a difference in the versions" do + it "should return a tuple with :version and the two different versions" do + expect(ruby_version.diff(other_ruby_version)).to eq([:version, Array(version).join(", "), Array(other_version).join(", ")]) + end + end + + shared_examples_for "there is a difference in the engine versions" do + it "should return a tuple with :engine_version and the two different engine versions" do + expect(ruby_version.diff(other_ruby_version)).to eq([:engine_version, Array(engine_version).join(", "), Array(other_engine_version).join(", ")]) + end + end + + shared_examples_for "there is a difference in the patchlevels" do + it "should return a tuple with :patchlevel and the two different patchlevels" do + expect(ruby_version.diff(other_ruby_version)).to eq([:patchlevel, patchlevel, other_patchlevel]) + end + end + + shared_examples_for "there are no differences" do + it "should return nil" do + expect(ruby_version.diff(other_ruby_version)).to be_nil + end + end + + context "all things match exactly" do + it_behaves_like "there are no differences" + end + + context "detects engine discrepancies first" do + let(:other_version) { "2.0.1" } + let(:other_patchlevel) { "643" } + let(:other_engine) { "rbx" } + let(:other_engine_version) { "2.0.0" } + + it_behaves_like "there is a difference in the engines" + end + + context "detects version discrepancies second" do + let(:other_version) { "2.0.1" } + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + it_behaves_like "there is a difference in the versions" + end + + context "detects version discrepancies with multiple requirements second" do + let(:other_version) { "2.0.1" } + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + let(:version) { ["> 2.0.0", "< 1.0.0"] } + + it_behaves_like "there is a difference in the versions" + end + + context "detects engine version discrepancies third" do + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + it_behaves_like "there is a difference in the engine versions" + end + + context "detects engine version discrepancies with multiple requirements third" do + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + let(:engine_version) { ["> 2.0.0", "< 1.0.0"] } + + it_behaves_like "there is a difference in the engine versions" + end + + context "detects patchlevel discrepancies last" do + let(:other_patchlevel) { "643" } + + it_behaves_like "there is a difference in the patchlevels" + end + + context "successfully matches gem requirements" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there are no differences" + end + + context "successfully matches multiple gem requirements" do + let(:version) { [">= 2.0.0", "< 2.4.5"] } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { ["~> 2.0.1", "< 2.4.5"] } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there are no differences" + end + + context "successfully detects bad gem requirements with versions with multiple requirements" do + let(:version) { ["~> 2.0.0", "< 2.0.5"] } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.5" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there is a difference in the versions" + end + + context "successfully detects bad gem requirements with versions" do + let(:version) { "~> 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.1.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there is a difference in the versions" + end + + context "successfully detects bad gem requirements with patchlevels" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "645" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there is a difference in the patchlevels" + end + + context "successfully detects bad gem requirements with engine versions" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.1.0" } + + it_behaves_like "there is a difference in the engine versions" + end + + context "with a patchlevel of -1" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "-1" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { version } + let(:other_engine) { engine } + let(:other_engine_version) { engine_version } + + context "and comparing with another patchlevel of -1" do + let(:other_patchlevel) { patchlevel } + + it_behaves_like "there are no differences" + end + + context "and comparing with a patchlevel that is not -1" do + let(:other_patchlevel) { "642" } + + it_behaves_like "there is a difference in the patchlevels" + end + end + end + + describe "#system" do + subject { Bundler::RubyVersion.system } + + let(:bundler_system_ruby_version) { subject } + + before do + Bundler::RubyVersion.instance_variable_set("@ruby_version", nil) + end + + it "should return an instance of Bundler::RubyVersion" do + expect(subject).to be_kind_of(Bundler::RubyVersion) + end + + it "memoizes the instance of Bundler::RubyVersion" do + expect(Bundler::RubyVersion).to receive(:new).once.and_call_original + 2.times { subject } + end + + describe "#version" do + it "should return a copy of the value of RUBY_VERSION" do + expect(subject.versions).to eq([RUBY_VERSION]) + expect(subject.versions.first).to_not be(RUBY_VERSION) + end + end + + describe "#engine" do + context "RUBY_ENGINE is defined" do + before { stub_const("RUBY_ENGINE", "jruby") } + before { stub_const("JRUBY_VERSION", "2.1.1") } + + it "should return a copy of the value of RUBY_ENGINE" do + expect(subject.engine).to eq("jruby") + expect(subject.engine).to_not be(RUBY_ENGINE) + end + end + + context "RUBY_ENGINE is not defined" do + before { stub_const("RUBY_ENGINE", nil) } + + it "should return the string 'ruby'" do + expect(subject.engine).to eq("ruby") + end + end + end + + describe "#engine_version" do + context "engine is ruby" do + before do + stub_const("RUBY_VERSION", "2.2.4") + stub_const("RUBY_ENGINE", "ruby") + end + + it "should return a copy of the value of RUBY_VERSION" do + expect(bundler_system_ruby_version.engine_versions).to eq(["2.2.4"]) + expect(bundler_system_ruby_version.engine_versions.first).to_not be(RUBY_VERSION) + end + end + + context "engine is rbx" do + before do + stub_const("RUBY_ENGINE", "rbx") + stub_const("Rubinius::VERSION", "2.0.0") + end + + it "should return a copy of the value of Rubinius::VERSION" do + expect(bundler_system_ruby_version.engine_versions).to eq(["2.0.0"]) + expect(bundler_system_ruby_version.engine_versions.first).to_not be(Rubinius::VERSION) + end + end + + context "engine is jruby" do + before do + stub_const("RUBY_ENGINE", "jruby") + stub_const("JRUBY_VERSION", "2.1.1") + end + + it "should return a copy of the value of JRUBY_VERSION" do + expect(subject.engine_versions).to eq(["2.1.1"]) + expect(bundler_system_ruby_version.engine_versions.first).to_not be(JRUBY_VERSION) + end + end + + context "engine is some other ruby engine" do + before do + stub_const("RUBY_ENGINE", "not_supported_ruby_engine") + stub_const("RUBY_ENGINE_VERSION", "1.2.3") + end + + it "returns RUBY_ENGINE_VERSION" do + expect(bundler_system_ruby_version.engine_versions).to eq(["1.2.3"]) + end + end + end + + describe "#patchlevel" do + it "should return a string with the value of RUBY_PATCHLEVEL" do + expect(subject.patchlevel).to eq(RUBY_PATCHLEVEL.to_s) + end + end + end + + describe "#to_gem_version_with_patchlevel" do + shared_examples_for "the patchlevel is omitted" do + it "does not include a patch level" do + expect(subject.to_gem_version_with_patchlevel.to_s).to eq(version) + end + end + + context "with nil patch number" do + let(:patchlevel) { nil } + + it_behaves_like "the patchlevel is omitted" + end + + context "with negative patch number" do + let(:patchlevel) { -1 } + + it_behaves_like "the patchlevel is omitted" + end + + context "with a valid patch number" do + it "uses the specified patchlevel as patchlevel" do + expect(subject.to_gem_version_with_patchlevel.to_s).to eq("#{version}.#{patchlevel}") + end + end + end + end +end diff --git a/spec/bundler/bundler/rubygems_integration_spec.rb b/spec/bundler/bundler/rubygems_integration_spec.rb new file mode 100644 index 00000000000000..b1b15d9e5deba6 --- /dev/null +++ b/spec/bundler/bundler/rubygems_integration_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::RubygemsIntegration do + it "uses the same chdir lock as rubygems", :rubygems => "2.1" do + expect(Bundler.rubygems.ext_lock).to eq(Gem::Ext::Builder::CHDIR_MONITOR) + end + + context "#validate" do + let(:spec) do + Gem::Specification.new do |s| + s.name = "to-validate" + s.version = "1.0.0" + s.loaded_from = __FILE__ + end + end + subject { Bundler.rubygems.validate(spec) } + + it "skips overly-strict gemspec validation", :rubygems => "< 1.7" do + expect(spec).to_not receive(:validate) + subject + end + + it "validates with packaging mode disabled", :rubygems => "1.7" do + expect(spec).to receive(:validate).with(false) + subject + end + + it "should set a summary to avoid an overly-strict error", :rubygems => "~> 1.7.0" do + spec.summary = nil + expect { subject }.not_to raise_error + expect(spec.summary).to eq("") + end + + context "with an invalid spec" do + before do + expect(spec).to receive(:validate).with(false). + and_raise(Gem::InvalidSpecificationException.new("TODO is not an author")) + end + + it "should raise a Gem::InvalidSpecificationException and produce a helpful warning message", + :rubygems => "1.7" do + expect { subject }.to raise_error(Gem::InvalidSpecificationException, + "The gemspec at #{__FILE__} is not valid. "\ + "Please fix this gemspec.\nThe validation error was 'TODO is not an author'\n") + end + end + end + + describe "#configuration" do + it "handles Gem::SystemExitException errors" do + allow(Gem).to receive(:configuration) { raise Gem::SystemExitException.new(1) } + expect { Bundler.rubygems.configuration }.to raise_error(Gem::SystemExitException) + end + end + + describe "#download_gem", :rubygems => ">= 2.0" do + let(:bundler_retry) { double(Bundler::Retry) } + let(:retry) { double("Bundler::Retry") } + let(:uri) { URI.parse("https://foo.bar") } + let(:path) { Gem.path.first } + let(:spec) do + spec = Bundler::RemoteSpecification.new("Foo", Gem::Version.new("2.5.2"), + Gem::Platform::RUBY, nil) + spec.remote = Bundler::Source::Rubygems::Remote.new(uri.to_s) + spec + end + let(:fetcher) { double("gem_remote_fetcher") } + + it "successfully downloads gem with retries" do + expect(Bundler.rubygems).to receive(:gem_remote_fetcher).and_return(fetcher) + expect(fetcher).to receive(:headers=).with("X-Gemfile-Source" => "https://foo.bar") + expect(Bundler::Retry).to receive(:new).with("download gem from #{uri}/"). + and_return(bundler_retry) + expect(bundler_retry).to receive(:attempts).and_yield + expect(fetcher).to receive(:download).with(spec, uri, path) + + Bundler.rubygems.download_gem(spec, uri, path) + end + end + + describe "#fetch_all_remote_specs", :rubygems => ">= 2.0" do + let(:uri) { URI("https://example.com") } + let(:fetcher) { double("gem_remote_fetcher") } + let(:specs_response) { Marshal.dump(["specs"]) } + let(:prerelease_specs_response) { Marshal.dump(["prerelease_specs"]) } + + context "when a rubygems source mirror is set" do + let(:orig_uri) { URI("http://zombo.com") } + let(:remote_with_mirror) { double("remote", :uri => uri, :original_uri => orig_uri) } + + it "sets the 'X-Gemfile-Source' header containing the original source" do + expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher) + expect(fetcher).to receive(:headers=).with("X-Gemfile-Source" => "http://zombo.com").twice + expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response) + expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response) + result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror) + expect(result).to eq(%w[specs prerelease_specs]) + end + end + + context "when there is no rubygems source mirror set" do + let(:remote_no_mirror) { double("remote", :uri => uri, :original_uri => nil) } + + it "does not set the 'X-Gemfile-Source' header" do + expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher) + expect(fetcher).to_not receive(:headers=) + expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response) + expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response) + result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror) + expect(result).to eq(%w[specs prerelease_specs]) + end + end + end +end diff --git a/spec/bundler/bundler/settings/validator_spec.rb b/spec/bundler/bundler/settings/validator_spec.rb new file mode 100644 index 00000000000000..e4ffd894353889 --- /dev/null +++ b/spec/bundler/bundler/settings/validator_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Settings::Validator do + describe ".validate!" do + def validate!(key, value, settings) + transformed_key = Bundler.settings.key_for(key) + if value.nil? + settings.delete(transformed_key) + else + settings[transformed_key] = value + end + described_class.validate!(key, value, settings) + settings + end + + it "path and path.system are mutually exclusive" do + expect(validate!("path", "bundle", {})).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH__SYSTEM" => true) + expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH__SYSTEM" => false) + expect(validate!("path", nil, {})).to eq({}) + + expect(validate!("path.system", true, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH__SYSTEM" => true) + expect(validate!("path.system", false, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle", "BUNDLE_PATH__SYSTEM" => false) + expect(validate!("path.system", nil, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path.system", true, {})).to eq("BUNDLE_PATH__SYSTEM" => true) + expect(validate!("path.system", false, {})).to eq("BUNDLE_PATH__SYSTEM" => false) + expect(validate!("path.system", nil, {})).to eq({}) + end + + it "a group cannot be in both `with` & `without` simultaneously" do + expect do + validate!("with", "", {}) + validate!("with", nil, {}) + validate!("with", "", "BUNDLE_WITHOUT" => "a") + validate!("with", nil, "BUNDLE_WITHOUT" => "a") + validate!("with", "b:c", "BUNDLE_WITHOUT" => "a") + + validate!("without", "", {}) + validate!("without", nil, {}) + validate!("without", "", "BUNDLE_WITH" => "a") + validate!("without", nil, "BUNDLE_WITH" => "a") + validate!("without", "b:c", "BUNDLE_WITH" => "a") + end.not_to raise_error + + expect { validate!("with", "b:c", "BUNDLE_WITHOUT" => "c:d") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + Setting `with` to "b:c" failed: + - a group cannot be in both `with` & `without` simultaneously + - `without` is current set to [:c, :d] + - the `c` groups conflict + EOS + + expect { validate!("without", "b:c", "BUNDLE_WITH" => "c:d") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + Setting `without` to "b:c" failed: + - a group cannot be in both `with` & `without` simultaneously + - `with` is current set to [:c, :d] + - the `c` groups conflict + EOS + end + end + + describe described_class::Rule do + let(:keys) { %w[key] } + let(:description) { "rule description" } + let(:validate) { proc { raise "validate called!" } } + subject(:rule) { described_class.new(keys, description, &validate) } + + describe "#validate!" do + it "calls the block" do + expect { rule.validate!("key", nil, {}) }.to raise_error(RuntimeError, /validate called!/) + end + end + + describe "#fail!" do + it "raises with a helpful message" do + expect { subject.fail!("key", "value", "reason1", "reason2") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + Setting `key` to "value" failed: + - rule description + - reason1 + - reason2 + EOS + end + end + + describe "#set" do + it "works when the value has not changed" do + allow(Bundler.ui).to receive(:info).never + + subject.set({}, "key", nil) + subject.set({ "BUNDLE_KEY" => "value" }, "key", "value") + end + + it "prints out when the value is changing" do + settings = {} + + expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value\", since rule description, reason1") + subject.set(settings, "key", "value", "reason1") + expect(settings).to eq("BUNDLE_KEY" => "value") + + expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value2\", since rule description, reason2") + subject.set(settings, "key", "value2", "reason2") + expect(settings).to eq("BUNDLE_KEY" => "value2") + + expect(Bundler.ui).to receive(:info).with("Setting `key` to nil, since rule description, reason3") + subject.set(settings, "key", nil, "reason3") + expect(settings).to eq({}) + end + end + end +end diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb new file mode 100644 index 00000000000000..1a31493e201678 --- /dev/null +++ b/spec/bundler/bundler/settings_spec.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +require "bundler/settings" + +RSpec.describe Bundler::Settings do + subject(:settings) { described_class.new(bundled_app) } + + describe "#set_local" do + context "when the local config file is not found" do + subject(:settings) { described_class.new(nil) } + + it "raises a GemfileNotFound error with explanation" do + expect { subject.set_local("foo", "bar") }. + to raise_error(Bundler::GemfileNotFound, "Could not locate Gemfile") + end + end + end + + describe "load_config" do + let(:hash) do + { + "build.thrift" => "--with-cppflags=-D_FORTIFY_SOURCE=0", + "build.libv8" => "--with-system-v8", + "build.therubyracer" => "--with-v8-dir", + "build.pg" => "--with-pg-config=/usr/local/Cellar/postgresql92/9.2.8_1/bin/pg_config", + "gem.coc" => "false", + "gem.mit" => "false", + "gem.test" => "minitest", + "thingy" => <<-EOS.tr("\n", " "), +--asdf --fdsa --ty=oh man i hope this doesnt break bundler because +that would suck --ehhh=oh geez it looks like i might have broken bundler somehow +--very-important-option=DontDeleteRoo +--very-important-option=DontDeleteRoo +--very-important-option=DontDeleteRoo +--very-important-option=DontDeleteRoo + EOS + "xyz" => "zyx", + } + end + + before do + hash.each do |key, value| + settings.set_local key, value + end + end + + it "can load the config" do + loaded = settings.send(:load_config, bundled_app("config")) + expected = Hash[hash.map do |k, v| + [settings.send(:key_for, k), v.to_s] + end] + expect(loaded).to eq(expected) + end + + context "when BUNDLE_IGNORE_CONFIG is set" do + before { ENV["BUNDLE_IGNORE_CONFIG"] = "TRUE" } + + it "ignores the config" do + loaded = settings.send(:load_config, bundled_app("config")) + expect(loaded).to eq({}) + end + end + end + + describe "#global_config_file" do + context "when $HOME is not accessible" do + context "when $TMPDIR is not writable" do + it "does not raise" do + expect(Bundler.rubygems).to receive(:user_home).twice.and_return(nil) + expect(FileUtils).to receive(:mkpath).twice.with(File.join(Dir.tmpdir, "bundler", "home")).and_raise(Errno::EROFS, "Read-only file system @ dir_s_mkdir - /tmp/bundler") + + expect(subject.send(:global_config_file)).to be_nil + end + end + end + end + + describe "#[]" do + context "when the local config file is not found" do + subject(:settings) { described_class.new } + + it "does not raise" do + expect do + subject["foo"] + end.not_to raise_error + end + end + + context "when not set" do + context "when default value present" do + it "retrieves value" do + expect(settings[:retry]).to be 3 + end + end + + it "returns nil" do + expect(settings[:buttermilk]).to be nil + end + end + + context "when is boolean" do + it "returns a boolean" do + settings.set_local :frozen, "true" + expect(settings[:frozen]).to be true + end + context "when specific gem is configured" do + it "returns a boolean" do + settings.set_local "ignore_messages.foobar", "true" + expect(settings["ignore_messages.foobar"]).to be true + end + end + end + + context "when is number" do + it "returns a number" do + settings.set_local :ssl_verify_mode, "1" + expect(settings[:ssl_verify_mode]).to be 1 + end + end + + context "when it's not possible to write to the file" do + it "raises an PermissionError with explanation" do + expect(bundler_fileutils).to receive(:mkdir_p).with(settings.send(:local_config_file).dirname). + and_raise(Errno::EACCES) + expect { settings.set_local :frozen, "1" }. + to raise_error(Bundler::PermissionError, /config/) + end + end + end + + describe "#temporary" do + it "reset after used" do + Bundler.settings.set_local :no_install, true + + Bundler.settings.temporary(:no_install => false) do + expect(Bundler.settings[:no_install]).to eq false + end + + expect(Bundler.settings[:no_install]).to eq true + end + + it "returns the return value of the block" do + ret = Bundler.settings.temporary({}) { :ret } + expect(ret).to eq :ret + end + + context "when called without a block" do + it "leaves the setting changed" do + Bundler.settings.temporary(:foo => :random) + expect(Bundler.settings[:foo]).to eq "random" + end + + it "returns nil" do + expect(Bundler.settings.temporary(:foo => :bar)).to be_nil + end + end + end + + describe "#set_global" do + context "when it's not possible to write to the file" do + it "raises an PermissionError with explanation" do + expect(bundler_fileutils).to receive(:mkdir_p).with(settings.send(:global_config_file).dirname). + and_raise(Errno::EACCES) + expect { settings.set_global(:frozen, "1") }. + to raise_error(Bundler::PermissionError, %r{\.bundle/config}) + end + end + end + + describe "#pretty_values_for" do + it "prints the converted value rather than the raw string" do + bool_key = described_class::BOOL_KEYS.first + settings.set_local(bool_key, "false") + expect(subject.pretty_values_for(bool_key)).to eq [ + "Set for your local app (#{bundled_app("config")}): false", + ] + end + end + + describe "#mirror_for" do + let(:uri) { URI("https://rubygems.org/") } + + context "with no configured mirror" do + it "returns the original URI" do + expect(settings.mirror_for(uri)).to eq(uri) + end + + it "converts a string parameter to a URI" do + expect(settings.mirror_for("https://rubygems.org/")).to eq(uri) + end + end + + context "with a configured mirror" do + let(:mirror_uri) { URI("https://rubygems-mirror.org/") } + + before { settings.set_local "mirror.https://rubygems.org/", mirror_uri.to_s } + + it "returns the mirror URI" do + expect(settings.mirror_for(uri)).to eq(mirror_uri) + end + + it "converts a string parameter to a URI" do + expect(settings.mirror_for("https://rubygems.org/")).to eq(mirror_uri) + end + + it "normalizes the URI" do + expect(settings.mirror_for("https://rubygems.org")).to eq(mirror_uri) + end + + it "is case insensitive" do + expect(settings.mirror_for("HTTPS://RUBYGEMS.ORG/")).to eq(mirror_uri) + end + + context "with a file URI" do + let(:mirror_uri) { URI("file:/foo/BAR/baz/qUx/") } + + it "returns the mirror URI" do + expect(settings.mirror_for(uri)).to eq(mirror_uri) + end + + it "converts a string parameter to a URI" do + expect(settings.mirror_for("file:/foo/BAR/baz/qUx/")).to eq(mirror_uri) + end + + it "normalizes the URI" do + expect(settings.mirror_for("file:/foo/BAR/baz/qUx")).to eq(mirror_uri) + end + end + end + end + + describe "#credentials_for" do + let(:uri) { URI("https://gemserver.example.org/") } + let(:credentials) { "username:password" } + + context "with no configured credentials" do + it "returns nil" do + expect(settings.credentials_for(uri)).to be_nil + end + end + + context "with credentials configured by URL" do + before { settings.set_local "https://gemserver.example.org/", credentials } + + it "returns the configured credentials" do + expect(settings.credentials_for(uri)).to eq(credentials) + end + end + + context "with credentials configured by hostname" do + before { settings.set_local "gemserver.example.org", credentials } + + it "returns the configured credentials" do + expect(settings.credentials_for(uri)).to eq(credentials) + end + end + end + + describe "URI normalization" do + it "normalizes HTTP URIs in credentials configuration" do + settings.set_local "http://gemserver.example.org", "username:password" + expect(settings.all).to include("http://gemserver.example.org/") + end + + it "normalizes HTTPS URIs in credentials configuration" do + settings.set_local "https://gemserver.example.org", "username:password" + expect(settings.all).to include("https://gemserver.example.org/") + end + + it "normalizes HTTP URIs in mirror configuration" do + settings.set_local "mirror.http://rubygems.org", "http://rubygems-mirror.org" + expect(settings.all).to include("mirror.http://rubygems.org/") + end + + it "normalizes HTTPS URIs in mirror configuration" do + settings.set_local "mirror.https://rubygems.org", "http://rubygems-mirror.org" + expect(settings.all).to include("mirror.https://rubygems.org/") + end + + it "does not normalize other config keys that happen to contain 'http'" do + settings.set_local "local.httparty", home("httparty") + expect(settings.all).to include("local.httparty") + end + + it "does not normalize other config keys that happen to contain 'https'" do + settings.set_local "local.httpsmarty", home("httpsmarty") + expect(settings.all).to include("local.httpsmarty") + end + + it "reads older keys without trailing slashes" do + settings.set_local "mirror.https://rubygems.org", "http://rubygems-mirror.org" + expect(settings.mirror_for("https://rubygems.org/")).to eq( + URI("http://rubygems-mirror.org/") + ) + end + + it "normalizes URIs with a fallback_timeout option" do + settings.set_local "mirror.https://rubygems.org/.fallback_timeout", "true" + expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout") + end + + it "normalizes URIs with a fallback_timeout option without a trailing slash" do + settings.set_local "mirror.https://rubygems.org.fallback_timeout", "true" + expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout") + end + end + + describe "BUNDLE_ keys format" do + let(:settings) { described_class.new(bundled_app(".bundle")) } + + it "converts older keys without double dashes" do + config("BUNDLE_MY__PERSONAL.RACK" => "~/Work/git/rack") + expect(settings["my.personal.rack"]).to eq("~/Work/git/rack") + end + + it "converts older keys without trailing slashes and double dashes" do + config("BUNDLE_MIRROR__HTTPS://RUBYGEMS.ORG" => "http://rubygems-mirror.org") + expect(settings["mirror.https://rubygems.org/"]).to eq("http://rubygems-mirror.org") + end + + it "reads newer keys format properly" do + config("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://rubygems-mirror.org") + expect(settings["mirror.https://rubygems.org/"]).to eq("http://rubygems-mirror.org") + end + end +end diff --git a/spec/bundler/bundler/shared_helpers_spec.rb b/spec/bundler/bundler/shared_helpers_spec.rb new file mode 100644 index 00000000000000..b66c43fd928c8f --- /dev/null +++ b/spec/bundler/bundler/shared_helpers_spec.rb @@ -0,0 +1,504 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::SharedHelpers do + let(:ext_lock_double) { double(:ext_lock) } + + before do + allow(Bundler.rubygems).to receive(:ext_lock).and_return(ext_lock_double) + allow(ext_lock_double).to receive(:synchronize) {|&block| block.call } + end + + subject { Bundler::SharedHelpers } + + describe "#default_gemfile" do + before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" } + + context "Gemfile is present" do + let(:expected_gemfile_path) { Pathname.new("/path/Gemfile") } + + it "returns the Gemfile path" do + expect(subject.default_gemfile).to eq(expected_gemfile_path) + end + end + + context "Gemfile is not present" do + before { ENV["BUNDLE_GEMFILE"] = nil } + + it "raises a GemfileNotFound error" do + expect { subject.default_gemfile }.to raise_error( + Bundler::GemfileNotFound, "Could not locate Gemfile" + ) + end + end + + context "Gemfile is not an absolute path" do + before { ENV["BUNDLE_GEMFILE"] = "Gemfile" } + + let(:expected_gemfile_path) { Pathname.new("Gemfile").expand_path } + + it "returns the Gemfile path" do + expect(subject.default_gemfile).to eq(expected_gemfile_path) + end + end + end + + describe "#default_lockfile" do + context "gemfile is gems.rb" do + let(:gemfile_path) { Pathname.new("/path/gems.rb") } + let(:expected_lockfile_path) { Pathname.new("/path/gems.locked") } + + before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) } + + it "returns the gems.locked path" do + expect(subject.default_lockfile).to eq(expected_lockfile_path) + end + end + + context "is a regular Gemfile" do + let(:gemfile_path) { Pathname.new("/path/Gemfile") } + let(:expected_lockfile_path) { Pathname.new("/path/Gemfile.lock") } + + before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) } + + it "returns the lock file path" do + expect(subject.default_lockfile).to eq(expected_lockfile_path) + end + end + end + + describe "#default_bundle_dir" do + context ".bundle does not exist" do + it "returns nil" do + expect(subject.default_bundle_dir).to be_nil + end + end + + context ".bundle is global .bundle" do + let(:global_rubygems_dir) { Pathname.new("#{bundled_app}") } + + before do + Dir.mkdir ".bundle" + allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir) + end + + it "returns nil" do + expect(subject.default_bundle_dir).to be_nil + end + end + + context ".bundle is not global .bundle" do + let(:global_rubygems_dir) { Pathname.new("/path/rubygems") } + let(:expected_bundle_dir_path) { Pathname.new("#{bundled_app}/.bundle") } + + before do + Dir.mkdir ".bundle" + allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir) + end + + it "returns the .bundle path" do + expect(subject.default_bundle_dir).to eq(expected_bundle_dir_path) + end + end + end + + describe "#in_bundle?" do + it "calls the find_gemfile method" do + expect(subject).to receive(:find_gemfile) + subject.in_bundle? + end + + shared_examples_for "correctly determines whether to return a Gemfile path" do + context "currently in directory with a Gemfile" do + before { File.new("Gemfile", "w") } + + it "returns path of the bundle Gemfile" do + expect(subject.in_bundle?).to eq("#{bundled_app}/Gemfile") + end + end + + context "currently in directory without a Gemfile" do + it "returns nil" do + expect(subject.in_bundle?).to be_nil + end + end + end + + context "ENV['BUNDLE_GEMFILE'] set" do + before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" } + + it "returns ENV['BUNDLE_GEMFILE']" do + expect(subject.in_bundle?).to eq("/path/Gemfile") + end + end + + context "ENV['BUNDLE_GEMFILE'] not set" do + before { ENV["BUNDLE_GEMFILE"] = nil } + + it_behaves_like "correctly determines whether to return a Gemfile path" + end + + context "ENV['BUNDLE_GEMFILE'] is blank" do + before { ENV["BUNDLE_GEMFILE"] = "" } + + it_behaves_like "correctly determines whether to return a Gemfile path" + end + end + + describe "#chdir" do + let(:op_block) { proc { Dir.mkdir "nested_dir" } } + + before { Dir.mkdir "chdir_test_dir" } + + it "executes the passed block while in the specified directory" do + subject.chdir("chdir_test_dir", &op_block) + expect(Pathname.new("chdir_test_dir/nested_dir")).to exist + end + end + + describe "#pwd" do + it "returns the current absolute path" do + expect(subject.pwd).to eq(bundled_app) + end + end + + describe "#with_clean_git_env" do + let(:with_clean_git_env_block) { proc { Dir.mkdir "with_clean_git_env_test_dir" } } + + before do + ENV["GIT_DIR"] = "ORIGINAL_ENV_GIT_DIR" + ENV["GIT_WORK_TREE"] = "ORIGINAL_ENV_GIT_WORK_TREE" + end + + it "executes the passed block" do + subject.with_clean_git_env(&with_clean_git_env_block) + expect(Pathname.new("with_clean_git_env_test_dir")).to exist + end + + context "when a block is passed" do + let(:with_clean_git_env_block) do + proc do + Dir.mkdir "git_dir_test_dir" unless ENV["GIT_DIR"].nil? + Dir.mkdir "git_work_tree_test_dir" unless ENV["GIT_WORK_TREE"].nil? + end end + + it "uses a fresh git env for execution" do + subject.with_clean_git_env(&with_clean_git_env_block) + expect(Pathname.new("git_dir_test_dir")).to_not exist + expect(Pathname.new("git_work_tree_test_dir")).to_not exist + end + end + + context "passed block does not throw errors" do + let(:with_clean_git_env_block) do + proc do + ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR" + ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE" + end end + + it "restores the git env after" do + subject.with_clean_git_env(&with_clean_git_env_block) + expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR") + expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE") + end + end + + context "passed block throws errors" do + let(:with_clean_git_env_block) do + proc do + ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR" + ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE" + raise RuntimeError.new + end end + + it "restores the git env after" do + expect { subject.with_clean_git_env(&with_clean_git_env_block) }.to raise_error(RuntimeError) + expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR") + expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE") + end + end + end + + describe "#set_bundle_environment" do + before do + ENV["BUNDLE_GEMFILE"] = "Gemfile" + end + + shared_examples_for "ENV['PATH'] gets set correctly" do + before { Dir.mkdir ".bundle" } + + it "ensures bundle bin path is in ENV['PATH']" do + subject.set_bundle_environment + paths = ENV["PATH"].split(File::PATH_SEPARATOR) + expect(paths).to include("#{Bundler.bundle_path}/bin") + end + end + + shared_examples_for "ENV['RUBYOPT'] gets set correctly" do + it "ensures -rbundler/setup is at the beginning of ENV['RUBYOPT']" do + subject.set_bundle_environment + expect(ENV["RUBYOPT"].split(" ")).to start_with("-rbundler/setup") + end + end + + shared_examples_for "ENV['RUBYLIB'] gets set correctly" do + let(:ruby_lib_path) { "stubbed_ruby_lib_dir" } + + before do + allow(Bundler::SharedHelpers).to receive(:bundler_ruby_lib).and_return(ruby_lib_path) + end + + it "ensures bundler's ruby version lib path is in ENV['RUBYLIB']" do + subject.set_bundle_environment + paths = (ENV["RUBYLIB"]).split(File::PATH_SEPARATOR) + expect(paths).to include(ruby_lib_path) + end + end + + it "calls the appropriate set methods" do + expect(subject).to receive(:set_path) + expect(subject).to receive(:set_rubyopt) + expect(subject).to receive(:set_rubylib) + subject.set_bundle_environment + end + + it "exits if bundle path contains the unix-like path separator" do + if Gem.respond_to?(:path_separator) + allow(Gem).to receive(:path_separator).and_return(":") + else + stub_const("File::PATH_SEPARATOR", ":".freeze) + end + allow(Bundler).to receive(:bundle_path) { Pathname.new("so:me/dir/bin") } + expect { subject.send(:validate_bundle_path) }.to raise_error( + Bundler::PathError, + "Your bundle path contains text matching \":\", which is the " \ + "path separator for your system. Bundler cannot " \ + "function correctly when the Bundle path contains the " \ + "system's PATH separator. Please change your " \ + "bundle path to not match \":\".\nYour current bundle " \ + "path is '#{Bundler.bundle_path}'." + ) + end + + context "with a jruby path_separator regex", :ruby => "1.9" do + # In versions of jruby that supported ruby 1.8, the path separator was the standard File::PATH_SEPARATOR + let(:regex) { Regexp.new("(? "1.9" do + let(:file_op_block) { proc {|_path| raise Errno::ENOTSUP } } + + it "raises a OperationNotSupportedError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::OperationNotSupportedError + ) + end + end + + context "system throws Errno::ENOSPC" do + let(:file_op_block) { proc {|_path| raise Errno::ENOSPC } } + + it "raises a NoSpaceOnDeviceError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::NoSpaceOnDeviceError + ) + end + end + + context "system throws an unhandled SystemCallError" do + let(:error) { SystemCallError.new("Shields down", 1337) } + let(:file_op_block) { proc {|_path| raise error } } + + it "raises a GenericSystemCallError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::GenericSystemCallError, /error accessing.+underlying.+Shields down/m + ) + end + end + end + + describe "#const_get_safely" do + module TargetNamespace + VALID_CONSTANT = 1 + end + + context "when the namespace does have the requested constant" do + it "returns the value of the requested constant" do + expect(subject.const_get_safely(:VALID_CONSTANT, TargetNamespace)).to eq(1) + end + end + + context "when the requested constant is passed as a string" do + it "returns the value of the requested constant" do + expect(subject.const_get_safely("VALID_CONSTANT", TargetNamespace)).to eq(1) + end + end + + context "when the namespace does not have the requested constant" do + it "returns nil" do + expect(subject.const_get_safely("INVALID_CONSTANT", TargetNamespace)).to be_nil + end + end + end +end diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb new file mode 100644 index 00000000000000..3a29c974615d98 --- /dev/null +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Git::GitProxy do + let(:path) { Pathname("path") } + let(:uri) { "https://github.com/bundler/bundler.git" } + let(:ref) { "HEAD" } + let(:revision) { nil } + let(:git_source) { nil } + subject { described_class.new(path, uri, ref, revision, git_source) } + + context "with configured credentials" do + it "adds username and password to URI" do + Bundler.settings.temporary(uri => "u:p") + expect(subject).to receive(:git_retry).with(match("https://u:p@github.com/bundler/bundler.git")) + subject.checkout + end + + it "adds username and password to URI for host" do + Bundler.settings.temporary("github.com" => "u:p") + expect(subject).to receive(:git_retry).with(match("https://u:p@github.com/bundler/bundler.git")) + subject.checkout + end + + it "does not add username and password to mismatched URI" do + Bundler.settings.temporary("https://u:p@github.com/bundler/bundler-mismatch.git" => "u:p") + expect(subject).to receive(:git_retry).with(match(uri)) + subject.checkout + end + + it "keeps original userinfo" do + Bundler.settings.temporary("github.com" => "u:p") + original = "https://orig:info@github.com/bundler/bundler.git" + subject = described_class.new(Pathname("path"), original, "HEAD") + expect(subject).to receive(:git_retry).with(match(original)) + subject.checkout + end + end + + describe "#version" do + context "with a normal version number" do + before do + expect(subject).to receive(:git).with("--version"). + and_return("git version 1.2.3") + end + + it "returns the git version number" do + expect(subject.version).to eq("1.2.3") + end + + it "does not raise an error when passed into Gem::Version.create" do + expect { Gem::Version.create subject.version }.not_to raise_error + end + end + + context "with a OSX version number" do + before do + expect(subject).to receive(:git).with("--version"). + and_return("git version 1.2.3 (Apple Git-BS)") + end + + it "strips out OSX specific additions in the version string" do + expect(subject.version).to eq("1.2.3") + end + + it "does not raise an error when passed into Gem::Version.create" do + expect { Gem::Version.create subject.version }.not_to raise_error + end + end + + context "with a msysgit version number" do + before do + expect(subject).to receive(:git).with("--version"). + and_return("git version 1.2.3.msysgit.0") + end + + it "strips out msysgit specific additions in the version string" do + expect(subject.version).to eq("1.2.3") + end + + it "does not raise an error when passed into Gem::Version.create" do + expect { Gem::Version.create subject.version }.not_to raise_error + end + end + end + + describe "#full_version" do + context "with a normal version number" do + before do + expect(subject).to receive(:git).with("--version"). + and_return("git version 1.2.3") + end + + it "returns the git version number" do + expect(subject.full_version).to eq("1.2.3") + end + end + + context "with a OSX version number" do + before do + expect(subject).to receive(:git).with("--version"). + and_return("git version 1.2.3 (Apple Git-BS)") + end + + it "does not strip out OSX specific additions in the version string" do + expect(subject.full_version).to eq("1.2.3 (Apple Git-BS)") + end + end + + context "with a msysgit version number" do + before do + expect(subject).to receive(:git).with("--version"). + and_return("git version 1.2.3.msysgit.0") + end + + it "does not strip out msysgit specific additions in the version string" do + expect(subject.full_version).to eq("1.2.3.msysgit.0") + end + end + end + + describe "#copy_to" do + let(:destination) { tmpdir("copy_to_path") } + let(:submodules) { false } + + context "when given a SHA as a revision" do + let(:revision) { "abcd" * 10 } + + it "fails gracefully when resetting to the revision fails" do + expect(subject).to receive(:git_retry).with(start_with("clone ")) { destination.mkpath } + expect(subject).to receive(:git_retry).with(start_with("fetch ")) + expect(subject).to receive(:git).with("reset --hard #{revision}").and_raise(Bundler::Source::Git::GitCommandError, "command") + expect(subject).not_to receive(:git) + + expect { subject.copy_to(destination, submodules) }. + to raise_error(Bundler::Source::Git::MissingGitRevisionError, + "Revision #{revision} does not exist in the repository #{uri}. Maybe you misspelled it?") + end + end + end +end diff --git a/spec/bundler/bundler/source/git_spec.rb b/spec/bundler/bundler/source/git_spec.rb new file mode 100644 index 00000000000000..f7475a35aa9559 --- /dev/null +++ b/spec/bundler/bundler/source/git_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Git do + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + let(:uri) { "https://github.com/foo/bar.git" } + let(:options) do + { "uri" => uri } + end + + subject { described_class.new(options) } + + describe "#to_s" do + it "returns a description" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (at master)" + end + + context "when the URI contains credentials" do + let(:uri) { "https://my-secret-token:x-oauth-basic@github.com/foo/bar.git" } + + it "filters credentials" do + expect(subject.to_s).to eq "https://x-oauth-basic@github.com/foo/bar.git (at master)" + end + end + end +end diff --git a/spec/bundler/bundler/source/path_spec.rb b/spec/bundler/bundler/source/path_spec.rb new file mode 100644 index 00000000000000..1d13e03ec17d11 --- /dev/null +++ b/spec/bundler/bundler/source/path_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Path do + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + describe "#eql?" do + subject { described_class.new("path" => "gems/a") } + + context "with two equivalent relative paths from different roots" do + let(:a_gem_opts) { { "path" => "../gems/a", "root_path" => Bundler.root.join("nested") } } + let(:a_gem) { described_class.new a_gem_opts } + + it "returns true" do + expect(subject).to eq a_gem + end + end + + context "with the same (but not equivalent) relative path from different roots" do + subject { described_class.new("path" => "gems/a") } + + let(:a_gem_opts) { { "path" => "gems/a", "root_path" => Bundler.root.join("nested") } } + let(:a_gem) { described_class.new a_gem_opts } + + it "returns false" do + expect(subject).to_not eq a_gem + end + end + end +end diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb new file mode 100644 index 00000000000000..9a7ab421281f6c --- /dev/null +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require "bundler/source/rubygems/remote" + +RSpec.describe Bundler::Source::Rubygems::Remote do + def remote(uri) + Bundler::Source::Rubygems::Remote.new(uri) + end + + before do + allow(Digest(:MD5)).to receive(:hexdigest).with(duck_type(:to_s)) {|string| "MD5HEX(#{string})" } + end + + let(:uri_no_auth) { URI("https://gems.example.com") } + let(:uri_with_auth) { URI("https://#{credentials}@gems.example.com") } + let(:credentials) { "username:password" } + + context "when the original URI has no credentials" do + describe "#uri" do + it "returns the original URI" do + expect(remote(uri_no_auth).uri).to eq(uri_no_auth) + end + + it "applies configured credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => credentials) + expect(remote(uri_no_auth).uri).to eq(uri_with_auth) + end + end + + describe "#anonymized_uri" do + it "returns the original URI" do + expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth) + end + + it "does not apply given credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => credentials) + expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth) + end + end + + describe "#cache_slug" do + it "returns the correct slug" do + expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.443.MD5HEX(gems.example.com.443./)") + end + + it "only applies the given user" do + Bundler.settings.temporary(uri_no_auth.to_s => credentials) + expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)") + end + end + end + + context "when the original URI has a username and password" do + describe "#uri" do + it "returns the original URI" do + expect(remote(uri_with_auth).uri).to eq(uri_with_auth) + end + + it "does not apply configured credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff") + expect(remote(uri_with_auth).uri).to eq(uri_with_auth) + end + end + + describe "#anonymized_uri" do + it "returns the URI without username and password" do + expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth) + end + + it "does not apply given credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff") + expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth) + end + end + + describe "#cache_slug" do + it "returns the correct slug" do + expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)") + end + + it "does not apply given credentials" do + Bundler.settings.temporary(uri_with_auth.to_s => credentials) + expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)") + end + end + end + + context "when the original URI has only a username" do + let(:uri) { URI("https://SeCrEt-ToKeN@gem.fury.io/me/") } + + describe "#anonymized_uri" do + it "returns the URI without username and password" do + expect(remote(uri).anonymized_uri).to eq(URI("https://gem.fury.io/me/")) + end + end + + describe "#cache_slug" do + it "returns the correct slug" do + expect(remote(uri).cache_slug).to eq("gem.fury.io.SeCrEt-ToKeN.443.MD5HEX(gem.fury.io.SeCrEt-ToKeN.443./me/)") + end + end + end + + context "when a mirror with inline credentials is configured for the URI" do + let(:uri) { URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { URI("https://username:password@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { URI("https://rubygems-mirror.org/") } + + before { Bundler.settings.set_local("mirror.https://rubygems.org/", mirror_uri_with_auth.to_s) } + + specify "#uri returns the mirror URI with credentials" do + expect(remote(uri).uri).to eq(mirror_uri_with_auth) + end + + specify "#anonymized_uri returns the mirror URI without credentials" do + expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth) + end + + specify "#original_uri returns the original source" do + expect(remote(uri).original_uri).to eq(uri) + end + + specify "#cache_slug returns the correct slug" do + expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)") + end + end + + context "when a mirror with configured credentials is configured for the URI" do + let(:uri) { URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { URI("https://#{credentials}@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { URI("https://rubygems-mirror.org/") } + + before do + Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_no_auth.to_s) + Bundler.settings.temporary(mirror_uri_no_auth.to_s => credentials) + end + + specify "#uri returns the mirror URI with credentials" do + expect(remote(uri).uri).to eq(mirror_uri_with_auth) + end + + specify "#anonymized_uri returns the mirror URI without credentials" do + expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth) + end + + specify "#original_uri returns the original source" do + expect(remote(uri).original_uri).to eq(uri) + end + + specify "#cache_slug returns the original source" do + expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)") + end + end + + context "when there is no mirror set" do + describe "#original_uri" do + it "is not set" do + expect(remote(uri_no_auth).original_uri).to be_nil + end + end + end +end diff --git a/spec/bundler/bundler/source/rubygems_spec.rb b/spec/bundler/bundler/source/rubygems_spec.rb new file mode 100644 index 00000000000000..7c457a7265945f --- /dev/null +++ b/spec/bundler/bundler/source/rubygems_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Rubygems do + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + describe "caches" do + it "includes Bundler.app_cache" do + expect(subject.caches).to include(Bundler.app_cache) + end + + it "includes GEM_PATH entries" do + Gem.path.each do |path| + expect(subject.caches).to include(File.expand_path("#{path}/cache")) + end + end + + it "is an array of strings or pathnames" do + subject.caches.each do |cache| + expect([String, Pathname]).to include(cache.class) + end + end + end + + describe "#add_remote" do + context "when the source is an HTTP(s) URI with no host" do + it "raises error" do + expect { subject.add_remote("https:rubygems.org") }.to raise_error(ArgumentError) + end + end + end +end diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb new file mode 100644 index 00000000000000..541a46c6d01113 --- /dev/null +++ b/spec/bundler/bundler/source_list_spec.rb @@ -0,0 +1,463 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::SourceList do + before do + allow(Bundler).to receive(:root) { Pathname.new "./tmp/bundled_app" } + + stub_const "ASourcePlugin", Class.new(Bundler::Plugin::API) + ASourcePlugin.source "new_source" + allow(Bundler::Plugin).to receive(:source?).with("new_source").and_return(true) + end + + subject(:source_list) { Bundler::SourceList.new } + + let(:rubygems_aggregate) { Bundler::Source::Rubygems.new } + let(:metadata_source) { Bundler::Source::Metadata.new } + + describe "adding sources" do + before do + source_list.add_path_source("path" => "/existing/path/to/gem") + source_list.add_git_source("uri" => "git://existing-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://existing-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://some.url/a") + end + + describe "#add_path_source" do + before do + @duplicate = source_list.add_path_source("path" => "/path/to/gem") + @new_source = source_list.add_path_source("path" => "/path/to/gem") + end + + it "returns the new path source" do + expect(@new_source).to be_instance_of(Bundler::Source::Path) + end + + it "passes the provided options to the new source" do + expect(@new_source.options).to eq("path" => "/path/to/gem") + end + + it "adds the source to the beginning of path_sources" do + expect(source_list.path_sources.first).to equal(@new_source) + end + + it "removes existing duplicates" do + expect(source_list.path_sources).not_to include equal(@duplicate) + end + end + + describe "#add_git_source" do + before do + @duplicate = source_list.add_git_source("uri" => "git://host/path.git") + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + end + + it "returns the new git source" do + expect(@new_source).to be_instance_of(Bundler::Source::Git) + end + + it "passes the provided options to the new source" do + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + expect(@new_source.options).to eq("uri" => "git://host/path.git") + end + + it "adds the source to the beginning of git_sources" do + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + expect(source_list.git_sources.first).to equal(@new_source) + end + + it "removes existing duplicates" do + @duplicate = source_list.add_git_source("uri" => "git://host/path.git") + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + expect(source_list.git_sources).not_to include equal(@duplicate) + end + + context "with the git: protocol" do + let(:msg) do + "The git source `git://existing-git.org/path.git` " \ + "uses the `git` protocol, which transmits data without encryption. " \ + "Disable this warning with `bundle config git.allow_insecure true`, " \ + "or switch to the `https` protocol to keep your data secure." + end + + it "warns about git protocols" do + expect(Bundler.ui).to receive(:warn).with(msg) + source_list.add_git_source("uri" => "git://existing-git.org/path.git") + end + + it "ignores git protocols on request" do + Bundler.settings.temporary(:"git.allow_insecure" => true) + expect(Bundler.ui).to_not receive(:warn).with(msg) + source_list.add_git_source("uri" => "git://existing-git.org/path.git") + end + end + end + + describe "#add_rubygems_source" do + before do + @duplicate = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"]) + @new_source = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"]) + end + + it "returns the new rubygems source" do + expect(@new_source).to be_instance_of(Bundler::Source::Rubygems) + end + + it "passes the provided options to the new source" do + expect(@new_source.options).to eq("remotes" => ["https://rubygems.org/"]) + end + + it "adds the source to the beginning of rubygems_sources" do + expect(source_list.rubygems_sources.first).to equal(@new_source) + end + + it "removes duplicates" do + expect(source_list.rubygems_sources).not_to include equal(@duplicate) + end + end + + describe "#add_rubygems_remote", :bundler => "< 3" do + let!(:returned_source) { source_list.add_rubygems_remote("https://rubygems.org/") } + + it "returns the aggregate rubygems source" do + expect(returned_source).to be_instance_of(Bundler::Source::Rubygems) + end + + it "adds the provided remote to the beginning of the aggregate source" do + source_list.add_rubygems_remote("https://othersource.org") + expect(returned_source.remotes).to eq [ + URI("https://othersource.org/"), + URI("https://rubygems.org/"), + ] + end + end + + describe "#add_plugin_source" do + before do + @duplicate = source_list.add_plugin_source("new_source", "uri" => "http://host/path.") + @new_source = source_list.add_plugin_source("new_source", "uri" => "http://host/path.") + end + + it "returns the new plugin source" do + expect(@new_source).to be_a(Bundler::Plugin::API::Source) + end + + it "passes the provided options to the new source" do + expect(@new_source.options).to eq("uri" => "http://host/path.") + end + + it "adds the source to the beginning of git_sources" do + expect(source_list.plugin_sources.first).to equal(@new_source) + end + + it "removes existing duplicates" do + expect(source_list.plugin_sources).not_to include equal(@duplicate) + end + end + end + + describe "#all_sources" do + it "includes the aggregate rubygems source when rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) + source_list.add_path_source("path" => "/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/a") + + expect(source_list.all_sources).to include rubygems_aggregate + end + + it "includes the aggregate rubygems source when no rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_path_source("path" => "/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/a") + + expect(source_list.all_sources).to include rubygems_aggregate + end + + it "returns sources of the same type in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"]) + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/b") + source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"]) + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://some.o.url/") + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"]) + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/c") + source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"]) + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.all_sources).to eq [ + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + ASourcePlugin.new("uri" => "https://some.url/c"), + ASourcePlugin.new("uri" => "https://some.o.url/"), + ASourcePlugin.new("uri" => "https://some.url/b"), + Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]), + rubygems_aggregate, + metadata_source, + ] + end + end + + describe "#path_sources" do + it "returns an empty array when no path sources have been added" do + source_list.add_rubygems_remote("https://rubygems.org") + source_list.add_git_source("uri" => "git://host/path.git") + expect(source_list.path_sources).to be_empty + end + + it "returns path sources in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_remote("https://fifth-rubygems.org") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_remote("https://fourth-rubygems.org") + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_remote("https://third-rubygems.org") + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_remote("https://second-rubygems.org") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_remote("https://first-rubygems.org") + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.path_sources).to eq [ + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + ] + end + end + + describe "#git_sources" do + it "returns an empty array when no git sources have been added" do + source_list.add_rubygems_remote("https://rubygems.org") + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.git_sources).to be_empty + end + + it "returns git sources in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_remote("https://fifth-rubygems.org") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_remote("https://fourth-rubygems.org") + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_remote("https://third-rubygems.org") + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_remote("https://second-rubygems.org") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_remote("https://first-rubygems.org") + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.git_sources).to eq [ + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + ] + end + end + + describe "#plugin_sources" do + it "returns an empty array when no plugin sources have been added" do + source_list.add_rubygems_remote("https://rubygems.org") + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.plugin_sources).to be_empty + end + + it "returns plugin sources in the reverse order that they were added" do + source_list.add_plugin_source("new_source", "uri" => "https://third-git.org/path.git") + source_list.add_git_source("https://new-git.org") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_remote("https://fourth-rubygems.org") + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_remote("https://third-rubygems.org") + source_list.add_plugin_source("new_source", "uri" => "git://second-git.org/path.git") + source_list.add_rubygems_remote("https://second-rubygems.org") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_remote("https://first-rubygems.org") + source_list.add_plugin_source("new_source", "uri" => "git://first-git.org/path.git") + + expect(source_list.plugin_sources).to eq [ + ASourcePlugin.new("uri" => "git://first-git.org/path.git"), + ASourcePlugin.new("uri" => "git://second-git.org/path.git"), + ASourcePlugin.new("uri" => "https://third-git.org/path.git"), + ] + end + end + + describe "#rubygems_sources" do + it "includes the aggregate rubygems source when rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.rubygems_sources).to include rubygems_aggregate + end + + it "returns only the aggregate rubygems source when no rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.rubygems_sources).to eq [rubygems_aggregate] + end + + it "returns rubygems sources in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"]) + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"]) + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"]) + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"]) + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"]) + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.rubygems_sources).to eq [ + Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]), + rubygems_aggregate, + ] + end + end + + describe "#get" do + context "when it includes an equal source" do + let(:rubygems_source) { Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"]) } + before { @equal_source = source_list.add_rubygems_remote("https://rubygems.org") } + + it "returns the equal source" do + expect(source_list.get(rubygems_source)).to be @equal_source + end + end + + context "when it does not include an equal source" do + let(:path_source) { Bundler::Source::Path.new("path" => "/path/to/gem") } + + it "returns nil" do + expect(source_list.get(path_source)).to be_nil + end + end + end + + describe "#lock_sources" do + before do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://third-bar.org/foo") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"]) + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"]) + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://second-plugin.org/random") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"]) + source_list.add_git_source("uri" => "git://first-git.org/path.git") + end + + it "combines the rubygems sources into a single instance, removing duplicate remotes from the end", :bundler => "< 3" do + expect(source_list.lock_sources).to eq [ + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + ASourcePlugin.new("uri" => "https://second-plugin.org/random"), + ASourcePlugin.new("uri" => "https://third-bar.org/foo"), + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + Bundler::Source::Rubygems.new("remotes" => [ + "https://duplicate-rubygems.org", + "https://first-rubygems.org", + "https://second-rubygems.org", + "https://third-rubygems.org", + ]), + ] + end + + it "returns all sources, without combining rubygems sources", :bundler => "3" do + expect(source_list.lock_sources).to eq [ + Bundler::Source::Rubygems.new, + Bundler::Source::Rubygems.new("remotes" => ["https://duplicate-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]), + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + ASourcePlugin.new("uri" => "https://second-plugin.org/random"), + ASourcePlugin.new("uri" => "https://third-bar.org/foo"), + ] + end + end + + describe "replace_sources!" do + let(:existing_locked_source) { Bundler::Source::Path.new("path" => "/existing/path") } + let(:removed_locked_source) { Bundler::Source::Path.new("path" => "/removed/path") } + + let(:locked_sources) { [existing_locked_source, removed_locked_source] } + + before do + @existing_source = source_list.add_path_source("path" => "/existing/path") + @new_source = source_list.add_path_source("path" => "/new/path") + source_list.replace_sources!(locked_sources) + end + + it "maintains the order and number of sources" do + expect(source_list.path_sources).to eq [@new_source, @existing_source] + end + + it "retains the same instance of the new source" do + expect(source_list.path_sources[0]).to be @new_source + end + + it "replaces the instance of the existing source" do + expect(source_list.path_sources[1]).to be existing_locked_source + end + end + + describe "#cached!" do + let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) } + let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") } + let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") } + + it "calls #cached! on all the sources" do + expect(rubygems_source).to receive(:cached!) + expect(git_source).to receive(:cached!) + expect(path_source).to receive(:cached!) + source_list.cached! + end + end + + describe "#remote!" do + let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) } + let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") } + let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") } + + it "calls #remote! on all the sources" do + expect(rubygems_source).to receive(:remote!) + expect(git_source).to receive(:remote!) + expect(path_source).to receive(:remote!) + source_list.remote! + end + end +end diff --git a/spec/bundler/bundler/source_spec.rb b/spec/bundler/bundler/source_spec.rb new file mode 100644 index 00000000000000..9ef8e7e50fe4fb --- /dev/null +++ b/spec/bundler/bundler/source_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source do + class ExampleSource < Bundler::Source + end + + subject { ExampleSource.new } + + describe "#unmet_deps" do + let(:specs) { double(:specs) } + let(:unmet_dependency_names) { double(:unmet_dependency_names) } + + before do + allow(subject).to receive(:specs).and_return(specs) + allow(specs).to receive(:unmet_dependency_names).and_return(unmet_dependency_names) + end + + it "should return the names of unmet dependencies" do + expect(subject.unmet_deps).to eq(unmet_dependency_names) + end + end + + describe "#version_message" do + let(:spec) { double(:spec, :name => "nokogiri", :version => ">= 1.6", :platform => rb) } + + shared_examples_for "the lockfile specs are not relevant" do + it "should return a string with the spec name and version" do + expect(subject.version_message(spec)).to eq("nokogiri >= 1.6") + end + end + + context "when there are locked gems" do + let(:locked_gems) { double(:locked_gems) } + + before { allow(Bundler).to receive(:locked_gems).and_return(locked_gems) } + + context "that contain the relevant gem spec" do + before do + specs = double(:specs) + allow(locked_gems).to receive(:specs).and_return(specs) + allow(specs).to receive(:find).and_return(locked_gem) + end + + context "without a version" do + let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => nil) } + + it_behaves_like "the lockfile specs are not relevant" + end + + context "with the same version" do + let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => ">= 1.6") } + + it_behaves_like "the lockfile specs are not relevant" + end + + context "with a different version" do + let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "< 1.5") } + + context "with color" do + before { Bundler.ui = Bundler::UI::Shell.new } + + it "should return a string with the spec name and version and locked spec version" do + expect(subject.version_message(spec)).to eq("nokogiri >= 1.6\e[32m (was < 1.5)\e[0m") + end + end + + context "without color" do + it "should return a string with the spec name and version and locked spec version" do + expect(subject.version_message(spec)).to eq("nokogiri >= 1.6 (was < 1.5)") + end + end + end + + context "with a more recent version" do + let(:spec) { double(:spec, :name => "nokogiri", :version => "1.6.1", :platform => rb) } + let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "1.7.0") } + + context "with color" do + before { Bundler.ui = Bundler::UI::Shell.new } + + it "should return a string with the locked spec version in yellow" do + expect(subject.version_message(spec)).to eq("nokogiri 1.6.1\e[33m (was 1.7.0)\e[0m") + end + end + end + + context "with an older version" do + let(:spec) { double(:spec, :name => "nokogiri", :version => "1.7.1", :platform => rb) } + let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "1.7.0") } + + context "with color" do + before { Bundler.ui = Bundler::UI::Shell.new } + + it "should return a string with the locked spec version in green" do + expect(subject.version_message(spec)).to eq("nokogiri 1.7.1\e[32m (was 1.7.0)\e[0m") + end + end + end + end + + context "that do not contain the relevant gem spec" do + before do + specs = double(:specs) + allow(locked_gems).to receive(:specs).and_return(specs) + allow(specs).to receive(:find).and_return(nil) + end + + it_behaves_like "the lockfile specs are not relevant" + end + end + + context "when there are no locked gems" do + before { allow(Bundler).to receive(:locked_gems).and_return(nil) } + + it_behaves_like "the lockfile specs are not relevant" + end + end + + describe "#can_lock?" do + context "when the passed spec's source is equivalent" do + let(:spec) { double(:spec, :source => subject) } + + it "should return true" do + expect(subject.can_lock?(spec)).to be_truthy + end + end + + context "when the passed spec's source is not equivalent" do + let(:spec) { double(:spec, :source => double(:other_source)) } + + it "should return false" do + expect(subject.can_lock?(spec)).to be_falsey + end + end + end + + describe "#include?" do + context "when the passed source is equivalent" do + let(:source) { subject } + + it "should return true" do + expect(subject).to include(source) + end + end + + context "when the passed source is not equivalent" do + let(:source) { double(:source) } + + it "should return false" do + expect(subject).to_not include(source) + end + end + end +end diff --git a/spec/bundler/bundler/spec_set_spec.rb b/spec/bundler/bundler/spec_set_spec.rb new file mode 100644 index 00000000000000..6fedd38b50fc4f --- /dev/null +++ b/spec/bundler/bundler/spec_set_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::SpecSet do + let(:specs) do + [ + build_spec("a", "1.0"), + build_spec("b", "1.0"), + build_spec("c", "1.1") do |s| + s.dep "a", "< 2.0" + s.dep "e", "> 0" + end, + build_spec("d", "2.0") do |s| + s.dep "a", "1.0" + s.dep "c", "~> 1.0" + end, + build_spec("e", "1.0.0.pre.1"), + ].flatten + end + + subject { described_class.new(specs) } + + context "enumerable methods" do + it "has a length" do + expect(subject.length).to eq(5) + end + + it "has a size" do + expect(subject.size).to eq(5) + end + end + + describe "#find_by_name_and_platform" do + let(:platform) { Gem::Platform.new("universal-darwin-64") } + let(:platform_spec) { build_spec("b", "2.0", platform).first } + let(:specs) do + [ + build_spec("a", "1.0"), + platform_spec, + ].flatten + end + + it "finds spec with given name and platform" do + spec = described_class.new(specs).find_by_name_and_platform("b", platform) + expect(spec).to eq platform_spec + end + end + + describe "#merge" do + let(:other_specs) do + [ + build_spec("f", "1.0"), + build_spec("g", "2.0"), + ].flatten + end + + let(:other_spec_set) { described_class.new(other_specs) } + + it "merges the items in each gemspec" do + new_spec_set = subject.merge(other_spec_set) + specs = new_spec_set.to_a.map(&:full_name) + expect(specs).to include("a-1.0") + expect(specs).to include("f-1.0") + end + end + + describe "#to_a" do + it "returns the specs in order" do + expect(subject.to_a.map(&:full_name)).to eq %w[ + a-1.0 + b-1.0 + e-1.0.0.pre.1 + c-1.1 + d-2.0 + ] + end + end +end diff --git a/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb b/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb new file mode 100644 index 00000000000000..56606a830f766e --- /dev/null +++ b/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require "bundler/ssl_certs/certificate_manager" + +RSpec.describe Bundler::SSLCerts::CertificateManager do + let(:rubygems_path) { root } + let(:stub_cert) { File.join(root.to_s, "lib", "rubygems", "ssl_certs", "rubygems.org", "ssl-cert.pem") } + let(:rubygems_certs_dir) { File.join(root.to_s, "lib", "rubygems", "ssl_certs", "rubygems.org") } + + subject { described_class.new(rubygems_path) } + + # Pretend bundler root is rubygems root + before do + # Backing up rubygems ceriticates + FileUtils.mv(rubygems_certs_dir, rubygems_certs_dir + ".back") if ruby_core? + + FileUtils.mkdir_p(rubygems_certs_dir) + FileUtils.touch(stub_cert) + end + + after do + FileUtils.rm_rf(rubygems_certs_dir) + + # Restore rubygems certificates + FileUtils.mv(rubygems_certs_dir + ".back", rubygems_certs_dir) if ruby_core? + end + + describe "#update_from" do + let(:cert_manager) { double(:cert_manager) } + + before { allow(described_class).to receive(:new).with(rubygems_path).and_return(cert_manager) } + + it "should update the certs through a new certificate manager" do + allow(cert_manager).to receive(:update!) + expect(described_class.update_from!(rubygems_path)).to be_nil + end + end + + describe "#initialize" do + it "should set bundler_cert_path as path of the subdir with bundler ssl certs" do + expect(subject.bundler_cert_path).to eq(File.join(root, "lib/bundler/ssl_certs")) + end + + it "should set bundler_certs as the paths of the bundler ssl certs" do + expect(subject.bundler_certs).to include(File.join(root, "lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem")) + expect(subject.bundler_certs).to include(File.join(root, "lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem")) + end + + context "when rubygems_path is not nil" do + it "should set rubygems_certs" do + expect(subject.rubygems_certs).to include(File.join(root, "lib", "rubygems", "ssl_certs", "rubygems.org", "ssl-cert.pem")) + end + end + end + + describe "#up_to_date?" do + context "when bundler certs and rubygems certs are the same" do + before do + bundler_certs = Dir[File.join(root.to_s, "lib", "bundler", "ssl_certs", "**", "*.pem")] + FileUtils.rm(stub_cert) + FileUtils.cp(bundler_certs, rubygems_certs_dir) + end + + it "should return true" do + expect(subject).to be_up_to_date + end + end + + context "when bundler certs and rubygems certs are not the same" do + it "should return false" do + expect(subject).to_not be_up_to_date + end + end + end + + describe "#update!" do + context "when certificate manager is not up to date" do + before do + allow(subject).to receive(:up_to_date?).and_return(false) + allow(bundler_fileutils).to receive(:rm) + allow(bundler_fileutils).to receive(:cp) + end + + it "should remove the current bundler certs" do + expect(bundler_fileutils).to receive(:rm).with(subject.bundler_certs) + subject.update! + end + + it "should copy the rubygems certs into bundler certs" do + expect(bundler_fileutils).to receive(:cp).with(subject.rubygems_certs, subject.bundler_cert_path) + subject.update! + end + + it "should return nil" do + expect(subject.update!).to be_nil + end + end + + context "when certificate manager is up to date" do + before { allow(subject).to receive(:up_to_date?).and_return(true) } + + it "should return nil" do + expect(subject.update!).to be_nil + end + end + end + + describe "#connect_to" do + let(:host) { "http://www.host.com" } + let(:http) { Net::HTTP.new(host, 443) } + let(:cert_store) { OpenSSL::X509::Store.new } + let(:http_header_response) { double(:http_header_response) } + + before do + allow(Net::HTTP).to receive(:new).with(host, 443).and_return(http) + allow(OpenSSL::X509::Store).to receive(:new).and_return(cert_store) + allow(http).to receive(:head).with("/").and_return(http_header_response) + end + + it "should use ssl for the http request" do + expect(http).to receive(:use_ssl=).with(true) + subject.connect_to(host) + end + + it "use verify peer mode" do + expect(http).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) + subject.connect_to(host) + end + + it "set its cert store as a OpenSSL::X509::Store populated with bundler certs" do + expect(cert_store).to receive(:add_file).at_least(:once) + expect(http).to receive(:cert_store=).with(cert_store) + subject.connect_to(host) + end + + it "return the headers of the request response" do + expect(subject.connect_to(host)).to eq(http_header_response) + end + end +end diff --git a/spec/bundler/bundler/stub_specification_spec.rb b/spec/bundler/bundler/stub_specification_spec.rb new file mode 100644 index 00000000000000..5521d837693c06 --- /dev/null +++ b/spec/bundler/bundler/stub_specification_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::StubSpecification do + let(:gemspec) do + Gem::Specification.new do |s| + s.name = "gemname" + s.version = "1.0.0" + s.loaded_from = __FILE__ + end + end + + let(:with_bundler_stub_spec) do + described_class.from_stub(gemspec) + end + + if Bundler.rubygems.provides?(">= 2.1") + describe "#from_stub" do + it "returns the same stub if already a Bundler::StubSpecification" do + stub = described_class.from_stub(with_bundler_stub_spec) + expect(stub).to be(with_bundler_stub_spec) + end + end + end +end diff --git a/spec/bundler/bundler/ui/shell_spec.rb b/spec/bundler/bundler/ui/shell_spec.rb new file mode 100644 index 00000000000000..951a446aff5860 --- /dev/null +++ b/spec/bundler/bundler/ui/shell_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::UI::Shell do + subject { described_class.new } + + before { subject.level = "debug" } + + describe "#info" do + before { subject.level = "info" } + it "prints to stdout" do + expect { subject.info("info") }.to output("info\n").to_stdout + end + end + + describe "#confirm" do + before { subject.level = "confirm" } + it "prints to stdout" do + expect { subject.confirm("confirm") }.to output("confirm\n").to_stdout + end + end + + describe "#warn" do + before { subject.level = "warn" } + it "prints to stdout", :bundler => "< 2" do + expect { subject.warn("warning") }.to output("warning\n").to_stdout + end + + it "prints to stderr", :bundler => "2" do + expect { subject.warn("warning") }.to output("warning\n").to_stderr + end + + context "when stderr flag is enabled" do + before { Bundler.settings.temporary(:error_on_stderr => true) } + it "prints to stderr" do + expect { subject.warn("warning!") }.to output("warning!\n").to_stderr + end + end + end + + describe "#debug" do + it "prints to stdout" do + expect { subject.debug("debug") }.to output("debug\n").to_stdout + end + end + + describe "#error" do + before { subject.level = "error" } + + it "prints to stdout", :bundler => "< 2" do + expect { subject.error("error!!!") }.to output("error!!!\n").to_stdout + end + + it "prints to stderr", :bundler => "2" do + expect { subject.error("error!!!") }.to output("error!!!\n").to_stderr + end + + context "when stderr flag is enabled" do + before { Bundler.settings.temporary(:error_on_stderr => true) } + it "prints to stderr" do + expect { subject.error("error!!!") }.to output("error!!!\n").to_stderr + end + + context "when stderr is closed" do + it "doesn't report anything" do + output = capture(:stderr, :closed => true) do + subject.error("Something went wrong") + end + expect(output).to_not eq("Something went wrong\n") + end + end + end + end +end diff --git a/spec/bundler/bundler/ui_spec.rb b/spec/bundler/bundler/ui_spec.rb new file mode 100644 index 00000000000000..6ef8729277ac6f --- /dev/null +++ b/spec/bundler/bundler/ui_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::UI do + describe Bundler::UI::Silent do + it "has the same instance methods as Shell", :ruby => ">= 1.9" do + shell = Bundler::UI::Shell + methods = proc do |cls| + cls.instance_methods.map do |i| + m = shell.instance_method(i) + [i, m.parameters] + end.sort_by(&:first) + end + expect(methods.call(described_class)).to eq(methods.call(shell)) + end + + it "has the same instance class as Shell", :ruby => ">= 1.9" do + shell = Bundler::UI::Shell + methods = proc do |cls| + cls.methods.map do |i| + m = shell.method(i) + [i, m.parameters] + end.sort_by(&:first) + end + expect(methods.call(described_class)).to eq(methods.call(shell)) + end + end + + describe Bundler::UI::Shell do + let(:options) { {} } + subject { described_class.new(options) } + describe "debug?" do + it "returns a boolean" do + subject.level = :debug + expect(subject.debug?).to eq(true) + + subject.level = :error + expect(subject.debug?).to eq(false) + end + end + end +end diff --git a/spec/bundler/bundler/uri_credentials_filter_spec.rb b/spec/bundler/bundler/uri_credentials_filter_spec.rb new file mode 100644 index 00000000000000..fe52d16306c126 --- /dev/null +++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::URICredentialsFilter do + subject { described_class } + + describe "#credential_filtered_uri" do + shared_examples_for "original type of uri is maintained" do + it "maintains same type for return value as uri input type" do + expect(subject.credential_filtered_uri(uri)).to be_kind_of(uri.class) + end + end + + shared_examples_for "sensitive credentials in uri are filtered out" do + context "authentication using oauth credentials" do + context "specified via 'x-oauth-basic'" do + let(:credentials) { "oauth_token:x-oauth-basic@" } + + it "returns the uri without the oauth token" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(URI("https://x-oauth-basic@github.com/company/private-repo").to_s) + end + + it_behaves_like "original type of uri is maintained" + end + + context "specified via 'x'" do + let(:credentials) { "oauth_token:x@" } + + it "returns the uri without the oauth token" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(URI("https://x@github.com/company/private-repo").to_s) + end + + it_behaves_like "original type of uri is maintained" + end + end + + context "authentication using login credentials" do + let(:credentials) { "username1:hunter3@" } + + it "returns the uri without the password" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(URI("https://username1@github.com/company/private-repo").to_s) + end + + it_behaves_like "original type of uri is maintained" + end + + context "authentication without credentials" do + let(:credentials) { "" } + + it "returns the same uri" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s) + end + + it_behaves_like "original type of uri is maintained" + end + end + + context "uri is a uri object" do + let(:uri) { URI("https://#{credentials}github.com/company/private-repo") } + + it_behaves_like "sensitive credentials in uri are filtered out" + end + + context "uri is a uri string" do + let(:uri) { "https://#{credentials}github.com/company/private-repo" } + + it_behaves_like "sensitive credentials in uri are filtered out" + end + + context "uri is a non-uri format string (ex. path)" do + let(:uri) { "/path/to/repo" } + + it "returns the same uri" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s) + end + + it_behaves_like "original type of uri is maintained" + end + + context "uri is nil" do + let(:uri) { nil } + + it "returns nil" do + expect(subject.credential_filtered_uri(uri)).to be_nil + end + + it_behaves_like "original type of uri is maintained" + end + end + + describe "#credential_filtered_string" do + let(:str_to_filter) { "This is a git message containing a uri #{uri}!" } + let(:credentials) { "" } + let(:uri) { URI("https://#{credentials}github.com/company/private-repo") } + + context "with a uri that contains credentials" do + let(:credentials) { "oauth_token:x-oauth-basic@" } + + it "returns the string without the sensitive credentials" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to eq( + "This is a git message containing a uri https://x-oauth-basic@github.com/company/private-repo!" + ) + end + end + + context "that does not contains credentials" do + it "returns the same string" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter) + end + end + + context "string to filter is nil" do + let(:str_to_filter) { nil } + + it "returns nil" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to be_nil + end + end + + context "uri to filter out is nil" do + let(:uri) { nil } + + it "returns the same string" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter) + end + end + end +end diff --git a/spec/bundler/bundler/vendored_persistent_spec.rb b/spec/bundler/bundler/vendored_persistent_spec.rb new file mode 100644 index 00000000000000..338431c4a674d3 --- /dev/null +++ b/spec/bundler/bundler/vendored_persistent_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "spec_helper" +require "bundler/vendored_persistent" + +RSpec.describe Bundler::PersistentHTTP do + describe "#warn_old_tls_version_rubygems_connection" do + let(:uri) { "https://index.rubygems.org" } + let(:connection) { instance_double(subject.http_class) } + let(:tls_version) { "TLSv1.2" } + let(:socket) { double("Socket") } + let(:socket_io) { double("SocketIO") } + + before do + allow(connection).to receive(:use_ssl?).and_return(!tls_version.nil?) + allow(socket).to receive(:io).and_return(socket_io) + connection.instance_variable_set(:@socket, socket) + + if tls_version + allow(socket_io).to receive(:ssl_version).and_return(tls_version) + end + end + + shared_examples_for "does not warn" do + it "does not warn" do + allow(Bundler.ui).to receive(:warn).never + subject.warn_old_tls_version_rubygems_connection(URI(uri), connection) + end + end + + shared_examples_for "does warn" do |*expected| + it "warns" do + expect(Bundler.ui).to receive(:warn).with(*expected) + subject.warn_old_tls_version_rubygems_connection(URI(uri), connection) + end + end + + context "an HTTPS uri with TLSv1.2" do + include_examples "does not warn" + end + + context "without SSL" do + let(:tls_version) { nil } + + include_examples "does not warn" + end + + context "without a socket" do + let(:socket) { nil } + + include_examples "does not warn" + end + + context "with a different TLD" do + let(:uri) { "https://foo.bar" } + include_examples "does not warn" + + context "and an outdated TLS version" do + let(:tls_version) { "TLSv1" } + include_examples "does not warn" + end + end + + context "with a nonsense TLS version" do + let(:tls_version) { "BlahBlah2.0Blah" } + include_examples "does not warn" + end + + context "with an outdated TLS version" do + let(:tls_version) { "TLSv1" } + include_examples "does warn", + "Warning: Your Ruby version is compiled against a copy of OpenSSL that is very old. " \ + "Starting in January 2018, RubyGems.org will refuse connection requests from these very old versions of OpenSSL. " \ + "If you will need to continue installing gems after January 2018, please follow this guide to upgrade: http://ruby.to/tls-outdated.", + :wrap => true + end + end +end diff --git a/spec/bundler/bundler/version_ranges_spec.rb b/spec/bundler/bundler/version_ranges_spec.rb new file mode 100644 index 00000000000000..ccbb9285d5af7f --- /dev/null +++ b/spec/bundler/bundler/version_ranges_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "bundler/version_ranges" + +RSpec.describe Bundler::VersionRanges do + describe ".empty?" do + shared_examples_for "empty?" do |exp, *req| + it "returns #{exp} for #{req}" do + r = Gem::Requirement.new(*req) + ranges = described_class.for(r) + expect(described_class.empty?(*ranges)).to eq(exp), "expected `#{r}` #{exp ? "" : "not "}to be empty" + end + end + + include_examples "empty?", false + include_examples "empty?", false, "!= 1" + include_examples "empty?", false, "!= 1", "= 2" + include_examples "empty?", false, "!= 1", "> 1" + include_examples "empty?", false, "!= 1", ">= 1" + include_examples "empty?", false, "= 1", ">= 0.1", "<= 1.1" + include_examples "empty?", false, "= 1", ">= 1", "<= 1" + include_examples "empty?", false, "= 1", "~> 1" + include_examples "empty?", false, ">= 0.z", "= 0" + include_examples "empty?", false, ">= 0" + include_examples "empty?", false, ">= 1.0.0", "< 2.0.0" + include_examples "empty?", false, "~> 1" + include_examples "empty?", false, "~> 2.0", "~> 2.1" + include_examples "empty?", true, "!= 1", "< 2", "> 2" + include_examples "empty?", true, "!= 1", "<= 1", ">= 1" + include_examples "empty?", true, "< 2", "> 2" + include_examples "empty?", true, "= 1", "!= 1" + include_examples "empty?", true, "= 1", "= 2" + include_examples "empty?", true, "= 1", "~> 2" + include_examples "empty?", true, ">= 0", "<= 0.a" + include_examples "empty?", true, "~> 2.0", "~> 3" + end +end diff --git a/spec/bundler/bundler/worker_spec.rb b/spec/bundler/bundler/worker_spec.rb new file mode 100644 index 00000000000000..2e5642709d0e09 --- /dev/null +++ b/spec/bundler/bundler/worker_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "bundler/worker" + +RSpec.describe Bundler::Worker do + let(:size) { 5 } + let(:name) { "Spec Worker" } + let(:function) { proc {|object, worker_number| [object, worker_number] } } + subject { described_class.new(size, name, function) } + + after { subject.stop } + + describe "#initialize" do + context "when Thread.start raises ThreadError" do + it "raises when no threads can be created" do + allow(Thread).to receive(:start).and_raise(ThreadError, "error creating thread") + + expect { subject.enq "a" }.to raise_error(Bundler::ThreadCreationError, "Failed to create threads for the Spec Worker worker: error creating thread") + end + end + end +end diff --git a/spec/bundler/bundler/yaml_serializer_spec.rb b/spec/bundler/bundler/yaml_serializer_spec.rb new file mode 100644 index 00000000000000..1241c74bbfd535 --- /dev/null +++ b/spec/bundler/bundler/yaml_serializer_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require "bundler/yaml_serializer" + +RSpec.describe Bundler::YAMLSerializer do + subject(:serializer) { Bundler::YAMLSerializer } + + describe "#dump" do + it "works for simple hash" do + hash = { "Q" => "Where does Thursday come before Wednesday? In the dictionary. :P" } + + expected = strip_whitespace <<-YAML + --- + Q: "Where does Thursday come before Wednesday? In the dictionary. :P" + YAML + + expect(serializer.dump(hash)).to eq(expected) + end + + it "handles nested hash" do + hash = { + "nice-one" => { + "read_ahead" => "All generalizations are false, including this one", + }, + } + + expected = strip_whitespace <<-YAML + --- + nice-one: + read_ahead: "All generalizations are false, including this one" + YAML + + expect(serializer.dump(hash)).to eq(expected) + end + + it "array inside an hash" do + hash = { + "nested_hash" => { + "contains_array" => [ + "Jack and Jill went up the hill", + "To fetch a pail of water.", + "Jack fell down and broke his crown,", + "And Jill came tumbling after.", + ], + }, + } + + expected = strip_whitespace <<-YAML + --- + nested_hash: + contains_array: + - "Jack and Jill went up the hill" + - "To fetch a pail of water." + - "Jack fell down and broke his crown," + - "And Jill came tumbling after." + YAML + + expect(serializer.dump(hash)).to eq(expected) + end + end + + describe "#load" do + it "works for simple hash" do + yaml = strip_whitespace <<-YAML + --- + Jon: "Air is free dude!" + Jack: "Yes.. until you buy a bag of chips!" + YAML + + hash = { + "Jon" => "Air is free dude!", + "Jack" => "Yes.. until you buy a bag of chips!", + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "works for nested hash" do + yaml = strip_whitespace <<-YAML + --- + baa: + baa: "black sheep" + have: "you any wool?" + yes: "merry have I" + three: "bags full" + YAML + + hash = { + "baa" => { + "baa" => "black sheep", + "have" => "you any wool?", + "yes" => "merry have I", + }, + "three" => "bags full", + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "handles colon in key/value" do + yaml = strip_whitespace <<-YAML + BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/: http://rubygems-mirror.org + YAML + + expect(serializer.load(yaml)).to eq("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://rubygems-mirror.org") + end + + it "handles arrays inside hashes" do + yaml = strip_whitespace <<-YAML + --- + nested_hash: + contains_array: + - "Why shouldn't you write with a broken pencil?" + - "Because it's pointless!" + YAML + + hash = { + "nested_hash" => { + "contains_array" => [ + "Why shouldn't you write with a broken pencil?", + "Because it's pointless!", + ], + }, + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "handles windows-style CRLF line endings" do + yaml = strip_whitespace(<<-YAML).gsub("\n", "\r\n") + --- + nested_hash: + contains_array: + - "Why shouldn't you write with a broken pencil?" + - "Because it's pointless!" + - oh so silly + YAML + + hash = { + "nested_hash" => { + "contains_array" => [ + "Why shouldn't you write with a broken pencil?", + "Because it's pointless!", + "oh so silly", + ], + }, + } + + expect(serializer.load(yaml)).to eq(hash) + end + end + + describe "against yaml lib" do + let(:hash) do + { + "a_joke" => { + "my-stand" => "I can totally keep secrets", + "but" => "The people I tell them to can't :P", + "wouldn't it be funny if this string were empty?" => "", + }, + "more" => { + "first" => [ + "Can a kangaroo jump higher than a house?", + "Of course, a house doesn't jump at all.", + ], + "second" => [ + "What did the sea say to the sand?", + "Nothing, it simply waved.", + ], + "array with empty string" => [""], + }, + "sales" => { + "item" => "A Parachute", + "description" => "Only used once, never opened.", + }, + "one-more" => "I'd tell you a chemistry joke but I know I wouldn't get a reaction.", + } + end + + context "#load" do + it "retrieves the original hash" do + require "yaml" + expect(serializer.load(YAML.dump(hash))).to eq(hash) + end + end + + context "#dump" do + it "retrieves the original hash" do + require "yaml" + expect(YAML.load(serializer.dump(hash))).to eq(hash) + end + end + end +end diff --git a/spec/bundler/cache/cache_path_spec.rb b/spec/bundler/cache/cache_path_spec.rb new file mode 100644 index 00000000000000..69d380996424da --- /dev/null +++ b/spec/bundler/cache/cache_path_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.describe "bundle package" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "with --cache-path" do + it "caches gems at given path" do + bundle :package, "cache-path" => "vendor/cache-foo" + expect(bundled_app("vendor/cache-foo/rack-1.0.0.gem")).to exist + end + end + + context "with config cache_path" do + it "caches gems at given path" do + bundle "config cache_path vendor/cache-foo" + bundle :package + expect(bundled_app("vendor/cache-foo/rack-1.0.0.gem")).to exist + end + end + + context "with absolute --cache-path" do + it "caches gems at given path" do + bundle :package, "cache-path" => "/tmp/cache-foo" + expect(bundled_app("/tmp/cache-foo/rack-1.0.0.gem")).to exist + end + end +end diff --git a/spec/bundler/cache/gems_spec.rb b/spec/bundler/cache/gems_spec.rb new file mode 100644 index 00000000000000..4a0b953830b1ad --- /dev/null +++ b/spec/bundler/cache/gems_spec.rb @@ -0,0 +1,304 @@ +# frozen_string_literal: true + +RSpec.describe "bundle cache" do + shared_examples_for "when there are only gemsources" do + before :each do + gemfile <<-G + gem 'rack' + G + + system_gems "rack-1.0.0", :path => :bundle_path + bundle! :cache + end + + it "copies the .gem file to vendor/cache" do + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "uses the cache as a source when installing gems" do + build_gem "omg", :path => bundled_app("vendor/cache") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "omg" + G + + expect(the_bundle).to include_gems "omg 1.0.0" + end + + it "uses the cache as a source when installing gems with --local" do + system_gems [], :path => :bundle_path + bundle "install --local" + + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "does not reinstall gems from the cache if they exist on the system" do + build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + install_gemfile <<-G + gem "rack" + G + + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "does not reinstall gems from the cache if they exist in the bundle" do + system_gems "rack-1.0.0", :path => :bundle_path + + gemfile <<-G + gem "rack" + G + + build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + bundle! :install, :local => true + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "creates a lockfile" do + cache_gems "rack-1.0.0" + + gemfile <<-G + gem "rack" + G + + bundle "cache" + + expect(bundled_app("Gemfile.lock")).to exist + end + end + + context "using system gems" do + before { bundle! "config path.system true" } + it_behaves_like "when there are only gemsources" + end + + context "installing into a local path" do + before { bundle! "config path ./.bundle" } + it_behaves_like "when there are only gemsources" + end + + describe "when there is a built-in gem", :ruby => "2.0" do + before :each do + build_repo2 do + build_gem "builtin_gem", "1.0.2" + end + + build_gem "builtin_gem", "1.0.2", :to_system => true do |s| + s.summary = "This builtin_gem is bundled with Ruby" + end + + FileUtils.rm("#{system_gem_path}/cache/builtin_gem-1.0.2.gem") + end + + it "uses builtin gems when installing to system gems" do + bundle! "config path.system true" + install_gemfile %(gem 'builtin_gem', '1.0.2') + expect(the_bundle).to include_gems("builtin_gem 1.0.2") + end + + it "caches remote and builtin gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'builtin_gem', '1.0.2' + gem 'rack', '1.0.0' + G + + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/builtin_gem-1.0.2.gem")).to exist + end + + it "doesn't make remote request after caching the gem" do + build_gem "builtin_gem_2", "1.0.2", :path => bundled_app("vendor/cache") do |s| + s.summary = "This builtin_gem is bundled with Ruby" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'builtin_gem_2', '1.0.2' + G + + bundle "install --local" + expect(the_bundle).to include_gems("builtin_gem_2 1.0.2") + end + + it "errors if the builtin gem isn't available to cache" do + bundle! "config path.system true" + + install_gemfile <<-G + gem 'builtin_gem', '1.0.2' + G + + bundle :cache + expect(exitstatus).to_not eq(0) if exitstatus + expect(out).to include("builtin_gem-1.0.2 is built in to Ruby, and can't be cached") + end + end + + describe "when there are also git sources" do + before do + build_git "foo" + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + gem 'rack' + G + end + + it "still works" do + bundle :cache + + system_gems [] + bundle "install --local" + + expect(the_bundle).to include_gems("rack 1.0.0", "foo 1.0") + end + + it "should not explode if the lockfile is not present" do + FileUtils.rm(bundled_app("Gemfile.lock")) + + bundle :cache + + expect(bundled_app("Gemfile.lock")).to exist + end + end + + describe "when previously cached" do + before :each do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + gem "actionpack" + G + bundle :cache + expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("actionpack-2.3.2")).to exist + expect(cached_gem("activesupport-2.3.2")).to exist + end + + it "re-caches during install" do + cached_gem("rack-1.0.0").rmtree + bundle :install + expect(out).to include("Updating files in vendor/cache") + expect(cached_gem("rack-1.0.0")).to exist + end + + it "adds and removes when gems are updated" do + update_repo2 + bundle "update", :all => bundle_update_requires_all? + expect(cached_gem("rack-1.2")).to exist + expect(cached_gem("rack-1.0.0")).not_to exist + end + + it "adds new gems and dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails" + G + expect(cached_gem("rails-2.3.2")).to exist + expect(cached_gem("activerecord-2.3.2")).to exist + end + + it "removes .gems for removed gems and dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("actionpack-2.3.2")).not_to exist + expect(cached_gem("activesupport-2.3.2")).not_to exist + end + + it "removes .gems when gem changes to git source" do + build_git "rack" + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", :git => "#{lib_path("rack-1.0")}" + gem "actionpack" + G + expect(cached_gem("rack-1.0.0")).not_to exist + expect(cached_gem("actionpack-2.3.2")).to exist + expect(cached_gem("activesupport-2.3.2")).to exist + end + + it "doesn't remove gems that are for another platform" do + simulate_platform "java" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + bundle :cache + expect(cached_gem("platform_specific-1.0-java")).to exist + end + + simulate_new_machine + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + expect(cached_gem("platform_specific-1.0-#{Bundler.local_platform}")).to exist + expect(cached_gem("platform_specific-1.0-java")).to exist + end + + it "doesn't remove gems with mismatched :rubygems_version or :date" do + cached_gem("rack-1.0.0").rmtree + build_gem "rack", "1.0.0", + :path => bundled_app("vendor/cache"), + :rubygems_version => "1.3.2" + simulate_new_machine + + bundle :install + expect(cached_gem("rack-1.0.0")).to exist + end + + it "handles directories and non .gem files in the cache" do + bundled_app("vendor/cache/foo").mkdir + File.open(bundled_app("vendor/cache/bar"), "w") {|f| f.write("not a gem") } + bundle :cache + end + + it "does not say that it is removing gems when it isn't actually doing so" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "cache" + bundle "install" + expect(out).not_to match(/removing/i) + end + + it "does not warn about all if it doesn't have any git/path dependency" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "cache" + expect(out).not_to match(/\-\-all/) + end + + it "should install gems with the name bundler in them (that aren't bundler)" do + build_gem "foo-bundler", "1.0", + :path => bundled_app("vendor/cache") + + install_gemfile <<-G + gem "foo-bundler" + G + + expect(the_bundle).to include_gems "foo-bundler 1.0" + end + end +end diff --git a/spec/bundler/cache/git_spec.rb b/spec/bundler/cache/git_spec.rb new file mode 100644 index 00000000000000..33387dbbb2100e --- /dev/null +++ b/spec/bundler/cache/git_spec.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +RSpec.describe "git base name" do + it "base_name should strip private repo uris" do + source = Bundler::Source::Git.new("uri" => "git@github.com:bundler.git") + expect(source.send(:base_name)).to eq("bundler") + end + + it "base_name should strip network share paths" do + source = Bundler::Source::Git.new("uri" => "//MachineName/ShareFolder") + expect(source.send(:base_name)).to eq("ShareFolder") + end +end + +%w[cache package].each do |cmd| + RSpec.describe "bundle #{cmd} with git" do + it "copies repository to vendor cache and uses it" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "copies repository to vendor cache and uses it even when installed with bundle --path" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "install --path vendor/bundle" + bundle "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "runs twice without exploding" do + build_git "foo" + + install_gemfile! <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle! "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + bundle! "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + + expect(last_command.stdout).to include "Updating files in vendor/cache" + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "tracks updates" do + git = build_git "foo" + old_ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + + update_git "foo" do |s| + s.write "lib/foo.rb", "puts :CACHE" + end + + ref = git.ref_for("master", 11) + expect(ref).not_to eq(old_ref) + + bundle! "update", :all => bundle_update_requires_all? + bundle! "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist + + FileUtils.rm_rf lib_path("foo-1.0") + run! "require 'foo'" + expect(out).to eq("CACHE") + end + + it "tracks updates when specifying the gem" do + git = build_git "foo" + old_ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle! cmd, forgotten_command_line_options([:all, :cache_all] => true) + + update_git "foo" do |s| + s.write "lib/foo.rb", "puts :CACHE" + end + + ref = git.ref_for("master", 11) + expect(ref).not_to eq(old_ref) + + bundle "update foo" + + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist + + FileUtils.rm_rf lib_path("foo-1.0") + run "require 'foo'" + expect(out).to eq("CACHE") + end + + it "uses the local repository to generate the cache" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + gemfile <<-G + gem "foo", :git => '#{lib_path("foo-invalid")}', :branch => :master + G + + bundle %(config local.foo #{lib_path("foo-1.0")}) + bundle "install" + bundle "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/foo-invalid-#{ref}")).to exist + + # Updating the local still uses the local. + update_git "foo" do |s| + s.write "lib/foo.rb", "puts :LOCAL" + end + + run "require 'foo'" + expect(out).to eq("LOCAL") + end + + it "copies repository to vendor cache, including submodules" do + build_git "submodule", "1.0" + + git = build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + ref = git.ref_for("master", 11) + bundle "#{cmd}", forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}/submodule-1.0")).to exist + expect(the_bundle).to include_gems "has_submodule 1.0" + end + + it "displays warning message when detecting git repo in Gemfile", :bundler => "< 2" do + build_git "foo" + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd}" + + expect(out).to include("Your Gemfile contains path and git dependencies.") + end + + it "does not display warning message if cache_all is set in bundle config" do + build_git "foo" + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + bundle cmd + + expect(out).not_to include("Your Gemfile contains path and git dependencies.") + end + + it "caches pre-evaluated gemspecs" do + git = build_git "foo" + + # Insert a gemspec method that shells out + spec_lines = lib_path("foo-1.0/foo.gemspec").read.split("\n") + spec_lines.insert(-2, "s.description = `echo bob`") + update_git("foo") {|s| s.write "foo.gemspec", spec_lines.join("\n") } + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + + ref = git.ref_for("master", 11) + gemspec = bundled_app("vendor/cache/foo-1.0-#{ref}/foo.gemspec").read + expect(gemspec).to_not match("`echo bob`") + end + end +end diff --git a/spec/bundler/cache/path_spec.rb b/spec/bundler/cache/path_spec.rb new file mode 100644 index 00000000000000..12be2dbcf89af4 --- /dev/null +++ b/spec/bundler/cache/path_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +%w[cache package].each do |cmd| + RSpec.describe "bundle #{cmd} with path" do + it "is no-op when the path is within the bundle" do + build_lib "foo", :path => bundled_app("lib/foo") + + install_gemfile <<-G + gem "foo", :path => '#{bundled_app("lib/foo")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + expect(bundled_app("vendor/cache/foo-1.0")).not_to exist + expect(the_bundle).to include_gems "foo 1.0" + end + + it "copies when the path is outside the bundle " do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + expect(bundled_app("vendor/cache/foo-1.0")).to exist + expect(bundled_app("vendor/cache/foo-1.0/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "copies when the path is outside the bundle and the paths intersect" do + libname = File.basename(Dir.pwd) + "_gem" + libpath = File.join(File.dirname(Dir.pwd), libname) + + build_lib libname, :path => libpath + + install_gemfile <<-G + gem "#{libname}", :path => '#{libpath}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + expect(bundled_app("vendor/cache/#{libname}")).to exist + expect(bundled_app("vendor/cache/#{libname}/.bundlecache")).to be_file + + FileUtils.rm_rf libpath + expect(the_bundle).to include_gems "#{libname} 1.0" + end + + it "updates the path on each cache" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + + build_lib "foo" do |s| + s.write "lib/foo.rb", "puts :CACHE" + end + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/foo-1.0")).to exist + FileUtils.rm_rf lib_path("foo-1.0") + + run "require 'foo'" + expect(out).to eq("CACHE") + end + + it "removes stale entries cache" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + + install_gemfile <<-G + gem "bar", :path => '#{lib_path("bar-1.0")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + expect(bundled_app("vendor/cache/bar-1.0")).not_to exist + end + + it "raises a warning without --all", :bundler => "< 3" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle cmd + expect(out).to match(/please pass the \-\-all flag/) + expect(bundled_app("vendor/cache/foo-1.0")).not_to exist + end + + it "stores the given flag" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + build_lib "bar" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + gem "bar", :path => '#{lib_path("bar-1.0")}' + G + + bundle cmd + expect(bundled_app("vendor/cache/bar-1.0")).to exist + end + + it "can rewind chosen configuration" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle cmd, forgotten_command_line_options([:all, :cache_all] => true) + build_lib "baz" + + gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + gem "baz", :path => '#{lib_path("baz-1.0")}' + G + + bundle "#{cmd} --no-all" + expect(bundled_app("vendor/cache/baz-1.0")).not_to exist + end + end +end diff --git a/spec/bundler/cache/platform_spec.rb b/spec/bundler/cache/platform_spec.rb new file mode 100644 index 00000000000000..c0622a3c949446 --- /dev/null +++ b/spec/bundler/cache/platform_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe "bundle cache with multiple platforms" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + platforms :mri, :rbx do + gem "rack", "1.0.0" + end + + platforms :jruby do + gem "activesupport", "2.3.5" + end + G + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + activesupport (2.3.5) + + PLATFORMS + ruby + java + + DEPENDENCIES + rack (1.0.0) + activesupport (2.3.5) + G + + cache_gems "rack-1.0.0", "activesupport-2.3.5" + end + + it "ensures that a successful bundle install does not delete gems for other platforms" do + bundle! "install" + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist + end + + it "ensures that a successful bundle update does not delete gems for other platforms" do + bundle! "update", :all => bundle_update_requires_all? + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist + end +end diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb new file mode 100644 index 00000000000000..9f11adbcf80c79 --- /dev/null +++ b/spec/bundler/commands/add_spec.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +RSpec.describe "bundle add" do + before :each do + build_repo2 do + build_gem "foo", "1.1" + build_gem "foo", "2.0" + build_gem "baz", "1.2.3" + build_gem "bar", "0.12.3" + build_gem "cat", "0.12.3.pre" + build_gem "dog", "1.1.3.pre" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "weakling", "~> 0.0.1" + G + end + + context "when no gems are specified" do + it "shows error" do + bundle "add" + + expect(last_command.bundler_err).to include("Please specify gems to add") + end + end + + describe "without version specified" do + it "version requirement becomes ~> major.minor.patch when resolved version is < 1.0" do + bundle "add 'bar'" + expect(bundled_app("Gemfile").read).to match(/gem "bar", "~> 0.12.3"/) + expect(the_bundle).to include_gems "bar 0.12.3" + end + + it "version requirement becomes ~> major.minor when resolved version is > 1.0" do + bundle "add 'baz'" + expect(bundled_app("Gemfile").read).to match(/gem "baz", "~> 1.2"/) + expect(the_bundle).to include_gems "baz 1.2.3" + end + + it "version requirement becomes ~> major.minor.patch.pre when resolved version is < 1.0" do + bundle "add 'cat'" + expect(bundled_app("Gemfile").read).to match(/gem "cat", "~> 0.12.3.pre"/) + expect(the_bundle).to include_gems "cat 0.12.3.pre" + end + + it "version requirement becomes ~> major.minor.pre when resolved version is > 1.0.pre" do + bundle "add 'dog'" + expect(bundled_app("Gemfile").read).to match(/gem "dog", "~> 1.1.pre"/) + expect(the_bundle).to include_gems "dog 1.1.3.pre" + end + end + + describe "with --version" do + it "adds dependency of specified version and runs install" do + bundle "add 'foo' --version='~> 1.0'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 1.0"/) + expect(the_bundle).to include_gems "foo 1.1" + end + + it "adds multiple version constraints when specified" do + requirements = ["< 3.0", "> 1.0"] + bundle "add 'foo' --version='#{requirements.join(", ")}'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", #{Gem::Requirement.new(requirements).as_list.map(&:dump).join(', ')}/) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --group" do + it "adds dependency for the specified group" do + bundle "add 'foo' --group='development'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :group => :development/) + expect(the_bundle).to include_gems "foo 2.0" + end + + it "adds dependency to more than one group" do + bundle "add 'foo' --group='development, test'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :groups => \[:development, :test\]/) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --source" do + it "adds dependency with specified source" do + bundle "add 'foo' --source='file://#{gem_repo2}'" + + expect(bundled_app("Gemfile").read).to match(%r{gem "foo", "~> 2.0", :source => "file:\/\/#{gem_repo2}"}) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --skip-install" do + it "adds gem to Gemfile but is not installed" do + bundle "add foo --skip-install --version=2.0" + + expect(bundled_app("Gemfile").read).to match(/gem "foo", "= 2.0"/) + expect(the_bundle).to_not include_gems "foo 2.0" + end + end + + it "using combination of short form options works like long form" do + bundle "add 'foo' -s='file://#{gem_repo2}' -g='development' -v='~>1.0'" + expect(bundled_app("Gemfile").read).to include %(gem "foo", "~> 1.0", :group => :development, :source => "file://#{gem_repo2}") + expect(the_bundle).to include_gems "foo 1.1" + end + + it "shows error message when version is not formatted correctly" do + bundle "add 'foo' -v='~>1 . 0'" + expect(out).to match("Invalid gem requirement pattern '~>1 . 0'") + end + + it "shows error message when gem cannot be found" do + bundle "add 'werk_it'" + expect(out).to match("Could not find gem 'werk_it' in") + + bundle "add 'werk_it' -s='file://#{gem_repo2}'" + expect(out).to match("Could not find gem 'werk_it' in rubygems repository") + end + + it "shows error message when source cannot be reached" do + bundle "add 'baz' --source='http://badhostasdf'" + expect(out).to include("Could not reach host badhostasdf. Check your network connection and try again.") + + bundle "add 'baz' --source='file://does/not/exist'" + expect(out).to include("Could not fetch specs from file://does/not/exist/") + end + + describe "with --optimistic" do + it "adds optimistic version" do + bundle! "add 'foo' --optimistic" + expect(bundled_app("Gemfile").read).to include %(gem "foo", ">= 2.0") + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --strict option" do + it "adds strict version" do + bundle! "add 'foo' --strict" + expect(bundled_app("Gemfile").read).to include %(gem "foo", "= 2.0") + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with no option" do + it "adds pessimistic version" do + bundle! "add 'foo'" + expect(bundled_app("Gemfile").read).to include %(gem "foo", "~> 2.0") + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --optimistic and --strict" do + it "throws error" do + bundle "add 'foo' --strict --optimistic" + + expect(out).to include("You can not specify `--strict` and `--optimistic` at the same time") + end + end + + context "multiple gems" do + it "adds multiple gems to gemfile" do + bundle! "add bar baz" + + expect(bundled_app("Gemfile").read).to match(/gem "bar", "~> 0.12.3"/) + expect(bundled_app("Gemfile").read).to match(/gem "baz", "~> 1.2"/) + end + + it "throws error if any of the specified gems are present in the gemfile with different version" do + bundle "add weakling bar" + + expect(out).to include("You cannot specify the same gem twice with different version requirements") + expect(out).to include("You specified: weakling (~> 0.0.1) and weakling (>= 0).") + end + end + + describe "when a gem is added which is already specified in Gemfile with version" do + it "shows an error when added with different version requirement" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", "1.0" + G + + bundle "add 'rack' --version=1.1" + + expect(out).to include("You cannot specify the same gem twice with different version requirements") + expect(out).to include("If you want to update the gem version, run `bundle update rack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive") + end + + it "shows error when added without version requirements" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", "1.0" + G + + bundle "add 'rack'" + + expect(out).to include("Gem already added.") + expect(out).to include("You cannot specify the same gem twice with different version requirements") + expect(out).not_to include("If you want to update the gem version, run `bundle update rack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive") + end + end + + describe "when a gem is added which is already specified in Gemfile without version" do + it "shows an error when added with different version requirement" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + + bundle "add 'rack' --version=1.1" + + expect(out).to include("You cannot specify the same gem twice with different version requirements") + expect(out).to include("If you want to update the gem version, run `bundle update rack`.") + expect(out).not_to include("You may also need to change the version requirement specified in the Gemfile if it's too restrictive") + end + end +end diff --git a/spec/bundler/commands/binstubs_spec.rb b/spec/bundler/commands/binstubs_spec.rb new file mode 100644 index 00000000000000..7f2e81c099d377 --- /dev/null +++ b/spec/bundler/commands/binstubs_spec.rb @@ -0,0 +1,480 @@ +# frozen_string_literal: true + +RSpec.describe "bundle binstubs " do + context "when the gem exists in the lockfile" do + it "sets up the binstub" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack" + + expect(bundled_app("bin/rackup")).to exist + end + + it "does not install other binstubs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle "binstubs rails" + + expect(bundled_app("bin/rackup")).not_to exist + expect(bundled_app("bin/rails")).to exist + end + + it "does install multiple binstubs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle "binstubs rails rack" + + expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/rails")).to exist + end + + it "allows installing all binstubs" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle! :binstubs, :all => true + + expect(bundled_app("bin/rails")).to exist + expect(bundled_app("bin/rake")).to exist + end + + it "displays an error when used without any gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs" + expect(exitstatus).to eq(1) if exitstatus + expect(out).to include("`bundle binstubs` needs at least one gem to run.") + end + + it "displays an error when used with --all and gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack", :all => true + expect(last_command).to be_failure + expect(last_command.bundler_err).to include("Cannot specify --all with specific gems") + end + + context "when generating bundle binstub outside bundler" do + it "should abort" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack" + + File.open("bin/bundle", "wb") do |file| + file.print "OMG" + end + + sys_exec "bin/rackup" + + expect(last_command.stderr).to include("was not generated by Bundler") + end + end + + context "the bundle binstub" do + before do + if system_bundler_version == :bundler + system_gems :bundler + elsif system_bundler_version + build_repo4 do + build_gem "bundler", system_bundler_version do |s| + s.executables = "bundle" + s.bindir = "exe" + s.write "exe/bundle", "puts %(system bundler #{system_bundler_version}\\n\#{ARGV.inspect})" + end + end + system_gems "bundler-#{system_bundler_version}", :gem_repo => gem_repo4 + end + build_repo2 do + build_gem "prints_loaded_gems", "1.0" do |s| + s.executables = "print_loaded_gems" + s.bindir = "exe" + s.write "exe/print_loaded_gems", <<-R + specs = Gem.loaded_specs.values.reject {|s| Bundler.rubygems.spec_default_gem?(s) } + puts specs.map(&:full_name).sort.inspect + R + end + end + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "rack" + gem "prints_loaded_gems" + G + bundle! "binstubs bundler rack prints_loaded_gems" + end + + # When environment has a same version of bundler as default gems. + # `system_gems "bundler-x.y.z"` will detect system binstub. + # We need to avoid it by virtual version of bundler. + let(:system_bundler_version) { Gem::Version.new(Bundler::VERSION).bump.to_s } + + context "when system bundler was used" do + # Support master branch of bundler + if ENV["BUNDLER_SPEC_SUB_VERSION"] + let(:system_bundler_version) { Bundler::VERSION } + end + + before do + gemfile <<-G + source "file:///Users/colby/Projects/bundler/tmp/gems/remote2" + gem "rack" + gem "prints_loaded_gems" + G + + lockfile <<-G + GEM + remote: file:///Users/colby/Projects/bundler/tmp/gems/remote2/ + specs: + prints_loaded_gems (1.0) + rack (1.2) + + PLATFORMS + ruby + + DEPENDENCIES + prints_loaded_gems + rack + + BUNDLED WITH + #{system_bundler_version} + G + end + + it "runs bundler" do + sys_exec! "#{bundled_app("bin/bundle")} install" + expect(out).to eq %(system bundler #{system_bundler_version}\n["install"]) + end + end + + context "when BUNDLER_VERSION is set" do + let(:system_bundler_version) { Bundler::VERSION } + + it "runs the correct version of bundler" do + sys_exec "BUNDLER_VERSION='999.999.999' #{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + end + + context "when a lockfile exists with a locked bundler version" do + let(:system_bundler_version) { Bundler::VERSION } + + it "runs the correct version of bundler when the version is newer" do + lockfile lockfile.gsub(system_bundler_version, "999.999.999") + sys_exec "#{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + + it "runs the correct version of bundler when the version is older" do + simulate_bundler_version "55" + lockfile lockfile.gsub(system_bundler_version, "44.0") + sys_exec "#{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (44.0) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '44.0'`") + end + + it "runs the correct version of bundler when the version is a pre-release" do + simulate_bundler_version "55" + lockfile lockfile.gsub(system_bundler_version, "2.12.0.a") + sys_exec "#{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (2.12.0.a) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '2.12.0.a'`") + end + end + + context "when update --bundler is called" do + before { lockfile.gsub(system_bundler_version, "1.1.1") } + + it "calls through to the latest bundler version" do + sys_exec! "#{bundled_app("bin/bundle")} update --bundler" + expect(last_command.stdout).to eq %(system bundler #{system_bundler_version}\n["update", "--bundler"]) + end + + it "calls through to the explicit bundler version" do + sys_exec "#{bundled_app("bin/bundle")} update --bundler=999.999.999" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + end + + context "without a lockfile" do + it "falls back to the latest installed bundler" do + FileUtils.rm bundled_app("Gemfile.lock") + sys_exec! bundled_app("bin/bundle").to_s + expect(out).to eq "system bundler #{system_bundler_version}\n[]" + end + end + + context "using another binstub" do + let(:system_bundler_version) { :bundler } + it "loads all gems" do + sys_exec! bundled_app("bin/print_loaded_gems").to_s + # RG < 2.0.14 didn't have a `Gem::Specification#default_gem?` + # This is dirty detection for old RG versions. + if File.dirname(Bundler.load.specs["bundler"][0].loaded_from) =~ %r{specifications/default} + expect(out).to eq %(["prints_loaded_gems-1.0", "rack-1.2"]) + else + expect(out).to eq %(["bundler-#{Bundler::VERSION}", "prints_loaded_gems-1.0", "rack-1.2"]) + end + end + + context "when requesting a different bundler version" do + before { lockfile lockfile.gsub(Bundler::VERSION, "999.999.999") } + + it "attempts to load that version", :ruby_repo do + sys_exec bundled_app("bin/rackup").to_s + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + end + end + end + + it "installs binstubs from git gems" do + FileUtils.mkdir_p(lib_path("foo/bin")) + FileUtils.touch(lib_path("foo/bin/foo")) + build_git "foo", "1.0", :path => lib_path("foo") do |s| + s.executables = %w[foo] + end + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}" + G + + bundle "binstubs foo" + + expect(bundled_app("bin/foo")).to exist + end + + it "installs binstubs from path gems" do + FileUtils.mkdir_p(lib_path("foo/bin")) + FileUtils.touch(lib_path("foo/bin/foo")) + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.executables = %w[foo] + end + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle "binstubs foo" + + expect(bundled_app("bin/foo")).to exist + end + + it "sets correct permissions for binstubs" do + with_umask(0o002) do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack" + binary = bundled_app("bin/rackup") + expect(File.stat(binary).mode.to_s(8)).to eq("100775") + end + end + + context "when using --shebang" do + it "sets the specified shebang for the the binstub" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack --shebang jruby" + + expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env jruby\n") + end + end + end + + context "when the gem doesn't exist" do + it "displays an error with correct status" do + install_gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle "binstubs doesnt_exist" + + expect(exitstatus).to eq(7) if exitstatus + expect(out).to include("Could not find gem 'doesnt_exist'.") + end + end + + context "--path" do + it "sets the binstubs dir" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack --path exec" + + expect(bundled_app("exec/rackup")).to exist + end + + it "setting is saved for bundle install", :bundler => "< 3" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle! "binstubs rack", forgotten_command_line_options([:path, :bin] => "exec") + bundle! :install + + expect(bundled_app("exec/rails")).to exist + end + end + + context "with --standalone option" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "generates a standalone binstub" do + bundle! "binstubs rack --standalone" + expect(bundled_app("bin/rackup")).to exist + end + + it "generates a binstub that does not depend on rubygems or bundler" do + bundle! "binstubs rack --standalone" + expect(File.read(bundled_app("bin/rackup"))).to_not include("Gem.bin_path") + end + + context "when specified --path option" do + it "generates a standalone binstub at the given path" do + bundle! "binstubs rack --standalone --path foo" + expect(bundled_app("foo/rackup")).to exist + end + end + end + + context "when the bin already exists" do + it "doesn't overwrite and warns" do + FileUtils.mkdir_p(bundled_app("bin")) + File.open(bundled_app("bin/rackup"), "wb") do |file| + file.print "OMG" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack" + + expect(bundled_app("bin/rackup")).to exist + expect(File.read(bundled_app("bin/rackup"))).to eq("OMG") + expect(out).to include("Skipped rackup") + expect(out).to include("overwrite skipped stubs, use --force") + end + + context "when using --force" do + it "overwrites the binstub" do + FileUtils.mkdir_p(bundled_app("bin")) + File.open(bundled_app("bin/rackup"), "wb") do |file| + file.print "OMG" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack --force" + + expect(bundled_app("bin/rackup")).to exist + expect(File.read(bundled_app("bin/rackup"))).not_to eq("OMG") + end + end + end + + context "when the gem has no bins" do + it "suggests child gems if they have bins" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + G + + bundle "binstubs rack-obama" + expect(out).to include("rack-obama has no executables") + expect(out).to include("rack has: rackup") + end + + it "works if child gems don't have bins" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "actionpack" + G + + bundle "binstubs actionpack" + expect(out).to include("no executables for the gem actionpack") + end + + it "works if the gem has development dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "with_development_dependency" + G + + bundle "binstubs with_development_dependency" + expect(out).to include("no executables for the gem with_development_dependency") + end + end + + context "when BUNDLE_INSTALL is specified" do + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config auto_install 1" + bundle "binstubs rack" + expect(out).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does nothing when already up to date" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config auto_install 1" + bundle "binstubs rack", :env => { "BUNDLE_INSTALL" => 1 } + expect(out).not_to include("Installing rack 1.0.0") + end + end +end diff --git a/spec/bundler/commands/check_spec.rb b/spec/bundler/commands/check_spec.rb new file mode 100644 index 00000000000000..890f4b135646fe --- /dev/null +++ b/spec/bundler/commands/check_spec.rb @@ -0,0 +1,354 @@ +# frozen_string_literal: true + +RSpec.describe "bundle check" do + it "returns success when the Gemfile is satisfied" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "works with the --gemfile flag when not in the directory" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + Dir.chdir tmp + bundle "check --gemfile bundled_app/Gemfile" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "creates a Gemfile.lock by default if one does not exist" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + FileUtils.rm("Gemfile.lock") + + bundle "check" + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "does not create a Gemfile.lock if --dry-run was passed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + FileUtils.rm("Gemfile.lock") + + bundle "check --dry-run" + + expect(bundled_app("Gemfile.lock")).not_to exist + end + + it "prints a generic error if the missing gems are unresolvable" do + system_gems ["rails-2.3.2"] + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle :check + expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "prints a generic error if a Gemfile.lock does not exist and a toplevel dependency does not exist" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle :check + expect(exitstatus).to be > 0 if exitstatus + expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "prints a generic message if you changed your lockfile" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rails' + G + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rails_fail' + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "rails_fail" + G + + bundle :check + expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "remembers --without option from install", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + group :foo do + gem "rack" + end + G + + bundle! "install --without foo" + bundle! "check" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "uses the without setting" do + bundle! "config without foo" + install_gemfile! <<-G + source "file://#{gem_repo1}" + group :foo do + gem "rack" + end + G + + bundle! "check" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "ensures that gems are actually installed and not just cached" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :group => :foo + G + + bundle :install, forgotten_command_line_options(:without => "foo") + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "check" + expect(out).to include("* rack (1.0.0)") + expect(exitstatus).to eq(1) if exitstatus + end + + it "ignores missing gems restricted to other platforms" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + platforms :#{not_local_tag} do + gem "activesupport" + end + G + + system_gems "rack-1.0.0", :path => :bundle_path + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + activesupport (2.3.5) + rack (1.0.0) + + PLATFORMS + #{local} + #{not_local} + + DEPENDENCIES + rack + activesupport + G + + bundle :check + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "works with env conditionals" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + env :NOT_GOING_TO_BE_SET do + gem "activesupport" + end + G + + system_gems "rack-1.0.0", :path => :bundle_path + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + activesupport (2.3.5) + rack (1.0.0) + + PLATFORMS + #{local} + #{not_local} + + DEPENDENCIES + rack + activesupport + G + + bundle :check + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "outputs an error when the default Gemfile is not found" do + bundle :check + expect(exitstatus).to eq(10) if exitstatus + expect(out).to include("Could not locate Gemfile") + end + + it "does not output fatal error message" do + bundle :check + expect(exitstatus).to eq(10) if exitstatus + expect(out).not_to include("Unfortunately, a fatal error has occurred. ") + end + + it "should not crash when called multiple times on a new machine" do + gemfile <<-G + gem 'rails', '3.0.0.beta3' + gem 'paperclip', :git => 'git://github.com/thoughtbot/paperclip.git' + G + + simulate_new_machine + bundle "check" + last_out = out + 3.times do + bundle :check + expect(out).to eq(last_out) + end + end + + it "fails when there's no lock file and frozen is set" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "foo" + G + + bundle! "install", forgotten_command_line_options(:deployment => true) + FileUtils.rm(bundled_app("Gemfile.lock")) + + bundle :check + expect(last_command).to be_failure + end + + context "--path", :bundler => "< 3" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + bundle "install --path vendor/bundle" + + FileUtils.rm_rf(bundled_app(".bundle")) + end + + it "returns success" do + bundle! "check --path vendor/bundle" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "should write to .bundle/config", :bundler => "< 3" do + bundle "check --path vendor/bundle" + bundle! "check" + end + end + + context "--path vendor/bundle after installing gems in the default directory" do + it "returns false" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle "check --path vendor/bundle" + expect(exitstatus).to eq(1) if exitstatus + expect(out).to match(/The following gems are missing/) + end + end + + describe "when locked" do + before :each do + system_gems "rack-1.0.0" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + end + + it "returns success when the Gemfile is satisfied" do + bundle :install + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "shows what is missing with the current Gemfile if it is not satisfied" do + simulate_new_machine + bundle :check + expect(out).to match(/The following gems are missing/) + expect(out).to include("* rack (1.0") + end + end + + describe "BUNDLED WITH" do + def lock_with(bundler_version = nil) + lock = <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + L + + if bundler_version + lock += "\n BUNDLED WITH\n #{bundler_version}\n" + end + + lock + end + + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "is not present" do + it "does not change the lock" do + lockfile lock_with(nil) + bundle :check + lockfile_should_be lock_with(nil) + end + end + + context "is newer" do + it "does not change the lock but warns" do + lockfile lock_with(Bundler::VERSION.succ) + bundle! :check + expect(last_command.bundler_err).to include("the running version of Bundler (#{Bundler::VERSION}) is older than the version that created the lockfile (#{Bundler::VERSION.succ})") + lockfile_should_be lock_with(Bundler::VERSION.succ) + end + end + + context "is older" do + it "does not change the lock" do + lockfile lock_with("1.10.1") + bundle :check + lockfile_should_be lock_with("1.10.1") + end + end + end +end diff --git a/spec/bundler/commands/clean_spec.rb b/spec/bundler/commands/clean_spec.rb new file mode 100644 index 00000000000000..158d58d67c489f --- /dev/null +++ b/spec/bundler/commands/clean_spec.rb @@ -0,0 +1,771 @@ +# frozen_string_literal: true + +RSpec.describe "bundle clean" do + def should_have_gems(*gems) + gems.each do |g| + expect(vendored_gems("gems/#{g}")).to exist + expect(vendored_gems("specifications/#{g}.gemspec")).to exist + expect(vendored_gems("cache/#{g}.gem")).to exist + end + end + + def should_not_have_gems(*gems) + gems.each do |g| + expect(vendored_gems("gems/#{g}")).not_to exist + expect(vendored_gems("specifications/#{g}.gemspec")).not_to exist + expect(vendored_gems("cache/#{g}.gem")).not_to exist + end + end + + it "removes unused gems that are different" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => false) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + bundle! "install" + + bundle! :clean + + expect(out).to include("Removing foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0" + should_not_have_gems "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes old version of gem if unused" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "0.9.1" + gem "foo" + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => false) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + gem "foo" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing rack (0.9.1)") + + should_have_gems "foo-1.0", "rack-1.0.0" + should_not_have_gems "rack-0.9.1" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes new version of gem if unused" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + gem "foo" + G + + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => false) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "0.9.1" + gem "foo" + G + bundle! "update rack" + + bundle! :clean + + expect(out).to include("Removing rack (1.0.0)") + + should_have_gems "foo-1.0", "rack-0.9.1" + should_not_have_gems "rack-1.0.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes gems in bundle without groups" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + + group :test_group do + gem "rack", "1.0.0" + end + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + bundle "install", forgotten_command_line_options(:without => "test_group") + bundle :clean + + expect(out).to include("Removing rack (1.0.0)") + + should_have_gems "foo-1.0" + should_not_have_gems "rack-1.0.0" + + expect(vendored_gems("bin/rackup")).to_not exist + end + + it "does not remove cached git dir if it's being used" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + git_path = lib_path("foo-1.0") + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + bundle :clean + + digest = Digest(:SHA1).hexdigest(git_path.to_s) + cache_path = Bundler.bundler_major_version < 3 ? vendored_gems("cache/bundler/git/foo-1.0-#{digest}") : home(".bundle/cache/git/foo-1.0-#{digest}") + expect(cache_path).to exist + end + + it "removes unused git gems" do + build_git "foo", :path => lib_path("foo") + git_path = lib_path("foo") + revision = revision_for(git_path) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing foo (#{revision[0..11]})") + + expect(vendored_gems("gems/rack-1.0.0")).to exist + expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).not_to exist + digest = Digest(:SHA1).hexdigest(git_path.to_s) + expect(vendored_gems("cache/bundler/git/foo-#{digest}")).not_to exist + + expect(vendored_gems("specifications/rack-1.0.0.gemspec")).to exist + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes old git gems" do + build_git "foo-bar", :path => lib_path("foo-bar") + revision = revision_for(lib_path("foo-bar")) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + git "#{lib_path("foo-bar")}" do + gem "foo-bar" + end + G + + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle") + + update_git "foo", :path => lib_path("foo-bar") + revision2 = revision_for(lib_path("foo-bar")) + + bundle! "update", :all => bundle_update_requires_all? + bundle! :clean + + expect(out).to include("Removing foo-bar (#{revision[0..11]})") + + expect(vendored_gems("gems/rack-1.0.0")).to exist + expect(vendored_gems("bundler/gems/foo-bar-#{revision[0..11]}")).not_to exist + expect(vendored_gems("bundler/gems/foo-bar-#{revision2[0..11]}")).to exist + + expect(vendored_gems("specifications/rack-1.0.0.gemspec")).to exist + + expect(vendored_gems("bin/rackup")).to exist + end + + it "does not remove nested gems in a git repo" do + build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") + build_git "rails", "3.0", :path => lib_path("rails") do |s| + s.add_dependency "activesupport", "= 3.0" + end + revision = revision_for(lib_path("rails")) + + gemfile <<-G + gem "activesupport", :git => "#{lib_path("rails")}", :ref => '#{revision}' + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + bundle :clean + expect(out).to include("") + + expect(vendored_gems("bundler/gems/rails-#{revision[0..11]}")).to exist + end + + it "does not remove git sources that are in without groups" do + build_git "foo", :path => lib_path("foo") + git_path = lib_path("foo") + revision = revision_for(git_path) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + group :test do + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + end + G + bundle "install", forgotten_command_line_options(:path => "vendor/bundle", :without => "test") + + bundle :clean + + expect(out).to include("") + expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).to exist + digest = Digest(:SHA1).hexdigest(git_path.to_s) + expect(vendored_gems("cache/bundler/git/foo-#{digest}")).to_not exist + end + + it "does not blow up when using without groups" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + + group :development do + gem "foo" + end + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle", :without => "development") + + bundle :clean + expect(exitstatus).to eq(0) if exitstatus + end + + it "displays an error when used without --path" do + bundle! "config path.system true" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + + bundle :clean + + expect(exitstatus).to eq(15) if exitstatus + expect(out).to include("--force") + end + + # handling bundle clean upgrade path from the pre's + it "removes .gem/.gemspec file even if there's no corresponding gem dir" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + bundle "install" + + FileUtils.rm(vendored_gems("bin/rackup")) + FileUtils.rm_rf(vendored_gems("gems/thin-1.0")) + FileUtils.rm_rf(vendored_gems("gems/rack-1.0.0")) + + bundle :clean + + should_not_have_gems "thin-1.0", "rack-1.0" + should_have_gems "foo-1.0" + + expect(vendored_gems("bin/rackup")).not_to exist + end + + it "does not call clean automatically when using system gems" do + bundle! "config path.system true" + + bundle! :config + + install_gemfile! <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + + bundle! "info thin" + + install_gemfile! <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + sys_exec! "gem list" + expect(out).to include("rack (1.0.0)").and include("thin (1.0)") + end + + it "--clean should override the bundle setting on install", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => true) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle "install" + + should_have_gems "rack-1.0.0" + should_not_have_gems "thin-1.0" + end + + it "--clean should override the bundle setting on update", :bundler => "< 3" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => true) + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle! "update", :all => bundle_update_requires_all? + + should_have_gems "foo-1.0.1" + should_not_have_gems "foo-1.0" + end + + it "automatically cleans when path has not been set", :bundler => "3" do + build_repo2 + + install_gemfile! <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle! "update", :all => true + + files = Pathname.glob(bundled_app(".bundle", Bundler.ruby_scope, "*", "*")) + files.map! {|f| f.to_s.sub(bundled_app(".bundle", Bundler.ruby_scope).to_s, "") } + expect(files.sort).to eq %w[ + /cache/foo-1.0.1.gem + /gems/foo-1.0.1 + /specifications/foo-1.0.1.gemspec + ] + end + + it "does not clean automatically on --path" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle "install" + + should_have_gems "rack-1.0.0", "thin-1.0" + end + + it "does not clean on bundle update with --path" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle") + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle! :update, :all => bundle_update_requires_all? + should_have_gems "foo-1.0", "foo-1.0.1" + end + + it "does not clean on bundle update when using --system" do + bundle! "config path.system true" + + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + bundle! "install" + + update_repo2 do + build_gem "foo", "1.0.1" + end + bundle! :update, :all => bundle_update_requires_all? + + sys_exec! "gem list" + expect(out).to include("foo (1.0.1, 1.0)") + end + + it "cleans system gems when --force is used" do + bundle! "config path.system true" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + gem "rack" + G + bundle :install + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle :install + bundle "clean --force" + + expect(out).to include("Removing foo (1.0)") + sys_exec "gem list" + expect(out).not_to include("foo (1.0)") + expect(out).to include("rack (1.0.0)") + end + + describe "when missing permissions" do + before { ENV["BUNDLE_PATH__SYSTEM"] = "true" } + let(:system_cache_path) { system_gem_path("cache") } + after do + FileUtils.chmod(0o755, system_cache_path) + end + it "returns a helpful error message" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + gem "rack" + G + bundle :install + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle :install + + FileUtils.chmod(0o500, system_cache_path) + + bundle :clean, :force => true + + expect(out).to include(system_gem_path.to_s) + expect(out).to include("grant write permissions") + + sys_exec "gem list" + expect(out).to include("foo (1.0)") + expect(out).to include("rack (1.0.0)") + end + end + + it "cleans git gems with a 7 length git revision" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + # mimic 7 length git revisions in Gemfile.lock + gemfile_lock = File.read(bundled_app("Gemfile.lock")).split("\n") + gemfile_lock.each_with_index do |line, index| + gemfile_lock[index] = line[0..(11 + 7)] if line.include?(" revision:") + end + File.open(bundled_app("Gemfile.lock"), "w") do |file| + file.print gemfile_lock.join("\n") + end + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + bundle :clean + + expect(out).not_to include("Removing foo (1.0 #{revision[0..6]})") + + expect(vendored_gems("bundler/gems/foo-1.0-#{revision[0..6]}")).to exist + end + + it "when using --force on system gems, it doesn't remove binaries" do + bundle! "config path.system true" + + build_repo2 + update_repo2 do + build_gem "bindir" do |s| + s.bindir = "exe" + s.executables = "foo" + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "bindir" + G + bundle :install + + bundle "clean --force" + + sys_exec "foo" + + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq("1.0") + end + + it "doesn't blow up on path gems without a .gempsec" do + relative_path = "vendor/private_gems/bar-1.0" + absolute_path = bundled_app(relative_path) + FileUtils.mkdir_p("#{absolute_path}/lib/bar") + File.open("#{absolute_path}/lib/bar/bar.rb", "wb") do |file| + file.puts "module Bar; end" + end + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + gem "bar", "1.0", :path => "#{relative_path}" + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + bundle! :clean + end + + it "doesn't remove gems in dry-run mode with path set" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => false) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + + bundle :install + + bundle "clean --dry-run" + + expect(out).not_to include("Removing foo (1.0)") + expect(out).to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0", "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "doesn't remove gems in dry-run mode with no path set" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => false) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + + bundle :install + + bundle "configuration --delete path" + + bundle "clean --dry-run" + + expect(out).not_to include("Removing foo (1.0)") + expect(out).to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0", "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "doesn't store dry run as a config setting" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => false) + bundle "config dry_run false" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + + bundle :install + + bundle "clean" + + expect(out).to include("Removing foo (1.0)") + expect(out).not_to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0" + should_not_have_gems "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle", :clean => false) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "weakling" + G + + bundle! "config auto_install 1" + bundle! :clean + expect(out).to include("Installing weakling 0.0.3") + should_have_gems "thin-1.0", "rack-1.0.0", "weakling-0.0.3" + should_not_have_gems "foo-1.0" + end + + it "doesn't remove extensions artifacts from bundled git gems after clean", :ruby_repo, :rubygems => "2.2" do + build_git "very_simple_git_binary", &:add_c_extension + + revision = revision_for(lib_path("very_simple_git_binary-1.0")) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" + G + + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle") + expect(vendored_gems("bundler/gems/extensions")).to exist + expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist + + bundle! :clean + expect(out).to eq("") + + expect(vendored_gems("bundler/gems/extensions")).to exist + expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist + end + + it "removes extension directories", :ruby_repo, :rubygems => "2.2" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "very_simple_binary" + gem "simple_binary" + G + + bundle! "install", forgotten_command_line_options(:path => "vendor/bundle") + + very_simple_binary_extensions_dir = + Pathname.glob("#{vendored_gems}/extensions/*/*/very_simple_binary-1.0").first + + simple_binary_extensions_dir = + Pathname.glob("#{vendored_gems}/extensions/*/*/simple_binary-1.0").first + + expect(very_simple_binary_extensions_dir).to exist + expect(simple_binary_extensions_dir).to exist + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "simple_binary" + G + + bundle! "install" + bundle! :clean + expect(out).to eq("Removing very_simple_binary (1.0)") + + expect(very_simple_binary_extensions_dir).not_to exist + expect(simple_binary_extensions_dir).to exist + end +end diff --git a/spec/bundler/commands/config_spec.rb b/spec/bundler/commands/config_spec.rb new file mode 100644 index 00000000000000..9e493574650a7a --- /dev/null +++ b/spec/bundler/commands/config_spec.rb @@ -0,0 +1,384 @@ +# frozen_string_literal: true + +RSpec.describe ".bundle/config" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + G + end + + describe "config" do + before { bundle "config foo bar" } + + it "prints a detailed report of local and user configuration" do + bundle "config" + + expect(out).to include("Settings are listed in order of priority. The top value will be used") + expect(out).to include("foo\nSet for the current user") + expect(out).to include(": \"bar\"") + end + + context "given --parseable flag" do + it "prints a minimal report of local and user configuration" do + bundle "config --parseable" + expect(out).to include("foo=bar") + end + + context "with global config" do + it "prints config assigned to local scope" do + bundle "config --local foo bar2" + bundle "config --parseable" + expect(out).to include("foo=bar2") + end + end + + context "with env overwrite" do + it "prints config with env" do + bundle "config --parseable", :env => { "BUNDLE_FOO" => "bar3" } + expect(out).to include("foo=bar3") + end + end + end + end + + describe "BUNDLE_APP_CONFIG" do + it "can be moved with an environment variable" do + ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + expect(bundled_app(".bundle")).not_to exist + expect(tmp("foo/bar/config")).to exist + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "can provide a relative path with the environment variable" do + FileUtils.mkdir_p bundled_app("omg") + Dir.chdir bundled_app("omg") + + ENV["BUNDLE_APP_CONFIG"] = "../foo" + bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + + expect(bundled_app(".bundle")).not_to exist + expect(bundled_app("../foo/config")).to exist + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + describe "global" do + before(:each) { bundle :install } + + it "is the default" do + bundle "config foo global" + run "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "can also be set explicitly" do + bundle! "config --global foo global" + run! "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "has lower precedence than local" do + bundle "config --local foo local" + + bundle "config --global foo global" + expect(out).to match(/Your application has set foo to "local"/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "has lower precedence than env" do + begin + ENV["BUNDLE_FOO"] = "env" + + bundle "config --global foo global" + expect(out).to match(/You have a bundler environment variable for foo set to "env"/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("env") + ensure + ENV.delete("BUNDLE_FOO") + end + end + + it "can be deleted" do + bundle "config --global foo global" + bundle "config --delete foo" + + run "puts Bundler.settings[:foo] == nil" + expect(out).to eq("true") + end + + it "warns when overriding" do + bundle "config --global foo previous" + bundle "config --global foo global" + expect(out).to match(/You are replacing the current global value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "does not warn when using the same value twice" do + bundle "config --global foo value" + bundle "config --global foo value" + expect(out).not_to match(/You are replacing the current global value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("value") + end + + it "expands the path at time of setting" do + bundle "config --global local.foo .." + run "puts Bundler.settings['local.foo']" + expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + end + + it "saves with parseable option" do + bundle "config --global --parseable foo value" + expect(out).to eq("foo=value") + run "puts Bundler.settings['foo']" + expect(out).to eq("value") + end + + context "when replacing a current value with the parseable flag" do + before { bundle "config --global foo value" } + it "prints the current value in a parseable format" do + bundle "config --global --parseable foo value2" + expect(out).to eq "foo=value2" + run "puts Bundler.settings['foo']" + expect(out).to eq("value2") + end + end + end + + describe "local" do + before(:each) { bundle :install } + + it "can also be set explicitly" do + bundle "config --local foo local" + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "has higher precedence than env" do + begin + ENV["BUNDLE_FOO"] = "env" + bundle "config --local foo local" + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + ensure + ENV.delete("BUNDLE_FOO") + end + end + + it "can be deleted" do + bundle "config --local foo local" + bundle "config --delete foo" + + run "puts Bundler.settings[:foo] == nil" + expect(out).to eq("true") + end + + it "warns when overriding" do + bundle "config --local foo previous" + bundle "config --local foo local" + expect(out).to match(/You are replacing the current local value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "expands the path at time of setting" do + bundle "config --local local.foo .." + run "puts Bundler.settings['local.foo']" + expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + end + + it "can be deleted with parseable option" do + bundle "config --local foo value" + bundle "config --delete --parseable foo" + expect(out).to eq "" + run "puts Bundler.settings['foo'] == nil" + expect(out).to eq("true") + end + end + + describe "env" do + before(:each) { bundle :install } + + it "can set boolean properties via the environment" do + ENV["BUNDLE_FROZEN"] = "true" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("true") + end + + it "can set negative boolean properties via the environment" do + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "false" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "0" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + end + + it "can set properties with periods via the environment" do + ENV["BUNDLE_FOO__BAR"] = "baz" + + run "puts Bundler.settings['foo.bar']" + expect(out).to eq("baz") + end + end + + describe "parseable option" do + it "prints an empty string" do + bundle "config foo --parseable" + + expect(out).to eq "" + end + + it "only prints the value of the config" do + bundle "config foo local" + bundle "config foo --parseable" + + expect(out).to eq "foo=local" + end + + it "can print global config" do + bundle "config --global bar value" + bundle "config bar --parseable" + + expect(out).to eq "bar=value" + end + + it "prefers local config over global" do + bundle "config --local bar value2" + bundle "config --global bar value" + bundle "config bar --parseable" + + expect(out).to eq "bar=value2" + end + end + + describe "gem mirrors" do + before(:each) { bundle :install } + + it "configures mirrors using keys with `mirror.`" do + bundle "config --local mirror.http://gems.example.org http://gem-mirror.example.org" + run(<<-E) +Bundler.settings.gem_mirrors.each do |k, v| + puts "\#{k} => \#{v}" +end +E + expect(out).to eq("http://gems.example.org/ => http://gem-mirror.example.org/") + end + end + + describe "quoting" do + before(:each) { gemfile "# no gems" } + let(:long_string) do + "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \ + "--with-xslt-dir=/usr/pkg" + end + + it "saves quotes" do + bundle "config foo something\\'" + run "puts Bundler.settings[:foo]" + expect(out).to eq("something'") + end + + it "doesn't return quotes around values", :ruby => "1.9" do + bundle "config foo '1'" + run "puts Bundler.settings.send(:global_config_file).read" + expect(out).to include('"1"') + run "puts Bundler.settings[:foo]" + expect(out).to eq("1") + end + + it "doesn't duplicate quotes around values", :if => (RUBY_VERSION >= "2.1") do + bundled_app(".bundle").mkpath + File.open(bundled_app(".bundle/config"), "w") do |f| + f.write 'BUNDLE_FOO: "$BUILD_DIR"' + end + + bundle "config bar baz" + run "puts Bundler.settings.send(:local_config_file).read" + + # Starting in Ruby 2.1, YAML automatically adds double quotes + # around some values, including $ and newlines. + expect(out).to include('BUNDLE_FOO: "$BUILD_DIR"') + end + + it "doesn't duplicate quotes around long wrapped values" do + bundle "config foo #{long_string}" + + run "puts Bundler.settings[:foo]" + expect(out).to eq(long_string) + + bundle "config bar baz" + + run "puts Bundler.settings[:foo]" + expect(out).to eq(long_string) + end + end + + describe "very long lines" do + before(:each) { bundle :install } + + let(:long_string) do + "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \ + "--with-xslt-dir=/usr/pkg" + end + + let(:long_string_without_special_characters) do + "here is quite a long string that will wrap to a second line but will not be " \ + "surrounded by quotes" + end + + it "doesn't wrap values" do + bundle "config foo #{long_string}" + run "puts Bundler.settings[:foo]" + expect(out).to match(long_string) + end + + it "can read wrapped unquoted values" do + bundle "config foo #{long_string_without_special_characters}" + run "puts Bundler.settings[:foo]" + expect(out).to match(long_string_without_special_characters) + end + end +end + +RSpec.describe "setting gemfile via config" do + context "when only the non-default Gemfile exists" do + it "persists the gemfile location to .bundle/config" do + File.open(bundled_app("NotGemfile"), "w") do |f| + f.write <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + end + + bundle "config --local gemfile #{bundled_app("NotGemfile")}" + expect(File.exist?(".bundle/config")).to eq(true) + + bundle "config" + expect(out).to include("NotGemfile") + end + end +end diff --git a/spec/bundler/commands/console_spec.rb b/spec/bundler/commands/console_spec.rb new file mode 100644 index 00000000000000..05b0a6c1e426e7 --- /dev/null +++ b/spec/bundler/commands/console_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +RSpec.describe "bundle console", :bundler => "< 3" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + G + end + + it "starts IRB with the default group loaded" do + bundle "console" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "uses IRB as default console" do + bundle "console" do |input, _, _| + input.puts("__method__") + input.puts("exit") + end + expect(out).to include(":irb_binding") + end + + it "starts another REPL if configured as such" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "pry" + G + bundle "config console pry" + + bundle "console" do |input, _, _| + input.puts("__method__") + input.puts("exit") + end + expect(out).to include(":__pry__") + end + + it "falls back to IRB if the other REPL isn't available" do + bundle "config console pry" + # make sure pry isn't there + + bundle "console" do |input, _, _| + input.puts("__method__") + input.puts("exit") + end + expect(out).to include(":irb_binding") + end + + it "doesn't load any other groups" do + bundle "console" do |input, _, _| + input.puts("puts ACTIVESUPPORT") + input.puts("exit") + end + expect(out).to include("NameError") + end + + describe "when given a group" do + it "loads the given group" do + bundle "console test" do |input, _, _| + input.puts("puts ACTIVESUPPORT") + input.puts("exit") + end + expect(out).to include("2.3.5") + end + + it "loads the default group" do + bundle "console test" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "doesn't load other groups" do + bundle "console test" do |input, _, _| + input.puts("puts RACK_MIDDLEWARE") + input.puts("exit") + end + expect(out).to include("NameError") + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + gem "foo" + G + + bundle "config auto_install 1" + bundle :console do |input, _, _| + input.puts("puts 'hello'") + input.puts("exit") + end + expect(out).to include("Installing foo 1.0") + expect(out).to include("hello") + expect(the_bundle).to include_gems "foo 1.0" + end +end diff --git a/spec/bundler/commands/doctor_spec.rb b/spec/bundler/commands/doctor_spec.rb new file mode 100644 index 00000000000000..5260e6cb36b09a --- /dev/null +++ b/spec/bundler/commands/doctor_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require "find" +require "stringio" +require "bundler/cli" +require "bundler/cli/doctor" + +RSpec.describe "bundle doctor" do + before(:each) do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + @stdout = StringIO.new + + [:error, :warn].each do |method| + allow(Bundler.ui).to receive(method).and_wrap_original do |m, message| + m.call message + @stdout.puts message + end + end + end + + context "when all files in home are readable/writable" do + before(:each) do + stat = double("stat") + unwritable_file = double("file") + allow(Find).to receive(:find).with(Bundler.home.to_s) { [unwritable_file] } + allow(File).to receive(:stat).with(unwritable_file) { stat } + allow(stat).to receive(:uid) { Process.uid } + allow(File).to receive(:writable?).with(unwritable_file) { true } + allow(File).to receive(:readable?).with(unwritable_file) { true } + end + + it "exits with no message if the installed gem has no C extensions" do + expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error + expect(@stdout.string).to be_empty + end + + it "exits with no message if the installed gem's C extension dylib breakage is fine" do + doctor = Bundler::CLI::Doctor.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/lib/libSystem.dylib"] + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("/usr/lib/libSystem.dylib").and_return(true) + expect { doctor.run }.not_to(raise_error, @stdout.string) + expect(@stdout.string).to be_empty + end + + it "exits with a message if one of the linked libraries is missing" do + doctor = Bundler::CLI::Doctor.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib"] + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(false) + expect { doctor.run }.to raise_error(Bundler::ProductionError, strip_whitespace(<<-E).strip), @stdout.string + The following gems are missing OS dependencies: + * bundler: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib + * rack: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib + E + end + end + + context "when home contains files that are not readable/writable" do + before(:each) do + @stat = double("stat") + @unwritable_file = double("file") + allow(Find).to receive(:find).with(Bundler.home.to_s) { [@unwritable_file] } + allow(File).to receive(:stat).with(@unwritable_file) { @stat } + end + + it "exits with an error if home contains files that are not readable/writable" do + allow(@stat).to receive(:uid) { Process.uid } + allow(File).to receive(:writable?).with(@unwritable_file) { false } + allow(File).to receive(:readable?).with(@unwritable_file) { false } + expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error + expect(@stdout.string).to include( + "Files exist in the Bundler home that are not readable/writable by the current user. These files are:\n - #{@unwritable_file}" + ) + expect(@stdout.string).not_to include("No issues") + end + + context "when home contains files that are not owned by the current process" do + before(:each) do + allow(@stat).to receive(:uid) { 0o0000 } + end + + it "exits with an error if home contains files that are not readable/writable and are not owned by the current user" do + allow(File).to receive(:writable?).with(@unwritable_file) { false } + allow(File).to receive(:readable?).with(@unwritable_file) { false } + expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error + expect(@stdout.string).to include( + "Files exist in the Bundler home that are owned by another user, and are not readable/writable. These files are:\n - #{@unwritable_file}" + ) + expect(@stdout.string).not_to include("No issues") + end + + it "exits with a warning if home contains files that are read/write but not owned by current user" do + allow(File).to receive(:writable?).with(@unwritable_file) { true } + allow(File).to receive(:readable?).with(@unwritable_file) { true } + expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error + expect(@stdout.string).to include( + "Files exist in the Bundler home that are owned by another user, but are still readable/writable. These files are:\n - #{@unwritable_file}" + ) + expect(@stdout.string).not_to include("No issues") + end + end + end +end diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb new file mode 100644 index 00000000000000..76841dcff66f46 --- /dev/null +++ b/spec/bundler/commands/exec_spec.rb @@ -0,0 +1,823 @@ +# frozen_string_literal: true + +RSpec.describe "bundle exec" do + let(:system_gems_to_install) { %w[rack-1.0.0 rack-0.9.1] } + before :each do + system_gems(system_gems_to_install, :path => :bundle_path) + end + + it "works with --gemfile flag" do + create_file "CustomGemfile", <<-G + gem "rack", "1.0.0" + G + + bundle "exec --gemfile CustomGemfile rackup" + expect(out).to eq("1.0.0") + end + + it "activates the correct gem" do + gemfile <<-G + gem "rack", "0.9.1" + G + + bundle "exec rackup" + expect(out).to eq("0.9.1") + end + + it "works when the bins are in ~/.bundle" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec rackup" + expect(out).to eq("1.0.0") + end + + it "works when running from a random directory", :ruby_repo do + install_gemfile <<-G + gem "rack" + G + + bundle "exec 'cd #{tmp("gems")} && rackup'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + + expect(out).to include("1.0.0") + end + + it "works when exec'ing something else" do + install_gemfile 'gem "rack"' + bundle "exec echo exec" + expect(out).to eq("exec") + end + + it "works when exec'ing to ruby" do + install_gemfile 'gem "rack"' + bundle "exec ruby -e 'puts %{hi}'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to eq("hi") + end + + it "accepts --verbose" do + install_gemfile 'gem "rack"' + bundle "exec --verbose echo foobar" + expect(out).to eq("foobar") + end + + it "passes --verbose to command if it is given after the command" do + install_gemfile 'gem "rack"' + bundle "exec echo --verbose" + expect(out).to eq("--verbose") + end + + it "handles --keep-file-descriptors" do + require "tempfile" + + command = Tempfile.new("io-test") + command.sync = true + command.write <<-G + if ARGV[0] + IO.for_fd(ARGV[0].to_i) + else + require 'tempfile' + io = Tempfile.new("io-test-fd") + args = %W[#{Gem.ruby} -I#{lib} #{bindir.join("bundle")} exec --keep-file-descriptors #{Gem.ruby} #{command.path} \#{io.to_i}] + args << { io.to_i => io } if RUBY_VERSION >= "2.0" + exec(*args) + end + G + + install_gemfile "" + with_env_vars "RUBYOPT" => "-r#{spec_dir.join("support/hax")}" do + sys_exec "#{Gem.ruby} #{command.path}" + end + + if Bundler.current_ruby.ruby_2? + expect(out).to eq("") + else + expect(out).to eq("Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec.") + end + + expect(err).to lack_errors + end + + it "accepts --keep-file-descriptors" do + install_gemfile "" + bundle "exec --keep-file-descriptors echo foobar" + + expect(err).to lack_errors + end + + it "can run a command named --verbose" do + install_gemfile 'gem "rack"' + File.open("--verbose", "w") do |f| + f.puts "#!/bin/sh" + f.puts "echo foobar" + end + File.chmod(0o744, "--verbose") + with_path_as(".") do + bundle "exec -- --verbose" + end + expect(out).to eq("foobar") + end + + it "handles different versions in different bundles" do + build_repo2 do + build_gem "rack_two", "1.0.0" do |s| + s.executables = "rackup" + end + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + Dir.chdir bundled_app2 do + install_gemfile bundled_app2("Gemfile"), <<-G + source "file://#{gem_repo2}" + gem "rack_two", "1.0.0" + G + end + + bundle! "exec rackup" + + expect(out).to eq("0.9.1") + + Dir.chdir bundled_app2 do + bundle! "exec rackup" + expect(out).to eq("1.0.0") + end + end + + it "handles gems installed with --without" do + install_gemfile <<-G, forgotten_command_line_options(:without => "middleware") + source "file://#{gem_repo1}" + gem "rack" # rack 0.9.1 and 1.0 exist + + group :middleware do + gem "rack_middleware" # rack_middleware depends on rack 0.9.1 + end + G + + bundle "exec rackup" + + expect(out).to eq("0.9.1") + expect(the_bundle).not_to include_gems "rack_middleware 1.0" + end + + it "does not duplicate already exec'ed RUBYOPT" do + install_gemfile <<-G + gem "rack" + G + + rubyopt = ENV["RUBYOPT"] + rubyopt = "-rbundler/setup #{rubyopt}" + + bundle "exec 'echo $RUBYOPT'" + expect(out).to have_rubyopts(rubyopt) + + bundle "exec 'echo $RUBYOPT'", :env => { "RUBYOPT" => rubyopt } + expect(out).to have_rubyopts(rubyopt) + end + + it "does not duplicate already exec'ed RUBYLIB" do + install_gemfile <<-G + gem "rack" + G + + rubylib = ENV["RUBYLIB"] + rubylib = "#{rubylib}".split(File::PATH_SEPARATOR).unshift "#{bundler_path}" + rubylib = rubylib.uniq.join(File::PATH_SEPARATOR) + + bundle "exec 'echo $RUBYLIB'" + expect(out).to include(rubylib) + + bundle "exec 'echo $RUBYLIB'", :env => { "RUBYLIB" => rubylib } + expect(out).to include(rubylib) + end + + it "errors nicely when the argument doesn't exist" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec foobarbaz" + expect(exitstatus).to eq(127) if exitstatus + expect(out).to include("bundler: command not found: foobarbaz") + expect(out).to include("Install missing gem executables with `bundle install`") + end + + it "errors nicely when the argument is not executable" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec touch foo" + bundle "exec ./foo" + expect(exitstatus).to eq(126) if exitstatus + expect(out).to include("bundler: not executable: ./foo") + end + + it "errors nicely when no arguments are passed" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec" + expect(exitstatus).to eq(128) if exitstatus + expect(out).to include("bundler: exec needs a command to run") + end + + it "raises a helpful error when exec'ing to something outside of the bundle", :ruby_repo, :rubygems => ">= 2.5.2" do + bundle! "config clean false" # want to keep the rackup binstub + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "with_license" + G + [true, false].each do |l| + bundle! "config disable_exec_load #{l}" + bundle "exec rackup" + expect(last_command.stderr).to include "can't find executable rackup for gem rack. rack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?" + end + end + + # Different error message on old RG versions (before activate_bin_path) because they + # called `Kernel#gem` directly + it "raises a helpful error when exec'ing to something outside of the bundle", :rubygems => "< 2.5.2" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "with_license" + G + [true, false].each do |l| + bundle! "config disable_exec_load #{l}" + bundle "exec rackup", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(last_command.stderr).to include "rack is not part of the bundle. Add it to your Gemfile." + end + end + + describe "with help flags" do + each_prefix = proc do |string, &blk| + 1.upto(string.length) {|l| blk.call(string[0, l]) } + end + each_prefix.call("exec") do |exec| + describe "when #{exec} is used" do + before(:each) do + install_gemfile <<-G + gem "rack" + G + + create_file("print_args", <<-'RUBY') + #!/usr/bin/env ruby + puts "args: #{ARGV.inspect}" + RUBY + bundled_app("print_args").chmod(0o755) + end + + it "shows executable's man page when --help is after the executable" do + bundle "#{exec} print_args --help" + expect(out).to eq('args: ["--help"]') + end + + it "shows executable's man page when --help is after the executable and an argument" do + bundle "#{exec} print_args foo --help" + expect(out).to eq('args: ["foo", "--help"]') + + bundle "#{exec} print_args foo bar --help" + expect(out).to eq('args: ["foo", "bar", "--help"]') + + bundle "#{exec} print_args foo --help bar" + expect(out).to eq('args: ["foo", "--help", "bar"]') + end + + it "shows executable's man page when the executable has a -" do + FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template")) + bundle "#{exec} docker-template build discourse --help" + expect(out).to eq('args: ["build", "discourse", "--help"]') + end + + it "shows executable's man page when --help is after another flag" do + bundle "#{exec} print_args --bar --help" + expect(out).to eq('args: ["--bar", "--help"]') + end + + it "uses executable's original behavior for -h" do + bundle "#{exec} print_args -h" + expect(out).to eq('args: ["-h"]') + end + + it "shows bundle-exec's man page when --help is between exec and the executable" do + with_fake_man do + bundle "#{exec} --help cat" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when --help is before exec" do + with_fake_man do + bundle "--help #{exec}" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when -h is before exec" do + with_fake_man do + bundle "-h #{exec}" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when --help is after exec" do + with_fake_man do + bundle "#{exec} --help" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when -h is after exec" do + with_fake_man do + bundle "#{exec} -h" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + end + end + end + + describe "with gem executables" do + describe "run from a random directory", :ruby_repo do + before(:each) do + install_gemfile <<-G + gem "rack" + G + end + + it "works when unlocked" do + bundle "exec 'cd #{tmp("gems")} && rackup'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to eq("1.0.0") + expect(out).to include("1.0.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + bundle "exec 'cd #{tmp("gems")} && rackup'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to include("1.0.0") + end + end + + describe "from gems bundled via :path" do + before(:each) do + build_lib "fizz", :path => home("fizz") do |s| + s.executables = "fizz" + end + + install_gemfile <<-G + gem "fizz", :path => "#{File.expand_path(home("fizz"))}" + G + end + + it "works when unlocked" do + bundle "exec fizz" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + + bundle "exec fizz" + expect(out).to eq("1.0") + end + end + + describe "from gems bundled via :git" do + before(:each) do + build_git "fizz_git" do |s| + s.executables = "fizz_git" + end + + install_gemfile <<-G + gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}" + G + end + + it "works when unlocked" do + bundle "exec fizz_git" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + bundle "exec fizz_git" + expect(out).to eq("1.0") + end + end + + describe "from gems bundled via :git with no gemspec" do + before(:each) do + build_git "fizz_no_gemspec", :gemspec => false do |s| + s.executables = "fizz_no_gemspec" + end + + install_gemfile <<-G + gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}" + G + end + + it "works when unlocked" do + bundle "exec fizz_no_gemspec" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + bundle "exec fizz_no_gemspec" + expect(out).to eq("1.0") + end + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + gem "foo" + G + + bundle "config auto_install 1" + bundle "exec rackup" + expect(out).to include("Installing foo 1.0") + end + + describe "with gems bundled via :path with invalid gemspecs", :ruby_repo do + it "outputs the gemspec validation errors", :rubygems => ">= 1.7.2" do + build_lib "foo" + + gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s + File.open(gemspec, "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = 'foo' + s.version = '1.0' + s.summary = 'TODO: Add summary' + s.authors = 'Me' + end + G + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle "exec irb" + + expect(err).to match("The gemspec at #{lib_path("foo-1.0").join("foo.gemspec")} is not valid") + expect(err).to match('"TODO" is not a summary') + end + end + + describe "with gems bundled for deployment" do + it "works when calling bundler from another script" do + gemfile <<-G + module Monkey + def bin_path(a,b,c) + raise Gem::GemNotFoundException.new('Fail') + end + end + Bundler.rubygems.extend(Monkey) + G + bundle "install --deployment" + bundle "exec ruby -e '`#{bindir.join("bundler")} -v`; puts $?.success?'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to match("true") + end + end + + context "`load`ing a ruby file instead of `exec`ing" do + let(:path) { bundled_app("ruby_executable") } + let(:shebang) { "#!/usr/bin/env ruby" } + let(:executable) { <<-RUBY.gsub(/^ */, "").strip } + #{shebang} + + require "rack" + puts "EXEC: \#{caller.grep(/load/).empty? ? 'exec' : 'load'}" + puts "ARGS: \#{$0} \#{ARGV.join(' ')}" + puts "RACK: \#{RACK}" + process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip + puts "PROCESS: \#{process_title}" + RUBY + + before do + path.open("w") {|f| f << executable } + path.chmod(0o755) + + install_gemfile <<-G + gem "rack" + G + end + + let(:exec) { "EXEC: load" } + let(:args) { "ARGS: #{path} arg1 arg2" } + let(:rack) { "RACK: 1.0.0" } + let(:process) do + title = "PROCESS: #{path}" + title += " arg1 arg2" if RUBY_VERSION >= "2.1" + title + end + let(:exit_code) { 0 } + let(:expected) { [exec, args, rack, process].join("\n") } + let(:expected_err) { "" } + + subject { bundle "exec #{path} arg1 arg2", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } } + + shared_examples_for "it runs" do + it "like a normally executed executable" do + subject + expect(exitstatus).to eq(exit_code) if exitstatus + expect(last_command.stderr).to eq(expected_err) + expect(last_command.stdout).to eq(expected) + end + end + + it_behaves_like "it runs" + + context "the executable exits explicitly" do + let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" } + + context "with exit 0" do + it_behaves_like "it runs" + end + + context "with exit 99" do + let(:exit_code) { 99 } + it_behaves_like "it runs" + end + end + + context "the executable exits by SignalException" do + let(:executable) do + ex = super() + ex << "\n" + if LessThanProc.with(RUBY_VERSION).call("1.9") + # Ruby < 1.9 needs a flush for a exit by signal, later + # rubies do not + ex << "STDOUT.flush\n" + end + ex << "raise SignalException, 'SIGTERM'\n" + ex + end + let(:expected_err) { ENV["TRAVIS"] ? "Terminated" : "" } + let(:exit_code) do + # signal mask 128 + plus signal 15 -> TERM + # this is specified by C99 + 128 + 15 + end + it_behaves_like "it runs" + end + + context "the executable is empty" do + let(:executable) { "" } + + let(:exit_code) { 0 } + let(:expected_err) { "#{path} is empty" } + let(:expected) { "" } + it_behaves_like "it runs" + end + + context "the executable raises" do + let(:executable) { super() << "\nraise 'ERROR'" } + let(:exit_code) { 1 } + let(:expected_err) do + "bundler: failed to load command: #{path} (#{path})" \ + "\nRuntimeError: ERROR\n #{path}:10:in `'" + end + it_behaves_like "it runs" + end + + context "the executable raises an error without a backtrace" do + let(:executable) { super() << "\nclass Err < Exception\ndef backtrace; end;\nend\nraise Err" } + let(:exit_code) { 1 } + let(:expected_err) { "bundler: failed to load command: #{path} (#{path})\nErr: Err" } + let(:expected) { super() } + + it_behaves_like "it runs" + end + + context "when the file uses the current ruby shebang", :ruby_repo do + let(:shebang) { "#!#{Gem.ruby}" } + it_behaves_like "it runs" + end + + context "when Bundler.setup fails", :bundler => "< 3" do + before do + gemfile <<-G + gem 'rack', '2' + G + ENV["BUNDLER_FORCE_TTY"] = "true" + end + + let(:exit_code) { Bundler::GemNotFound.new.status_code } + let(:expected) { <<-EOS.strip } +\e[31mCould not find gem 'rack (= 2)' in any of the gem sources listed in your Gemfile.\e[0m +\e[33mRun `bundle install` to install missing gems.\e[0m + EOS + + it_behaves_like "it runs" + end + + context "when Bundler.setup fails", :bundler => "3" do + before do + gemfile <<-G + gem 'rack', '2' + G + ENV["BUNDLER_FORCE_TTY"] = "true" + end + + let(:exit_code) { Bundler::GemNotFound.new.status_code } + let(:expected) { <<-EOS.strip } +\e[31mCould not find gem 'rack (= 2)' in locally installed gems. +The source contains 'rack' at: 1.0.0\e[0m +\e[33mRun `bundle install` to install missing gems.\e[0m + EOS + + it_behaves_like "it runs" + end + + context "when the executable exits non-zero via at_exit" do + let(:executable) { super() + "\n\nat_exit { $! ? raise($!) : exit(1) }" } + let(:exit_code) { 1 } + + it_behaves_like "it runs" + end + + context "when disable_exec_load is set" do + let(:exec) { "EXEC: exec" } + let(:process) { "PROCESS: ruby #{path} arg1 arg2" } + + before do + bundle "config disable_exec_load true" + end + + it_behaves_like "it runs" + end + + context "regarding $0 and __FILE__" do + let(:executable) { super() + <<-'RUBY' } + + puts "$0: #{$0.inspect}" + puts "__FILE__: #{__FILE__.inspect}" + RUBY + + let(:expected) { super() + <<-EOS.chomp } + +$0: #{path.to_s.inspect} +__FILE__: #{path.to_s.inspect} + EOS + + it_behaves_like "it runs" + + context "when the path is relative" do + let(:path) { super().relative_path_from(bundled_app) } + + if LessThanProc.with(RUBY_VERSION).call("1.9") + pending "relative paths have ./ __FILE__" + else + it_behaves_like "it runs" + end + end + + context "when the path is relative with a leading ./" do + let(:path) { Pathname.new("./#{super().relative_path_from(Pathname.pwd)}") } + + if LessThanProc.with(RUBY_VERSION).call("< 1.9") + pending "relative paths with ./ have absolute __FILE__" + else + it_behaves_like "it runs" + end + end + end + + context "signal handling" do + let(:test_signals) do + open3_reserved_signals = %w[CHLD CLD PIPE] + reserved_signals = %w[SEGV BUS ILL FPE VTALRM KILL STOP EXIT] + bundler_signals = %w[INT] + + Signal.list.keys - (bundler_signals + reserved_signals + open3_reserved_signals) + end + + context "signals being trapped by bundler" do + let(:executable) { strip_whitespace <<-RUBY } + #{shebang} + begin + Thread.new do + puts 'Started' # For process sync + STDOUT.flush + sleep 1 # ignore quality_spec + raise "Didn't receive INT at all" + end.join + rescue Interrupt + puts "foo" + end + RUBY + + it "receives the signal", :ruby => ">= 1.9.3" do + bundle!("exec #{path}") do |_, o, thr| + o.gets # Consumes 'Started' and ensures that thread has started + Process.kill("INT", thr.pid) + end + + expect(out).to eq("foo") + end + end + + context "signals not being trapped by bunder" do + let(:executable) { strip_whitespace <<-RUBY } + #{shebang} + + signals = #{test_signals.inspect} + result = signals.map do |sig| + Signal.trap(sig, "IGNORE") + end + puts result.select { |ret| ret == "IGNORE" }.count + RUBY + + it "makes sure no unexpected signals are restored to DEFAULT" do + test_signals.each do |n| + Signal.trap(n, "IGNORE") + end + + bundle!("exec #{path}") + + expect(out).to eq(test_signals.count.to_s) + end + end + end + end + + context "nested bundle exec" do + let(:system_gems_to_install) { super() << :bundler } + + context "with shared gems disabled" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle :install, :system_bundler => true, :path => "vendor/bundler" + end + + it "overrides disable_shared_gems so bundler can be found" do + skip "bundler 1.16.x is not support with Ruby 2.6 on Travis CI" if RUBY_VERSION >= "2.6" + + file = bundled_app("file_that_bundle_execs.rb") + create_file(file, <<-RB) + #!#{Gem.ruby} + puts `bundle exec echo foo` + RB + file.chmod(0o777) + bundle! "exec #{file}", :system_bundler => true + expect(out).to eq("foo") + end + end + + context "with a system gem that shadows a default gem" do + let(:openssl_version) { "99.9.9" } + let(:expected) { ruby "gem 'openssl', '< 999999'; require 'openssl'; puts OpenSSL::VERSION", :artifice => nil } + + it "only leaves the default gem in the stdlib available" do + skip "openssl isn't a default gem" if expected.empty? + + install_gemfile! "" # must happen before installing the broken system gem + + build_repo4 do + build_gem "openssl", openssl_version do |s| + s.write("lib/openssl.rb", <<-RB) + raise "custom openssl should not be loaded, it's not in the gemfile!" + RB + end + end + + system_gems(:bundler, "openssl-#{openssl_version}", :gem_repo => gem_repo4) + + file = bundled_app("require_openssl.rb") + create_file(file, <<-RB) + #!/usr/bin/env ruby + require "openssl" + puts OpenSSL::VERSION + warn Gem.loaded_specs.values.map(&:full_name) + RB + file.chmod(0o777) + + aggregate_failures do + expect(bundle!("exec #{file}", :artifice => nil)).to eq(expected) + expect(bundle!("exec bundle exec #{file}", :artifice => nil)).to eq(expected) + expect(bundle!("exec ruby #{file}", :artifice => nil)).to eq(expected) + # Ignore expectaion for default bundler gem conflict. + unless ENV["BUNDLER_SPEC_SUB_VERSION"] + expect(run!(file.read, :artifice => nil)).to eq(expected) + end + end + + # sanity check that we get the newer, custom version without bundler + sys_exec("#{Gem.ruby} #{file}") + expect(last_command.stderr).to include("custom openssl should not be loaded") + end + end + end +end diff --git a/spec/bundler/commands/help_spec.rb b/spec/bundler/commands/help_spec.rb new file mode 100644 index 00000000000000..56b1b6f722cf6f --- /dev/null +++ b/spec/bundler/commands/help_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +RSpec.describe "bundle help" do + # RubyGems 1.4+ no longer load gem plugins so this test is no longer needed + it "complains if older versions of bundler are installed", :rubygems => "< 1.4" do + system_gems "bundler-0.8.1" + + bundle "help" + expect(err).to include("older than 0.9") + expect(err).to include("running `gem cleanup bundler`.") + end + + it "uses mann when available" do + with_fake_man do + bundle "help gemfile" + end + expect(out).to eq(%(["#{root}/man/gemfile.5"])) + end + + it "prefixes bundle commands with bundle- when finding the groff files" do + with_fake_man do + bundle "help install" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "simply outputs the txt file when there is no man on the path" do + with_path_as("") do + bundle "help install" + end + expect(out).to match(/BUNDLE-INSTALL/) + end + + it "still outputs the old help for commands that do not have man pages yet" do + bundle "help version" + expect(out).to include("Prints the bundler's version information") + end + + it "looks for a binary and executes it with --help option if it's named bundler-" do + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nputs ARGV.join(' ')\n" + end + + with_path_added(tmp) do + bundle "help testtasks" + end + + expect(exitstatus).to be_zero if exitstatus + expect(out).to eq("--help") + end + + it "is called when the --help flag is used after the command" do + with_fake_man do + bundle "install --help" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "is called when the --help flag is used before the command" do + with_fake_man do + bundle "--help install" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "is called when the -h flag is used before the command" do + with_fake_man do + bundle "-h install" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "is called when the -h flag is used after the command" do + with_fake_man do + bundle "install -h" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "has helpful output when using --help flag for a non-existent command" do + with_fake_man do + bundle "instill -h" + end + expect(out).to include('Could not find command "instill".') + end + + it "is called when only using the --help flag" do + with_fake_man do + bundle "--help" + end + expect(out).to eq(%(["#{root}/man/bundle.1"])) + + with_fake_man do + bundle "-h" + end + expect(out).to eq(%(["#{root}/man/bundle.1"])) + end +end diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb new file mode 100644 index 00000000000000..a9ab8fc210e5f4 --- /dev/null +++ b/spec/bundler/commands/info_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.describe "bundle info" do + context "info from specific gem in gemfile" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "prints information about the current gem" do + bundle "info rails" + expect(out).to include "* rails (2.3.2) +\tSummary: This is just a fake gem for testing +\tHomepage: http://example.com" + expect(out).to match(%r{Path\: .*\/rails\-2\.3\.2}) + end + + context "given a gem that is not installed" do + it "prints missing gem error" do + bundle "info foo" + expect(out).to eq "Could not find gem 'foo'." + end + end + + context "given a default gem shippped in ruby", :ruby_repo do + it "prints information about the default gem", :if => (RUBY_VERSION >= "2.0") do + bundle "info rdoc" + expect(out).to include("* rdoc") + expect(out).to include("Default Gem: yes") + end + end + + context "when gem does not have homepage" do + before do + build_repo1 do + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.summary = "Just another test gem" + end + end + end + + it "excludes the homepage field from the output" do + expect(out).to_not include("Homepage:") + end + end + + context "given --path option" do + it "prints the path to the gem" do + bundle "info rails" + expect(out).to match(%r{.*\/rails\-2\.3\.2}) + end + end + end +end diff --git a/spec/bundler/commands/init_spec.rb b/spec/bundler/commands/init_spec.rb new file mode 100644 index 00000000000000..8a8f0effa070c4 --- /dev/null +++ b/spec/bundler/commands/init_spec.rb @@ -0,0 +1,181 @@ +# frozen_string_literal: true + +RSpec.describe "bundle init" do + it "generates a Gemfile", :bundler => "< 3" do + bundle! :init + expect(out).to include("Writing new Gemfile") + expect(bundled_app("Gemfile")).to be_file + end + + it "generates a gems.rb", :bundler => "3" do + bundle! :init + expect(out).to include("Writing new gems.rb") + expect(bundled_app("gems.rb")).to be_file + end + + context "when a Gemfile already exists", :bundler => "< 3" do + before do + create_file "Gemfile", <<-G + gem "rails" + G + end + + it "does not change existing Gemfiles" do + expect { bundle :init }.not_to change { File.read(bundled_app("Gemfile")) } + end + + it "notifies the user that an existing Gemfile already exists" do + bundle :init + expect(out).to include("Gemfile already exists") + end + end + + context "when gems.rb already exists", :bundler => ">= 3" do + before do + create_file("gems.rb", <<-G) + gem "rails" + G + end + + it "does not change existing Gemfiles" do + expect { bundle :init }.not_to change { File.read(bundled_app("gems.rb")) } + end + + it "notifies the user that an existing gems.rb already exists" do + bundle :init + expect(out).to include("gems.rb already exists") + end + end + + context "when a Gemfile exists in a parent directory", :bundler => "< 3" do + let(:subdir) { "child_dir" } + + it "lets users generate a Gemfile in a child directory" do + bundle! :init + + FileUtils.mkdir bundled_app(subdir) + + Dir.chdir bundled_app(subdir) do + bundle! :init + end + + expect(out).to include("Writing new Gemfile") + expect(bundled_app("#{subdir}/Gemfile")).to be_file + end + end + + context "when the dir is not writable by the current user" do + let(:subdir) { "child_dir" } + + it "notifies the user that it can not write to it" do + FileUtils.mkdir bundled_app(subdir) + # chmod a-w it + mode = File.stat(bundled_app(subdir)).mode ^ 0o222 + FileUtils.chmod mode, bundled_app(subdir) + + Dir.chdir bundled_app(subdir) do + bundle :init + end + + expect(out).to include("directory is not writable") + expect(Dir[bundled_app("#{subdir}/*")]).to be_empty + end + end + + context "when a gems.rb file exists in a parent directory", :bundler => ">= 3" do + let(:subdir) { "child_dir" } + + it "lets users generate a Gemfile in a child directory" do + bundle! :init + + FileUtils.mkdir bundled_app(subdir) + + Dir.chdir bundled_app(subdir) do + bundle! :init + end + + expect(out).to include("Writing new gems.rb") + expect(bundled_app("#{subdir}/gems.rb")).to be_file + end + end + + context "given --gemspec option", :bundler => "< 3" do + let(:spec_file) { tmp.join("test.gemspec") } + + it "should generate from an existing gemspec" do + File.open(spec_file, "w") do |file| + file << <<-S + Gem::Specification.new do |s| + s.name = 'test' + s.add_dependency 'rack', '= 1.0.1' + s.add_development_dependency 'rspec', '1.2' + end + S + end + + bundle :init, :gemspec => spec_file + + gemfile = if Bundler::VERSION[0, 2].to_i < 3 + bundled_app("Gemfile").read + else + bundled_app("gems.rb").read + end + expect(gemfile).to match(%r{source 'https://rubygems.org'}) + expect(gemfile.scan(/gem "rack", "= 1.0.1"/).size).to eq(1) + expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1) + expect(gemfile.scan(/group :development/).size).to eq(1) + end + + context "when gemspec file is invalid" do + it "notifies the user that specification is invalid" do + File.open(spec_file, "w") do |file| + file << <<-S + Gem::Specification.new do |s| + s.name = 'test' + s.invalid_method_name + end + S + end + + bundle :init, :gemspec => spec_file + expect(last_command.bundler_err).to include("There was an error while loading `test.gemspec`") + end + end + end + + context "when init_gems_rb setting is enabled" do + before { bundle "config init_gems_rb true" } + + context "given --gemspec option", :bundler => "< 3" do + let(:spec_file) { tmp.join("test.gemspec") } + + before do + File.open(spec_file, "w") do |file| + file << <<-S + Gem::Specification.new do |s| + s.name = 'test' + s.add_dependency 'rack', '= 1.0.1' + s.add_development_dependency 'rspec', '1.2' + end + S + end + end + + it "should generate from an existing gemspec" do + bundle :init, :gemspec => spec_file + + gemfile = bundled_app("gems.rb").read + expect(gemfile).to match(%r{source 'https://rubygems.org'}) + expect(gemfile.scan(/gem "rack", "= 1.0.1"/).size).to eq(1) + expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1) + expect(gemfile.scan(/group :development/).size).to eq(1) + end + + it "prints message to user" do + bundle :init, :gemspec => spec_file + + expect(out).to include("Writing new gems.rb") + end + end + end +end diff --git a/spec/bundler/commands/inject_spec.rb b/spec/bundler/commands/inject_spec.rb new file mode 100644 index 00000000000000..822644f39ac01d --- /dev/null +++ b/spec/bundler/commands/inject_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +RSpec.describe "bundle inject", :bundler => "< 3" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "without a lockfile" do + it "locks with the injected gems" do + expect(bundled_app("Gemfile.lock")).not_to exist + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) + end + end + + context "with a lockfile" do + before do + bundle "install" + end + + it "adds the injected gems to the Gemfile" do + expect(bundled_app("Gemfile").read).not_to match(/rack-obama/) + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile").read).to match(/rack-obama/) + end + + it "locks with the injected gems" do + expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) + end + end + + context "with injected gems already in the Gemfile" do + it "doesn't add existing gems" do + bundle "inject 'rack' '> 0'" + expect(out).to match(/cannot specify the same gem twice/i) + end + end + + context "incorrect arguments" do + it "fails when more than 2 arguments are passed" do + bundle "inject gem_name 1 v" + expect(out).to eq(<<-E.strip) +ERROR: "bundle inject" was called with arguments ["gem_name", "1", "v"] +Usage: "bundle inject GEM VERSION" + E + end + end + + context "with source option" do + it "add gem with source option in gemfile" do + bundle "inject 'foo' '>0' --source file://#{gem_repo1}" + gemfile = bundled_app("Gemfile").read + str = "gem \"foo\", \"> 0\", :source => \"file://#{gem_repo1}\"" + expect(gemfile).to include str + end + end + + context "with group option" do + it "add gem with group option in gemfile" do + bundle "inject 'rack-obama' '>0' --group=development" + gemfile = bundled_app("Gemfile").read + str = "gem \"rack-obama\", \"> 0\", :group => :development" + expect(gemfile).to include str + end + + it "add gem with multiple groups in gemfile" do + bundle "inject 'rack-obama' '>0' --group=development,test" + gemfile = bundled_app("Gemfile").read + str = "gem \"rack-obama\", \"> 0\", :groups => [:development, :test]" + expect(gemfile).to include str + end + end + + context "when frozen" do + before do + bundle "install" + if Bundler.feature_flag.bundler_2_mode? + bundle! "config --local deployment true" + else + bundle! "config --local frozen true" + end + end + + it "injects anyway" do + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile").read).to match(/rack-obama/) + end + + it "locks with the injected gems" do + expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) + end + + it "restores frozen afterwards" do + bundle "inject 'rack-obama' '> 0'" + config = YAML.load(bundled_app(".bundle/config").read) + expect(config["BUNDLE_DEPLOYMENT"] || config["BUNDLE_FROZEN"]).to eq("true") + end + + it "doesn't allow Gemfile changes" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + G + bundle "inject 'rack' '> 0'" + expect(out).to match(/trying to install in deployment mode after changing/) + + expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) + end + end +end diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb new file mode 100644 index 00000000000000..326f98161ef287 --- /dev/null +++ b/spec/bundler/commands/install_spec.rb @@ -0,0 +1,587 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with gem sources" do + describe "the simple case" do + it "prints output and returns if no dependencies are specified" do + gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle :install + expect(out).to match(/no dependencies/) + end + + it "does not make a lockfile if the install fails" do + install_gemfile <<-G + raise StandardError, "FAIL" + G + + expect(last_command.bundler_err).to include('StandardError, "FAIL"') + expect(bundled_app("Gemfile.lock")).not_to exist + end + + it "creates a Gemfile.lock" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "does not create ./.bundle by default", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle! :install # can't use install_gemfile since it sets retry + expect(bundled_app(".bundle")).not_to exist + end + + it "does not create ./.bundle by default when installing to system gems" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle! :install, :env => { "BUNDLE_PATH__SYSTEM" => true } # can't use install_gemfile since it sets retry + expect(bundled_app(".bundle")).not_to exist + end + + it "creates lock files based on the Gemfile name" do + gemfile bundled_app("OmgFile"), <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + bundle "install --gemfile OmgFile" + + expect(bundled_app("OmgFile.lock")).to exist + end + + it "doesn't delete the lockfile if one already exists" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + lockfile = File.read(bundled_app("Gemfile.lock")) + + install_gemfile <<-G + raise StandardError, "FAIL" + G + + expect(File.read(bundled_app("Gemfile.lock"))).to eq(lockfile) + end + + it "does not touch the lockfile if nothing changed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect { run "1" }.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + + it "fetches gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + expect(default_bundle_path("gems/rack-1.0.0")).to exist + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "fetches gems when multiple versions are specified" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', "> 0.9", "< 1.0" + G + + expect(default_bundle_path("gems/rack-0.9.1")).to exist + expect(the_bundle).to include_gems("rack 0.9.1") + end + + it "fetches gems when multiple versions are specified take 2" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', "< 1.0", "> 0.9" + G + + expect(default_bundle_path("gems/rack-0.9.1")).to exist + expect(the_bundle).to include_gems("rack 0.9.1") + end + + it "raises an appropriate error when gems are specified using symbols" do + install_gemfile(<<-G) + source "file://#{gem_repo1}" + gem :rack + G + expect(exitstatus).to eq(4) if exitstatus + end + + it "pulls in dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + expect(the_bundle).to include_gems "actionpack 2.3.2", "rails 2.3.2" + end + + it "does the right version" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + expect(the_bundle).to include_gems "rack 0.9.1" + end + + it "does not install the development dependency" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "with_development_dependency" + G + + expect(the_bundle).to include_gems("with_development_dependency 1.0.0"). + and not_include_gems("activesupport 2.3.5") + end + + it "resolves correctly" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activemerchant" + gem "rails" + G + + expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2" + end + + it "activates gem correctly according to the resolved gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport", "2.3.5" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activemerchant" + gem "rails" + G + + expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2" + end + + it "does not reinstall any gem that is already available locally" do + system_gems "activesupport-2.3.2", :path => :bundle_path + + build_repo2 do + build_gem "activesupport", "2.3.2" do |s| + s.write "lib/activesupport.rb", "ACTIVESUPPORT = 'fail'" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activerecord", "2.3.2" + G + + expect(the_bundle).to include_gems "activesupport 2.3.2" + end + + it "works when the gemfile specifies gems that only exist in the system" do + build_gem "foo", :to_bundle => true + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "foo" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "foo 1.0.0" + end + + it "prioritizes local gems over remote gems" do + build_gem "rack", "1.0.0", :to_bundle => true do |s| + s.add_dependency "activesupport", "2.3.5" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + + describe "with a gem that installs multiple platforms" do + it "installs gems for the local platform as first choice" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 #{Bundler.local_platform}") + end + + it "falls back on plain ruby" do + simulate_platform "foo-bar-baz" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 RUBY") + end + + it "installs gems for java" do + simulate_platform "java" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 JAVA") + end + + it "installs gems for windows" do + simulate_platform mswin + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 MSWIN") + end + end + + describe "doing bundle install foo" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "works" do + bundle "install", forgotten_command_line_options(:path => "vendor") + expect(the_bundle).to include_gems "rack 1.0" + end + + it "allows running bundle install --system without deleting foo", :bundler => "< 3" do + bundle "install", forgotten_command_line_options(:path => "vendor") + bundle "install", forgotten_command_line_options(:system => true) + FileUtils.rm_rf(bundled_app("vendor")) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "allows running bundle install --system after deleting foo", :bundler => "< 3" do + bundle "install", forgotten_command_line_options(:path => "vendor") + FileUtils.rm_rf(bundled_app("vendor")) + bundle "install", forgotten_command_line_options(:system => true) + expect(the_bundle).to include_gems "rack 1.0" + end + end + + it "finds gems in multiple sources", :bundler => "< 3" do + build_repo2 + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo1}" + source "file://#{gem_repo2}" + + gem "activesupport", "1.2.3" + gem "rack", "1.2" + G + + expect(the_bundle).to include_gems "rack 1.2", "activesupport 1.2.3" + end + + it "gives a useful error if no sources are set" do + install_gemfile <<-G + gem "rack" + G + + bundle :install + expect(out).to include("Your Gemfile has no gem server sources") + end + + it "creates a Gemfile.lock on a blank Gemfile" do + install_gemfile <<-G + G + + expect(File.exist?(bundled_app("Gemfile.lock"))).to eq(true) + end + + context "throws a warning if a gem is added twice in Gemfile" do + it "without version requirements" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + gem "rack" + G + + expect(out).to include("Your Gemfile lists the gem rack (>= 0) more than once.") + expect(out).to include("Remove any duplicate entries and specify the gem only once (per group).") + expect(out).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + + it "with same versions" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", "1.0" + gem "rack", "1.0" + G + + expect(out).to include("Your Gemfile lists the gem rack (= 1.0) more than once.") + expect(out).to include("Remove any duplicate entries and specify the gem only once (per group).") + expect(out).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + end + + context "throws an error if a gem is added twice in Gemfile" do + it "when version of one dependency is not specified" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + gem "rack", "1.0" + G + + expect(out).to include("You cannot specify the same gem twice with different version requirements") + expect(out).to include("You specified: rack (>= 0) and rack (= 1.0).") + end + + it "when different versions of both dependencies are specified" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", "1.0" + gem "rack", "1.1" + G + + expect(out).to include("You cannot specify the same gem twice with different version requirements") + expect(out).to include("You specified: rack (= 1.0) and rack (= 1.1).") + end + end + + it "gracefully handles error when rubygems server is unavailable" do + install_gemfile <<-G, :artifice => nil + source "file://#{gem_repo1}" + source "http://localhost:9384" do + gem 'foo' + end + G + + bundle :install, :artifice => nil + expect(out).to include("Could not fetch specs from http://localhost:9384/") + expect(out).not_to include("file://") + end + + it "fails gracefully when downloading an invalid specification from the full index", :rubygems => "2.5" do + build_repo2 do + build_gem "ajp-rails", "0.0.0", :gemspec => false, :skip_validation => true do |s| + bad_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]] + s. + instance_variable_get(:@spec). + instance_variable_set(:@dependencies, bad_deps) + + raise "failed to set bad deps" unless s.dependencies == bad_deps + end + build_gem "ruby-ajp", "1.0.0" + end + + install_gemfile <<-G, :full_index => true + source "file:\/\/localhost#{gem_repo2}" + + gem "ajp-rails", "0.0.0" + G + + expect(last_command.stdboth).not_to match(/Error Report/i) + expect(last_command.bundler_err).to include("An error occurred while installing ajp-rails (0.0.0), and Bundler cannot continue."). + and include(normalize_uri_file("Make sure that `gem install ajp-rails -v '0.0.0' --source 'file://localhost#{gem_repo2}/'` succeeds before bundling.")) + end + + it "doesn't blow up when the local .bundle/config is empty" do + FileUtils.mkdir_p(bundled_app(".bundle")) + FileUtils.touch(bundled_app(".bundle/config")) + + install_gemfile(<<-G) + source "file://#{gem_repo1}" + + gem 'foo' + G + expect(exitstatus).to eq(0) if exitstatus + end + + it "doesn't blow up when the global .bundle/config is empty" do + FileUtils.mkdir_p("#{Bundler.rubygems.user_home}/.bundle") + FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config") + + install_gemfile(<<-G) + source "file://#{gem_repo1}" + + gem 'foo' + G + expect(exitstatus).to eq(0) if exitstatus + end + end + + describe "Ruby version in Gemfile.lock" do + include Bundler::GemHelpers + + context "and using an unsupported Ruby version" do + it "prints an error" do + install_gemfile <<-G + ::RUBY_VERSION = '2.0.1' + ruby '~> 2.2' + G + expect(out).to include("Your Ruby version is 2.0.1, but your Gemfile specified ~> 2.2") + end + end + + context "and using a supported Ruby version" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.3' + ::RUBY_PATCHLEVEL = 100 + ruby '~> 2.1.0' + G + end + + it "writes current Ruby version to Gemfile.lock" do + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + RUBY VERSION + ruby 2.1.3p100 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "updates Gemfile.lock with updated incompatible ruby version" do + install_gemfile <<-G + ::RUBY_VERSION = '2.2.3' + ::RUBY_PATCHLEVEL = 100 + ruby '~> 2.2.0' + G + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + RUBY VERSION + ruby 2.2.3p100 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + + describe "when Bundler root contains regex chars" do + before do + root_dir = tmp("foo[]bar") + + FileUtils.mkdir_p(root_dir) + in_app_root_custom(root_dir) + end + + it "doesn't blow up" do + build_lib "foo" + gemfile = <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + File.open("Gemfile", "w") do |file| + file.puts gemfile + end + + bundle :install + + expect(exitstatus).to eq(0) if exitstatus + end + end + + describe "when requesting a quiet install via --quiet" do + it "should be quiet" do + gemfile <<-G + gem 'rack' + G + + bundle :install, :quiet => true + expect(out).to include("Could not find gem 'rack'") + expect(out).to_not include("Your Gemfile has no gem server sources") + end + end + + describe "when bundle path does not have write access" do + before do + FileUtils.mkdir_p(bundled_app("vendor")) + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod(0o500, bundled_app("vendor")) + + bundle :install, forgotten_command_line_options(:path => "vendor") + expect(out).to include(bundled_app("vendor").to_s) + expect(out).to include("grant write permissions") + end + end + + context "after installing with --standalone" do + before do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + forgotten_command_line_options(:path => "bundle") + bundle! "install", :standalone => true + end + + it "includes the standalone path" do + bundle! "binstubs rack", :standalone => true + standalone_line = File.read(bundled_app("bin/rackup")).each_line.find {|line| line.include? "$:.unshift" }.strip + expect(standalone_line).to eq %($:.unshift File.expand_path "../../bundle", path.realpath) + end + end + + describe "when bundle install is executed with unencoded authentication" do + before do + gemfile <<-G + source 'https://rubygems.org/' + gem "." + G + end + + it "should display a helpful messag explaining how to fix it" do + bundle :install, :env => { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" } + expect(exitstatus).to eq(17) if exitstatus + expect(out).to eq("Please CGI escape your usernames and passwords before " \ + "setting them for authentication.") + end + end +end diff --git a/spec/bundler/commands/issue_spec.rb b/spec/bundler/commands/issue_spec.rb new file mode 100644 index 00000000000000..04c575130e985e --- /dev/null +++ b/spec/bundler/commands/issue_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.describe "bundle issue" do + it "exits with a message" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle "issue" + expect(out).to include "Did you find an issue with Bundler?" + expect(out).to include "## Environment" + expect(out).to include "## Gemfile" + expect(out).to include "## Bundle Doctor" + end +end diff --git a/spec/bundler/commands/licenses_spec.rb b/spec/bundler/commands/licenses_spec.rb new file mode 100644 index 00000000000000..d61d3492f30175 --- /dev/null +++ b/spec/bundler/commands/licenses_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RSpec.describe "bundle licenses" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "with_license" + G + end + + it "prints license information for all gems in the bundle" do + bundle "licenses" + + loaded_bundler_spec = Bundler.load.specs["bundler"] + expected = if !loaded_bundler_spec.empty? + loaded_bundler_spec[0].license + else + "Unknown" + end + + expect(out).to include("bundler: #{expected}") + expect(out).to include("with_license: MIT") + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "with_license" + gem "foo" + G + + bundle "config auto_install 1" + bundle :licenses + expect(out).to include("Installing foo 1.0") + end +end diff --git a/spec/bundler/commands/list_spec.rb b/spec/bundler/commands/list_spec.rb new file mode 100644 index 00000000000000..ba4bb9b0408a8a --- /dev/null +++ b/spec/bundler/commands/list_spec.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +RSpec.describe "bundle list", :bundler => "3" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + gem "rspec", :group => [:test] + G + end + + context "with name-only and paths option" do + it "raises an error" do + bundle "list --name-only --paths" + + expect(out).to eq "The `--name-only` and `--paths` options cannot be used together" + end + end + + context "with without-group and only-group option" do + it "raises an error" do + bundle "list --without-group dev --only-group test" + + expect(out).to eq "The `--only-group` and `--without-group` options cannot be used together" + end + end + + describe "with without-group option" do + context "when group is present" do + it "prints the gems not in the specified group" do + bundle! "list --without-group test" + + expect(out).to include(" * rack (1.0.0)") + expect(out).not_to include(" * rspec (1.2.7)") + end + end + + context "when group is not found" do + it "raises an error" do + bundle "list --without-group random" + + expect(out).to eq "`random` group could not be found." + end + end + end + + describe "with only-group option" do + context "when group is present" do + it "prints the gems in the specified group" do + bundle! "list --only-group default" + + expect(out).to include(" * rack (1.0.0)") + expect(out).not_to include(" * rspec (1.2.7)") + end + end + + context "when group is not found" do + it "raises an error" do + bundle "list --only-group random" + + expect(out).to eq "`random` group could not be found." + end + end + end + + context "with name-only option" do + it "prints only the name of the gems in the bundle" do + bundle "list --name-only" + + expect(out).to include("rack") + expect(out).to include("rspec") + end + end + + context "with paths option" do + before do + build_repo2 do + build_gem "bar" + end + + build_git "git_test", "1.0.0", :path => lib_path("git_test") + + build_lib("gemspec_test", :path => tmp.join("gemspec_test")) do |s| + s.write("Gemfile", "source :rubygems\ngemspec") + s.add_dependency "bar", "=1.0.0" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + gem "rails" + gem "git_test", :git => "#{lib_path("git_test")}" + gemspec :path => "#{tmp.join("gemspec_test")}" + G + + bundle! "install" + end + + it "prints the path of each gem in the bundle" do + bundle "list --paths" + expect(out).to match(%r{.*\/rails\-2\.3\.2}) + expect(out).to match(%r{.*\/rack\-1\.2}) + expect(out).to match(%r{.*\/git_test\-\w}) + expect(out).to match(%r{.*\/gemspec_test}) + end + end + + context "when no gems are in the gemfile" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + G + end + + it "prints message saying no gems are in the bundle" do + bundle "list" + expect(out).to include("No gems in the Gemfile") + end + end + + it "lists gems installed in the bundle" do + bundle "list" + expect(out).to include(" * rack (1.0.0)") + end + + it "aliases the ls command to list" do + bundle "ls" + expect(out).to include("Gems included by the bundle") + end +end diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb new file mode 100644 index 00000000000000..0b77605f0133a7 --- /dev/null +++ b/spec/bundler/commands/lock_spec.rb @@ -0,0 +1,322 @@ +# frozen_string_literal: true + +RSpec.describe "bundle lock" do + def strip_lockfile(lockfile) + strip_whitespace(lockfile).sub(/\n\Z/, "") + end + + def read_lockfile(file = "Gemfile.lock") + strip_lockfile bundled_app(file).read + end + + let(:repo) { gem_repo1 } + + before :each do + gemfile <<-G + source "file://localhost#{repo}" + gem "rails" + gem "with_license" + gem "foo" + G + + @lockfile = strip_lockfile(normalize_uri_file(<<-L)) + GEM + remote: file://localhost#{repo}/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + foo (1.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= 10.0.2) + rake (10.0.2) + with_license (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + with_license + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "prints a lockfile when there is no existing lockfile with --print" do + bundle "lock --print" + + expect(out).to eq(@lockfile) + end + + it "prints a lockfile when there is an existing lockfile with --print" do + lockfile @lockfile + + bundle "lock --print" + + expect(out).to eq(@lockfile) + end + + it "writes a lockfile when there is no existing lockfile" do + bundle "lock" + + expect(read_lockfile).to eq(@lockfile) + end + + it "writes a lockfile when there is an outdated lockfile using --update" do + lockfile @lockfile.gsub("2.3.2", "2.3.1") + + bundle! "lock --update" + + expect(read_lockfile).to eq(@lockfile) + end + + it "does not fetch remote specs when using the --local option" do + bundle "lock --update --local" + + expect(out).to match(/sources listed in your Gemfile|installed locally/) + end + + it "writes to a custom location using --lockfile" do + bundle "lock --lockfile=lock" + + expect(out).to match(/Writing lockfile to.+lock/) + expect(read_lockfile "lock").to eq(@lockfile) + expect { read_lockfile }.to raise_error(Errno::ENOENT) + end + + it "writes to custom location using --lockfile when a default lockfile is present" do + bundle "install" + bundle "lock --lockfile=lock" + + expect(out).to match(/Writing lockfile to.+lock/) + expect(read_lockfile("lock")).to eq(@lockfile) + end + + it "update specific gems using --update" do + lockfile @lockfile.gsub("2.3.2", "2.3.1").gsub("10.0.2", "10.0.1") + + bundle "lock --update rails rake" + + expect(read_lockfile).to eq(@lockfile) + end + + it "errors when updating a missing specific gems using --update" do + lockfile @lockfile + + bundle "lock --update blahblah" + expect(out).to eq("Could not find gem 'blahblah'.") + + expect(read_lockfile).to eq(@lockfile) + end + + # see update_spec for more coverage on same options. logic is shared so it's not necessary + # to repeat coverage here. + context "conservative updates" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] + build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo' + gem 'qux' + G + end + + it "single gem updates dependent gem to minor" do + bundle "lock --update foo --patch" + + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.4.5 bar-2.1.1 qux-1.0.0].sort) + end + + it "minor preferred with strict" do + bundle "lock --update --minor --strict" + + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort) + end + end + + it "supports adding new platforms" do + bundle! "lock --add-platform java x86-mingw32" + + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to match_array(local_platforms.unshift(java, mingw).uniq) + end + + it "supports adding the `ruby` platform" do + bundle! "lock --add-platform ruby" + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to match_array(local_platforms.unshift("ruby").uniq) + end + + it "warns when adding an unknown platform" do + bundle "lock --add-platform foobarbaz" + expect(out).to include("The platform `foobarbaz` is unknown to RubyGems and adding it will likely lead to resolution errors") + end + + it "allows removing platforms" do + bundle! "lock --add-platform java x86-mingw32" + + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to match_array(local_platforms.unshift(java, mingw).uniq) + + bundle! "lock --remove-platform java" + + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to match_array(local_platforms.unshift(mingw).uniq) + end + + it "errors when removing all platforms" do + bundle "lock --remove-platform #{local_platforms.join(" ")}" + expect(last_command.bundler_err).to include("Removing all platforms from the bundle is not allowed") + end + + # from https://github.com/bundler/bundler/issues/4896 + it "properly adds platforms when platform requirements come from different dependencies" do + build_repo4 do + build_gem "ffi", "1.9.14" + build_gem "ffi", "1.9.14" do |s| + s.platform = mingw + end + + build_gem "gssapi", "0.1" + build_gem "gssapi", "0.2" + build_gem "gssapi", "0.3" + build_gem "gssapi", "1.2.0" do |s| + s.add_dependency "ffi", ">= 1.0.1" + end + + build_gem "mixlib-shellout", "2.2.6" + build_gem "mixlib-shellout", "2.2.6" do |s| + s.platform = "universal-mingw32" + s.add_dependency "win32-process", "~> 0.8.2" + end + + # we need all these versions to get the sorting the same as it would be + # pulling from rubygems.org + %w[0.8.3 0.8.2 0.8.1 0.8.0].each do |v| + build_gem "win32-process", v do |s| + s.add_dependency "ffi", ">= 1.0.0" + end + end + end + + gemfile <<-G + source "file://localhost#{gem_repo4}" + + gem "mixlib-shellout" + gem "gssapi" + G + + simulate_platform(mingw) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(normalize_uri_file(strip_whitespace(<<-G))) + GEM + remote: file://localhost#{gem_repo4}/ + specs: + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + + simulate_platform(rb) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(normalize_uri_file(strip_whitespace(<<-G))) + GEM + remote: file://localhost#{gem_repo4}/ + specs: + ffi (1.9.14) + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + ruby + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + context "when an update is available" do + let(:repo) { gem_repo2 } + + before do + lockfile(@lockfile) + build_repo2 do + build_gem "foo", "2.0" + end + end + + it "does not implicitly update" do + bundle! "lock" + + expect(read_lockfile).to eq(@lockfile) + end + + it "accounts for changes in the gemfile" do + gemfile gemfile.gsub('"foo"', '"foo", "2.0"') + bundle! "lock" + + expect(read_lockfile).to eq(@lockfile.sub("foo (1.0)", "foo (2.0)").sub(/foo$/, "foo (= 2.0)")) + end + end +end diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb new file mode 100644 index 00000000000000..e6d6e19122ab21 --- /dev/null +++ b/spec/bundler/commands/newgem_spec.rb @@ -0,0 +1,912 @@ +# frozen_string_literal: true + +RSpec.describe "bundle gem" do + def reset! + super + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + end + + def remove_push_guard(gem_name) + # Remove exception that prevents public pushes on older RubyGems versions + if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + path = "#{gem_name}/#{gem_name}.gemspec" + content = File.read(path).sub(/raise "RubyGems 2\.0 or newer.*/, "") + File.open(path, "w") {|f| f.write(content) } + end + end + + def execute_bundle_gem(gem_name, flag = "", to_remove_push_guard = true) + bundle! "gem #{gem_name} #{flag}" + remove_push_guard(gem_name) if to_remove_push_guard + # reset gemspec cache for each test because of commit 3d4163a + Bundler.clear_gemspec_cache + end + + def gem_skeleton_assertions(gem_name) + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist + expect(bundled_app("#{gem_name}/README.md")).to exist + expect(bundled_app("#{gem_name}/Gemfile")).to exist + expect(bundled_app("#{gem_name}/Rakefile")).to exist + expect(bundled_app("#{gem_name}/lib/test/gem.rb")).to exist + expect(bundled_app("#{gem_name}/lib/test/gem/version.rb")).to exist + end + + before do + git_config_content = <<-EOF + [user] + name = "Bundler User" + email = user@example.com + [github] + user = bundleuser + EOF + @git_config_location = ENV["GIT_CONFIG"] + path = "#{File.expand_path(tmp, File.dirname(__FILE__))}/test_git_config.txt" + File.open(path, "w") {|f| f.write(git_config_content) } + ENV["GIT_CONFIG"] = path + end + + after do + FileUtils.rm(ENV["GIT_CONFIG"]) if File.exist?(ENV["GIT_CONFIG"]) + ENV["GIT_CONFIG"] = @git_config_location + end + + shared_examples_for "git config is present" do + context "git config user.{name,email} present" do + it "sets gemspec author to git user.name if available" do + expect(generated_gem.gemspec.authors.first).to eq("Bundler User") + end + + it "sets gemspec email to git user.email if available" do + expect(generated_gem.gemspec.email.first).to eq("user@example.com") + end + end + end + + shared_examples_for "git config is absent" do + it "sets gemspec author to default message if git user.name is not set or empty" do + expect(generated_gem.gemspec.authors.first).to eq("TODO: Write your name") + end + + it "sets gemspec email to default message if git user.email is not set or empty" do + expect(generated_gem.gemspec.email.first).to eq("TODO: Write your email address") + end + end + + shared_examples_for "--mit flag" do + before do + execute_bundle_gem(gem_name, "--mit") + end + it "generates a gem skeleton with MIT license" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/LICENSE.txt")).to exist + skel = Bundler::GemHelper.new(bundled_app(gem_name).to_s) + expect(skel.gemspec.license).to eq("MIT") + end + end + + shared_examples_for "--no-mit flag" do + before do + execute_bundle_gem(gem_name, "--no-mit") + end + it "generates a gem skeleton without MIT license" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/LICENSE.txt")).to_not exist + end + end + + shared_examples_for "--coc flag" do + before do + execute_bundle_gem(gem_name, "--coc", false) + end + it "generates a gem skeleton with MIT license" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/CODE_OF_CONDUCT.md")).to exist + end + + describe "README additions" do + it "generates the README with a section for the Code of Conduct" do + expect(bundled_app("test-gem/README.md").read).to include("## Code of Conduct") + expect(bundled_app("test-gem/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") + end + end + end + + shared_examples_for "--no-coc flag" do + before do + execute_bundle_gem(gem_name, "--no-coc", false) + end + it "generates a gem skeleton without Code of Conduct" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/CODE_OF_CONDUCT.md")).to_not exist + end + + describe "README additions" do + it "generates the README without a section for the Code of Conduct" do + expect(bundled_app("test-gem/README.md").read).not_to include("## Code of Conduct") + expect(bundled_app("test-gem/README.md").read).not_to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") + end + end + end + + context "README.md" do + let(:gem_name) { "test_gem" } + let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + + context "git config github.user present" do + before do + execute_bundle_gem(gem_name) + end + + it "contribute URL set to git username" do + expect(bundled_app("test_gem/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("test_gem/README.md").read).to include("github.com/bundleuser") + end + end + + context "git config github.user is absent" do + before do + sys_exec("git config --unset github.user") + reset! + in_app_root + bundle "gem #{gem_name}" + remove_push_guard(gem_name) + end + + it "contribute URL set to [USERNAME]" do + expect(bundled_app("test_gem/README.md").read).to include("[USERNAME]") + expect(bundled_app("test_gem/README.md").read).not_to include("github.com/bundleuser") + end + end + end + + it "creates a new git repository" do + in_app_root + bundle "gem test_gem" + expect(bundled_app("test_gem/.git")).to exist + end + + context "when git is not available" do + let(:gem_name) { "test_gem" } + + # This spec cannot have `git` available in the test env + before do + load_paths = [lib, spec] + load_path_str = "-I#{load_paths.join(File::PATH_SEPARATOR)}" + + sys_exec "PATH=\"\" #{Gem.ruby} #{load_path_str} #{bindir.join("bundle")} gem #{gem_name}" + end + + it "creates the gem without the need for git" do + expect(bundled_app("#{gem_name}/README.md")).to exist + end + + it "doesn't create a git repo" do + expect(bundled_app("#{gem_name}/.git")).to_not exist + end + + it "doesn't create a .gitignore file" do + expect(bundled_app("#{gem_name}/.gitignore")).to_not exist + end + end + + it "generates a valid gemspec" do + in_app_root + bundle! "gem newgem --bin" + + process_file(bundled_app("newgem", "newgem.gemspec")) do |line| + # Simulate replacing TODOs with real values + case line + when /spec\.metadata\["(?:allowed_push_host|homepage_uri|source_code_uri|changelog_uri)"\]/, /spec\.homepage/ + line.gsub(/\=.*$/, "= 'http://example.org'") + when /spec\.summary/ + line.gsub(/\=.*$/, "= %q{A short summary of my new gem.}") + when /spec\.description/ + line.gsub(/\=.*$/, "= %q{A longer description of my new gem.}") + # Remove exception that prevents public pushes on older RubyGems versions + when /raise "RubyGems 2.0 or newer/ + line.gsub(/.*/, "") if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + else + line + end + end + + Dir.chdir(bundled_app("newgem")) do + gems = ["rake-10.0.2", :bundler] + # for Ruby core repository, Ruby 2.6+ has bundler as standard library. + gems.delete(:bundler) if ruby_core? + system_gems gems, :path => :bundle_path + bundle! "exec rake build" + end + + expect(last_command.stdboth).not_to include("ERROR") + end + + context "gem naming with relative paths" do + before do + reset! + in_app_root + end + + it "resolves ." do + create_temporary_dir("tmp") + + bundle "gem ." + + expect(bundled_app("tmp/lib/tmp.rb")).to exist + end + + it "resolves .." do + create_temporary_dir("temp/empty_dir") + + bundle "gem .." + + expect(bundled_app("temp/lib/temp.rb")).to exist + end + + it "resolves relative directory" do + create_temporary_dir("tmp/empty/tmp") + + bundle "gem ../../empty" + + expect(bundled_app("tmp/empty/lib/empty.rb")).to exist + end + + def create_temporary_dir(dir) + FileUtils.mkdir_p(dir) + Dir.chdir(dir) + end + end + + context "gem naming with underscore" do + let(:gem_name) { "test_gem" } + + before do + execute_bundle_gem(gem_name) + end + + let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + + it "generates a gem skeleton" do + expect(bundled_app("test_gem/test_gem.gemspec")).to exist + expect(bundled_app("test_gem/Gemfile")).to exist + expect(bundled_app("test_gem/Rakefile")).to exist + expect(bundled_app("test_gem/lib/test_gem.rb")).to exist + expect(bundled_app("test_gem/lib/test_gem/version.rb")).to exist + expect(bundled_app("test_gem/.gitignore")).to exist + + expect(bundled_app("test_gem/bin/setup")).to exist + expect(bundled_app("test_gem/bin/console")).to exist + expect(bundled_app("test_gem/bin/setup")).to be_executable + expect(bundled_app("test_gem/bin/console")).to be_executable + end + + it "starts with version 0.1.0" do + expect(bundled_app("test_gem/lib/test_gem/version.rb").read).to match(/VERSION = "0.1.0"/) + end + + it "does not nest constants" do + expect(bundled_app("test_gem/lib/test_gem/version.rb").read).to match(/module TestGem/) + expect(bundled_app("test_gem/lib/test_gem.rb").read).to match(/module TestGem/) + end + + it_should_behave_like "git config is present" + + context "git config user.{name,email} is not set" do + before do + `git config --unset user.name` + `git config --unset user.email` + reset! + in_app_root + bundle "gem #{gem_name}" + remove_push_guard(gem_name) + end + + it_should_behave_like "git config is absent" + end + + it "sets gemspec metadata['allowed_push_host']", :rubygems => "2.0" do + expect(generated_gem.gemspec.metadata["allowed_push_host"]). + to match(/mygemserver\.com/) + end + + it "requires the version file" do + expect(bundled_app("test_gem/lib/test_gem.rb").read).to match(%r{require "test_gem/version"}) + end + + it "creates a base error class" do + expect(bundled_app("test_gem/lib/test_gem.rb").read).to match(/class Error < StandardError; end$/) + end + + it "runs rake without problems" do + system_gems ["rake-10.0.2"] + + rakefile = strip_whitespace <<-RAKEFILE + task :default do + puts 'SUCCESS' + end + RAKEFILE + File.open(bundled_app("test_gem/Rakefile"), "w") do |file| + file.puts rakefile + end + + Dir.chdir(bundled_app(gem_name)) do + sys_exec(rake) + expect(out).to include("SUCCESS") + end + end + + context "--exe parameter set" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --exe" + end + + it "builds exe skeleton" do + expect(bundled_app("test_gem/exe/test_gem")).to exist + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/exe/test_gem").read).to match(/require "test_gem"/) + end + end + + context "--bin parameter set" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --bin" + end + + it "builds exe skeleton" do + expect(bundled_app("test_gem/exe/test_gem")).to exist + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/exe/test_gem").read).to match(/require "test_gem"/) + end + end + + context "no --test parameter" do + before do + reset! + in_app_root + bundle "gem #{gem_name}" + end + + it "doesn't create any spec/test file" do + expect(bundled_app("test_gem/.rspec")).to_not exist + expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to_not exist + expect(bundled_app("test_gem/spec/spec_helper.rb")).to_not exist + expect(bundled_app("test_gem/test/test_test_gem.rb")).to_not exist + expect(bundled_app("test_gem/test/minitest_helper.rb")).to_not exist + end + end + + context "--test parameter set to rspec" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=rspec" + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/.rspec")).to exist + expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to exist + expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist + end + + it "depends on a specific version of rspec", :rubygems => ">= 1.8.1" do + remove_push_guard(gem_name) + rspec_dep = generated_gem.gemspec.development_dependencies.find {|d| d.name == "rspec" } + expect(rspec_dep).to be_specific + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/spec/spec_helper.rb").read).to include(%(require "test_gem")) + end + + it "creates a default test which fails" do + expect(bundled_app("test_gem/spec/test_gem_spec.rb").read).to include("expect(false).to eq(true)") + end + end + + context "gem.test setting set to rspec" do + before do + reset! + in_app_root + bundle "config gem.test rspec" + bundle "gem #{gem_name}" + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/.rspec")).to exist + expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to exist + expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist + end + end + + context "gem.test setting set to rspec and --test is set to minitest" do + before do + reset! + in_app_root + bundle "config gem.test rspec" + bundle "gem #{gem_name} --test=minitest" + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist + expect(bundled_app("test_gem/test/test_helper.rb")).to exist + end + end + + context "--test parameter set to minitest" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=minitest" + end + + it "depends on a specific version of minitest", :rubygems => ">= 1.8.1" do + remove_push_guard(gem_name) + rspec_dep = generated_gem.gemspec.development_dependencies.find {|d| d.name == "minitest" } + expect(rspec_dep).to be_specific + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist + expect(bundled_app("test_gem/test/test_helper.rb")).to exist + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/test/test_helper.rb").read).to include(%(require "test_gem")) + end + + it "requires 'minitest_helper'" do + expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include(%(require "test_helper")) + end + + it "creates a default test which fails" do + expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include("assert false") + end + end + + context "gem.test setting set to minitest" do + before do + reset! + in_app_root + bundle "config gem.test minitest" + bundle "gem #{gem_name}" + end + + it "creates a default rake task to run the test suite" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rake/testtask" + + Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] + end + + task :default => :test + RAKEFILE + + expect(bundled_app("test_gem/Rakefile").read).to eq(rakefile) + end + end + + context "--test with no arguments" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test" + end + + it "defaults to rspec" do + expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist + expect(bundled_app("test_gem/test/minitest_helper.rb")).to_not exist + end + + it "creates a .travis.yml file to test the library against the current Ruby version on Travis CI" do + expect(bundled_app("test_gem/.travis.yml").read).to match(/- #{RUBY_VERSION}/) + end + end + + context "--edit option" do + it "opens the generated gemspec in the user's text editor" do + reset! + in_app_root + output = bundle "gem #{gem_name} --edit=echo" + gemspec_path = File.join(Dir.pwd, gem_name, "#{gem_name}.gemspec") + expect(output).to include("echo \"#{gemspec_path}\"") + end + end + end + + context "testing --mit and --coc options against bundle config settings" do + let(:gem_name) { "test-gem" } + + context "with mit option in bundle config settings set to true" do + before do + global_config "BUNDLE_GEM__MIT" => "true", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + end + after { reset! } + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with mit option in bundle config settings set to false" do + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with coc option in bundle config settings set to true" do + before do + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "true" + end + after { reset! } + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + + context "with coc option in bundle config settings set to false" do + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + end + + context "gem naming with dashed" do + let(:gem_name) { "test-gem" } + + before do + execute_bundle_gem(gem_name) + end + + let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + + it "generates a gem skeleton" do + expect(bundled_app("test-gem/test-gem.gemspec")).to exist + expect(bundled_app("test-gem/Gemfile")).to exist + expect(bundled_app("test-gem/Rakefile")).to exist + expect(bundled_app("test-gem/lib/test/gem.rb")).to exist + expect(bundled_app("test-gem/lib/test/gem/version.rb")).to exist + end + + it "starts with version 0.1.0" do + expect(bundled_app("test-gem/lib/test/gem/version.rb").read).to match(/VERSION = "0.1.0"/) + end + + it "nests constants so they work" do + expect(bundled_app("test-gem/lib/test/gem/version.rb").read).to match(/module Test\n module Gem/) + expect(bundled_app("test-gem/lib/test/gem.rb").read).to match(/module Test\n module Gem/) + end + + it_should_behave_like "git config is present" + + context "git config user.{name,email} is not set" do + before do + `git config --unset user.name` + `git config --unset user.email` + reset! + in_app_root + bundle "gem #{gem_name}" + remove_push_guard(gem_name) + end + + it_should_behave_like "git config is absent" + end + + it "requires the version file" do + expect(bundled_app("test-gem/lib/test/gem.rb").read).to match(%r{require "test/gem/version"}) + end + + it "runs rake without problems" do + system_gems ["rake-10.0.2"] + + rakefile = strip_whitespace <<-RAKEFILE + task :default do + puts 'SUCCESS' + end + RAKEFILE + File.open(bundled_app("test-gem/Rakefile"), "w") do |file| + file.puts rakefile + end + + Dir.chdir(bundled_app(gem_name)) do + sys_exec(rake) + expect(out).to include("SUCCESS") + end + end + + context "--bin parameter set" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --bin" + end + + it "builds bin skeleton" do + expect(bundled_app("test-gem/exe/test-gem")).to exist + end + + it "requires 'test/gem'" do + expect(bundled_app("test-gem/exe/test-gem").read).to match(%r{require "test/gem"}) + end + end + + context "no --test parameter" do + before do + reset! + in_app_root + bundle "gem #{gem_name}" + end + + it "doesn't create any spec/test file" do + expect(bundled_app("test-gem/.rspec")).to_not exist + expect(bundled_app("test-gem/spec/test/gem_spec.rb")).to_not exist + expect(bundled_app("test-gem/spec/spec_helper.rb")).to_not exist + expect(bundled_app("test-gem/test/test_test/gem.rb")).to_not exist + expect(bundled_app("test-gem/test/minitest_helper.rb")).to_not exist + end + end + + context "--test parameter set to rspec" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=rspec" + end + + it "builds spec skeleton" do + expect(bundled_app("test-gem/.rspec")).to exist + expect(bundled_app("test-gem/spec/test/gem_spec.rb")).to exist + expect(bundled_app("test-gem/spec/spec_helper.rb")).to exist + end + + it "requires 'test/gem'" do + expect(bundled_app("test-gem/spec/spec_helper.rb").read).to include(%(require "test/gem")) + end + + it "creates a default test which fails" do + expect(bundled_app("test-gem/spec/test/gem_spec.rb").read).to include("expect(false).to eq(true)") + end + + it "creates a default rake task to run the specs" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task :default => :spec + RAKEFILE + + expect(bundled_app("test-gem/Rakefile").read).to eq(rakefile) + end + end + + context "--test parameter set to minitest" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=minitest" + end + + it "builds spec skeleton" do + expect(bundled_app("test-gem/test/test/gem_test.rb")).to exist + expect(bundled_app("test-gem/test/test_helper.rb")).to exist + end + + it "requires 'test/gem'" do + expect(bundled_app("test-gem/test/test_helper.rb").read).to match(%r{require "test/gem"}) + end + + it "requires 'test_helper'" do + expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/require "test_helper"/) + end + + it "creates a default test which fails" do + expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/assert false/) + end + + it "creates a default rake task to run the test suite" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rake/testtask" + + Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] + end + + task :default => :test + RAKEFILE + + expect(bundled_app("test-gem/Rakefile").read).to eq(rakefile) + end + end + + context "--test with no arguments" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test" + end + + it "defaults to rspec" do + expect(bundled_app("test-gem/spec/spec_helper.rb")).to exist + expect(bundled_app("test-gem/test/minitest_helper.rb")).to_not exist + end + end + + context "--ext parameter set" do + before do + reset! + in_app_root + bundle "gem test_gem --ext" + end + + it "builds ext skeleton" do + expect(bundled_app("test_gem/ext/test_gem/extconf.rb")).to exist + expect(bundled_app("test_gem/ext/test_gem/test_gem.h")).to exist + expect(bundled_app("test_gem/ext/test_gem/test_gem.c")).to exist + end + + it "includes rake-compiler" do + expect(bundled_app("test_gem/test_gem.gemspec").read).to include('spec.add_development_dependency "rake-compiler"') + end + + it "depends on compile task for build" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rake/extensiontask" + + task :build => :compile + + Rake::ExtensionTask.new("test_gem") do |ext| + ext.lib_dir = "lib/test_gem" + end + + task :default => [:clobber, :compile, :spec] + RAKEFILE + + expect(bundled_app("test_gem/Rakefile").read).to eq(rakefile) + end + end + end + + describe "uncommon gem names" do + it "can deal with two dashes" do + bundle "gem a--a" + Bundler.clear_gemspec_cache + + expect(bundled_app("a--a/a--a.gemspec")).to exist + end + + it "fails gracefully with a ." do + bundle "gem foo.gemspec" + expect(last_command.bundler_err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") + end + + it "fails gracefully with a ^" do + bundle "gem ^" + expect(last_command.bundler_err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") + end + + it "fails gracefully with a space" do + bundle "gem 'foo bar'" + expect(last_command.bundler_err).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name") + end + + it "fails gracefully when multiple names are passed" do + bundle "gem foo bar baz" + expect(last_command.bundler_err).to eq(<<-E.strip) +ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] +Usage: "bundle gem NAME [OPTIONS]" + E + end + end + + describe "#ensure_safe_gem_name" do + before do + bundle "gem #{subject}" + end + after do + Bundler.clear_gemspec_cache + end + + context "with an existing const name" do + subject { "gem" } + it { expect(out).to include("Invalid gem name #{subject}") } + end + + context "with an existing hyphenated const name" do + subject { "gem-specification" } + it { expect(out).to include("Invalid gem name #{subject}") } + end + + context "starting with an existing const name" do + subject { "gem-somenewconstantname" } + it { expect(out).not_to include("Invalid gem name #{subject}") } + end + + context "ending with an existing const name" do + subject { "somenewconstantname-gem" } + it { expect(out).not_to include("Invalid gem name #{subject}") } + end + end + + context "on first run" do + before do + in_app_root + end + + it "asks about test framework" do + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__COC" => "false" + + bundle "gem foobar" do |input, _, _| + input.puts "rspec" + end + + expect(bundled_app("foobar/spec/spec_helper.rb")).to exist + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task :default => :spec + RAKEFILE + + expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) + expect(bundled_app("foobar/foobar.gemspec").read).to include('spec.add_development_dependency "rspec"') + end + + it "asks about MIT license" do + global_config "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + + bundle :config + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/LICENSE.txt")).to exist + end + + it "asks about CoC" do + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false" + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist + end + end + + context "on conflicts with a previously created file" do + it "should fail gracefully" do + in_app_root do + FileUtils.touch("conflict-foobar") + end + bundle "gem conflict-foobar" + expect(last_command.bundler_err).to include("Errno::ENOTDIR") + expect(exitstatus).to eql(32) if exitstatus + end + end + + context "on conflicts with a previously created directory" do + it "should succeed" do + in_app_root do + FileUtils.mkdir_p("conflict-foobar/Gemfile") + end + bundle! "gem conflict-foobar" + expect(last_command.stdout).to include("file_clash conflict-foobar/Gemfile"). + and include "Initializing git repo in #{bundled_app("conflict-foobar")}" + end + end +end diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb new file mode 100644 index 00000000000000..5cab846fb56e1c --- /dev/null +++ b/spec/bundler/commands/open_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.describe "bundle open" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "opens the gem with BUNDLER_EDITOR as highest priority" do + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + expect(out).to include("bundler_editor #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "opens the gem with VISUAL as 2nd highest priority" do + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "" } + expect(out).to include("visual #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "opens the gem with EDITOR as 3rd highest priority" do + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "complains if no EDITOR is set" do + bundle "open rails", :env => { "EDITOR" => "", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to eq("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") + end + + it "complains if gem not in bundle" do + bundle "open missing", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to match(/could not find gem 'missing'/i) + end + + it "does not blow up if the gem to open does not have a Gemfile" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + bundle "open foo", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to match("editor #{default_bundle_path.join("bundler/gems/foo-1.0-#{ref}")}") + end + + it "suggests alternatives for similar-sounding gems" do + bundle "open Rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to match(/did you mean rails\?/i) + end + + it "opens the gem with short words" do + bundle "open rec", :env => { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + + expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}") + end + + it "select the gem from many match gems" do + env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + bundle "open active", :env => env do |input, _, _| + input.puts "2" + end + + expect(out).to match(/bundler_editor #{default_bundle_path('gems', 'activerecord-2.3.2')}\z/) + end + + it "allows selecting exit from many match gems" do + env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + bundle! "open active", :env => env do |input, _, _| + input.puts "0" + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "foo" + G + + bundle "config auto_install 1" + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("Installing foo 1.0") + end + + it "opens the editor with a clean env" do + bundle "open", :env => { "EDITOR" => "sh -c 'env'", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).not_to include("BUNDLE_GEMFILE=") + end +end diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb new file mode 100644 index 00000000000000..c9d3ac1de4a6d6 --- /dev/null +++ b/spec/bundler/commands/outdated_spec.rb @@ -0,0 +1,782 @@ +# frozen_string_literal: true + +RSpec.describe "bundle outdated" do + before :each do + build_repo2 do + build_git "foo", :path => lib_path("foo") + build_git "zebra", :path => lib_path("zebra") + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + end + + describe "with no arguments" do + it "returns a sorted list of outdated gems" do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "weakling", "0.2" + update_git "foo", :path => lib_path("foo") + update_git "zebra", :path => lib_path("zebra") + end + + bundle "outdated" + + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)") + expect(out).to include("foo (newest 1.0") + + # Gem names are one per-line, between "*" and their parenthesized version. + gem_list = out.split("\n").map {|g| g[/\* (.*) \(/, 1] }.compact + expect(gem_list).to eq(gem_list.sort) + end + + it "returns non zero exit status if outdated gems present" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + bundle "outdated" + + expect(exitstatus).to_not be_zero if exitstatus + end + + it "returns success exit status if no outdated gems present" do + bundle "outdated" + + expect(exitstatus).to be_zero if exitstatus + end + + it "adds gem group to dependency output when repo is updated" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + group :development, :test do + gem 'activesupport', '2.3.5' + end + G + + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated --verbose" + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5) in groups \"development, test\"") + end + end + + describe "with --group option" do + def test_group_option(group = nil, gems_list_size = 1) + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem "duradura", '7.0' + gem 'activesupport', '2.3.5' + end + G + + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "terranova", "9" + build_gem "duradura", "8.0" + end + + bundle "outdated --group #{group}" + + # Gem names are one per-line, between "*" and their parenthesized version. + gem_list = out.split("\n").map {|g| g[/\* (.*) \(/, 1] }.compact + expect(gem_list).to eq(gem_list.sort) + expect(gem_list.size).to eq gems_list_size + end + + it "not outdated gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + bundle "outdated --group" + expect(out).to include("Bundle up to date!") + end + + it "returns a sorted list of outdated gems from one group => 'default'" do + test_group_option("default") + + expect(out).to include("===== Group default =====") + expect(out).to include("terranova (") + + expect(out).not_to include("===== Group development, test =====") + expect(out).not_to include("activesupport") + expect(out).not_to include("duradura") + end + + it "returns a sorted list of outdated gems from one group => 'development'" do + test_group_option("development", 2) + + expect(out).not_to include("===== Group default =====") + expect(out).not_to include("terranova (") + + expect(out).to include("===== Group development, test =====") + expect(out).to include("activesupport") + expect(out).to include("duradura") + end + end + + describe "with --groups option" do + it "not outdated gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + bundle "outdated --groups" + expect(out).to include("Bundle up to date!") + end + + it "returns a sorted list of outdated gems by groups" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "terranova", "9" + build_gem "duradura", "8.0" + end + + bundle "outdated --groups" + expect(out).to include("===== Group default =====") + expect(out).to include("terranova (newest 9, installed 8, requested = 8)") + expect(out).to include("===== Group development, test =====") + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("duradura (newest 8.0, installed 7.0, requested = 7.0)") + + expect(out).not_to include("weakling (") + + # TODO: check gems order inside the group + end + end + + describe "with --local option" do + it "uses local cache to return a list of outdated gems" do + update_repo2 do + build_gem "activesupport", "2.3.4" + end + + bundle! "config clean false" + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.4" + G + + bundle "outdated --local" + + expect(out).to include("activesupport (newest 2.3.5, installed 2.3.4, requested = 2.3.4)") + end + + it "doesn't hit repo2" do + FileUtils.rm_rf(gem_repo2) + + bundle "outdated --local" + expect(out).not_to match(/Fetching (gem|version|dependency) metadata from/) + end + end + + shared_examples_for "a minimal output is desired" do + context "and gems are outdated" do + before do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "weakling", "0.2" + end + end + + it "outputs a sorted list of outdated gems with a more minimal format" do + minimal_output = "activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)\n" \ + "weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)" + subject + expect(out).to eq(minimal_output) + end + end + + context "and no gems are outdated" do + it "has empty output" do + subject + expect(out).to eq("") + end + end + end + + describe "with --parseable option" do + subject { bundle "outdated --parseable" } + + it_behaves_like "a minimal output is desired" + end + + describe "with aliased --porcelain option" do + subject { bundle "outdated --porcelain" } + + it_behaves_like "a minimal output is desired" + end + + describe "with specified gems" do + it "returns list of outdated gems" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + bundle "outdated foo" + expect(out).not_to include("activesupport (newest") + expect(out).to include("foo (newest 1.0") + end + end + + describe "pre-release gems" do + context "without the --pre option" do + it "ignores pre-release versions" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta" + end + + bundle "outdated" + expect(out).not_to include("activesupport (3.0.0.beta > 2.3.5)") + end + end + + context "with the --pre option" do + it "includes pre-release versions" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta" + end + + bundle "outdated --pre" + expect(out).to include("activesupport (newest 3.0.0.beta, installed 2.3.5, requested = 2.3.5)") + end + end + + context "when current gem is a pre-release" do + it "includes the gem" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta.1" + build_gem "activesupport", "3.0.0.beta.2" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "3.0.0.beta.1" + G + + bundle "outdated" + expect(out).to include("(newest 3.0.0.beta.2, installed 3.0.0.beta.1, requested = 3.0.0.beta.1)") + end + end + end + + describe "with --strict option" do + it "only reports gems that have a newer version that matches the specified dependency version requirements" do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "weakling", "0.0.5" + end + + bundle "outdated --strict" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 0.0.5, installed 0.0.3, requested ~> 0.0.1)") + end + + it "only reports gem dependencies when they can actually be updated" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack_middleware", "1.0" + G + + bundle "outdated --strict" + + expect(out).to_not include("rack (1.2") + end + + describe "and filter options" do + it "only reports gems that match requirement and patch filter level" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w[2.4.0 3.0.0] + build_gem "weakling", "0.0.5" + end + + bundle "outdated --strict --filter-patch" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 0.0.5, installed 0.0.3") + end + + it "only reports gems that match requirement and minor filter level" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w[2.3.9] + build_gem "weakling", "0.1.5" + end + + bundle "outdated --strict --filter-minor" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 0.1.5, installed 0.0.3") + end + + it "only reports gems that match requirement and major filter level" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w[2.4.0 2.5.0] + build_gem "weakling", "1.1.5" + end + + bundle "outdated --strict --filter-major" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 1.1.5, installed 0.0.3") + end + end + end + + describe "with invalid gem name" do + it "returns could not find gem name" do + bundle "outdated invalid_gem_name" + expect(out).to include("Could not find gem 'invalid_gem_name'.") + end + + it "returns non-zero exit code" do + bundle "outdated invalid_gem_name" + expect(exitstatus).to_not be_zero if exitstatus + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + gem "foo" + G + + bundle "config auto_install 1" + bundle :outdated + expect(out).to include("Installing foo 1.0") + end + + context "after bundle install --deployment", :bundler => "< 3" do + before do + install_gemfile <<-G, forgotten_command_line_options(:deployment => true) + source "file://#{gem_repo2}" + + gem "rack" + gem "foo" + G + end + + it "outputs a helpful message about being in deployment mode" do + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated" + expect(last_command).to be_failure + expect(out).to include("You are trying to check outdated gems in deployment mode.") + expect(out).to include("Run `bundle outdated` elsewhere.") + expect(out).to include("If this is a development machine, remove the ") + expect(out).to include("Gemfile freeze\nby running `bundle install --no-deployment`.") + end + end + + context "after bundle config deployment true" do + before do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "rack" + gem "foo" + G + bundle! "config deployment true" + end + + it "outputs a helpful message about being in deployment mode" do + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated" + expect(last_command).to be_failure + expect(out).to include("You are trying to check outdated gems in deployment mode.") + expect(out).to include("Run `bundle outdated` elsewhere.") + expect(out).to include("If this is a development machine, remove the ") + expect(out).to include("Gemfile freeze\nby running `bundle config --delete deployment`.") + end + end + + context "update available for a gem on a different platform" do + before do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "laduradura", '= 5.15.2' + G + end + + it "reports that no updates are available" do + bundle "outdated" + expect(out).to include("Bundle up to date!") + end + end + + context "update available for a gem on the same platform while multiple platforms used for gem" do + it "reports that updates are available if the Ruby platform is used" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated" + expect(out).to include("Bundle up to date!") + end + + it "reports that updates are available if the JRuby platform is used" do + simulate_ruby_engine "jruby", "1.6.7" do + simulate_platform "jruby" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated" + expect(out).to include("Outdated gems included in the bundle:") + expect(out).to include("laduradura (newest 5.15.3, installed 5.15.2, requested = 5.15.2)") + end + end + end + end + + shared_examples_for "version update is detected" do + it "reports that a gem has a newer version" do + subject + expect(out).to include("Outdated gems included in the bundle:") + expect(out).to include("activesupport (newest") + expect(out).to_not include("ERROR REPORT TEMPLATE") + end + end + + shared_examples_for "major version updates are detected" do + before do + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + it_behaves_like "version update is detected" + end + + context "when on a new machine" do + before do + simulate_new_machine + + update_git "foo", :path => lib_path("foo") + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + subject { bundle "outdated" } + it_behaves_like "version update is detected" + end + + shared_examples_for "minor version updates are detected" do + before do + update_repo2 do + build_gem "activesupport", "2.7.5" + build_gem "weakling", "2.0.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "patch version updates are detected" do + before do + update_repo2 do + build_gem "activesupport", "2.3.7" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "no version updates are detected" do + it "does not detect any version updates" do + subject + expect(out).to include("updates to display.") + expect(out).to_not include("ERROR REPORT TEMPLATE") + expect(out).to_not include("activesupport (newest") + expect(out).to_not include("weakling (newest") + end + end + + shared_examples_for "major version is ignored" do + before do + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "1.0.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "minor version is ignored" do + before do + update_repo2 do + build_gem "activesupport", "2.4.5" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "patch version is ignored" do + before do + update_repo2 do + build_gem "activesupport", "2.3.6" + build_gem "weakling", "0.0.4" + end + end + + it_behaves_like "no version updates are detected" + end + + describe "with --filter-major option" do + subject { bundle "outdated --filter-major" } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-minor option" do + subject { bundle "outdated --filter-minor" } + + it_behaves_like "minor version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-patch option" do + subject { bundle "outdated --filter-patch" } + + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-minor --filter-patch" } + + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + end + + describe "with --filter-major --filter-minor options" do + subject { bundle "outdated --filter-major --filter-minor" } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-major --filter-patch options" do + subject { bundle "outdated --filter-major --filter-patch" } + + it_behaves_like "major version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-major --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-major --filter-minor --filter-patch" } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + end + + context "conservative updates" do + context "without update-strict" do + before do + build_repo4 do + build_gem "patch", %w[1.0.0 1.0.1] + build_gem "minor", %w[1.0.0 1.0.1 1.1.0] + build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.0.0 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'patch', '1.0.0' + gem 'minor', '1.0.0' + gem 'major', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'patch' + gem 'minor' + gem 'major' + G + end + + it "shows nothing when patching and filtering to minor" do + bundle "outdated --patch --filter-minor" + + expect(out).to include("No minor updates to display.") + expect(out).not_to include("patch (newest") + expect(out).not_to include("minor (newest") + expect(out).not_to include("major (newest") + end + + it "shows all gems when patching and filtering to patch" do + bundle "outdated --patch --filter-patch" + + expect(out).to include("patch (newest 1.0.1") + expect(out).to include("minor (newest 1.0.1") + expect(out).to include("major (newest 1.0.1") + end + + it "shows minor and major when updating to minor and filtering to patch and minor" do + bundle "outdated --minor --filter-minor" + + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest 1.1.0") + expect(out).to include("major (newest 1.1.0") + end + + it "shows minor when updating to major and filtering to minor with parseable" do + bundle "outdated --major --filter-minor --parseable" + + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest") + expect(out).not_to include("major (newest") + end + end + + context "with update-strict" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] + build_gem "qux", %w[1.0.0 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo' + gem 'qux' + G + end + + it "shows gems with update-strict updating to patch and filtering to patch" do + bundle "outdated --patch --update-strict --filter-patch" + + expect(out).to include("foo (newest 1.4.4") + expect(out).to include("bar (newest 2.0.5") + expect(out).not_to include("qux (newest") + end + end + end + + describe "with --only-explicit" do + it "does not report outdated dependent gems" do + build_repo4 do + build_gem "weakling", %w[0.2 0.3] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "bar", %w[2.1 2.2] + end + + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'weakling', '0.2' + gem 'bar', '2.1' + G + + gemfile <<-G + source "file://#{gem_repo4}" + gem 'weakling' + G + + bundle "outdated --only-explicit" + + expect(out).to include("weakling (newest 0.3") + expect(out).not_to include("bar (newest 2.2") + end + end +end diff --git a/spec/bundler/commands/package_spec.rb b/spec/bundler/commands/package_spec.rb new file mode 100644 index 00000000000000..a8426e63228485 --- /dev/null +++ b/spec/bundler/commands/package_spec.rb @@ -0,0 +1,306 @@ +# frozen_string_literal: true + +RSpec.describe "bundle package" do + context "with --gemfile" do + it "finds the gemfile" do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle "package --gemfile=NotGemfile" + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with --all" do + context "without a gemspec" do + it "caches all dependencies except bundler itself" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gem 'bundler' + D + + bundle :package, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + + context "with a gemspec" do + context "that has the same name as the gem" do + before do + File.open(bundled_app("mygem.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gem" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gemspec + D + + bundle! :package, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist + expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + + context "that has a different name as the gem" do + before do + File.open(bundled_app("mygem_diffname.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gem" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gemspec + D + + bundle! :package, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist + expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + end + + context "with multiple gemspecs" do + before do + File.open(bundled_app("mygem.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + File.open(bundled_app("mygem_client.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem_test" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "weakling", "=0.0.3" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gems" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gemspec :name => 'mygem' + gemspec :name => 'mygem_test' + D + + bundle! :package, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist + expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist + expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/mygem_test-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + end + + context "with --path", :bundler => "< 3" do + it "sets root directory for gems" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle! :package, forgotten_command_line_options(:path => bundled_app("test")) + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(bundled_app("test/vendor/cache/")).to exist + end + end + + context "with --no-install" do + it "puts the gems in vendor/cache but does not install them" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle! "package --no-install" + + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "does not prevent installing gems with bundle install" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle! "package --no-install" + bundle! "install" + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with --all-platforms" do + it "puts the gems in vendor/cache even for other rubies", :ruby => "2.1" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack', :platforms => :ruby_19 + D + + bundle "package --all-platforms" + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + end + + context "with --frozen" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "install" + end + + subject { bundle :package, forgotten_command_line_options(:frozen => true) } + + it "tries to install with frozen" do + bundle! "config deployment true" + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + subject + expect(exitstatus).to eq(16) if exitstatus + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama") + bundle "env" + expect(out).to include("frozen").or include("deployment") + end + end +end + +RSpec.describe "bundle install with gem sources" do + describe "when cached and locked" do + it "does not hit the remote at all" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + + bundle :pack + simulate_new_machine + FileUtils.rm_rf gem_repo2 + + bundle "install --local" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not hit the remote at all" do + build_repo2 + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "rack" + G + + bundle! :pack + simulate_new_machine + FileUtils.rm_rf gem_repo2 + + bundle! :install, forgotten_command_line_options(:deployment => true, :path => "vendor/bundle") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not reinstall already-installed gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle :pack + + build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| + s.write "lib/rack.rb", "raise 'omg'" + end + + bundle :install + expect(err).to lack_errors + expect(the_bundle).to include_gems "rack 1.0" + end + + it "ignores cached gems for the wrong platform" do + simulate_platform "java" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + bundle :pack + end + + simulate_new_machine + + simulate_platform "ruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 RUBY") + end + end + + it "does not update the cache if --no-cache is passed" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundled_app("vendor/cache").mkpath + expect(bundled_app("vendor/cache").children).to be_empty + + bundle "install --no-cache" + expect(bundled_app("vendor/cache").children).to be_empty + end + end +end diff --git a/spec/bundler/commands/pristine_spec.rb b/spec/bundler/commands/pristine_spec.rb new file mode 100644 index 00000000000000..d8761bba26dcdb --- /dev/null +++ b/spec/bundler/commands/pristine_spec.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "bundler/vendored_fileutils" + +RSpec.describe "bundle pristine", :ruby_repo do + before :each do + build_lib "baz", :path => bundled_app do |s| + s.version = "1.0.0" + s.add_development_dependency "baz-dev", "=1.0.0" + end + + build_repo2 do + build_gem "weakling" + build_gem "baz-dev", "1.0.0" + build_gem "very_simple_binary", &:add_c_extension + build_git "foo", :path => lib_path("foo") + build_git "git_with_ext", :path => lib_path("git_with_ext"), &:add_c_extension + build_lib "bar", :path => lib_path("bar") + end + + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "weakling" + gem "very_simple_binary" + gem "foo", :git => "#{lib_path("foo")}" + gem "git_with_ext", :git => "#{lib_path("git_with_ext")}" + gem "bar", :path => "#{lib_path("bar")}" + + gemspec + G + end + + context "when sourced from RubyGems" do + it "reverts using cached .gem file" do + spec = Bundler.definition.specs["weakling"].first + changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") + + FileUtils.touch(changes_txt) + expect(changes_txt).to be_file + + bundle "pristine" + expect(changes_txt).to_not be_file + end + + it "does not delete the bundler gem" do + ENV["BUNDLER_SPEC_KEEP_DEFAULT_BUNDLER_GEM"] = "true" + system_gems :bundler + bundle! "install" + bundle! "pristine", :system_bundler => true + bundle! "-v", :system_bundler => true + + expected = if Bundler::VERSION < "3.0" + "Bundler version" + else + Bundler::VERSION + end + + expect(out).to start_with(expected) + end + end + + context "when sourced from git repo" do + it "reverts by resetting to current revision`" do + spec = Bundler.definition.specs["foo"].first + changed_file = Pathname.new(spec.full_gem_path).join("lib/foo.rb") + diff = "#Pristine spec changes" + + File.open(changed_file, "a") {|f| f.puts diff } + expect(File.read(changed_file)).to include(diff) + + bundle! "pristine" + expect(File.read(changed_file)).to_not include(diff) + end + + it "removes added files" do + spec = Bundler.definition.specs["foo"].first + changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") + + FileUtils.touch(changes_txt) + expect(changes_txt).to be_file + + bundle! "pristine" + expect(changes_txt).not_to be_file + end + end + + context "when sourced from gemspec" do + it "displays warning and ignores changes when sourced from gemspec" do + spec = Bundler.definition.specs["baz"].first + changed_file = Pathname.new(spec.full_gem_path).join("lib/baz.rb") + diff = "#Pristine spec changes" + + File.open(changed_file, "a") {|f| f.puts diff } + expect(File.read(changed_file)).to include(diff) + + bundle "pristine" + expect(File.read(changed_file)).to include(diff) + expect(out).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + end + + it "reinstall gemspec dependency" do + spec = Bundler.definition.specs["baz-dev"].first + changed_file = Pathname.new(spec.full_gem_path).join("lib/baz-dev.rb") + diff = "#Pristine spec changes" + + File.open(changed_file, "a") {|f| f.puts "#Pristine spec changes" } + expect(File.read(changed_file)).to include(diff) + + bundle "pristine" + expect(File.read(changed_file)).to_not include(diff) + end + end + + context "when sourced from path" do + it "displays warning and ignores changes when sourced from local path" do + spec = Bundler.definition.specs["bar"].first + changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") + FileUtils.touch(changes_txt) + expect(changes_txt).to be_file + bundle "pristine" + expect(out).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + expect(changes_txt).to be_file + end + end + + context "when passing a list of gems to pristine" do + it "resets them" do + foo = Bundler.definition.specs["foo"].first + foo_changes_txt = Pathname.new(foo.full_gem_path).join("lib/changes.txt") + FileUtils.touch(foo_changes_txt) + expect(foo_changes_txt).to be_file + + bar = Bundler.definition.specs["bar"].first + bar_changes_txt = Pathname.new(bar.full_gem_path).join("lib/changes.txt") + FileUtils.touch(bar_changes_txt) + expect(bar_changes_txt).to be_file + + weakling = Bundler.definition.specs["weakling"].first + weakling_changes_txt = Pathname.new(weakling.full_gem_path).join("lib/changes.txt") + FileUtils.touch(weakling_changes_txt) + expect(weakling_changes_txt).to be_file + + bundle! "pristine foo bar weakling" + + expect(out).to include("Cannot pristine bar (1.0). Gem is sourced from local path."). + and include("Installing weakling 1.0") + + expect(weakling_changes_txt).not_to be_file + expect(foo_changes_txt).not_to be_file + expect(bar_changes_txt).to be_file + end + + it "raises when one of them is not in the lockfile" do + bundle "pristine abcabcabc" + expect(out).to include("Could not find gem 'abcabcabc'.") + end + end + + context "when a build config exists for one of the gems" do + let(:very_simple_binary) { Bundler.definition.specs["very_simple_binary"].first } + let(:c_ext_dir) { Pathname.new(very_simple_binary.full_gem_path).join("ext") } + let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" } + before { bundle "config build.very_simple_binary -- #{build_opt}" } + + # This just verifies that the generated Makefile from the c_ext gem makes + # use of the build_args from the bundle config + it "applies the config when installing the gem" do + bundle! "pristine" + + makefile_contents = File.read(c_ext_dir.join("Makefile").to_s) + expect(makefile_contents).to match(/libpath =.*#{c_ext_dir}/) + expect(makefile_contents).to match(/LIBPATH =.*-L#{c_ext_dir}/) + end + end + + context "when a build config exists for a git sourced gem" do + let(:git_with_ext) { Bundler.definition.specs["git_with_ext"].first } + let(:c_ext_dir) { Pathname.new(git_with_ext.full_gem_path).join("ext") } + let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" } + before { bundle "config build.git_with_ext -- #{build_opt}" } + + # This just verifies that the generated Makefile from the c_ext gem makes + # use of the build_args from the bundle config + it "applies the config when installing the gem" do + bundle! "pristine" + + makefile_contents = File.read(c_ext_dir.join("Makefile").to_s) + expect(makefile_contents).to match(/libpath =.*#{c_ext_dir}/) + expect(makefile_contents).to match(/LIBPATH =.*-L#{c_ext_dir}/) + end + end +end diff --git a/spec/bundler/commands/remove_spec.rb b/spec/bundler/commands/remove_spec.rb new file mode 100644 index 00000000000000..faeb654b14c8b9 --- /dev/null +++ b/spec/bundler/commands/remove_spec.rb @@ -0,0 +1,583 @@ +# frozen_string_literal: true + +RSpec.describe "bundle remove" do + context "when no gems are specified" do + it "throws error" do + gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle "remove" + + expect(out).to include("Please specify gems to remove.") + end + end + + context "when --install flag is specified" do + it "removes gems from .bundle" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + bundle! "remove rack --install" + + expect(out).to include("rack was removed.") + expect(the_bundle).to_not include_gems "rack" + end + end + + describe "remove single gem from gemfile" do + context "when gem is present in gemfile" do + it "shows success for removed gem" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + bundle! "remove rack" + + expect(out).to include("rack was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "when gem is not present in gemfile" do + it "shows warning for gem that could not be removed" do + gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle "remove rack" + + expect(out).to include("`rack` is not specified in #{bundled_app("Gemfile")} so it could not be removed.") + end + end + end + + describe "remove mutiple gems from gemfile" do + context "when all gems are present in gemfile" do + it "shows success fir all removed gems" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + gem "rails" + G + + bundle! "remove rack rails" + + expect(out).to include("rack was removed.") + expect(out).to include("rails was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "when some gems are not present in the gemfile" do + it "shows warning for those not present and success for those that can be removed" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rails" + gem "minitest" + gem "rspec" + G + + bundle "remove rails rack minitest" + + expect(out).to include("`rack` is not specified in #{bundled_app("Gemfile")} so it could not be removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "rails" + gem "minitest" + gem "rspec" + G + end + end + end + + context "with inline groups" do + it "removes the specified gem" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", :group => [:dev] + G + + bundle! "remove rack" + + expect(out).to include("rack was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + describe "with group blocks" do + context "when single group block with gem to be removed is present" do + it "removes the group block" do + gemfile <<-G + source "file://#{gem_repo1}" + + group :test do + gem "rspec" + end + G + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "when an empty block is also present" do + it "removes all empty blocks" do + gemfile <<-G + source "file://#{gem_repo1}" + + group :test do + gem "rspec" + end + + group :dev do + end + G + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "when the gem belongs to mutiple groups" do + it "removes the groups" do + gemfile <<-G + source "file://#{gem_repo1}" + + group :test, :serioustest do + gem "rspec" + end + G + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "when the gem is present in mutiple groups" do + it "removes all empty blocks" do + gemfile <<-G + source "file://#{gem_repo1}" + + group :one do + gem "rspec" + end + + group :two do + gem "rspec" + end + G + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + end + + describe "nested group blocks" do + context "when all the groups will be empty after removal" do + it "removes the empty nested blocks" do + gemfile <<-G + source "file://#{gem_repo1}" + + group :test do + group :serioustest do + gem "rspec" + end + end + G + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "when outer group will not be empty after removal" do + it "removes only empty blocks" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + group :test do + gem "rack-test" + + group :serioustest do + gem "rspec" + end + end + G + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + group :test do + gem "rack-test" + + end + G + end + end + + context "when inner group will not be empty after removal" do + it "removes only empty blocks" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + group :test do + group :serioustest do + gem "rspec" + gem "rack-test" + end + end + G + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + group :test do + group :serioustest do + gem "rack-test" + end + end + G + end + end + end + + describe "arbitrary gemfile" do + context "when mutiple gems are present in same line" do + it "shows warning for gems not removed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack"; gem "rails" + G + + bundle "remove rails" + + if Gem::VERSION >= "1.6.0" + expect(out).to include("Gems could not be removed. rack (>= 0) would also have been removed.") + else + expect(out).to include("Gems could not be removed. rack (>= 0, runtime) would also have been removed.") + end + gemfile_should_be <<-G + source "file://#{gem_repo1}" + gem "rack"; gem "rails" + G + end + end + + context "when some gems could not be removed" do + it "shows warning for gems not removed and success for those removed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem"rack" + gem"rspec" + gem "rails" + gem "minitest" + G + + bundle! "remove rails rack rspec minitest" + + expect(out).to include("rails was removed.") + expect(out).to include("minitest was removed.") + expect(out).to include("rack, rspec could not be removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + gem"rack" + gem"rspec" + G + end + end + end + + context "with sources" do + before do + build_repo gem_repo3 do + build_gem "rspec" + end + end + + it "removes gems and empty source blocks" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + + source "file://#{gem_repo3}" do + gem "rspec" + end + G + + bundle! "install" + + bundle! "remove rspec" + + expect(out).to include("rspec was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + end + end + + describe "with eval_gemfile" do + context "when gems are present in both gemfiles" do + it "removes the gems" do + create_file "Gemfile-other", <<-G + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + + gem "rack" + G + + bundle! "remove rack" + + expect(out).to include("rack was removed.") + end + end + + context "when gems are present in other gemfile" do + it "removes the gems" do + create_file "Gemfile-other", <<-G + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + G + + bundle! "remove rack" + + expect(bundled_app("Gemfile-other").read).to_not include("gem \"rack\"") + expect(out).to include("rack was removed.") + end + end + + context "when gems to be removed are not specified in any of the gemfiles" do + it "throws error for the gems not present" do + # an empty gemfile + # indicating the gem is not present in the gemfile + create_file "Gemfile-other", <<-G + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + G + + bundle "remove rack" + + expect(out).to include("`rack` is not specified in #{bundled_app("Gemfile")} so it could not be removed.") + end + end + + context "when the gem is present in parent file but not in gemfile specified by eval_gemfile" do + it "removes the gem" do + create_file "Gemfile-other", <<-G + gem "rails" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + gem "rack" + G + + bundle "remove rack" + + expect(out).to include("rack was removed.") + expect(out).to include("`rack` is not specified in #{bundled_app("Gemfile-other")} so it could not be removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + G + end + end + + context "when gems can not be removed from other gemfile" do + it "shows error" do + create_file "Gemfile-other", <<-G + gem "rails"; gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + gem "rack" + G + + bundle "remove rack" + + expect(out).to include("rack was removed.") + if Gem::VERSION >= "1.6.0" + expect(out).to include("Gems could not be removed. rails (>= 0) would also have been removed.") + else + expect(out).to include("Gems could not be removed. rails (>= 0, runtime) would also have been removed.") + end + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + G + end + end + + context "when gems could not be removed from parent gemfile" do + it "shows error" do + create_file "Gemfile-other", <<-G + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + gem "rails"; gem "rack" + G + + bundle "remove rack" + + if Gem::VERSION >= "1.6.0" + expect(out).to include("Gems could not be removed. rails (>= 0) would also have been removed.") + else + expect(out).to include("Gems could not be removed. rails (>= 0, runtime) would also have been removed.") + end + expect(bundled_app("Gemfile-other").read).to include("gem \"rack\"") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + gem "rails"; gem "rack" + G + end + end + + context "when gem present in gemfiles but could not be removed from one from one of them" do + it "removes gem which can be removed and shows warning for file from which it can not be removed" do + create_file "Gemfile-other", <<-G + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + eval_gemfile "Gemfile-other" + gem"rack" + G + + bundle! "remove rack" + + expect(out).to include("rack was removed.") + expect(bundled_app("Gemfile-other").read).to_not include("gem \"rack\"") + end + end + end + + context "with install_if" do + it "removes gems inside blocks and empty blocks" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + install_if(lambda { false }) do + gem "rack" + end + G + + bundle! "remove rack" + + expect(out).to include("rack was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "with env" do + it "removes gems inside blocks and empty blocks" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + env "BUNDLER_TEST" do + gem "rack" + end + G + + bundle! "remove rack" + + expect(out).to include("rack was removed.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + G + end + end + + context "with gemspec" do + it "should not remove the gem" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("foo.gemspec", "") + s.add_dependency "rack" + end + + install_gemfile(<<-G) + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + bundle! "remove foo" + + expect(out).to include("foo could not be removed.") + end + end +end diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb new file mode 100644 index 00000000000000..a5c6beec1af4fb --- /dev/null +++ b/spec/bundler/commands/show_spec.rb @@ -0,0 +1,244 @@ +# frozen_string_literal: true + +RSpec.describe "bundle show", :bundler => "< 3", :ruby => ">= 2.0" do + context "with a standard Gemfile" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "creates a Gemfile.lock if one did not exist" do + FileUtils.rm("Gemfile.lock") + + bundle "show" + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "creates a Gemfile.lock when invoked with a gem name" do + FileUtils.rm("Gemfile.lock") + + bundle "show rails" + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "prints path if gem exists in bundle" do + bundle "show rails" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + context "when show command deprecation is enabled" do + before { bundle "config major_deprecations yes" } + + it "prints path if gem exists in bundle" do + bundle "show rails" + expect(out).to eq( + "[DEPRECATED FOR 3.0] use `bundle info rails` instead of `bundle show rails`\n" + + default_bundle_path("gems", "rails-2.3.2").to_s + ) + end + + it "prints the path to the running bundler" do + bundle "show bundler" + expect(out).to eq( + "[DEPRECATED FOR 3.0] use `bundle info bundler` instead of `bundle show bundler`\n" + + root.to_s + ) + end + + it "prints path if gem exists in bundle (with --paths option)" do + bundle "show rails --paths" + expect(out).to eq( + "[DEPRECATED FOR 3.0] use `bundle info rails --path` instead of `bundle show rails --paths`\n" + + default_bundle_path("gems", "rails-2.3.2").to_s + ) + end + + it "prints path of all gems in bundle sorted by name" do + bundle "show --paths" + + expect(out).to include(default_bundle_path("gems", "rake-10.0.2").to_s) + expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + + out_lines = out.split("\n") + expect(out_lines[0]).to eq("[DEPRECATED FOR 3.0] use `bundle list` instead of `bundle show --paths`") + + # Gem names are the last component of their path. + gem_list = out_lines[1..-1].map {|p| p.split("/").last } + expect(gem_list).to eq(gem_list.sort) + end + end + + it "prints path if gem exists in bundle (with --paths option)" do + bundle "show rails --paths" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "warns if path no longer exists on disk" do + FileUtils.rm_rf(default_bundle_path("gems", "rails-2.3.2")) + + bundle "show rails" + + expect(out).to match(/has been deleted/i). + and include(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints the path to the running bundler" do + bundle "show bundler" + expect(out).to eq(root.to_s) + end + + it "complains if gem not in bundle" do + bundle "show missing" + expect(out).to match(/could not find gem 'missing'/i) + end + + it "prints path of all gems in bundle sorted by name" do + bundle "show --paths" + + expect(out).to include(default_bundle_path("gems", "rake-10.0.2").to_s) + expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + + # Gem names are the last component of their path. + gem_list = out.split.map {|p| p.split("/").last } + expect(gem_list).to eq(gem_list.sort) + end + + it "prints summary of gems" do + bundle "show --verbose" + + loaded_bundler_spec = Bundler.load.specs["bundler"] + expected = if !loaded_bundler_spec.empty? + loaded_bundler_spec[0].homepage + else + "No website available." + end + + expect(out).to include("* actionmailer (2.3.2)") + expect(out).to include("\tSummary: This is just a fake gem for testing") + expect(out).to include("\tHomepage: #{expected}") + expect(out).to include("\tStatus: Up to date") + end + end + + context "with a git repo in the Gemfile" do + before :each do + @git = build_git "foo", "1.0" + end + + it "prints out git info" do + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + expect(the_bundle).to include_gems "foo 1.0" + + bundle :show + expect(out).to include("foo (1.0 #{@git.ref_for("master", 6)}") + end + + it "prints out branch names other than master" do + update_git "foo", :branch => "omg" do |s| + s.write "lib/foo.rb", "FOO = '1.0.omg'" + end + @revision = revision_for(lib_path("foo-1.0"))[0...6] + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" + G + expect(the_bundle).to include_gems "foo 1.0.omg" + + bundle :show + expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}") + end + + it "doesn't print the branch when tied to a ref" do + sha = revision_for(lib_path("foo-1.0")) + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}" + G + + bundle :show + expect(out).to include("foo (1.0 #{sha[0..6]})") + end + + it "handles when a version is a '-' prerelease", :rubygems => "2.1" do + @git = build_git("foo", "1.0.0-beta.1", :path => lib_path("foo")) + install_gemfile <<-G + gem "foo", "1.0.0-beta.1", :git => "#{lib_path("foo")}" + G + expect(the_bundle).to include_gems "foo 1.0.0.pre.beta.1" + + bundle! :show + expect(out).to include("foo (1.0.0.pre.beta.1") + end + end + + context "in a fresh gem in a blank git repo" do + before :each do + build_git "foo", :path => lib_path("foo") + in_app_root_custom lib_path("foo") + File.open("Gemfile", "w") {|f| f.puts "gemspec" } + sys_exec "rm -rf .git && git init" + end + + it "does not output git errors" do + bundle :show + expect(err).to lack_errors + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "foo" + G + + bundle "config auto_install 1" + bundle :show + expect(out).to include("Installing foo 1.0") + end + + context "with an invalid regexp for gem name" do + it "does not find the gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + invalid_regexp = "[]" + + bundle "show #{invalid_regexp}" + expect(out).to include("Could not find gem '#{invalid_regexp}'.") + end + end + + context "--outdated option" do + # Regression test for https://github.com/bundler/bundler/issues/5375 + before do + build_repo2 + end + + it "doesn't update gems to newer versions" do + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "rails" + G + + expect(the_bundle).to include_gem("rails 2.3.2") + + update_repo2 do + build_gem "rails", "3.0.0" do |s| + s.executables = "rails" + end + end + + bundle! "show --outdated" + + bundle! "install" + expect(the_bundle).to include_gem("rails 2.3.2") + end + end +end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb new file mode 100644 index 00000000000000..1effba6526c617 --- /dev/null +++ b/spec/bundler/commands/update_spec.rb @@ -0,0 +1,943 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + before :each do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + G + end + + describe "with no arguments", :bundler => "< 3" do + it "updates the entire bundle" do + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update" + expect(out).to include("Bundle updated!") + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + end + + it "doesn't delete the Gemfile.lock file if something goes wrong" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + exit! + G + bundle "update" + expect(bundled_app("Gemfile.lock")).to exist + end + end + + describe "with --all", :bundler => "3" do + it "updates the entire bundle" do + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle! "update", :all => true + expect(out).to include("Bundle updated!") + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + end + + it "doesn't delete the Gemfile.lock file if something goes wrong" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + exit! + G + bundle "update", :all => true + expect(bundled_app("Gemfile.lock")).to exist + end + end + + describe "with --gemfile" do + it "creates lock files based on the Gemfile name" do + gemfile bundled_app("OmgFile"), <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + bundle! "update --gemfile OmgFile", :all => bundle_update_requires_all? + + expect(bundled_app("OmgFile.lock")).to exist + end + end + + context "when update_requires_all_flag is set" do + before { bundle! "config update_requires_all_flag true" } + + it "errors when passed nothing" do + install_gemfile! "" + bundle :update + expect(out).to eq("To update everything, pass the `--all` flag.") + end + + it "errors when passed --all and another option" do + install_gemfile! "" + bundle "update --all foo" + expect(out).to eq("Cannot specify --all along with specific options.") + end + + it "updates everything when passed --all" do + install_gemfile! "" + bundle "update --all" + expect(out).to include("Bundle updated!") + end + end + + describe "--quiet argument" do + it "hides UI messages" do + bundle "update --quiet" + expect(out).not_to include("Bundle updated!") + end + end + + describe "with a top level dependency" do + it "unlocks all child dependencies that are unrelated to other locked dependencies" do + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update rack-obama" + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 2.3.5" + end + end + + describe "with an unknown dependency" do + it "should inform the user" do + bundle "update halting-problem-solver" + expect(out).to include "Could not find gem 'halting-problem-solver'" + end + it "should suggest alternatives" do + bundle "update active-support" + expect(out).to include "Did you mean activesupport?" + end + end + + describe "with a child dependency" do + it "should update the child dependency" do + update_repo2 + bundle "update rack" + expect(the_bundle).to include_gems "rack 1.2" + end + end + + describe "when a possible resolve requires an older version of a locked gem" do + context "and only_update_to_newer_versions is set" do + before do + bundle! "config only_update_to_newer_versions true" + end + + it "does not go to an older version" do + build_repo4 do + build_gem "tilt", "2.0.8" + build_gem "slim", "3.0.9" do |s| + s.add_dependency "tilt", [">= 1.3.3", "< 2.1"] + end + build_gem "slim_lint", "0.16.1" do |s| + s.add_dependency "slim", [">= 3.0", "< 5.0"] + end + build_gem "slim-rails", "0.2.1" do |s| + s.add_dependency "slim", ">= 0.9.2" + end + build_gem "slim-rails", "3.1.3" do |s| + s.add_dependency "slim", "~> 3.0" + end + end + + install_gemfile! <<-G + source "file:#{gem_repo4}" + gem "slim-rails" + gem "slim_lint" + G + + expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + + update_repo4 do + build_gem "slim", "4.0.0" do |s| + s.add_dependency "tilt", [">= 2.0.6", "< 2.1"] + end + end + + bundle! "update", :all => bundle_update_requires_all? + + expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + end + + it "should still downgrade if forced by the Gemfile" do + build_repo4 do + build_gem "a" + build_gem "b", "1.0" + build_gem "b", "2.0" + end + + install_gemfile! <<-G + source "file:#{gem_repo4}" + gem "a" + gem "b" + G + + expect(the_bundle).to include_gems("a 1.0", "b 2.0") + + gemfile <<-G + source "file://#{gem_repo4}" + gem "a" + gem "b", "1.0" + G + + bundle! "update b" + + expect(the_bundle).to include_gems("a 1.0", "b 1.0") + end + end + end + + describe "with --local option" do + it "doesn't hit repo2" do + FileUtils.rm_rf(gem_repo2) + + bundle "update --local --all" + expect(out).not_to include("Fetching source index") + end + end + + describe "with --group option" do + it "should update only specified group gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", :group => :development + gem "rack" + G + update_repo2 do + build_gem "activesupport", "3.0" + end + bundle "update --group development" + expect(the_bundle).to include_gems "activesupport 3.0" + expect(the_bundle).not_to include_gems "rack 1.2" + end + + context "when conservatively updating a group with non-group sub-deps" do + it "should update only specified group gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activemerchant", :group => :development + gem "activesupport" + G + update_repo2 do + build_gem "activemerchant", "2.0" + build_gem "activesupport", "3.0" + end + bundle "update --conservative --group development" + expect(the_bundle).to include_gems "activemerchant 2.0" + expect(the_bundle).not_to include_gems "activesupport 3.0" + end + end + + context "when there is a source with the same name as a gem in a group" do + before :each do + build_git "foo", :path => lib_path("activesupport") + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", :group => :development + gem "foo", :git => "#{lib_path("activesupport")}" + G + end + + it "should not update the gems from that source" do + update_repo2 { build_gem "activesupport", "3.0" } + update_git "foo", "2.0", :path => lib_path("activesupport") + + bundle "update --group development" + expect(the_bundle).to include_gems "activesupport 3.0" + expect(the_bundle).not_to include_gems "foo 2.0" + end + end + + context "when bundler itself is a transitive dependency" do + it "executes without error" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport", :group => :development + gem "rack" + G + update_repo2 do + build_gem "activesupport", "3.0" + end + bundle "update --group development" + expect(the_bundle).to include_gems "activesupport 2.3.5" + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" + expect(the_bundle).not_to include_gems "rack 1.2" + end + end + end + + describe "in a frozen bundle" do + it "should fail loudly", :bundler => "< 3" do + bundle! "install --deployment" + bundle "update", :all => bundle_update_requires_all? + + expect(last_command).to be_failure + expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m) + expect(out).to match(/freeze \nby running `bundle install --no-deployment`./m) + end + + it "should suggest different command when frozen is set globally", :bundler => "< 3" do + bundle! "config --global frozen 1" + bundle "update", :all => bundle_update_requires_all? + expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m). + and match(/freeze \nby running `bundle config --delete frozen`./m) + end + + it "should suggest different command when frozen is set globally", :bundler => "3" do + bundle! "config --global deployment true" + bundle "update", :all => bundle_update_requires_all? + expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m). + and match(/freeze \nby running `bundle config --delete deployment`./m) + end + end + + describe "with --source option" do + it "should not update gems not included in the source that happen to have the same name", :bundler => "< 3" do + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + update_repo2 { build_gem "activesupport", "3.0" } + + bundle! "update --source activesupport" + expect(the_bundle).to include_gem "activesupport 3.0" + end + + it "should not update gems not included in the source that happen to have the same name", :bundler => "3" do + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + update_repo2 { build_gem "activesupport", "3.0" } + + bundle! "update --source activesupport" + expect(the_bundle).not_to include_gem "activesupport 3.0" + end + + context "with unlock_source_unlocks_spec set to false" do + before { bundle! "config unlock_source_unlocks_spec false" } + + it "should not update gems not included in the source that happen to have the same name" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "update --source activesupport" + expect(the_bundle).not_to include_gems "activesupport 3.0" + end + end + end + + context "when there is a child dependency that is also in the gemfile" do + before do + build_repo2 do + build_gem "fred", "1.0" + build_gem "harry", "1.0" do |s| + s.add_dependency "fred" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "harry" + gem "fred" + G + end + + it "should not update the child dependencies of a gem that has the same name as the source", :bundler => "< 3" do + update_repo2 do + build_gem "fred", "2.0" + build_gem "harry", "2.0" do |s| + s.add_dependency "fred" + end + end + + bundle "update --source harry" + expect(the_bundle).to include_gems "harry 2.0" + expect(the_bundle).to include_gems "fred 1.0" + end + + it "should not update the child dependencies of a gem that has the same name as the source", :bundler => "3" do + update_repo2 do + build_gem "fred", "2.0" + build_gem "harry", "2.0" do |s| + s.add_dependency "fred" + end + end + + bundle "update --source harry" + expect(the_bundle).to include_gems "harry 1.0", "fred 1.0" + end + end + + context "when there is a child dependency that appears elsewhere in the dependency graph" do + before do + build_repo2 do + build_gem "fred", "1.0" do |s| + s.add_dependency "george" + end + build_gem "george", "1.0" + build_gem "harry", "1.0" do |s| + s.add_dependency "george" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "harry" + gem "fred" + G + end + + it "should not update the child dependencies of a gem that has the same name as the source", :bundler => "< 3" do + update_repo2 do + build_gem "george", "2.0" + build_gem "harry", "2.0" do |s| + s.add_dependency "george" + end + end + + bundle "update --source harry" + expect(the_bundle).to include_gems "harry 2.0" + expect(the_bundle).to include_gems "fred 1.0" + expect(the_bundle).to include_gems "george 1.0" + end + + it "should not update the child dependencies of a gem that has the same name as the source", :bundler => "3" do + update_repo2 do + build_gem "george", "2.0" + build_gem "harry", "2.0" do |s| + s.add_dependency "george" + end + end + + bundle "update --source harry" + expect(the_bundle).to include_gems "harry 1.0", "fred 1.0", "george 1.0" + end + end +end + +RSpec.describe "bundle update in more complicated situations" do + before :each do + build_repo2 + end + + it "will eagerly unlock dependencies of a specified gem" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "thin" + gem "rack-obama" + G + + update_repo2 do + build_gem "thin", "2.0" do |s| + s.add_dependency "rack" + end + end + + bundle "update thin" + expect(the_bundle).to include_gems "thin 2.0", "rack 1.2", "rack-obama 1.0" + end + + it "will warn when some explicitly updated gems are not updated" do + install_gemfile! <<-G + source "file:#{gem_repo2}" + + gem "thin" + gem "rack-obama" + G + + update_repo2 do + build_gem("thin", "2.0") {|s| s.add_dependency "rack" } + build_gem "rack", "10.0" + end + + bundle! "update thin rack-obama" + expect(last_command.stdboth).to include "Bundler attempted to update rack-obama but its version stayed the same" + expect(the_bundle).to include_gems "thin 2.0", "rack 10.0", "rack-obama 1.0" + end + + it "will update only from pinned source" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + source "file://#{gem_repo1}" do + gem "thin" + end + G + + update_repo2 do + build_gem "thin", "2.0" + end + + bundle "update" + expect(the_bundle).to include_gems "thin 1.0" + end + + context "when the lockfile is for a different platform" do + before do + build_repo4 do + build_gem("a", "0.9") + build_gem("a", "0.9") {|s| s.platform = "java" } + build_gem("a", "1.1") + build_gem("a", "1.1") {|s| s.platform = "java" } + end + + gemfile <<-G + source "file://#{gem_repo4}" + gem "a" + G + + lockfile <<-L + GEM + remote: file://#{gem_repo4} + specs: + a (0.9-java) + + PLATFORMS + java + + DEPENDENCIES + a + L + + simulate_platform linux + end + + it "allows updating" do + bundle! :update, :all => true + expect(the_bundle).to include_gem "a 1.1" + end + + it "allows updating a specific gem" do + bundle! "update a" + expect(the_bundle).to include_gem "a 1.1" + end + end +end + +RSpec.describe "bundle update without a Gemfile.lock" do + it "should not explode" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "rack", "1.0" + G + + bundle "update", :all => bundle_update_requires_all? + + expect(the_bundle).to include_gems "rack 1.0.0" + end +end + +RSpec.describe "bundle update when a gem depends on a newer version of bundler" do + before(:each) do + build_repo2 do + build_gem "rails", "3.0.1" do |s| + s.add_dependency "bundler", Bundler::VERSION.succ + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0.1" + G + end + + it "should explain that bundler conflicted", :bundler => "< 3" do + bundle "update", :all => bundle_update_requires_all? + expect(last_command.stdboth).not_to match(/in snapshot/i) + expect(last_command.bundler_err).to match(/current Bundler version/i). + and match(/perhaps you need to update bundler/i) + end + + it "should warn that the newer version of Bundler would conflict", :bundler => "3" do + bundle! "update", :all => true + expect(last_command.bundler_err).to include("rails (3.0.1) has dependency bundler"). + and include("so the dependency is being ignored") + expect(the_bundle).to include_gem "rails 3.0.1" + end +end + +RSpec.describe "bundle update" do + it "shows the previous version of the gem when updated from rubygems source", :bundler => "< 3" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + + bundle "update", :all => bundle_update_requires_all? + expect(out).to include("Using activesupport 2.3.5") + + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update", :all => bundle_update_requires_all? + expect(out).to include("Installing activesupport 3.0 (was 2.3.5)") + end + + context "with suppress_install_using_messages set" do + before { bundle! "config suppress_install_using_messages true" } + + it "only prints `Using` for versions that have changed" do + build_repo4 do + build_gem "bar" + build_gem "foo" + end + + install_gemfile! <<-G + source "file://#{gem_repo4}" + gem "bar" + gem "foo" + G + + bundle! "update", :all => bundle_update_requires_all? + out.gsub!(/RubyGems [\d\.]+ is not threadsafe.*\n?/, "") + expect(out).to include "Resolving dependencies...\nBundle updated!" + + update_repo4 do + build_gem "foo", "2.0" + end + + bundle! "update", :all => bundle_update_requires_all? + out.sub!("Removing foo (1.0)\n", "") + out.gsub!(/RubyGems [\d\.]+ is not threadsafe.*\n?/, "") + expect(out).to include strip_whitespace(<<-EOS).strip + Resolving dependencies... + Fetching foo 2.0 (was 1.0) + Installing foo 2.0 (was 1.0) + Bundle updated + EOS + end + end + + it "shows error message when Gemfile.lock is not preset and gem is specified" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + + bundle "update nonexisting" + expect(out).to include("This Bundle hasn't been installed yet. Run `bundle install` to update and install the bundled gems.") + expect(exitstatus).to eq(22) if exitstatus + end +end + +RSpec.describe "bundle update --ruby" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.3' + ::RUBY_PATCHLEVEL = 100 + ruby '~> 2.1.0' + G + bundle "update --ruby" + end + + context "when the Gemfile removes the ruby" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.4' + ::RUBY_PATCHLEVEL = 222 + G + end + it "removes the Ruby from the Gemfile.lock" do + bundle "update --ruby" + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when the Gemfile specified an updated Ruby version" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.4' + ::RUBY_PATCHLEVEL = 222 + ruby '~> 2.1.0' + G + end + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + RUBY VERSION + ruby 2.1.4p222 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when a different Ruby is being used than has been versioned" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.2.2' + ::RUBY_PATCHLEVEL = 505 + ruby '~> 2.1.0' + G + end + it "shows a helpful error message" do + bundle "update --ruby" + + expect(out).to include("Your Ruby version is 2.2.2, but your Gemfile specified ~> 2.1.0") + end + end + + context "when updating Ruby version and Gemfile `ruby`" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '1.8.3' + ::RUBY_PATCHLEVEL = 55 + ruby '~> 1.8.0' + G + end + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + RUBY VERSION + ruby 1.8.3p55 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end +end + +RSpec.describe "bundle update --bundler" do + it "updates the bundler version in the lockfile without re-resolving" do + build_repo4 do + build_gem "rack", "1.0" + end + + install_gemfile! <<-G + source "file:#{gem_repo4}" + gem "rack" + G + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') + + FileUtils.rm_r gem_repo4 + + bundle! :update, :bundler => true, :verbose => true + expect(the_bundle).to include_gem "rack 1.0" + + expect(the_bundle.locked_gems.bundler_version).to eq v(Bundler::VERSION) + end +end + +# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that. +RSpec.describe "bundle update conservative" do + context "patch and minor options" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] + build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo' + gem 'qux' + G + end + + context "patch preferred" do + it "single gem updates dependent gem to minor" do + bundle! "update --patch foo" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" + end + + it "update all" do + bundle! "update --patch", :all => bundle_update_requires_all? + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" + end + end + + context "minor preferred" do + it "single gem updates dependent gem to major" do + bundle! "update --minor foo" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" + end + end + + context "strict" do + it "patch preferred" do + bundle! "update --patch foo bar --strict" + + expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0" + end + + it "minor preferred" do + bundle! "update --minor --strict", :all => bundle_update_requires_all? + + expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0" + end + end + end + + context "eager unlocking" do + before do + build_repo4 do + build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s| + s.add_dependency "isolated_dep", "~> 2.0" + end + build_gem "isolated_dep", %w[2.0.1 2.0.2] + + build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_dep", %w[5.0.1 5.0.2] + end + + gemfile <<-G + source "file://#{gem_repo4}" + gem 'isolated_owner' + + gem 'shared_owner_a' + gem 'shared_owner_b' + G + + lockfile <<-L + GEM + remote: file://#{gem_repo4} + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.1) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.1) + shared_dep (~> 5.0) + shared_owner_b (4.0.1) + shared_dep (~> 5.0) + + PLATFORMS + ruby + + DEPENDENCIES + shared_owner_a + shared_owner_b + isolated_owner + + BUNDLED WITH + 1.13.0 + L + end + + it "should eagerly unlock isolated dependency" do + bundle "update isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1" + end + + it "should eagerly unlock shared dependency" do + bundle "update shared_owner_a" + + expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should not eagerly unlock with --conservative" do + bundle "update --conservative shared_owner_a isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should match bundle install conservative update behavior when not eagerly unlocking" do + gemfile <<-G + source "file://#{gem_repo4}" + gem 'isolated_owner', '1.0.2' + + gem 'shared_owner_a', '3.0.2' + gem 'shared_owner_b' + G + + bundle "install" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + end + + context "error handling" do + before do + gemfile "" + end + + it "raises if too many flags are provided" do + bundle "update --patch --minor", :all => bundle_update_requires_all? + + expect(last_command.bundler_err).to eq "Provide only one of the following options: minor, patch" + end + end +end diff --git a/spec/bundler/commands/version_spec.rb b/spec/bundler/commands/version_spec.rb new file mode 100644 index 00000000000000..66c79303974d8f --- /dev/null +++ b/spec/bundler/commands/version_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe "bundle version" do + context "with -v" do + it "outputs the version", :bundler => "< 3" do + bundle! "-v" + expect(out).to eq("Bundler version #{Bundler::VERSION}") + end + + it "outputs the version", :bundler => "3" do + bundle! "-v" + expect(out).to eq(Bundler::VERSION) + end + end + + context "with --version" do + it "outputs the version", :bundler => "< 3" do + bundle! "--version" + expect(out).to eq("Bundler version #{Bundler::VERSION}") + end + + it "outputs the version", :bundler => "3" do + bundle! "--version" + expect(out).to eq(Bundler::VERSION) + end + end + + context "with version" do + it "outputs the version with build metadata", :bundler => "< 3" do + bundle! "version" + expect(out).to match(/\ABundler version #{Regexp.escape(Bundler::VERSION)} \(\d{4}-\d{2}-\d{2} commit [a-fA-F0-9]{7,}\)\z/) + end + + it "outputs the version with build metadata", :bundler => "3" do + bundle! "version" + expect(out).to match(/\A#{Regexp.escape(Bundler::VERSION)} \(\d{4}-\d{2}-\d{2} commit [a-fA-F0-9]{7,}\)\z/) + end + end +end diff --git a/spec/bundler/commands/viz_spec.rb b/spec/bundler/commands/viz_spec.rb new file mode 100644 index 00000000000000..61414956a99e77 --- /dev/null +++ b/spec/bundler/commands/viz_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +RSpec.describe "bundle viz", :ruby => "1.9.3", :bundler => "< 3", :if => Bundler.which("dot") do + let(:ruby_graphviz) do + graphviz_glob = base_system_gems.join("cache/ruby-graphviz*") + Pathname.glob(graphviz_glob).first + end + + before do + system_gems ruby_graphviz + end + + it "graphs gems from the Gemfile" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + bundle! "viz" + expect(out).to include("gem_graph.png") + + bundle! "viz", :format => "debug" + expect(out).to eq(strip_whitespace(<<-DOT).strip) + digraph Gemfile { + concentrate = "true"; + normalize = "true"; + nodesep = "0.55"; + edge[ weight = "2"]; + node[ fontname = "Arial, Helvetica, SansSerif"]; + edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; + default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; + rack [style = "filled", fillcolor = "#B9B9D5", label = "rack"]; + default -> rack [constraint = "false"]; + "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama"]; + default -> "rack-obama" [constraint = "false"]; + "rack-obama" -> rack; + } + debugging bundle viz... + DOT + end + + it "graphs gems that are prereleases" do + build_repo2 do + build_gem "rack", "1.3.pre" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", "= 1.3.pre" + gem "rack-obama" + G + + bundle! "viz" + expect(out).to include("gem_graph.png") + + bundle! "viz", :format => :debug, :version => true + expect(out).to eq(strip_whitespace(<<-EOS).strip) + digraph Gemfile { + concentrate = "true"; + normalize = "true"; + nodesep = "0.55"; + edge[ weight = "2"]; + node[ fontname = "Arial, Helvetica, SansSerif"]; + edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; + default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; + rack [style = "filled", fillcolor = "#B9B9D5", label = "rack\\n1.3.pre"]; + default -> rack [constraint = "false"]; + "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama\\n1.0"]; + default -> "rack-obama" [constraint = "false"]; + "rack-obama" -> rack; + } + debugging bundle viz... + EOS + end + + context "with another gem that has a graphviz file" do + before do + build_repo4 do + build_gem "graphviz", "999" do |s| + s.write("lib/graphviz.rb", "abort 'wrong graphviz gem loaded'") + end + end + + system_gems ruby_graphviz, "graphviz-999", :gem_repo => gem_repo4 + end + + it "loads the correct ruby-graphviz gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + bundle! "viz", :format => "debug" + expect(out).to eq(strip_whitespace(<<-DOT).strip) + digraph Gemfile { + concentrate = "true"; + normalize = "true"; + nodesep = "0.55"; + edge[ weight = "2"]; + node[ fontname = "Arial, Helvetica, SansSerif"]; + edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; + default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; + rack [style = "filled", fillcolor = "#B9B9D5", label = "rack"]; + default -> rack [constraint = "false"]; + "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama"]; + default -> "rack-obama" [constraint = "false"]; + "rack-obama" -> rack; + } + debugging bundle viz... + DOT + end + end + + context "--without option" do + it "one group" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + + group :rails do + gem "rails" + end + G + + bundle! "viz --without=rails" + expect(out).to include("gem_graph.png") + end + + it "two groups" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + + group :rack do + gem "rack" + end + + group :rails do + gem "rails" + end + G + + bundle! "viz --without=rails:rack" + expect(out).to include("gem_graph.png") + end + end +end diff --git a/spec/bundler/install/allow_offline_install_spec.rb b/spec/bundler/install/allow_offline_install_spec.rb new file mode 100644 index 00000000000000..d4bb595771dd3e --- /dev/null +++ b/spec/bundler/install/allow_offline_install_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with :allow_offline_install" do + before do + bundle "config allow_offline_install true" + end + + context "with no cached data locally" do + it "still installs" do + install_gemfile! <<-G, :artifice => "compact_index" + source "http://testgemserver.local" + gem "rack-obama" + G + expect(the_bundle).to include_gem("rack 1.0") + end + + it "still fails when the network is down" do + install_gemfile <<-G, :artifice => "fail" + source "http://testgemserver.local" + gem "rack-obama" + G + expect(out).to include("Could not reach host testgemserver.local.") + expect(the_bundle).to_not be_locked + end + end + + context "with cached data locally" do + it "will install from the compact index" do + system_gems ["rack-1.0.0"], :path => :bundle_path + + bundle! "config clean false" + install_gemfile! <<-G, :artifice => "compact_index" + source "http://testgemserver.local" + gem "rack-obama" + gem "rack", "< 1.0" + G + + expect(the_bundle).to include_gems("rack-obama 1.0", "rack 0.9.1") + + gemfile <<-G + source "http://testgemserver.local" + gem "rack-obama" + G + + bundle! :update, :artifice => "fail", :all => true + expect(last_command.stdboth).to include "Using the cached data for the new index because of a network error" + + expect(the_bundle).to include_gems("rack-obama 1.0", "rack 1.0.0") + end + + def break_git_remote_ops! + FileUtils.mkdir_p(tmp("broken_path")) + File.open(tmp("broken_path/git"), "w", 0o755) do |f| + f.puts strip_whitespace(<<-RUBY) + #!/usr/bin/env ruby + if %w(fetch --force --quiet --tags refs/heads/*:refs/heads/*).-(ARGV).empty? || %w(clone --bare --no-hardlinks --quiet).-(ARGV).empty? + warn "git remote ops have been disabled" + exit 1 + end + ENV["PATH"] = ENV["PATH"].sub(/^.*?:/, "") + exec("git", *ARGV) + RUBY + end + + old_path = ENV["PATH"] + ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}" + yield if block_given? + ensure + ENV["PATH"] = old_path if block_given? + end + + it "will install from a cached git repo" do + git = build_git "a", "1.0.0", :path => lib_path("a") + update_git("a", :path => git.path, :branch => "new_branch") + install_gemfile! <<-G + gem "a", :git => #{git.path.to_s.dump} + G + + break_git_remote_ops! { bundle! :update, :all => true } + expect(out).to include("Using cached git data because of network errors") + expect(the_bundle).to be_locked + + break_git_remote_ops! do + install_gemfile! <<-G + gem "a", :git => #{git.path.to_s.dump}, :branch => "new_branch" + G + end + expect(out).to include("Using cached git data because of network errors") + expect(the_bundle).to be_locked + end + end +end diff --git a/spec/bundler/install/binstubs_spec.rb b/spec/bundler/install/binstubs_spec.rb new file mode 100644 index 00000000000000..f04d3fe654805c --- /dev/null +++ b/spec/bundler/install/binstubs_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install", :bundler => "< 3" do + describe "when system_bindir is set" do + # On OS X, Gem.bindir defaults to /usr/bin, so system_bindir is useful if + # you want to avoid sudo installs for system gems with OS X's default ruby + it "overrides Gem.bindir" do + expect(Pathname.new("/usr/bin")).not_to be_writable unless Process.euid == 0 + gemfile <<-G + require 'rubygems' + def Gem.bindir; "/usr/bin"; end + source "file://#{gem_repo1}" + gem "rack" + G + + config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s + bundle :install + expect(the_bundle).to include_gems "rack 1.0.0" + expect(system_gem_path("altbin/rackup")).to exist + end + end + + describe "when multiple gems contain the same exe", :bundler => "< 3" do + before do + build_repo2 do + build_gem "fake", "14" do |s| + s.executables = "rackup" + end + end + + install_gemfile <<-G, :binstubs => true + source "file://#{gem_repo2}" + gem "fake" + gem "rack" + G + end + + it "loads the correct spec's executable" do + gembin("rackup") + expect(out).to eq("1.2") + end + end +end diff --git a/spec/bundler/install/bundler_spec.rb b/spec/bundler/install/bundler_spec.rb new file mode 100644 index 00000000000000..42863ed89b407d --- /dev/null +++ b/spec/bundler/install/bundler_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + describe "with bundler dependencies" do + before(:each) do + build_repo2 do + build_gem "rails", "3.0" do |s| + s.add_dependency "bundler", ">= 0.9.0.pre" + end + build_gem "bundler", "0.9.1" + build_gem "bundler", Bundler::VERSION + end + end + + it "are forced to the current bundler version" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" + end + + it "are not added if not already present" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + expect(the_bundle).not_to include_gems "bundler #{Bundler::VERSION}" + end + + it "causes a conflict if explicitly requesting a different version" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + gem "bundler", "0.9.2" + G + + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Bundler could not find compatible versions for gem "bundler": + In Gemfile: + bundler (= 0.9.2) + + Current Bundler version: + bundler (#{Bundler::VERSION}) + This Gemfile requires a different version of Bundler. + Perhaps you need to update Bundler by running `gem install bundler`? + + Could not find gem 'bundler (= 0.9.2)' in any + E + expect(last_command.bundler_err).to include(nice_error) + end + + it "works for gems with multiple versions in its dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "multiple_versioned_deps" + G + + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "multiple_versioned_deps" + gem "rack" + G + + expect(the_bundle).to include_gems "multiple_versioned_deps 1.0.0" + end + + it "includes bundler in the bundle when it's a child dependency" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError; puts 'FAIL'; end" + expect(out).to eq("WIN") + end + + it "allows gem 'bundler' when Bundler is not in the Gemfile or its dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + + run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError => e; puts e.backtrace; end" + expect(out).to eq("WIN") + end + + it "causes a conflict if child dependencies conflict" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activemerchant" + gem "rails_fail" + G + + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Bundler could not find compatible versions for gem "activesupport": + In Gemfile: + activemerchant was resolved to 1.0, which depends on + activesupport (>= 2.0.0) + + rails_fail was resolved to 1.0, which depends on + activesupport (= 1.2.3) + E + expect(last_command.bundler_err).to include(nice_error) + end + + it "causes a conflict if a child dependency conflicts with the Gemfile" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails_fail" + gem "activesupport", "2.3.5" + G + + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Bundler could not find compatible versions for gem "activesupport": + In Gemfile: + activesupport (= 2.3.5) + + rails_fail was resolved to 1.0, which depends on + activesupport (= 1.2.3) + E + expect(last_command.bundler_err).to include(nice_error) + end + + it "can install dependencies with newer bundler version with system gems", :ruby => "> 2" do + bundle! "config path.system true" + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + simulate_bundler_version "99999999.99.1" + + bundle! "check", :env => { "BUNDLER_SPEC_IGNORE_COMPATIBILITY_GUARD" => "1" } + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "can install dependencies with newer bundler version with a local path", :ruby => "> 2" do + bundle! "config path .bundle" + bundle! "config global_path_appends_ruby_scope true" + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + simulate_bundler_version "99999999.99.1" + + bundle! "check", :env => { "BUNDLER_SPEC_IGNORE_COMPATIBILITY_GUARD" => "1" } + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + context "with allow_bundler_dependency_conflicts set" do + before { bundle! "config allow_bundler_dependency_conflicts true" } + + it "are forced to the current bundler version with warnings when no compatible version is found" do + build_repo4 do + build_gem "requires_nonexistant_bundler" do |s| + s.add_runtime_dependency "bundler", "99.99.99.99" + end + end + + install_gemfile! <<-G + source "file://#{gem_repo4}" + gem "requires_nonexistant_bundler" + G + + expect(out).to include "requires_nonexistant_bundler (1.0) has dependency bundler (= 99.99.99.99), " \ + "which is unsatisfied by the current bundler version #{Bundler::VERSION}, so the dependency is being ignored" + + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}", "requires_nonexistant_bundler 1.0" + end + end + end +end diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb new file mode 100644 index 00000000000000..ec72ff69fc46d5 --- /dev/null +++ b/spec/bundler/install/deploy_spec.rb @@ -0,0 +1,423 @@ +# frozen_string_literal: true + +RSpec.describe "install with --deployment or --frozen" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "with CLI flags", :bundler => "< 3" do + it "fails without a lockfile and says that --deployment requires a lock" do + bundle "install --deployment" + expect(out).to include("The --deployment flag requires a Gemfile.lock") + end + + it "fails without a lockfile and says that --frozen requires a lock" do + bundle "install --frozen" + expect(out).to include("The --frozen flag requires a Gemfile.lock") + end + + it "disallows --deployment --system" do + bundle "install --deployment --system" + expect(out).to include("You have specified both --deployment") + expect(out).to include("Please choose only one option") + expect(exitstatus).to eq(15) if exitstatus + end + + it "disallows --deployment --path --system" do + bundle "install --deployment --path . --system" + expect(out).to include("You have specified both --path") + expect(out).to include("as well as --system") + expect(out).to include("Please choose only one option") + expect(exitstatus).to eq(15) if exitstatus + end + + it "works after you try to deploy without a lock" do + bundle "install --deployment" + bundle! :install + expect(the_bundle).to include_gems "rack 1.0" + end + end + + it "still works if you are not in the app directory and specify --gemfile" do + bundle "install" + Dir.chdir tmp do + simulate_new_machine + bundle! :install, + forgotten_command_line_options(:gemfile => "#{tmp}/bundled_app/Gemfile", + :deployment => true, + :path => "vendor/bundle") + end + expect(the_bundle).to include_gems "rack 1.0" + end + + it "works if you exclude a group with a git gem" do + build_git "foo" + gemfile <<-G + group :test do + gem "foo", :git => "#{lib_path("foo-1.0")}" + end + G + bundle :install + bundle! :install, forgotten_command_line_options(:deployment => true, :without => "test") + end + + it "works when you bundle exec bundle", :ruby_repo do + bundle :install + bundle "install --deployment" + bundle! "exec bundle check" + end + + it "works when using path gems from the same path and the version is specified" do + build_lib "foo", :path => lib_path("nested/foo") + build_lib "bar", :path => lib_path("nested/bar") + gemfile <<-G + gem "foo", "1.0", :path => "#{lib_path("nested")}" + gem "bar", :path => "#{lib_path("nested")}" + G + + bundle! :install + bundle! :install, forgotten_command_line_options(:deployment => true) + end + + it "works when there are credentials in the source URL" do + install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + source "http://user:pass@localgemserver.test/" + + gem "rack-obama", ">= 1.0" + G + + bundle! :install, forgotten_command_line_options(:deployment => true).merge(:artifice => "endpoint_strict_basic_authentication") + end + + it "works with sources given by a block" do + install_gemfile! <<-G + source "file://#{gem_repo1}" do + gem "rack" + end + G + + bundle! :install, forgotten_command_line_options(:deployment => true) + + expect(the_bundle).to include_gems "rack 1.0" + end + + describe "with an existing lockfile" do + before do + bundle "install" + end + + it "works with the --deployment flag if you didn't change anything", :bundler => "< 3" do + bundle! "install --deployment" + end + + it "works with the --frozen flag if you didn't change anything", :bundler => "< 3" do + bundle! "install --frozen" + end + + it "works with BUNDLE_FROZEN if you didn't change anything" do + bundle! :install, :env => { "BUNDLE_FROZEN" => "true" } + end + + it "explodes with the --deployment flag if you make a change and don't check in the lockfile" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + bundle :install, forgotten_command_line_options(:deployment => true) + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "works if a path gem is missing but is in a without group" do + build_lib "path_gem" + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development + G + expect(the_bundle).to include_gems "path_gem 1.0" + FileUtils.rm_r lib_path("path_gem-1.0") + + bundle! :install, forgotten_command_line_options(:path => ".bundle", :without => "development", :deployment => true).merge(:env => { :DEBUG => "1" }) + run! "puts :WIN" + expect(out).to eq("WIN") + end + + it "explodes if a path gem is missing" do + build_lib "path_gem" + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development + G + expect(the_bundle).to include_gems "path_gem 1.0" + FileUtils.rm_r lib_path("path_gem-1.0") + + bundle :install, forgotten_command_line_options(:path => ".bundle", :deployment => true) + expect(out).to include("The path `#{lib_path("path_gem-1.0")}` does not exist.") + end + + it "can have --frozen set via an environment variable", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + ENV["BUNDLE_FROZEN"] = "1" + bundle "install" + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "can have --deployment set via an environment variable" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + ENV["BUNDLE_DEPLOYMENT"] = "true" + bundle "install" + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "can have --frozen set to false via an environment variable" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + ENV["BUNDLE_FROZEN"] = "false" + ENV["BUNDLE_DEPLOYMENT"] = "false" + bundle "install" + expect(out).not_to include("deployment mode") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("* rack-obama") + end + + it "explodes with the --frozen flag if you make a change and don't check in the lockfile", :bundler => "< 2" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama", "1.1" + G + + bundle :install, forgotten_command_line_options(:frozen => true) + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama (= 1.1)") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you remove a gem and don't check in the lockfile" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + bundle :install, forgotten_command_line_options(:deployment => true) + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile:\n* activesupport\n\n") + expect(out).to include("You have deleted from the Gemfile:\n* rack") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you add a source" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "git://hubz.com" + G + + bundle :install, forgotten_command_line_options(:deployment => true) + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile:\n* source: git://hubz.com (at master)") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you unpin a source" do + build_git "rack" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-1.0")}" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, forgotten_command_line_options(:deployment => true) + expect(out).to include("deployment mode") + expect(out).to include("You have deleted from the Gemfile:\n* source: #{lib_path("rack-1.0")} (at master@#{revision_for(lib_path("rack-1.0"))[0..6]}") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you unpin a source, leaving it pinned somewhere else" do + build_lib "foo", :path => lib_path("rack/foo") + build_git "rack", :path => lib_path("rack") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack")}" + gem "foo", :git => "#{lib_path("rack")}" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "foo", :git => "#{lib_path("rack")}" + G + + bundle :install, forgotten_command_line_options(:deployment => true) + expect(out).to include("deployment mode") + expect(out).to include("You have changed in the Gemfile:\n* rack from `no specified source` to `#{lib_path("rack")} (at master@#{revision_for(lib_path("rack"))[0..6]})`") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("You have deleted from the Gemfile") + end + + context "when replacing a host with the same host with credentials" do + let(:success_message) do + if Bundler.bundler_major_version < 3 + "Could not reach host localgemserver.test" + else + "Bundle complete!" + end + end + + before do + install_gemfile <<-G + source "http://user_name:password@localgemserver.test/" + gem "rack" + G + + lockfile <<-G + GEM + remote: http://localgemserver.test/ + specs: + rack (1.0.0) + + PLATFORMS + #{local} + + DEPENDENCIES + rack + G + end + + it "prevents the replace by default" do + bundle :install, forgotten_command_line_options(:deployment => true) + + expect(out).to match(/The list of sources changed/) + end + + context "when allow_deployment_source_credential_changes is true" do + before { bundle! "config allow_deployment_source_credential_changes true" } + + it "allows the replace" do + bundle :install, forgotten_command_line_options(:deployment => true) + + expect(out).to match(/#{success_message}/) + end + end + + context "when allow_deployment_source_credential_changes is false" do + before { bundle! "config allow_deployment_source_credential_changes false" } + + it "prevents the replace" do + bundle :install, forgotten_command_line_options(:deployment => true) + + expect(out).to match(/The list of sources changed/) + end + end + + context "when BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES env var is true" do + before { ENV["BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES"] = "true" } + + it "allows the replace" do + bundle :install, forgotten_command_line_options(:deployment => true) + + expect(out).to match(/#{success_message}/) + end + end + + context "when BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES env var is false" do + before { ENV["BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES"] = "false" } + + it "prevents the replace" do + bundle :install, forgotten_command_line_options(:deployment => true) + + expect(out).to match(/The list of sources changed/) + end + end + end + + it "remembers that the bundle is frozen at runtime" do + bundle! :lock + + bundle! "config deployment true" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + gem "rack-obama" + G + + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(err).to include strip_whitespace(<<-E).strip +The dependencies in your gemfile changed + +You have added to the Gemfile: +* rack (= 1.0.0) +* rack-obama + +You have deleted from the Gemfile: +* rack + E + end + end + + context "with path in Gemfile and packed" do + it "works fine after bundle package and bundle install --local" do + build_lib "foo", :path => lib_path("foo") + install_gemfile! <<-G + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle! :install + expect(the_bundle).to include_gems "foo 1.0" + bundle! :package, forgotten_command_line_options([:all, :cache_all] => true) + expect(bundled_app("vendor/cache/foo")).to be_directory + + bundle! "install --local" + expect(out).to include("Updating files in vendor/cache") + + simulate_new_machine + bundle! "install --verbose", forgotten_command_line_options(:deployment => true) + expect(out).not_to include("You are trying to install in deployment mode after changing your Gemfile") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).to include("vendor/cache/foo") + expect(the_bundle).to include_gems "foo 1.0" + end + end +end diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb new file mode 100644 index 00000000000000..b4cdf138571567 --- /dev/null +++ b/spec/bundler/install/failure_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "installing a gem fails" do + it "prints out why that gem was being installed" do + build_repo2 do + build_gem "activesupport", "2.3.2" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + abort "make installing activesupport-2.3.2 fail" + end + RUBY + end + end + + install_gemfile <<-G + source "file:\/\/localhost#{gem_repo2}" + gem "rails" + G + expect(last_command.bundler_err).to end_with(normalize_uri_file(<<-M.strip)) +An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. +Make sure that `gem install activesupport -v '2.3.2' --source 'file://localhost#{gem_repo2}/'` succeeds before bundling. + +In Gemfile: + rails was resolved to 2.3.2, which depends on + actionmailer was resolved to 2.3.2, which depends on + activesupport + M + end + + context "when installing a git gem" do + it "does not tell the user to run 'gem install'" do + build_git "activesupport", "2.3.2", :path => lib_path("activesupport") do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + abort "make installing activesupport-2.3.2 fail" + end + RUBY + end + + install_gemfile <<-G + source "file:\/\/localhost#{gem_repo1}" + gem "rails" + gem "activesupport", :git => "#{lib_path("activesupport")}" + G + + expect(last_command.bundler_err).to end_with(<<-M.strip) +An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. + +In Gemfile: + rails was resolved to 2.3.2, which depends on + actionmailer was resolved to 2.3.2, which depends on + activesupport + M + end + end + + context "when installing a gem using a git block" do + it "does not tell the user to run 'gem install'" do + build_git "activesupport", "2.3.2", :path => lib_path("activesupport") do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + abort "make installing activesupport-2.3.2 fail" + end + RUBY + end + + install_gemfile <<-G + source "file:\/\/localhost#{gem_repo1}" + gem "rails" + + git "#{lib_path("activesupport")}" do + gem "activesupport" + end + G + + expect(last_command.bundler_err).to end_with(<<-M.strip) +An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. + + +In Gemfile: + rails was resolved to 2.3.2, which depends on + actionmailer was resolved to 2.3.2, which depends on + activesupport + M + end + end + + it "prints out the hint for the remote source when available" do + build_repo2 do + build_gem "activesupport", "2.3.2" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + abort "make installing activesupport-2.3.2 fail" + end + RUBY + end + end + + build_repo4 do + build_gem "a" + end + + install_gemfile <<-G + source "file:\/\/localhost#{gem_repo4}" + source "file:\/\/localhost#{gem_repo2}" do + gem "rails" + end + G + expect(last_command.bundler_err).to end_with(normalize_uri_file(<<-M.strip)) +An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. +Make sure that `gem install activesupport -v '2.3.2' --source 'file://localhost#{gem_repo2}/'` succeeds before bundling. + +In Gemfile: + rails was resolved to 2.3.2, which depends on + actionmailer was resolved to 2.3.2, which depends on + activesupport + M + end + end +end diff --git a/spec/bundler/install/gemfile/eval_gemfile_spec.rb b/spec/bundler/install/gemfile/eval_gemfile_spec.rb new file mode 100644 index 00000000000000..035d3692aa19c7 --- /dev/null +++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with gemfile that uses eval_gemfile" do + before do + build_lib("gunks", :path => bundled_app.join("gems/gunks")) do |s| + s.name = "gunks" + s.version = "0.0.1" + end + end + + context "eval-ed Gemfile points to an internal gemspec" do + before do + create_file "Gemfile-other", <<-G + gemspec :path => 'gems/gunks' + G + end + + it "installs the gemspec specified gem" do + install_gemfile <<-G + eval_gemfile 'Gemfile-other' + G + expect(out).to include("Resolving dependencies") + expect(out).to include("Bundle complete") + + expect(the_bundle).to include_gem "gunks 0.0.1", :source => "path@#{bundled_app("gems", "gunks")}" + end + end + + context "eval-ed Gemfile has relative-path gems" do + before do + build_lib("a", :path => "gems/a") + create_file "nested/Gemfile-nested", <<-G + gem "a", :path => "../gems/a" + G + + gemfile <<-G + eval_gemfile "nested/Gemfile-nested" + G + end + + it "installs the path gem" do + bundle! :install + expect(the_bundle).to include_gem("a 1.0") + end + + # Make sure that we are properly comparing path based gems between the + # parsed lockfile and the evaluated gemfile. + it "bundles with --deployment" do + bundle! :install + bundle! :install, forgotten_command_line_options(:deployment => true) + end + end + + context "Gemfile uses gemspec paths after eval-ing a Gemfile" do + before { create_file "other/Gemfile-other" } + + it "installs the gemspec specified gem" do + install_gemfile <<-G + eval_gemfile 'other/Gemfile-other' + gemspec :path => 'gems/gunks' + G + expect(out).to include("Resolving dependencies") + expect(out).to include("Bundle complete") + + expect(the_bundle).to include_gem "gunks 0.0.1", :source => "path@#{bundled_app("gems", "gunks")}" + end + end + + context "eval-ed Gemfile references other gemfiles" do + it "works with relative paths" do + create_file "other/Gemfile-other", "gem 'rack'" + create_file "other/Gemfile", "eval_gemfile 'Gemfile-other'" + create_file "Gemfile-alt", <<-G + source "file:#{gem_repo1}" + eval_gemfile "other/Gemfile" + G + install_gemfile! "eval_gemfile File.expand_path('Gemfile-alt')" + + expect(the_bundle).to include_gem "rack 1.0.0" + end + end +end diff --git a/spec/bundler/install/gemfile/gemspec_spec.rb b/spec/bundler/install/gemfile/gemspec_spec.rb new file mode 100644 index 00000000000000..bbb56da5a4ff4f --- /dev/null +++ b/spec/bundler/install/gemfile/gemspec_spec.rb @@ -0,0 +1,672 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install from an existing gemspec" do + before(:each) do + build_repo2 do + build_gem "bar" + build_gem "bar-dev" + end + end + + it "should install runtime and development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source :rubygems\ngemspec") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + install_gemfile <<-G + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + end + + it "that is hidden should install runtime and development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source :rubygems\ngemspec") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + FileUtils.mv tmp.join("foo", "foo.gemspec"), tmp.join("foo", ".gemspec") + + install_gemfile <<-G + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + end + + it "should handle a list of requirements" do + update_repo2 do + build_gem "baz", "1.0" + build_gem "baz", "1.1" + end + + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source :rubygems\ngemspec") + s.add_dependency "baz", ">= 1.0", "< 1.1" + end + install_gemfile <<-G + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + + expect(the_bundle).to include_gems "baz 1.0" + end + + it "should raise if there are no gemspecs available" do + build_lib("foo", :path => tmp.join("foo"), :gemspec => false) + + install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + expect(last_command.bundler_err).to match(/There are no gemspecs at #{tmp.join('foo')}/) + end + + it "should raise if there are too many gemspecs available" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("foo2.gemspec", build_spec("foo", "4.0").first.to_ruby) + end + + install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + expect(last_command.bundler_err).to match(/There are multiple gemspecs at #{tmp.join('foo')}/) + end + + it "should pick a specific gemspec" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("foo2.gemspec", "") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + + install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + end + + it "should use a specific group for development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("foo2.gemspec", "") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + + install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo', :development_group => :dev + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).not_to include_gems "bar-dev 1.0.0", :groups => :development + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :dev + end + + it "should match a lockfile even if the gemspec defines development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source 'file://#{gem_repo1}'\ngemspec") + s.add_dependency "actionpack", "=2.3.2" + s.add_development_dependency "rake", "=10.0.2" + end + + Dir.chdir(tmp.join("foo")) do + bundle "install" + # This should really be able to rely on $stderr, but, it's not written + # right, so we can't. In fact, this is a bug negation test, and so it'll + # ghost pass in future, and will only catch a regression if the message + # doesn't change. Exit codes should be used correctly (they can be more + # than just 0 and 1). + output = bundle("install --deployment") + expect(output).not_to match(/You have added to the Gemfile/) + expect(output).not_to match(/You have deleted from the Gemfile/) + expect(output).not_to match(/install in deployment mode after changing/) + end + end + + it "should match a lockfile without needing to re-resolve" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "rack" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}' + G + + bundle! "install", :verbose => true + + message = "Found no changes, using resolution from the lockfile" + expect(out.scan(message).size).to eq(1) + end + + it "should match a lockfile without needing to re-resolve with development dependencies" do + simulate_platform java + + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "rack" + s.add_development_dependency "thin" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}' + G + + bundle! "install", :verbose => true + + message = "Found no changes, using resolution from the lockfile" + expect(out.scan(message).size).to eq(1) + end + + it "should match a lockfile on non-ruby platforms with a transitive platform dependency" do + simulate_platform java + simulate_ruby_engine "jruby" + + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "platform_specific" + end + + system_gems "platform_specific-1.0-java", :path => :bundle_path, :keep_path => true + + install_gemfile! <<-G + gemspec :path => '#{tmp.join("foo")}' + G + + bundle! "update --bundler", :verbose => true + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 JAVA" + end + + it "should evaluate the gemspec in its directory" do + build_lib("foo", :path => tmp.join("foo")) + File.open(tmp.join("foo/foo.gemspec"), "w") do |s| + s.write "raise 'ahh' unless Dir.pwd == '#{tmp.join("foo")}'" + end + + install_gemfile <<-G + gemspec :path => '#{tmp.join("foo")}' + G + expect(last_command.stdboth).not_to include("ahh") + end + + it "allows the gemspec to activate other gems" do + ENV["BUNDLE_PATH__SYSTEM"] = "true" + # see https://github.com/bundler/bundler/issues/5409 + # + # issue was caused by rubygems having an unresolved gem during a require, + # so emulate that + system_gems %w[rack-1.0.0 rack-0.9.1 rack-obama-1.0] + + build_lib("foo", :path => bundled_app) + gemspec = bundled_app("foo.gemspec").read + bundled_app("foo.gemspec").open("w") do |f| + f.write "#{gemspec.strip}.tap { gem 'rack-obama'; require 'rack-obama' }" + end + + install_gemfile! <<-G + gemspec + G + + expect(the_bundle).to include_gem "foo 1.0" + end + + it "allows conflicts" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.version = "1.0.0" + s.add_dependency "bar", "= 1.0.0" + end + build_gem "deps", :to_bundle => true do |s| + s.add_dependency "foo", "= 0.0.1" + end + build_gem "foo", "0.0.1", :to_bundle => true + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "deps" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "foo 1.0.0" + end + + it "does not break Gem.finish_resolve with conflicts", :rubygems => ">= 2" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.version = "1.0.0" + s.add_dependency "bar", "= 1.0.0" + end + update_repo2 do + build_gem "deps" do |s| + s.add_dependency "foo", "= 0.0.1" + end + build_gem "foo", "0.0.1" + end + + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "deps" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "foo 1.0.0" + + run! "Gem.finish_resolve; puts 'WIN'" + expect(out).to eq("WIN") + end + + context "in deployment mode" do + context "when the lockfile was not updated after a change to the gemspec's dependencies" do + it "reports that installation failed" do + build_lib "cocoapods", :path => bundled_app do |s| + s.add_dependency "activesupport", ">= 1" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec + G + + expect(the_bundle).to include_gems("cocoapods 1.0", "activesupport 2.3.5") + + build_lib "cocoapods", :path => bundled_app do |s| + s.add_dependency "activesupport", ">= 1.0.1" + end + + bundle :install, forgotten_command_line_options(:deployment => true) + + expect(out).to include("changed") + end + end + end + + context "when child gemspecs conflict with a released gemspec" do + before do + # build the "parent" gem that depends on another gem in the same repo + build_lib "source_conflict", :path => bundled_app do |s| + s.add_dependency "rack_middleware" + end + + # build the "child" gem that is the same version as a released gem, but + # has completely different and conflicting dependency requirements + build_lib "rack_middleware", "1.0", :path => bundled_app("rack_middleware") do |s| + s.add_dependency "rack", "1.0" # anything other than 0.9.1 + end + end + + it "should install the child gemspec's deps" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gemspec + G + + expect(the_bundle).to include_gems "rack 1.0" + end + end + + context "with a lockfile and some missing dependencies" do + let(:source_uri) { "http://localgemserver.test" } + + context "previously bundled for Ruby" do + let(:platform) { "ruby" } + let(:explicit_platform) { false } + + before do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "rack", "=1.0.0" + end + + if explicit_platform + create_file( + tmp.join("foo", "foo-#{platform}.gemspec"), + build_spec("foo", "1.0", platform) do + dep "rack", "=1.0.0" + @spec.authors = "authors" + @spec.summary = "summary" + end.first.to_ruby + ) + end + + gemfile <<-G + source "#{source_uri}" + gemspec :path => "../foo" + G + + lockfile <<-L + PATH + remote: ../foo + specs: + foo (1.0) + rack (= 1.0.0) + + GEM + remote: #{source_uri} + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + context "using JRuby with explicit platform" do + let(:platform) { "java" } + let(:explicit_platform) { true } + + it "should install" do + simulate_ruby_engine "jruby" do + simulate_platform "java" do + results = bundle "install", :artifice => "endpoint" + expect(results).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + context "using JRuby" do + let(:platform) { "java" } + + it "should install" do + simulate_ruby_engine "jruby" do + simulate_platform "java" do + results = bundle "install", :artifice => "endpoint" + expect(results).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + context "using Windows" do + it "should install" do + simulate_windows do + results = bundle "install", :artifice => "endpoint" + expect(results).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + context "bundled for ruby and jruby" do + let(:platform_specific_type) { :runtime } + let(:dependency) { "platform_specific" } + before do + build_repo2 do + build_gem "indirect_platform_specific" do |s| + s.add_runtime_dependency "platform_specific" + end + end + + build_lib "foo", :path => "." do |s| + if platform_specific_type == :runtime + s.add_runtime_dependency dependency + elsif platform_specific_type == :development + s.add_development_dependency dependency + else + raise "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" + end + end + + %w[ruby jruby].each do |platform| + simulate_platform(platform) do + install_gemfile <<-G + source "file://localhost#{gem_repo2}" + gemspec + G + end + end + end + + context "on ruby", :bundler => "< 3" do + before do + simulate_platform("ruby") + bundle :install + end + + context "as a runtime dependency" do + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq normalize_uri_file(strip_whitespace(<<-L)) + PATH + remote: . + specs: + foo (1.0) + platform_specific + + GEM + remote: file://localhost#{gem_repo2}/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "as a development dependency" do + let(:platform_specific_type) { :development } + + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq normalize_uri_file(strip_whitespace(<<-L)) + PATH + remote: . + specs: + foo (1.0) + + GEM + remote: file://localhost#{gem_repo2}/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "with an indirect platform-specific development dependency" do + let(:platform_specific_type) { :development } + let(:dependency) { "indirect_platform_specific" } + + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq normalize_uri_file(strip_whitespace(<<-L)) + PATH + remote: . + specs: + foo (1.0) + + GEM + remote: file://localhost#{gem_repo2}/ + specs: + indirect_platform_specific (1.0) + platform_specific + platform_specific (1.0) + platform_specific (1.0-java) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + indirect_platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + + context "on ruby", :bundler => "3" do + before do + simulate_platform("ruby") + bundle :install + end + + context "as a runtime dependency" do + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq normalize_uri_file(strip_whitespace(<<-L)) + GEM + remote: file://localhost#{gem_repo2}/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) + + PATH + remote: . + specs: + foo (1.0) + platform_specific + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "as a development dependency" do + let(:platform_specific_type) { :development } + + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq normalize_uri_file(strip_whitespace(<<-L)) + GEM + remote: file://localhost#{gem_repo2}/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) + + PATH + remote: . + specs: + foo (1.0) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "with an indirect platform-specific development dependency" do + let(:platform_specific_type) { :development } + let(:dependency) { "indirect_platform_specific" } + + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq normalize_uri_file(strip_whitespace(<<-L)) + GEM + remote: file://localhost#{gem_repo2}/ + specs: + indirect_platform_specific (1.0) + platform_specific + platform_specific (1.0) + platform_specific (1.0-java) + + PATH + remote: . + specs: + foo (1.0) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + indirect_platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + end + end + + context "with multiple platforms" do + before do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.version = "1.0.0" + s.add_development_dependency "rack" + s.write "foo-universal-java.gemspec", build_spec("foo", "1.0.0", "universal-java") {|sj| sj.runtime "rack", "1.0.0" }.first.to_ruby + end + end + + it "installs the ruby platform gemspec" do + simulate_platform "ruby" + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "foo 1.0.0", "rack 1.0.0" + end + + it "installs the ruby platform gemspec and skips dev deps with --without development" do + simulate_platform "ruby" + + install_gemfile! <<-G, forgotten_command_line_options(:without => "development") + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gem "foo 1.0.0" + expect(the_bundle).not_to include_gem "rack" + end + end +end diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb new file mode 100644 index 00000000000000..fe396c7a0ff7a0 --- /dev/null +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -0,0 +1,1351 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with git sources" do + describe "when floating on master" do + before :each do + build_git "foo" do |s| + s.executables = "foobar" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + end + + it "fetches gems" do + expect(the_bundle).to include_gems("foo 1.0") + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "caches the git repo", :bundler => "< 3" do + expect(Dir["#{default_bundle_path}/cache/bundler/git/foo-1.0-*"]).to have_attributes :size => 1 + end + + it "caches the git repo globally" do + simulate_new_machine + bundle! "config global_gem_cache true" + bundle! :install + expect(Dir["#{home}/.bundle/cache/git/foo-1.0-*"]).to have_attributes :size => 1 + end + + it "caches the evaluated gemspec" do + git = update_git "foo" do |s| + s.executables = ["foobar"] # we added this the first time, so keep it now + s.files = ["bin/foobar"] # updating git nukes the files list + foospec = s.to_ruby.gsub(/s\.files.*/, 's.files = `git ls-files -z`.split("\x0")') + s.write "foo.gemspec", foospec + end + + bundle "update foo" + + sha = git.ref_for("master", 11) + spec_file = default_bundle_path.join("bundler/gems/foo-1.0-#{sha}/foo.gemspec").to_s + ruby_code = Gem::Specification.load(spec_file).to_ruby + file_code = File.read(spec_file) + expect(file_code).to eq(ruby_code) + end + + it "does not update the git source implicitly" do + update_git "foo" + + in_app_root2 do + install_gemfile bundled_app2("Gemfile"), <<-G + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + end + + in_app_root do + run <<-RUBY + require 'foo' + puts "fail" if defined?(FOO_PREV_REF) + RUBY + + expect(out).to be_empty + end + end + + it "sets up git gem executables on the path" do + bundle "exec foobar" + expect(out).to eq("1.0") + end + + it "complains if pinned specs don't exist in the git repo" do + build_git "foo" + + install_gemfile <<-G + gem "foo", "1.1", :git => "#{lib_path("foo-1.0")}" + G + + expect(out).to include("The source contains 'foo' at: 1.0") + end + + it "complains with version and platform if pinned specs don't exist in the git repo" do + simulate_platform "java" + + build_git "only_java" do |s| + s.platform = "java" + end + + install_gemfile <<-G + platforms :jruby do + gem "only_java", "1.2", :git => "#{lib_path("only_java-1.0-java")}" + end + G + + expect(out).to include("The source contains 'only_java' at: 1.0 java") + end + + it "complains with multiple versions and platforms if pinned specs don't exist in the git repo" do + simulate_platform "java" + + build_git "only_java", "1.0" do |s| + s.platform = "java" + end + + build_git "only_java", "1.1" do |s| + s.platform = "java" + s.write "only_java1-0.gemspec", File.read("#{lib_path("only_java-1.0-java")}/only_java.gemspec") + end + + install_gemfile <<-G + platforms :jruby do + gem "only_java", "1.2", :git => "#{lib_path("only_java-1.1-java")}" + end + G + + expect(out).to include("The source contains 'only_java' at: 1.0 java, 1.1 java") + end + + it "still works after moving the application directory" do + bundle "install --path vendor/bundle" + FileUtils.mv bundled_app, tmp("bundled_app.bck") + + Dir.chdir tmp("bundled_app.bck") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "can still install after moving the application directory" do + bundle "install --path vendor/bundle" + FileUtils.mv bundled_app, tmp("bundled_app.bck") + + update_git "foo", "1.1", :path => lib_path("foo-1.0") + + Dir.chdir tmp("bundled_app.bck") + gemfile tmp("bundled_app.bck/Gemfile"), <<-G + source "file://#{gem_repo1}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + + gem "rack", "1.0" + G + + bundle "update foo" + + expect(the_bundle).to include_gems "foo 1.1", "rack 1.0" + end + end + + describe "with an empty git block" do + before do + build_git "foo" + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + git "#{lib_path("foo-1.0")}" do + # this page left intentionally blank + end + G + end + + it "does not explode" do + bundle "install" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when specifying a revision" do + before(:each) do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + end + + it "works" do + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :ref => "#{@revision}" do + gem "foo" + end + G + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "works when the revision is a symbol" do + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :ref => #{@revision.to_sym.inspect} do + gem "foo" + end + G + expect(err).to lack_errors + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "works when the revision is a non-head ref" do + # want to ensure we don't fallback to master + update_git "foo", :path => lib_path("foo-1.0") do |s| + s.write("lib/foo.rb", "raise 'FAIL'") + end + + Dir.chdir(lib_path("foo-1.0")) do + `git update-ref -m 'Bundler Spec!' refs/bundler/1 master~1` + end + + # want to ensure we don't fallback to HEAD + update_git "foo", :path => lib_path("foo-1.0"), :branch => "rando" do |s| + s.write("lib/foo.rb", "raise 'FAIL'") + end + + install_gemfile! <<-G + git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do + gem "foo" + end + G + expect(err).to lack_errors + + run! <<-RUBY + require 'foo' + puts "WIN" if defined?(FOO) + RUBY + + expect(out).to eq("WIN") + end + + it "works when the revision is a non-head ref and it was previously downloaded" do + install_gemfile! <<-G + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + # want to ensure we don't fallback to master + update_git "foo", :path => lib_path("foo-1.0") do |s| + s.write("lib/foo.rb", "raise 'FAIL'") + end + + Dir.chdir(lib_path("foo-1.0")) do + `git update-ref -m 'Bundler Spec!' refs/bundler/1 master~1` + end + + # want to ensure we don't fallback to HEAD + update_git "foo", :path => lib_path("foo-1.0"), :branch => "rando" do |s| + s.write("lib/foo.rb", "raise 'FAIL'") + end + + install_gemfile! <<-G + git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do + gem "foo" + end + G + expect(err).to lack_errors + + run! <<-RUBY + require 'foo' + puts "WIN" if defined?(FOO) + RUBY + + expect(out).to eq("WIN") + end + + it "does not download random non-head refs" do + Dir.chdir(lib_path("foo-1.0")) do + sys_exec!("git update-ref -m 'Bundler Spec!' refs/bundler/1 master~1") + end + + bundle! "config global_gem_cache true" + + install_gemfile! <<-G + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + # ensure we also git fetch after cloning + bundle! :update, :all => bundle_update_requires_all? + + Dir.chdir(Dir[home(".bundle/cache/git/foo-*")].first) do + sys_exec("git ls-remote .") + end + + expect(out).not_to include("refs/bundler/1") + end + end + + describe "when specifying a branch" do + let(:branch) { "branch" } + let(:repo) { build_git("foo").path } + before(:each) do + update_git("foo", :path => repo, :branch => branch) + end + + it "works" do + install_gemfile <<-G + git "#{repo}", :branch => #{branch.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + context "when the branch starts with a `#`" do + let(:branch) { "#149/redirect-url-fragment" } + it "works" do + install_gemfile <<-G + git "#{repo}", :branch => #{branch.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + + context "when the branch includes quotes" do + let(:branch) { %('") } + it "works" do + install_gemfile <<-G + git "#{repo}", :branch => #{branch.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + end + + describe "when specifying a tag" do + let(:tag) { "tag" } + let(:repo) { build_git("foo").path } + before(:each) do + update_git("foo", :path => repo, :tag => tag) + end + + it "works" do + install_gemfile <<-G + git "#{repo}", :tag => #{tag.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + context "when the tag starts with a `#`" do + let(:tag) { "#149/redirect-url-fragment" } + it "works" do + install_gemfile <<-G + git "#{repo}", :tag => #{tag.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + + context "when the tag includes quotes" do + let(:tag) { %('") } + it "works" do + install_gemfile <<-G + git "#{repo}", :tag => #{tag.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + end + + describe "when specifying local override" do + it "uses the local repository instead of checking a new one out" do + # We don't generate it because we actually don't need it + # build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle! %(config local.rack #{lib_path("local-rack")}) + bundle! :install + + run "require 'rack'" + expect(out).to eq("LOCAL") + end + + it "chooses the local repository on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + update_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + run "require 'rack'" + expect(out).to eq("LOCAL") + end + + it "unlocks the source when the dependencies have changed while switching to the local" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + update_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "rack.gemspec", build_spec("rack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle! %(config local.rack #{lib_path("local-rack")}) + bundle! :install + run! "require 'rack'" + expect(out).to eq("LOCAL") + end + + it "updates specs on runtime" do + system_gems "nokogiri-1.4.2" + + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + lockfile0 = File.read(bundled_app("Gemfile.lock")) + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + update_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.add_dependency "nokogiri", "1.4.2" + end + + bundle %(config local.rack #{lib_path("local-rack")}) + run "require 'rack'" + + lockfile1 = File.read(bundled_app("Gemfile.lock")) + expect(lockfile1).not_to eq(lockfile0) + end + + it "updates ref on install" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + lockfile0 = File.read(bundled_app("Gemfile.lock")) + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + update_git "rack", "0.8", :path => lib_path("local-rack") + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + + lockfile1 = File.read(bundled_app("Gemfile.lock")) + expect(lockfile1).not_to eq(lockfile0) + end + + it "explodes if given path does not exist on install" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/Cannot use local override for rack-0.8 because #{Regexp.escape(lib_path('local-rack').to_s)} does not exist/) + end + + it "explodes if branch is not given on install" do + build_git "rack", "0.8" + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/cannot use local override/i) + end + + it "does not explode if disable_local_branch_check is given" do + build_git "rack", "0.8" + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle %(config disable_local_branch_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end + + it "explodes on different branches on install" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + update_git "rack", "0.8", :path => lib_path("local-rack"), :branch => "another" do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/is using branch another but Gemfile specifies master/) + end + + it "explodes on invalid revision on install" do + build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/The Gemfile lock is pointing to revision \w+/) + end + end + + describe "specified inline" do + # TODO: Figure out how to write this test so that it is not flaky depending + # on the current network situation. + # it "supports private git URLs" do + # gemfile <<-G + # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git" + # G + # + # bundle :install + # + # # p out + # # p err + # puts err unless err.empty? # This spec fails randomly every so often + # err.should include("notthere.fallingsnow.net") + # err.should include("ssh") + # end + + it "installs from git even if a newer gem is available elsewhere" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + expect(the_bundle).to include_gems "rack 0.8" + end + + it "installs dependencies from git even if a newer gem is available elsewhere" do + system_gems "rack-1.0.0" + + build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| + s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + end + + build_git "foo", :path => lib_path("nested") do |s| + s.add_dependency "rack", "= 1.0" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("nested")}" + G + + run "require 'rack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "correctly unlocks when changing to a git source" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + build_git "rack", :path => lib_path("rack") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0", :git => "#{lib_path("rack")}" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "correctly unlocks when changing to a git source without versions" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + build_git "rack", "1.2", :path => lib_path("rack") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack")}" + G + + expect(the_bundle).to include_gems "rack 1.2" + end + end + + describe "block syntax" do + it "pulls all gems from a git block" do + build_lib "omg", :path => lib_path("hi2u/omg") + build_lib "hi2u", :path => lib_path("hi2u") + + install_gemfile <<-G + path "#{lib_path("hi2u")}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + end + + it "uses a ref if specified" do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" + G + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "correctly handles cases with invalid gemspecs" do + build_git "foo" do |s| + s.summary = nil + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "runs the gemspec in the context of its parent directory" do + build_lib "bar", :path => lib_path("foo/bar"), :gemspec => false do |s| + s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + $:.unshift Dir.pwd # For 1.9 + require 'lib/version' + Gem::Specification.new do |s| + s.name = 'bar' + s.author = 'no one' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + end + G + end + + build_git "foo", :path => lib_path("foo") do |s| + s.write "bin/foo", "" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :git => "#{lib_path("foo")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "bar 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "installs from git even if a rubygems gem is present" do + build_gem "foo", "1.0", :path => lib_path("fake_foo"), :to_system => true do |s| + s.write "lib/foo.rb", "raise 'FAIL'" + end + + build_git "foo", "1.0" + + install_gemfile <<-G + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fakes the gem out if there is no gemspec" do + build_git "foo", :gemspec => false + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems("foo 1.0") + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "catches git errors and spits out useful output" do + gemfile <<-G + gem "foo", "1.0", :git => "omgomg" + G + + bundle :install + + expect(out).to include("Git error:") + expect(err).to include("fatal") + expect(err).to include("omgomg") + end + + it "works when the gem path has spaces in it" do + build_git "foo", :path => lib_path("foo space-1.0") + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo space-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "handles repos that have been force-pushed" do + build_git "forced", "1.0" + + install_gemfile <<-G + git "#{lib_path("forced-1.0")}" do + gem 'forced' + end + G + expect(the_bundle).to include_gems "forced 1.0" + + update_git "forced" do |s| + s.write "lib/forced.rb", "FORCED = '1.1'" + end + + bundle "update", :all => bundle_update_requires_all? + expect(the_bundle).to include_gems "forced 1.1" + + Dir.chdir(lib_path("forced-1.0")) do + `git reset --hard HEAD^` + end + + bundle "update", :all => bundle_update_requires_all? + expect(the_bundle).to include_gems "forced 1.0" + end + + it "ignores submodules if :submodule is not passed" do + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(out).to match(/could not find gem 'submodule/i) + + expect(the_bundle).not_to include_gems "has_submodule 1.0" + end + + it "handles repos with submodules" do + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + expect(the_bundle).to include_gems "has_submodule 1.0" + end + + it "handles implicit updates when modifying the source info" do + git = build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + update_git "foo" + update_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do + gem "foo" + end + G + + run <<-RUBY + require 'foo' + puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}' + RUBY + + expect(out).to eq("WIN") + end + + it "does not to a remote fetch if the revision is cached locally" do + build_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + FileUtils.rm_rf(lib_path("foo-1.0")) + + bundle "install" + expect(out).not_to match(/updating/i) + end + + it "doesn't blow up if bundle install is run twice in a row" do + build_git "foo" + + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle "install" + bundle "install" + expect(exitstatus).to eq(0) if exitstatus + end + + it "prints a friendly error if a file blocks the git repo" do + build_git "foo" + + FileUtils.mkdir_p(default_bundle_path) + FileUtils.touch(default_bundle_path("bundler")) + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(exitstatus).to_not eq(0) if exitstatus + expect(out).to include("Bundler could not install a gem because it " \ + "needs to create a directory, but a file exists " \ + "- #{default_bundle_path("bundler")}") + end + + it "does not duplicate git gem sources" do + build_lib "foo", :path => lib_path("nested/foo") + build_lib "bar", :path => lib_path("nested/bar") + + build_git "foo", :path => lib_path("nested") + build_git "bar", :path => lib_path("nested") + + gemfile <<-G + gem "foo", :git => "#{lib_path("nested")}" + gem "bar", :git => "#{lib_path("nested")}" + G + + bundle "install" + expect(File.read(bundled_app("Gemfile.lock")).scan("GIT").size).to eq(1) + end + + describe "switching sources" do + it "doesn't explode when switching Path to Git sources" do + build_gem "foo", "1.0", :to_system => true do |s| + s.write "lib/foo.rb", "raise 'fail'" + end + build_lib "foo", "1.0", :path => lib_path("bar/foo") + build_git "bar", "1.0", :path => lib_path("bar") do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :path => "#{lib_path("bar")}" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :git => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + end + + it "doesn't explode when switching Gem to Git source" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + gem "rack", "1.0.0" + G + + build_git "rack", "1.0" do |s| + s.write "lib/new_file.rb", "puts 'USING GIT'" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + gem "rack", "1.0.0", :git => "#{lib_path("rack-1.0")}" + G + + run "require 'new_file'" + expect(out).to eq("USING GIT") + end + end + + describe "bundle install after the remote has been updated" do + it "installs" do + build_git "valim" + + install_gemfile <<-G + gem "valim", :git => "file://#{lib_path("valim-1.0")}" + G + + old_revision = revision_for(lib_path("valim-1.0")) + update_git "valim" + new_revision = revision_for(lib_path("valim-1.0")) + + lockfile = File.read(bundled_app("Gemfile.lock")) + File.open(bundled_app("Gemfile.lock"), "w") do |file| + file.puts lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}") + end + + bundle "install" + + run <<-R + require "valim" + puts VALIM_PREV_REF + R + + expect(out).to eq(old_revision) + end + + it "gives a helpful error message when the remote ref no longer exists" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + install_gemfile <<-G + gem "foo", :git => "file://#{lib_path("foo-1.0")}", :ref => "#{revision}" + G + bundle "install" + expect(out).to_not match(/Revision.*does not exist/) + + install_gemfile <<-G + gem "foo", :git => "file://#{lib_path("foo-1.0")}", :ref => "deadbeef" + G + bundle "install" + expect(out).to include("Revision deadbeef does not exist in the repository") + end + end + + describe "bundle install --deployment with git sources" do + it "works" do + build_git "valim", :path => lib_path("valim") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "valim", "= 1.0", :git => "#{lib_path("valim")}" + G + + simulate_new_machine + + bundle! :install, forgotten_command_line_options(:deployment => true) + end + end + + describe "gem install hooks" do + it "runs pre-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran pre-install hook: foo-1.0") + end + + it "runs post-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran post-install hook: foo-1.0") + end + + it "complains if the install hook fails" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(out).to include("failed for foo-1.0") + end + end + + context "with an extension" do + it "installs the extension", :ruby_repo do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = 'YES'" + end + end + RUBY + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq("YES") + + run! <<-R + puts $:.grep(/ext/) + R + expect(out).to eq(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) + end + + it "does not use old extension after ref changes", :ruby_repo do + git_reader = build_git "foo", :no_default => true do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-RUBY + require "mkmf" + create_makefile("foo") + RUBY + s.write "ext/foo.c", "void Init_foo() {}" + end + + 2.times do |i| + Dir.chdir(git_reader.path) do + File.open("ext/foo.c", "w") do |file| + file.write <<-C + #include "ruby.h" + VALUE foo() { return INT2FIX(#{i}); } + void Init_foo() { rb_define_global_function("foo", &foo, 0); } + C + end + `git commit -m 'commit for iteration #{i}' ext/foo.c` + end + git_commit_sha = git_reader.ref_for("HEAD") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" + G + + run <<-R + require 'foo' + puts foo + R + + expect(out).to eq(i.to_s) + end + end + + it "does not prompt to gem install if extension fails" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + raise + end + RUBY + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(last_command.bundler_err).to end_with(<<-M.strip) +An error occurred while installing foo (1.0), and Bundler cannot continue. + +In Gemfile: + foo + M + expect(out).not_to include("gem install foo") + end + + it "does not reinstall the extension", :ruby_repo, :rubygems => ">= 2.3.0" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run! <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run! <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + end + + it "ignores git environment variables" do + build_git "xxxxxx" do |s| + s.executables = "xxxxxxbar" + end + + Bundler::SharedHelpers.with_clean_git_env do + ENV["GIT_DIR"] = "bar" + ENV["GIT_WORK_TREE"] = "bar" + + install_gemfile <<-G + source "file://#{gem_repo1}" + git "#{lib_path("xxxxxx-1.0")}" do + gem 'xxxxxx' + end + G + + expect(exitstatus).to eq(0) if exitstatus + expect(ENV["GIT_DIR"]).to eq("bar") + expect(ENV["GIT_WORK_TREE"]).to eq("bar") + end + end + + describe "without git installed" do + it "prints a better error message" do + build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + with_path_as("") do + bundle "update", :all => bundle_update_requires_all? + end + expect(last_command.bundler_err). + to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + end + + it "installs a packaged git gem successfully" do + build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + bundle :package, forgotten_command_line_options([:all, :cache_all] => true) + simulate_new_machine + + bundle! "install", :env => { "PATH" => "" } + expect(out).to_not include("You need to install git to be able to use gems from git repositories.") + end + end + + describe "when the git source is overridden with a local git repo" do + before do + bundle! "config --global local.foo #{lib_path("foo")}" + end + + describe "and git output is colorized" do + before do + File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f| + f.write("[color]\n\tui = always\n") + end + end + + it "installs successfully" do + build_git "foo", "1.0", :path => lib_path("foo") + + gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}", :branch => "master" + G + + bundle :install + expect(the_bundle).to include_gems "foo 1.0" + end + end + end + + context "git sources that include credentials" do + context "that are username and password" do + let(:credentials) { "user1:password1" } + + it "does not display the password" do + install_gemfile <<-G + git "https://#{credentials}@github.com/company/private-repo" do + gem "foo" + end + G + + bundle :install + expect(last_command.stdboth).to_not include("password1") + expect(last_command.stdout).to include("Fetching https://user1@github.com/company/private-repo") + end + end + + context "that is an oauth token" do + let(:credentials) { "oauth_token" } + + it "displays the oauth scheme but not the oauth token" do + install_gemfile <<-G + git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do + gem "foo" + end + G + + bundle :install + expect(last_command.stdboth).to_not include("oauth_token") + expect(last_command.stdout).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") + end + end + end +end diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb new file mode 100644 index 00000000000000..45395e606f710b --- /dev/null +++ b/spec/bundler/install/gemfile/groups_spec.rb @@ -0,0 +1,384 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with groups" do + describe "installing with no options" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo do + gem "activesupport", "2.3.5" + end + gem "thin", :groups => [:emo] + G + end + + it "installs gems in the default group" do + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems in a group block into that group" do + expect(the_bundle).to include_gems "activesupport 2.3.5" + + load_error_run <<-R, "activesupport", :default + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "installs gems with inline :groups into those groups" do + expect(the_bundle).to include_gems "thin 1.0" + + load_error_run <<-R, "thin", :default + require 'thin' + puts THIN + R + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "sets up everything if Bundler.setup is used with no groups" do + output = run("require 'rack'; puts RACK") + expect(output).to eq("1.0.0") + + output = run("require 'activesupport'; puts ACTIVESUPPORT") + expect(output).to eq("2.3.5") + + output = run("require 'thin'; puts THIN") + expect(output).to eq("1.0") + end + + it "removes old groups when new groups are set up" do + load_error_run <<-RUBY, "thin", :emo + Bundler.setup(:default) + require 'thin' + puts THIN + RUBY + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "sets up old groups when they have previously been removed" do + output = run <<-RUBY, :emo + Bundler.setup(:default) + Bundler.setup(:default, :emo) + require 'thin'; puts THIN + RUBY + expect(output).to eq("1.0") + end + end + + describe "installing --without" do + describe "with gems assigned to a single group" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo do + gem "activesupport", "2.3.5" + end + group :debugging, :optional => true do + gem "thin" + end + G + end + + it "installs gems in the default group" do + bundle! :install, forgotten_command_line_options(:without => "emo") + expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + end + + it "does not install gems from the excluded group" do + bundle :install, :without => "emo" + expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + end + + it "does not install gems from the previously excluded group" do + bundle :install, forgotten_command_line_options(:without => "emo") + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + bundle :install + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does not say it installed gems from the excluded group" do + bundle! :install, forgotten_command_line_options(:without => "emo") + expect(out).not_to include("activesupport") + end + + it "allows Bundler.setup for specific groups" do + bundle :install, forgotten_command_line_options(:without => "emo") + run!("require 'rack'; puts RACK", :default) + expect(out).to eq("1.0.0") + end + + it "does not effect the resolve" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + group :emo do + gem "rails", "2.3.2" + end + G + + bundle :install, forgotten_command_line_options(:without => "emo") + expect(the_bundle).to include_gems "activesupport 2.3.2", :groups => [:default] + end + + it "still works on a different machine and excludes gems" do + bundle :install, forgotten_command_line_options(:without => "emo") + + simulate_new_machine + bundle :install, forgotten_command_line_options(:without => "emo") + + expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + end + + it "still works when BUNDLE_WITHOUT is set" do + ENV["BUNDLE_WITHOUT"] = "emo" + + bundle :install + expect(out).not_to include("activesupport") + + expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + + ENV["BUNDLE_WITHOUT"] = nil + end + + it "clears without when passed an empty list" do + bundle :install, forgotten_command_line_options(:without => "emo") + + bundle :install, forgotten_command_line_options(:without => "") + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "doesn't clear without when nothing is passed" do + bundle :install, forgotten_command_line_options(:without => "emo") + + bundle :install + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does not install gems from the optional group" do + bundle :install + expect(the_bundle).not_to include_gems "thin 1.0" + end + + it "does install gems from the optional group when requested" do + bundle :install, forgotten_command_line_options(:with => "debugging") + expect(the_bundle).to include_gems "thin 1.0" + end + + it "does install gems from the previously requested group" do + bundle :install, forgotten_command_line_options(:with => "debugging") + expect(the_bundle).to include_gems "thin 1.0" + bundle :install + expect(the_bundle).to include_gems "thin 1.0" + end + + it "does install gems from the optional groups requested with BUNDLE_WITH" do + ENV["BUNDLE_WITH"] = "debugging" + bundle :install + expect(the_bundle).to include_gems "thin 1.0" + ENV["BUNDLE_WITH"] = nil + end + + it "clears with when passed an empty list" do + bundle :install, forgotten_command_line_options(:with => "debugging") + bundle :install, forgotten_command_line_options(:with => "") + expect(the_bundle).not_to include_gems "thin 1.0" + end + + it "does remove groups from without when passed at --with", :bundler => "< 3" do + bundle :install, forgotten_command_line_options(:without => "emo") + bundle :install, forgotten_command_line_options(:with => "emo") + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "does remove groups from with when passed at --without", :bundler => "< 3" do + bundle :install, forgotten_command_line_options(:with => "debugging") + bundle :install, forgotten_command_line_options(:without => "debugging") + expect(the_bundle).not_to include_gem "thin 1.0" + end + + it "errors out when passing a group to with and without via CLI flags", :bundler => "< 3" do + bundle :install, forgotten_command_line_options(:with => "emo debugging", :without => "emo") + expect(last_command).to be_failure + expect(out).to include("The offending groups are: emo") + end + + it "allows the BUNDLE_WITH setting to override BUNDLE_WITHOUT" do + ENV["BUNDLE_WITH"] = "debugging" + + bundle! :install + expect(the_bundle).to include_gem "thin 1.0" + + ENV["BUNDLE_WITHOUT"] = "debugging" + expect(the_bundle).to include_gem "thin 1.0" + + bundle! :install + expect(the_bundle).to include_gem "thin 1.0" + end + + it "can add and remove a group at the same time" do + bundle :install, forgotten_command_line_options(:with => "debugging", :without => "emo") + expect(the_bundle).to include_gems "thin 1.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does have no effect when listing a not optional group in with" do + bundle :install, forgotten_command_line_options(:with => "emo") + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "does have no effect when listing an optional group in without" do + bundle :install, forgotten_command_line_options(:without => "debugging") + expect(the_bundle).not_to include_gems "thin 1.0" + end + end + + describe "with gems assigned to multiple groups" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo, :lolercoaster do + gem "activesupport", "2.3.5" + end + G + end + + it "installs gems in the default group" do + bundle! :install, forgotten_command_line_options(:without => "emo lolercoaster") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs the gem if any of its groups are installed" do + bundle! :install, forgotten_command_line_options(:without => "emo") + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + + describe "with a gem defined multiple times in different groups" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + group :emo do + gem "activesupport", "2.3.5" + end + + group :lolercoaster do + gem "activesupport", "2.3.5" + end + G + end + + it "installs the gem w/ option --without emo" do + bundle :install, forgotten_command_line_options(:without => "emo") + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "installs the gem w/ option --without lolercoaster" do + bundle :install, forgotten_command_line_options(:without => "lolercoaster") + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "does not install the gem w/ option --without emo lolercoaster" do + bundle :install, forgotten_command_line_options(:without => "emo lolercoaster") + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does not install the gem w/ option --without 'emo lolercoaster'" do + bundle :install, forgotten_command_line_options(:without => "'emo lolercoaster'") + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + end + end + + describe "nesting groups" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo do + group :lolercoaster do + gem "activesupport", "2.3.5" + end + end + G + end + + it "installs gems in the default group" do + bundle! :install, forgotten_command_line_options(:without => "emo lolercoaster") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs the gem if any of its groups are installed" do + bundle! :install, forgotten_command_line_options(:without => "emo") + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + end + end + + describe "when loading only the default group" do + it "should not load all groups" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :groups => :development + G + + ruby <<-R + require "bundler" + Bundler.setup :default + Bundler.require :default + puts RACK + begin + require "activesupport" + rescue LoadError + puts "no activesupport" + end + R + + expect(out).to include("1.0") + expect(out).to include("no activesupport") + end + end + + describe "when locked and installed with --without" do + before(:each) do + build_repo2 + system_gems "rack-0.9.1" do + install_gemfile <<-G, forgotten_command_line_options(:without => "rack") + source "file://#{gem_repo2}" + gem "rack" + + group :rack do + gem "rack_middleware" + end + G + end + end + + it "uses the correct versions even if --without was used on the original" do + expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).not_to include_gems "rack_middleware 1.0" + simulate_new_machine + + bundle :install + + expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "rack_middleware 1.0" + end + + it "does not hit the remote a second time" do + FileUtils.rm_rf gem_repo2 + bundle! :install, forgotten_command_line_options(:without => "rack").merge(:verbose => true) + expect(last_command.stdboth).not_to match(/fetching/i) + end + end +end diff --git a/spec/bundler/install/gemfile/install_if.rb b/spec/bundler/install/gemfile/install_if.rb new file mode 100644 index 00000000000000..1319051fdb9298 --- /dev/null +++ b/spec/bundler/install/gemfile/install_if.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +describe "bundle install with install_if conditionals" do + it "follows the install_if DSL" do + install_gemfile <<-G + source "file://#{gem_repo1}" + install_if(lambda { true }) do + gem "activesupport", "2.3.5" + end + gem "thin", :install_if => false + install_if(lambda { false }) do + gem "foo" + end + gem "rack" + G + + expect(the_bundle).to include_gems("rack 1.0", "activesupport 2.3.5") + expect(the_bundle).not_to include_gems("thin") + expect(the_bundle).not_to include_gems("foo") + + lockfile_should_be <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + activesupport (2.3.5) + foo (1.0) + rack (1.0.0) + thin (1.0) + rack + + PLATFORMS + ruby + + DEPENDENCIES + activesupport (= 2.3.5) + foo + rack + thin + + BUNDLED WITH + #{Bundler::VERSION} + L + end +end diff --git a/spec/bundler/install/gemfile/lockfile_spec.rb b/spec/bundler/install/gemfile/lockfile_spec.rb new file mode 100644 index 00000000000000..dc1baca6ea5d0f --- /dev/null +++ b/spec/bundler/install/gemfile/lockfile_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with a lockfile present" do + let(:gf) { <<-G } + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + + subject do + install_gemfile(gf) + end + + context "gemfile evaluation" do + let(:gf) { super() + "\n\n File.open('evals', 'a') {|f| f << %(1\n) } unless ENV['BUNDLER_SPEC_NO_APPEND']" } + + context "with plugins disabled" do + before do + bundle! "config plugins false" + subject + end + + it "does not evaluate the gemfile twice" do + bundle! :install + + with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "rack 1.0.0" } + + # The first eval is from the initial install, we're testing that the + # second install doesn't double-eval + expect(bundled_app("evals").read.lines.to_a.size).to eq(2) + end + + context "when the gem is not installed" do + before { FileUtils.rm_rf ".bundle" } + + it "does not evaluate the gemfile twice" do + bundle! :install + + with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "rack 1.0.0" } + + # The first eval is from the initial install, we're testing that the + # second install doesn't double-eval + expect(bundled_app("evals").read.lines.to_a.size).to eq(2) + end + end + end + end +end diff --git a/spec/bundler/install/gemfile/path_spec.rb b/spec/bundler/install/gemfile/path_spec.rb new file mode 100644 index 00000000000000..a508c971add58d --- /dev/null +++ b/spec/bundler/install/gemfile/path_spec.rb @@ -0,0 +1,630 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with explicit source paths" do + it "fetches gems with a global path source", :bundler => "< 3" do + build_lib "foo" + + install_gemfile <<-G + path "#{lib_path("foo-1.0")}" + gem 'foo' + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "fetches gems" do + build_lib "foo" + + install_gemfile <<-G + path "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "supports pinned paths" do + build_lib "foo" + + install_gemfile <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "supports relative paths" do + build_lib "foo" + + relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new(Dir.pwd)) + + install_gemfile <<-G + gem 'foo', :path => "#{relative_path}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "expands paths" do + build_lib "foo" + + relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("~").expand_path) + + install_gemfile <<-G + gem 'foo', :path => "~/#{relative_path}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "expands paths raise error with not existing user's home dir" do + build_lib "foo" + username = "some_unexisting_user" + relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("/home/#{username}").expand_path) + + install_gemfile <<-G + gem 'foo', :path => "~#{username}/#{relative_path}" + G + expect(out).to match("There was an error while trying to use the path `~#{username}/#{relative_path}`.") + expect(out).to match("user #{username} doesn't exist") + end + + it "expands paths relative to Bundler.root" do + build_lib "foo", :path => bundled_app("foo-1.0") + + install_gemfile <<-G + gem 'foo', :path => "./foo-1.0" + G + + bundled_app("subdir").mkpath + Dir.chdir(bundled_app("subdir")) do + expect(the_bundle).to include_gems("foo 1.0") + end + end + + it "expands paths when comparing locked paths to Gemfile paths" do + build_lib "foo", :path => bundled_app("foo-1.0") + + install_gemfile <<-G + gem 'foo', :path => File.expand_path("../foo-1.0", __FILE__) + G + + bundle! :install, forgotten_command_line_options(:frozen => true) + expect(exitstatus).to eq(0) if exitstatus + end + + it "installs dependencies from the path even if a newer gem is available elsewhere" do + system_gems "rack-1.0.0" + + build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| + s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + end + + build_lib "foo", :path => lib_path("nested") do |s| + s.add_dependency "rack", "= 1.0" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :path => "#{lib_path("nested")}" + G + + run "require 'rack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "works" do + build_gem "foo", "1.0.0", :to_system => true do |s| + s.write "lib/foo.rb", "puts 'FAIL'" + end + + build_lib "omg", "1.0", :path => lib_path("omg") do |s| + s.add_dependency "foo" + end + + build_lib "foo", "1.0.0", :path => lib_path("omg/foo") + + install_gemfile <<-G + gem "omg", :path => "#{lib_path("omg")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "prefers gemspecs closer to the path root" do + build_lib "premailer", "1.0.0", :path => lib_path("premailer") do |s| + s.write "gemfiles/ruby187.gemspec", <<-G + Gem::Specification.new do |s| + s.name = 'premailer' + s.version = '1.0.0' + s.summary = 'Hi' + s.authors = 'Me' + end + G + end + + install_gemfile <<-G + gem "premailer", :path => "#{lib_path("premailer")}" + G + + # Installation of the 'gemfiles' gemspec would fail since it will be unable + # to require 'premailer.rb' + expect(the_bundle).to include_gems "premailer 1.0.0" + end + + it "warns on invalid specs", :rubygems => "1.7" do + build_lib "foo" + + gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s + File.open(gemspec, "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "foo" + end + G + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + expect(out).to_not include("ERROR REPORT") + expect(out).to_not include("Your Gemfile has no gem server sources.") + expect(out).to match(/is not valid. Please fix this gemspec./) + expect(out).to match(/The validation error was 'missing value for attribute version'/) + expect(out).to match(/You have one or more invalid gemspecs that need to be fixed/) + end + + it "supports gemspec syntax" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + gemfile = <<-G + source "file://#{gem_repo1}" + gemspec + G + + File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts gemfile } + + Dir.chdir(lib_path("foo")) do + bundle "install" + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + it "supports gemspec syntax with an alternative path" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gemspec :path => "#{lib_path("foo")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "doesn't automatically unlock dependencies when using the gemspec syntax" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", ">= 1.0" + end + + Dir.chdir lib_path("foo") + + install_gemfile lib_path("foo/Gemfile"), <<-G + source "file://#{gem_repo1}" + gemspec + G + + build_gem "rack", "1.0.1", :to_system => true + + bundle "install" + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "doesn't automatically unlock dependencies when using the gemspec syntax and the gem has development dependencies" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", ">= 1.0" + s.add_development_dependency "activesupport" + end + + Dir.chdir lib_path("foo") + + install_gemfile lib_path("foo/Gemfile"), <<-G + source "file://#{gem_repo1}" + gemspec + G + + build_gem "rack", "1.0.1", :to_system => true + + bundle "install" + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "raises if there are multiple gemspecs" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.write "bar.gemspec", build_spec("bar", "1.0").first.to_ruby + end + + install_gemfile <<-G + gemspec :path => "#{lib_path("foo")}" + G + + expect(exitstatus).to eq(15) if exitstatus + expect(out).to match(/There are multiple gemspecs/) + end + + it "allows :name to be specified to resolve ambiguity" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.write "bar.gemspec" + end + + install_gemfile <<-G + gemspec :path => "#{lib_path("foo")}", :name => "foo" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "sets up executables" do + build_lib "foo" do |s| + s.executables = "foobar" + end + + install_gemfile <<-G + path "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + expect(the_bundle).to include_gems "foo 1.0" + + bundle "exec foobar" + expect(out).to eq("1.0") + end + + it "handles directories in bin/" do + build_lib "foo" + lib_path("foo-1.0").join("foo.gemspec").rmtree + lib_path("foo-1.0").join("bin/performance").mkpath + + install_gemfile <<-G + gem 'foo', '1.0', :path => "#{lib_path("foo-1.0")}" + G + expect(err).to lack_errors + end + + it "removes the .gem file after installing" do + build_lib "foo" + + install_gemfile <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + + expect(lib_path("foo-1.0").join("foo-1.0.gem")).not_to exist + end + + describe "block syntax" do + it "pulls all gems from a path block" do + build_lib "omg" + build_lib "hi2u" + + install_gemfile <<-G + path "#{lib_path}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + end + + it "keeps source pinning" do + build_lib "foo", "1.0", :path => lib_path("foo") + build_lib "omg", "1.0", :path => lib_path("omg") + build_lib "foo", "1.0", :path => lib_path("omg/foo") do |s| + s.write "lib/foo.rb", "puts 'FAIL'" + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + gem "omg", :path => "#{lib_path("omg")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works when the path does not have a gemspec" do + build_lib "foo", :gemspec => false + + gemfile <<-G + gem "foo", "1.0", :path => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works when the path does not have a gemspec but there is a lockfile" do + lockfile <<-L + PATH + remote: vendor/bar + specs: + + GEM + remote: http://rubygems.org + L + + in_app_root { FileUtils.mkdir_p("vendor/bar") } + + install_gemfile <<-G + gem "bar", "1.0.0", path: "vendor/bar", require: "bar/nyard" + G + expect(exitstatus).to eq(0) if exitstatus + end + + context "existing lockfile" do + it "rubygems gems don't re-resolve without changes" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack-obama', '1.0' + gem 'net-ssh', '1.0' + G + + bundle :check, :env => { "DEBUG" => 1 } + expect(out).to match(/using resolution from the lockfile/) + expect(the_bundle).to include_gems "rack-obama 1.0", "net-ssh 1.0" + end + + it "source path gems w/deps don't re-resolve without changes" do + build_lib "rack-obama", "1.0", :path => lib_path("omg") do |s| + s.add_dependency "yard" + end + + build_lib "net-ssh", "1.0", :path => lib_path("omg") do |s| + s.add_dependency "yard" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack-obama', :path => "#{lib_path("omg")}" + gem 'net-ssh', :path => "#{lib_path("omg")}" + G + + bundle :check, :env => { "DEBUG" => 1 } + expect(out).to match(/using resolution from the lockfile/) + expect(the_bundle).to include_gems "rack-obama 1.0", "net-ssh 1.0" + end + end + + it "installs executable stubs" do + build_lib "foo" do |s| + s.executables = ["foo"] + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle "exec foo" + expect(out).to eq("1.0") + end + + describe "when the gem version in the path is updated" do + before :each do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "bar" + end + build_lib "bar", "1.0", :path => lib_path("foo/bar") + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + G + end + + it "unlocks all gems when the top level gem is updated" do + build_lib "foo", "2.0", :path => lib_path("foo") do |s| + s.add_dependency "bar" + end + + bundle "install" + + expect(the_bundle).to include_gems "foo 2.0", "bar 1.0" + end + + it "unlocks all gems when a child dependency gem is updated" do + build_lib "bar", "2.0", :path => lib_path("foo/bar") + + bundle "install" + + expect(the_bundle).to include_gems "foo 1.0", "bar 2.0" + end + end + + describe "when dependencies in the path are updated" do + before :each do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + end + + it "gets dependencies that are updated in the path" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack" + end + + bundle "install" + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + describe "switching sources" do + it "doesn't switch pinned git sources to rubygems when pinning the parent gem to a path source" do + build_gem "foo", "1.0", :to_system => true do |s| + s.write "lib/foo.rb", "raise 'fail'" + end + build_lib "foo", "1.0", :path => lib_path("bar/foo") + build_git "bar", "1.0", :path => lib_path("bar") do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :git => "#{lib_path("bar")}" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :path => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + end + + it "switches the source when the gem existed in rubygems and the path was already being used for another gem" do + build_lib "foo", "1.0", :path => lib_path("foo") + build_gem "bar", "1.0", :to_system => true do |s| + s.write "lib/bar.rb", "raise 'fail'" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar" + path "#{lib_path("foo")}" do + gem "foo" + end + G + + build_lib "bar", "1.0", :path => lib_path("foo/bar") + + install_gemfile <<-G + source "file://#{gem_repo1}" + path "#{lib_path("foo")}" do + gem "foo" + gem "bar" + end + G + + expect(the_bundle).to include_gems "bar 1.0" + end + end + + describe "when there are both a gemspec and remote gems" do + it "doesn't query rubygems for local gemspec name" do + build_lib "private_lib", "2.2", :path => lib_path("private_lib") + gemfile = <<-G + source "http://localgemserver.test" + gemspec + gem 'rack' + G + File.open(lib_path("private_lib/Gemfile"), "w") {|f| f.puts gemfile } + + Dir.chdir(lib_path("private_lib")) do + bundle :install, :env => { "DEBUG" => 1 }, :artifice => "endpoint" + expect(out).to match(%r{^HTTP GET http://localgemserver\.test/api/v1/dependencies\?gems=rack$}) + expect(out).not_to match(/^HTTP GET.*private_lib/) + expect(the_bundle).to include_gems "private_lib 2.2" + expect(the_bundle).to include_gems "rack 1.0" + end + end + end + + describe "gem install hooks" do + it "runs pre-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran pre-install hook: foo-1.0") + end + + it "runs post-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran post-install hook: foo-1.0") + end + + it "complains if the install hook fails" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(out).to include("failed for foo-1.0") + end + + it "loads plugins from the path gem" do + foo_file = home("foo_plugin_loaded") + bar_file = home("bar_plugin_loaded") + expect(foo_file).not_to be_file + expect(bar_file).not_to be_file + + build_lib "foo" do |s| + s.write("lib/rubygems_plugin.rb", "FileUtils.touch('#{foo_file}')") + end + + build_git "bar" do |s| + s.write("lib/rubygems_plugin.rb", "FileUtils.touch('#{bar_file}')") + end + + install_gemfile! <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + gem "bar", :path => "#{lib_path("bar-1.0")}" + G + + expect(foo_file).to be_file + expect(bar_file).to be_file + end + end +end diff --git a/spec/bundler/install/gemfile/platform_spec.rb b/spec/bundler/install/gemfile/platform_spec.rb new file mode 100644 index 00000000000000..b5dbc41a33713f --- /dev/null +++ b/spec/bundler/install/gemfile/platform_spec.rb @@ -0,0 +1,426 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install across platforms" do + it "maintains the same lockfile if all gems are compatible across platforms" do + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + #{not_local} + + DEPENDENCIES + rack + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + expect(the_bundle).to include_gems "rack 0.9.1" + end + + it "pulls in the correct platform specific gem" do + lockfile <<-G + GEM + remote: file:#{gem_repo1} + specs: + platform_specific (1.0) + platform_specific (1.0-java) + platform_specific (1.0-x86-mswin32) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + G + + simulate_platform "java" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific 1.0 JAVA" + end + + it "works with gems that have different dependencies" do + simulate_platform "java" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "nokogiri" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2 JAVA", "weakling 0.0.3" + + simulate_new_machine + + simulate_platform "ruby" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "nokogiri" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + expect(the_bundle).not_to include_gems "weakling" + end + + it "does not keep unneeded platforms for gems that are used" do + build_repo4 do + build_gem "empyrean", "0.1.0" + build_gem "coderay", "1.1.2" + build_gem "method_source", "0.9.0" + build_gem("spoon", "0.0.6") {|s| s.add_runtime_dependency "ffi" } + build_gem "pry", "0.11.3" do |s| + s.platform = "java" + s.add_runtime_dependency "coderay", "~> 1.1.0" + s.add_runtime_dependency "method_source", "~> 0.9.0" + s.add_runtime_dependency "spoon", "~> 0.0" + end + build_gem "pry", "0.11.3" do |s| + s.add_runtime_dependency "coderay", "~> 1.1.0" + s.add_runtime_dependency "method_source", "~> 0.9.0" + end + build_gem("ffi", "1.9.23") {|s| s.platform = "java" } + build_gem("ffi", "1.9.23") + end + + simulate_platform java + + install_gemfile! <<-G + source "file://localhost/#{gem_repo4}" + + gem "empyrean", "0.1.0" + gem "pry" + G + + expect(the_bundle.lockfile).to read_as normalize_uri_file(strip_whitespace(<<-L)) + GEM + remote: file://localhost/#{gem_repo4}/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + + DEPENDENCIES + empyrean (= 0.1.0) + pry + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle! "lock --add-platform ruby" + + good_lockfile = strip_whitespace(<<-L) + GEM + remote: file://localhost/#{gem_repo4}/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + ruby + + DEPENDENCIES + empyrean (= 0.1.0) + pry + + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(the_bundle.lockfile).to read_as normalize_uri_file(good_lockfile) + + bad_lockfile = strip_whitespace <<-L + GEM + remote: file://localhost/#{gem_repo4}/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + ruby + + DEPENDENCIES + empyrean (= 0.1.0) + pry + + BUNDLED WITH + #{Bundler::VERSION} + L + + aggregate_failures do + lockfile bad_lockfile + bundle! :install + expect(the_bundle.lockfile).to read_as normalize_uri_file(good_lockfile) + + lockfile bad_lockfile + bundle! :update, :all => true + expect(the_bundle.lockfile).to read_as normalize_uri_file(good_lockfile) + + lockfile bad_lockfile + bundle! "update ffi" + expect(the_bundle.lockfile).to read_as normalize_uri_file(good_lockfile) + + lockfile bad_lockfile + bundle! "update empyrean" + expect(the_bundle.lockfile).to read_as normalize_uri_file(good_lockfile) + + lockfile bad_lockfile + bundle! :lock + expect(the_bundle.lockfile).to read_as normalize_uri_file(good_lockfile) + end + end + + it "works the other way with gems that have different dependencies" do + simulate_platform "ruby" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "nokogiri" + G + + simulate_platform "java" + bundle "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2 JAVA", "weakling 0.0.3" + end + + it "works with gems that have extra platform-specific runtime dependencies", :bundler => "< 3" do + simulate_platform x64_mac + + update_repo2 do + build_gem "facter", "2.4.6" + build_gem "facter", "2.4.6" do |s| + s.platform = "universal-darwin" + s.add_runtime_dependency "CFPropertyList" + end + build_gem "CFPropertyList" + end + + install_gemfile! <<-G + source "file://#{gem_repo2}" + + gem "facter" + G + + expect(out).to include "Unable to use the platform-specific (universal-darwin) version of facter (2.4.6) " \ + "because it has different dependencies from the ruby version. " \ + "To use the platform-specific version of the gem, run `bundle config specific_platform true` and install again." + + expect(the_bundle).to include_gem "facter 2.4.6" + expect(the_bundle).not_to include_gem "CFPropertyList" + end + + it "fetches gems again after changing the version of Ruby" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + + new_version = Gem::ConfigMap[:ruby_version] == "1.8" ? "1.9.1" : "1.8" + FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, new_version)) + + bundle! :install + expect(vendored_gems("gems/rack-1.0.0")).to exist + end +end + +RSpec.describe "bundle install with platform conditionals" do + it "installs gems tagged w/ the current platforms" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + platforms :#{local_tag} do + gem "nokogiri" + end + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "does not install gems tagged w/ another platforms" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + platforms :#{not_local_tag} do + gem "nokogiri" + end + G + + expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).not_to include_gems "nokogiri 1.4.2" + end + + it "installs gems tagged w/ the current platforms inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platforms => :#{local_tag} + G + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "does not install gems tagged w/ another platforms inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "nokogiri", :platforms => :#{not_local_tag} + G + expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).not_to include_gems "nokogiri 1.4.2" + end + + it "installs gems tagged w/ the current platform inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platform => :#{local_tag} + G + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "doesn't install gems tagged w/ another platform inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platform => :#{not_local_tag} + G + expect(the_bundle).not_to include_gems "nokogiri 1.4.2" + end + + it "does not blow up on sources with all platform-excluded specs" do + build_git "foo" + + install_gemfile <<-G + platform :#{not_local_tag} do + gem "foo", :git => "#{lib_path("foo-1.0")}" + end + G + + bundle :list + expect(exitstatus).to eq(0) if exitstatus + end + + it "does not attempt to install gems from :rbx when using --local" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "some_gem", :platform => :rbx + G + + bundle "install --local" + expect(out).not_to match(/Could not find gem 'some_gem/) + end + + it "does not attempt to install gems from other rubies when using --local" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + other_ruby_version_tag = RUBY_VERSION =~ /^1\.8/ ? :ruby_19 : :ruby_18 + + gemfile <<-G + source "file://#{gem_repo1}" + gem "some_gem", platform: :#{other_ruby_version_tag} + G + + bundle "install --local" + expect(out).not_to match(/Could not find gem 'some_gem/) + end + + it "prints a helpful warning when a dependency is unused on any platform" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", :platform => [:mingw, :mswin, :x64_mingw, :jruby] + G + + bundle! "install" + + expect(out).to include <<-O.strip +The dependency #{Gem::Dependency.new("rack", ">= 0")} will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`. + O + end + + context "when disable_platform_warnings is true" do + before { bundle! "config disable_platform_warnings true" } + + it "does not print the warning when a dependency is unused on any platform" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", :platform => [:mingw, :mswin, :x64_mingw, :jruby] + G + + bundle! "install" + + expect(out).not_to match(/The dependency (.*) will be unused/) + end + end +end + +RSpec.describe "when a gem has no architecture" do + it "still installs correctly" do + simulate_platform mswin + + gemfile <<-G + # Try to install gem with nil arch + source "http://localgemserver.test/" + gem "rcov" + G + + bundle :install, :artifice => "windows" + expect(the_bundle).to include_gems "rcov 1.0.0" + end +end diff --git a/spec/bundler/install/gemfile/ruby_spec.rb b/spec/bundler/install/gemfile/ruby_spec.rb new file mode 100644 index 00000000000000..24fe021fa35d56 --- /dev/null +++ b/spec/bundler/install/gemfile/ruby_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +RSpec.describe "ruby requirement" do + def locked_ruby_version + Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(lockfile).ruby_version) + end + + # As discovered by https://github.com/bundler/bundler/issues/4147, there is + # no test coverage to ensure that adding a gem is possible with a ruby + # requirement. This test verifies the fix, committed in bfbad5c5. + it "allows adding gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby "#{RUBY_VERSION}" + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby "#{RUBY_VERSION}" + gem "rack" + gem "rack-obama" + G + + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "rack-obama 1.0" + end + + it "allows removing the ruby version requirement" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby "~> #{RUBY_VERSION}" + gem "rack" + G + + expect(lockfile).to include("RUBY VERSION") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(lockfile).not_to include("RUBY VERSION") + end + + it "allows changing the ruby version requirement to something compatible" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 1.0.0" + gem "rack" + G + + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + + simulate_ruby_version "5100" + + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 1.0.1" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + end + + it "allows changing the ruby version requirement to something incompatible" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 1.0.0" + gem "rack" + G + + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + + simulate_ruby_version "5100" + + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 5000.0" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(locked_ruby_version.versions).to eq(["5100"]) + end + + it "allows requirements with trailing whitespace" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + ruby "#{RUBY_VERSION}\\n \t\\n" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "fails gracefully with malformed requirements" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 0", "-.\\0" + gem "rack" + G + + expect(out).to include("There was an error parsing") # i.e. DSL error, not error template + end +end diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb new file mode 100644 index 00000000000000..efe6ccce7f26ab --- /dev/null +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -0,0 +1,619 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with gems on multiple sources" do + # repo1 is built automatically before all of the specs run + # it contains rack-obama 1.0.0 and rack 0.9.1 & 1.0.0 amongst other gems + + context "without source affinity" do + before do + # Oh no! Someone evil is trying to hijack rack :( + # need this to be broken to check for correct source ordering + build_repo gem_repo3 do + build_gem "rack", repo3_rack_version do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + end + + context "with multiple toplevel sources" do + let(:repo3_rack_version) { "1.0.0" } + + before do + gemfile <<-G + source "file://localhost#{gem_repo3}" + source "file://localhost#{gem_repo1}" + gem "rack-obama" + gem "rack" + G + bundle "config major_deprecations true" + end + + it "warns about ambiguous gems, but installs anyway, prioritizing sources last to first", :bundler => "< 3" do + bundle :install + + expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).to include(normalize_uri_file("Installed from: file://localhost#{gem_repo1}")) + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") + end + + it "errors when disable_multisource is set" do + bundle "config disable_multisource true" + bundle :install + expect(out).to include("Each source after the first must include a block") + expect(exitstatus).to eq(4) if exitstatus + end + end + + context "when different versions of the same gem are in multiple sources" do + let(:repo3_rack_version) { "1.2" } + + before do + gemfile <<-G + source "file://localhost#{gem_repo3}" + source "file://localhost#{gem_repo1}" + gem "rack-obama" + gem "rack", "1.0.0" # force it to install the working version in repo1 + G + bundle "config major_deprecations true" + end + + it "warns about ambiguous gems, but installs anyway", :bundler => "< 3" do + bundle :install + + expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).to include(normalize_uri_file("Installed from: file://localhost#{gem_repo1}")) + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") + end + end + end + + context "with source affinity" do + context "with sources given by a block" do + before do + # Oh no! Someone evil is trying to hijack rack :( + # need this to be broken to check for correct source ordering + build_repo gem_repo3 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + build_gem "rack-obama" do |s| + s.add_dependency "rack" + end + end + + gemfile <<-G + source "file://#{gem_repo3}" + source "file://#{gem_repo1}" do + gem "thin" # comes first to test name sorting + gem "rack" + end + gem "rack-obama" # shoud come from repo3! + G + end + + it "installs the gems without any warning" do + bundle! :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("rack-obama 1.0.0") + expect(the_bundle).to include_gems("rack 1.0.0", :source => "remote1") + end + + it "can cache and deploy" do + bundle! :package + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/rack-obama-1.0.gem")).to exist + + bundle! :install, forgotten_command_line_options(:deployment => true) + + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0") + end + end + + context "with sources set by an option" do + before do + # Oh no! Someone evil is trying to hijack rack :( + # need this to be broken to check for correct source ordering + build_repo gem_repo3 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + build_gem "rack-obama" do |s| + s.add_dependency "rack" + end + end + + gemfile <<-G + source "file://#{gem_repo3}" + gem "rack-obama" # should come from repo3! + gem "rack", :source => "file://#{gem_repo1}" + G + end + + it "installs the gems without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0") + end + end + + context "when a pinned gem has an indirect dependency" do + before do + build_repo gem_repo3 do + build_gem "depends_on_rack", "1.0.1" do |s| + s.add_dependency "rack" + end + end + end + + context "when the indirect dependency is in the pinned source" do + before do + # we need a working rack gem in repo3 + update_repo gem_repo3 do + build_gem "rack", "1.0.0" + end + + gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{gem_repo3}" do + gem "depends_on_rack" + end + G + end + + context "and not in any other sources" do + before do + build_repo(gem_repo2) {} + end + + it "installs from the same source without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + + context "and in another source" do + before do + # need this to be broken to check for correct source ordering + build_repo gem_repo2 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + end + + context "when lockfile_uses_separate_rubygems_sources is set" do + before do + bundle! "config lockfile_uses_separate_rubygems_sources true" + bundle! "config disable_multisource true" + end + + it "installs from the same source without any warning" do + bundle! :install + + expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(err).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + + # when there is already a lock file, and the gems are missing, so try again + system_gems [] + bundle! :install + + expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(err).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + end + end + + context "when the indirect dependency is in a different source" do + before do + # In these tests, we need a working rack gem in repo2 and not repo3 + build_repo gem_repo2 do + build_gem "rack", "1.0.0" + end + end + + context "and not in any other sources" do + before do + gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{gem_repo3}" do + gem "depends_on_rack" + end + G + end + + it "installs from the other source without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + + context "and in yet another source" do + before do + gemfile <<-G + source "file://localhost#{gem_repo1}" + source "file://localhost#{gem_repo2}" + source "file://localhost#{gem_repo3}" do + gem "depends_on_rack" + end + G + end + + it "installs from the other source and warns about ambiguous gems", :bundler => "< 3" do + bundle "config major_deprecations true" + bundle :install + expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).to include(normalize_uri_file("Installed from: file://localhost#{gem_repo2}")) + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + + context "and only the dependency is pinned" do + before do + # need this to be broken to check for correct source ordering + build_repo gem_repo2 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + + gemfile <<-G + source "file://#{gem_repo3}" # contains depends_on_rack + source "file://#{gem_repo2}" # contains broken rack + + gem "depends_on_rack" # installed from gem_repo3 + gem "rack", :source => "file://#{gem_repo1}" + G + end + + it "installs the dependency from the pinned source without warning", :bundler => "< 3" do + bundle :install + + expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + + # In https://github.com/bundler/bundler/issues/3585 this failed + # when there is already a lock file, and the gems are missing, so try again + system_gems [] + bundle :install + + expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + end + end + + context "when a top-level gem has an indirect dependency" do + context "when lockfile_uses_separate_rubygems_sources is set" do + before do + bundle! "config lockfile_uses_separate_rubygems_sources true" + bundle! "config disable_multisource true" + end + + before do + build_repo gem_repo2 do + build_gem "depends_on_rack", "1.0.1" do |s| + s.add_dependency "rack" + end + end + + build_repo gem_repo3 do + build_gem "unrelated_gem", "1.0.0" + end + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "depends_on_rack" + + source "file://#{gem_repo3}" do + gem "unrelated_gem" + end + G + end + + context "and the dependency is only in the top-level source" do + before do + update_repo gem_repo2 do + build_gem "rack", "1.0.0" + end + end + + it "installs all gems without warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0", "unrelated_gem 1.0.0") + end + end + + context "and the dependency is only in a pinned source" do + before do + update_repo gem_repo3 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + end + + it "does not find the dependency" do + bundle :install + expect(out).to include("Could not find gem 'rack', which is required by gem 'depends_on_rack', in any of the relevant sources") + end + end + + context "and the dependency is in both the top-level and a pinned source" do + before do + update_repo gem_repo2 do + build_gem "rack", "1.0.0" + end + + update_repo gem_repo3 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + end + + it "installs the dependency from the top-level source without warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0", "unrelated_gem 1.0.0") + end + end + end + end + + context "with a gem that is only found in the wrong source" do + before do + build_repo gem_repo3 do + build_gem "not_in_repo1", "1.0.0" + end + + gemfile <<-G + source "file://#{gem_repo3}" + gem "not_in_repo1", :source => "file://#{gem_repo1}" + G + end + + it "does not install the gem" do + bundle :install + expect(out).to include("Could not find gem 'not_in_repo1'") + end + end + + context "with an existing lockfile" do + before do + system_gems "rack-0.9.1", "rack-1.0.0", :path => :bundle_path + + lockfile <<-L + GEM + remote: file:#{gem_repo1} + remote: file:#{gem_repo3} + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack! + L + + gemfile <<-G + source "file://#{gem_repo1}" + source "file://#{gem_repo3}" do + gem 'rack' + end + G + end + + # Reproduction of https://github.com/bundler/bundler/issues/3298 + it "does not unlock the installed gem on exec" do + expect(the_bundle).to include_gems("rack 0.9.1") + end + end + + context "with a path gem in the same Gemfile" do + before do + build_lib "foo" + + gemfile <<-G + gem "rack", :source => "file://#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + end + + it "does not unlock the non-path gem after install" do + bundle! :install + + bundle! %(exec ruby -e 'puts "OK"'), :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + + expect(out).to include("OK") + end + end + end + + context "when an older version of the same gem also ships with Ruby" do + before do + system_gems "rack-0.9.1" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" # shoud come from repo1! + G + end + + it "installs the gems without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("rack 1.0.0") + end + end + + context "when a single source contains multiple locked gems" do + before do + # 1. With these gems, + build_repo4 do + build_gem "foo", "0.1" + build_gem "bar", "0.1" + end + + # 2. Installing this gemfile will produce... + gemfile <<-G + source 'file://#{gem_repo1}' + gem 'rack' + gem 'foo', '~> 0.1', :source => 'file://#{gem_repo4}' + gem 'bar', '~> 0.1', :source => 'file://#{gem_repo4}' + G + + # 3. this lockfile. + lockfile <<-L + GEM + remote: file:/Users/andre/src/bundler/bundler/tmp/gems/remote1/ + remote: file:/Users/andre/src/bundler/bundler/tmp/gems/remote4/ + specs: + bar (0.1) + foo (0.1) + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + bar (~> 0.1)! + foo (~> 0.1)! + rack + L + + bundle! :install, forgotten_command_line_options(:path => "../gems/system") + + # 4. Then we add some new versions... + update_repo4 do + build_gem "foo", "0.2" + build_gem "bar", "0.3" + end + end + + it "allows them to be unlocked separately" do + # 5. and install this gemfile, updating only foo. + install_gemfile <<-G + source 'file://#{gem_repo1}' + gem 'rack' + gem 'foo', '~> 0.2', :source => 'file://#{gem_repo4}' + gem 'bar', '~> 0.1', :source => 'file://#{gem_repo4}' + G + + # 6. Which should update foo to 0.2, but not the (locked) bar 0.1 + expect(the_bundle).to include_gems("foo 0.2", "bar 0.1") + end + end + + context "re-resolving" do + context "when there is a mix of sources in the gemfile" do + before do + build_repo3 + build_lib "path1" + build_lib "path2" + build_git "git1" + build_git "git2" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + source "file://#{gem_repo3}" do + gem "rack" + end + + gem "path1", :path => "#{lib_path("path1-1.0")}" + gem "path2", :path => "#{lib_path("path2-1.0")}" + gem "git1", :git => "#{lib_path("git1-1.0")}" + gem "git2", :git => "#{lib_path("git2-1.0")}" + G + end + + it "does not re-resolve" do + bundle :install, :verbose => true + expect(out).to include("using resolution from the lockfile") + expect(out).not_to include("re-resolving dependencies") + end + end + end + + context "when a gem is installed to system gems" do + before do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "and the gemfile changes" do + it "is still able to find that gem from remote sources" do + source_uri = "file://#{gem_repo1}" + second_uri = "file://#{gem_repo4}" + + build_repo4 do + build_gem "rack", "2.0.1.1.forked" + build_gem "thor", "0.19.1.1.forked" + end + + # When this gemfile is installed... + gemfile <<-G + source "#{source_uri}" + + source "#{second_uri}" do + gem "rack", "2.0.1.1.forked" + gem "thor" + end + gem "rack-obama" + G + + # It creates this lockfile. + lockfile <<-L + GEM + remote: #{source_uri}/ + remote: #{second_uri}/ + specs: + rack (2.0.1.1.forked) + rack-obama (1.0) + rack + thor (0.19.1.1.forked) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 2.0.1.1.forked)! + rack-obama + thor! + L + + # Then we change the Gemfile by adding a version to thor + gemfile <<-G + source "#{source_uri}" + + source "#{second_uri}" do + gem "rack", "2.0.1.1.forked" + gem "thor", "0.19.1.1.forked" + end + gem "rack-obama" + G + + # But we should still be able to find rack 2.0.1.1.forked and install it + bundle! :install + end + end + end +end diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb new file mode 100644 index 00000000000000..9c725416d5478a --- /dev/null +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with specific_platform enabled" do + before do + bundle "config specific_platform true" + + build_repo2 do + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") + + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "universal-darwin" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") + + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.4.0") + build_gem("google-protobuf", "3.0.0.alpha.3.1.pre") + build_gem("google-protobuf", "3.0.0.alpha.3") + build_gem("google-protobuf", "3.0.0.alpha.2.0") + build_gem("google-protobuf", "3.0.0.alpha.1.1") + build_gem("google-protobuf", "3.0.0.alpha.1.0") + + build_gem("facter", "2.4.6") + build_gem("facter", "2.4.6") do |s| + s.platform = "universal-darwin" + s.add_runtime_dependency "CFPropertyList" + end + build_gem("CFPropertyList") + end + end + + let(:google_protobuf) { <<-G } + source "file:#{gem_repo2}" + gem "google-protobuf" + G + + context "when on a darwin machine" do + before { simulate_platform "x86_64-darwin-15" } + + it "locks to both the specific darwin platform and ruby" do + install_gemfile!(google_protobuf) + expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[ + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + ]) + end + + it "caches both the universal-darwin and ruby gems when --all-platforms is passed" do + gemfile(google_protobuf) + bundle! "package --all-platforms" + expect([cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1"), cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")]). + to all(exist) + end + + it "uses the platform-specific gem with extra dependencies" do + install_gemfile! <<-G + source "file:#{gem_repo2}" + gem "facter" + G + + expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + expect(the_bundle).to include_gems("facter 2.4.6 universal-darwin", "CFPropertyList 1.0") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(["CFPropertyList-1.0", + "facter-2.4.6", + "facter-2.4.6-universal-darwin"]) + end + + context "when adding a platform via lock --add_platform" do + it "adds the foreign platform" do + install_gemfile!(google_protobuf) + bundle! "lock --add-platform=#{x64_mingw}" + + expect(the_bundle.locked_gems.platforms).to eq([rb, x64_mingw, pl("x86_64-darwin-15")]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[ + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw32 + ]) + end + + it "falls back on plain ruby when that version doesnt have a platform-specific gem" do + install_gemfile!(google_protobuf) + bundle! "lock --add-platform=#{java}" + + expect(the_bundle.locked_gems.platforms).to eq([java, rb, pl("x86_64-darwin-15")]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[ + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + ]) + end + end + end +end diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb new file mode 100644 index 00000000000000..e74c5ffe59cad8 --- /dev/null +++ b/spec/bundler/install/gemfile_spec.rb @@ -0,0 +1,145 @@ +# encoding: utf-8 +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "with duplicated gems" do + it "will display a warning" do + install_gemfile <<-G + gem 'rails', '~> 4.0.0' + gem 'rails', '~> 4.0.0' + G + expect(out).to include("more than once") + end + end + + context "with --gemfile" do + it "finds the gemfile" do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle :install, :gemfile => bundled_app("NotGemfile") + + # Specify BUNDLE_GEMFILE for `the_bundle` + # to retrieve the proper Gemfile + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with gemfile set via config" do + before do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle "config --local gemfile #{bundled_app("NotGemfile")}" + end + it "uses the gemfile to install" do + bundle "install" + bundle "list" + + expect(out).to include("rack (1.0.0)") + end + it "uses the gemfile while in a subdirectory" do + bundled_app("subdir").mkpath + Dir.chdir(bundled_app("subdir")) do + bundle "install" + bundle "list" + + expect(out).to include("rack (1.0.0)") + end + end + end + + context "with deprecated features" do + before :each do + in_app_root + end + + it "reports that lib is an invalid option" do + gemfile <<-G + gem "rack", :lib => "rack" + G + + bundle :install + expect(out).to match(/You passed :lib as an option for gem 'rack', but it is invalid/) + end + end + + context "with prefer_gems_rb set" do + before { bundle! "config prefer_gems_rb true" } + + it "prefers gems.rb to Gemfile" do + create_file("gems.rb", "gem 'bundler'") + create_file("Gemfile", "raise 'wrong Gemfile!'") + + bundle! :install + + expect(bundled_app("gems.rb")).to be_file + expect(bundled_app("Gemfile.lock")).not_to be_file + + expect(the_bundle).to include_gem "bundler #{Bundler::VERSION}" + end + end + + context "with engine specified in symbol" do + it "does not raise any error parsing Gemfile" do + simulate_ruby_version "2.3.0" do + simulate_ruby_engine "jruby", "9.1.2.0" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + ruby "2.3.0", :engine => :jruby, :engine_version => "9.1.2.0" + G + + expect(out).to match(/Bundle complete!/) + end + end + end + + it "installation succeeds" do + simulate_ruby_version "2.3.0" do + simulate_ruby_engine "jruby", "9.1.2.0" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + ruby "2.3.0", :engine => :jruby, :engine_version => "9.1.2.0" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + context "with a Gemfile containing non-US-ASCII characters" do + it "reads the Gemfile with the UTF-8 encoding by default" do + skip "Ruby 1.8 has no encodings" if RUBY_VERSION < "1.9" + + install_gemfile <<-G + str = "Il était une fois ..." + puts "The source encoding is: " + str.encoding.name + G + + expect(out).to include("The source encoding is: UTF-8") + expect(out).not_to include("The source encoding is: ASCII-8BIT") + expect(out).to include("Bundle complete!") + end + + it "respects the magic encoding comment" do + skip "Ruby 1.8 has no encodings" if RUBY_VERSION < "1.9" + + # NOTE: This works thanks to #eval interpreting the magic encoding comment + install_gemfile <<-G + # encoding: iso-8859-1 + str = "Il #{"\xE9".dup.force_encoding("binary")}tait une fois ..." + puts "The source encoding is: " + str.encoding.name + G + + expect(out).to include("The source encoding is: ISO-8859-1") + expect(out).to include("Bundle complete!") + end + end +end diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb new file mode 100644 index 00000000000000..2acade1166ed01 --- /dev/null +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -0,0 +1,940 @@ +# frozen_string_literal: true + +RSpec.describe "compact index api" do + let(:source_hostname) { "localgemserver.test" } + let(:source_uri) { "http://#{source_hostname}" } + + it "should use the API" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should URI encode gem names" do + gemfile <<-G + source "#{source_uri}" + gem " sinatra" + G + + bundle :install, :artifice => "compact_index" + expect(out).to include("' sinatra' is not a valid gem name because it contains whitespace.") + end + + it "should handle nested dependencies" do + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems( + "rails 2.3.2", + "actionpack 2.3.2", + "activerecord 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2" + ) + end + + it "should handle case sensitivity conflicts" do + build_repo4 do + build_gem "rack", "1.0" do |s| + s.add_runtime_dependency("Rack", "0.1") + end + build_gem "Rack", "0.1" + end + + install_gemfile! <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo4 } + source "#{source_uri}" + gem "rack", "1.0" + gem "Rack", "0.1" + G + + # can't use `include_gems` here since the `require` will conflict on a + # case-insensitive FS + run! "Bundler.require; puts Gem.loaded_specs.values_at('rack', 'Rack').map(&:full_name)" + expect(last_command.stdout).to eq("rack-1.0\nRack-0.1") + end + + it "should handle multiple gem dependencies on the same gem" do + gemfile <<-G + source "#{source_uri}" + gem "net-sftp" + G + + bundle! :install, :artifice => "compact_index" + expect(the_bundle).to include_gems "net-sftp 1.1.1" + end + + it "should use the endpoint when using --deployment" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + bundle! :install, :artifice => "compact_index" + + bundle! :install, forgotten_command_line_options(:deployment => true, :path => "vendor/bundle").merge(:artifice => "compact_index") + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles git dependencies that are in rubygems" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + git "file:///#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + bundle! :install, :artifice => "compact_index" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "handles git dependencies that are in rubygems using --deployment" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle! :install, :artifice => "compact_index" + + bundle "install --deployment", :artifice => "compact_index" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "doesn't fail if you only have a git gem with no deps when using --deployment" do + build_git "foo" + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle "install", :artifice => "compact_index" + bundle! :install, forgotten_command_line_options(:deployment => true).merge(:artifice => "compact_index") + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "falls back when the API errors out" do + simulate_platform mswin + + gemfile <<-G + source "#{source_uri}" + gem "rcov" + G + + bundle! :install, :artifice => "windows" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rcov 1.0.0" + end + + it "falls back when the API URL returns 403 Forbidden" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :verbose => true, :artifice => "compact_index_forbidden" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "falls back when the versions endpoint has a checksum mismatch" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :verbose => true, :artifice => "compact_index_checksum_mismatch" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(out).to include <<-'WARN' +The checksum of /versions does not match the checksum provided by the server! Something is wrong (local checksum is "\"d41d8cd98f00b204e9800998ecf8427e\"", was expecting "\"123\""). + WARN + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "falls back when the user's home directory does not exist or is not writable" do + ENV["HOME"] = tmp("missing_home").to_s + + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_host_redirect" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects without Net::HTTP::Persistent" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + FileUtils.mkdir_p lib_path + File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h| + h.write <<-H + module Kernel + alias require_without_disabled_net_http require + def require(*args) + raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty? + require_without_disabled_net_http(*args) + end + end + H + end + + bundle! :install, :artifice => "compact_index_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + expect(out).to_not match(/Too many redirects/) + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "times out when Bundler::Fetcher redirects too much" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "compact_index_redirects" + expect(out).to match(/Too many redirects/) + end + + context "when --full-index is specified" do + it "should use the modern index for install" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --full-index", :artifice => "compact_index" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the modern index for update" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! "update --full-index", :artifice => "compact_index", :all => bundle_update_requires_all? + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + it "does not double check for gems that are only installed locally" do + system_gems %w[rack-1.0.0 thin-1.0 net_a-1.0] + bundle! "config --local path.system true" + ENV["BUNDLER_SPEC_ALL_REQUESTS"] = strip_whitespace(<<-EOS).strip + #{source_uri}/versions + #{source_uri}/info/rack + EOS + + install_gemfile! <<-G, :artifice => "compact_index", :verbose => true + source "#{source_uri}" + gem "rack" + G + + expect(last_command.stdboth).not_to include "Double checking" + end + + it "fetches again when more dependencies are found in subsequent sources", :bundler => "< 3" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle! :install, :artifice => "compact_index_extra" + expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources with source blocks" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + install_gemfile! <<-G, :artifice => "compact_index_extra", :verbose => true + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0" + end + + it "fetches gem versions even when those gems are already installed" do + gemfile <<-G + source "#{source_uri}" + gem "rack", "1.0.0" + G + bundle! :install, :artifice => "compact_index_extra_api" + expect(the_bundle).to include_gems "rack 1.0.0" + + build_repo4 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + end + + gemfile <<-G + source "#{source_uri}" do; end + source "#{source_uri}/extra" + gem "rack", "1.2" + G + bundle! :install, :artifice => "compact_index_extra_api" + expect(the_bundle).to include_gems "rack 1.2" + end + + it "considers all possible versions of dependencies from all api gem sources", :bundler => "< 3" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that + # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 + # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other + # repo and installs it. + build_repo4 do + build_gem "activesupport", "1.2.0" + build_gem "somegem", "1.0.0" do |s| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem 'somegem', '1.0.0' + G + + bundle! :install, :artifice => "compact_index_extra_api" + + expect(the_bundle).to include_gems "somegem 1.0.0" + expect(the_bundle).to include_gems "activesupport 1.2.3" + end + + it "considers all possible versions of dependencies from all api gem sources when using blocks", :bundler => "< 3" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that + # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 + # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other + # repo and installs it. + build_repo4 do + build_gem "activesupport", "1.2.0" + build_gem "somegem", "1.0.0" do |s| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem 'somegem', '1.0.0' + end + G + + bundle! :install, :artifice => "compact_index_extra_api" + + expect(the_bundle).to include_gems "somegem 1.0.0" + expect(the_bundle).to include_gems "activesupport 1.2.3" + end + + it "prints API output properly with back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle! :install, :artifice => "compact_index_extra" + + expect(out).to include("Fetching gem metadata from http://localgemserver.test/") + expect(out).to include("Fetching source index from http://localgemserver.test/extra") + end + + it "does not fetch every spec if the index of gems is large when doing back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + # need to hit the limit + 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| + build_gem "gem#{i}" + end + + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle! :install, :artifice => "compact_index_extra_missing" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "does not fetch every spec if the index of gems is large when doing back deps & everything is the compact index" do + build_repo4 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + # need to hit the limit + 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| + build_gem "gem#{i}" + end + + FileUtils.rm_rf Dir[gem_repo4("gems/foo-*.gem")] + end + + install_gemfile! <<-G, :artifice => "compact_index_extra_api_missing" + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + expect(the_bundle).to include_gem "back_deps 1.0" + end + + it "uses the endpoint if all sources support it" do + gemfile <<-G + source "#{source_uri}" + + gem 'foo' + G + + bundle! :install, :artifice => "compact_index_api_missing" + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources using --deployment", :bundler => "< 3" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle! :install, :artifice => "compact_index_extra" + + bundle "install --deployment", :artifice => "compact_index_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources using --deployment with blocks" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle! :install, :artifice => "compact_index_extra" + + bundle "install --deployment", :artifice => "compact_index_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "does not refetch if the only unmet dependency is bundler" do + gemfile <<-G + source "#{source_uri}" + + gem "bundler_dep" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + end + + it "should install when EndpointSpecification has a bin dir owned by root", :sudo => true do + sudo "mkdir -p #{system_gem_path("bin")}" + sudo "chown -R root #{system_gem_path("bin")}" + + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + bundle! :install, :artifice => "compact_index" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "installs the binstubs", :bundler => "< 3" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --binstubs", :artifice => "compact_index" + + gembin "rackup" + expect(out).to eq("1.0.0") + end + + it "installs the bins when using --path and uses autoclean", :bundler => "< 3" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle", :artifice => "compact_index" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "installs the bins when using --path and uses bundle clean", :bundler => "< 3" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle --no-clean", :artifice => "compact_index" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "prints post_install_messages" do + gemfile <<-G + source "#{source_uri}" + gem 'rack-obama' + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Post-install message from rack:") + end + + it "should display the post install message for a dependency" do + gemfile <<-G + source "#{source_uri}" + gem 'rack_middleware' + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + + context "when using basic authentication" do + let(:user) { "user" } + let(:password) { "pass" } + let(:basic_auth_source_uri) do + uri = URI.parse(source_uri) + uri.user = user + uri.password = password + + uri + end + + it "passes basic authentication details and strips out creds" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic authentication creds for modern index" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "endopint_marshal_fail_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic auth creds when it can't reach the server" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_500" + expect(out).not_to include("#{user}:#{password}") + end + + it "strips http basic auth creds when warning about ambiguous sources", :bundler => "< 3" do + gemfile <<-G + source "#{basic_auth_source_uri}" + source "file://#{gem_repo1}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_basic_authentication" + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not pass the user / password to different hosts on redirect" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_creds_diff_host" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + describe "with authentication details in bundle config" do + before do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + end + + it "reads authentication details by host name from bundle config" do + bundle "config #{source_hostname} #{user}:#{password}" + + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "reads authentication details by full url from bundle config" do + # The trailing slash is necessary here; Fetcher canonicalizes the URI. + bundle "config #{source_uri}/ #{user}:#{password}" + + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the API" do + bundle "config #{source_hostname} #{user}:#{password}" + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "prefers auth supplied in the source uri" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle "config #{source_hostname} otheruser:wrong" + + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "shows instructions if auth is not provided for the source" do + bundle :install, :artifice => "compact_index_strict_basic_authentication" + expect(out).to include("bundle config #{source_hostname} username:password") + end + + it "fails if authentication has already been provided, but failed" do + bundle "config #{source_hostname} #{user}:wrong" + + bundle :install, :artifice => "compact_index_strict_basic_authentication" + expect(out).to include("Bad username or password") + end + end + + describe "with no password" do + let(:password) { nil } + + it "passes basic authentication details" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + + context "when ruby is compiled without openssl" do + before do + # Install a monkeypatch that reproduces the effects of openssl being + # missing when the fetcher runs, as happens in real life. The reason + # we can't just overwrite openssl.rb is that Artifice uses it. + bundled_app("broken_ssl").mkpath + bundled_app("broken_ssl/openssl.rb").open("w") do |f| + f.write <<-RUBY + raise LoadError, "cannot load such file -- openssl" + RUBY + end + end + + it "explains what to do to get it" do + gemfile <<-G + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } + expect(out).to include("OpenSSL") + end + end + + context "when SSL certificate verification fails" do + it "explains what happened" do + # Install a monkeypatch that reproduces the effects of openssl raising + # a certificate validation error when RubyGems tries to connect. + gemfile <<-G + class Net::HTTP + def start + raise OpenSSL::SSL::SSLError, "certificate verify failed" + end + end + + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install + expect(out).to match(/could not verify the SSL certificate/i) + end + end + + context ".gemrc with sources is present" do + before do + File.open(home(".gemrc"), "w") do |file| + file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + end + end + + after do + home(".gemrc").rmtree + end + + it "uses other sources declared in the Gemfile" do + gemfile <<-G + source "#{source_uri}" + gem 'rack' + G + + bundle! :install, :artifice => "compact_index_forbidden" + end + end + + it "performs partial update with a non-empty range" do + gemfile <<-G + source "#{source_uri}" + gem 'rack', '0.9.1' + G + + # Initial install creates the cached versions file + bundle! :install, :artifice => "compact_index" + + # Update the Gemfile so we can check subsequent install was successful + gemfile <<-G + source "#{source_uri}" + gem 'rack', '1.0.0' + G + + # Second install should make only a partial request to /versions + bundle! :install, :artifice => "compact_index_partial_update" + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "performs partial update while local cache is updated by another process" do + gemfile <<-G + source "#{source_uri}" + gem 'rack' + G + + # Create an empty file to trigger a partial download + versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") + FileUtils.mkdir_p(File.dirname(versions)) + FileUtils.touch(versions) + + bundle! :install, :artifice => "compact_index_concurrent_download" + + expect(File.read(versions)).to start_with("created_at") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "performs full update of compact index info cache if range is not satisfiable" do + gemfile <<-G + source "#{source_uri}" + gem 'rack', '0.9.1' + G + + rake_info_path = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "info", "rack") + + bundle! :install, :artifice => "compact_index" + + expected_rack_info_content = File.read(rake_info_path) + + # Modify the cache files. We expect them to be reset to the normal ones when we re-run :install + File.open(rake_info_path, "w") {|f| f << (expected_rack_info_content + "this is different") } + + # Update the Gemfile so the next install does its normal things + gemfile <<-G + source "#{source_uri}" + gem 'rack', '1.0.0' + G + + # The cache files now being longer means the requested range is going to be not satisfiable + # Bundler must end up requesting the whole file to fix things up. + bundle! :install, :artifice => "compact_index_range_not_satisfiable" + + resulting_rack_info_content = File.read(rake_info_path) + + expect(resulting_rack_info_content).to eq(expected_rack_info_content) + end + + it "fails gracefully when the source URI has an invalid scheme" do + install_gemfile <<-G + source "htps://rubygems.org" + gem "rack" + G + expect(exitstatus).to eq(15) if exitstatus + expect(out).to end_with(<<-E.strip) + The request uri `htps://index.rubygems.org/versions` has an invalid scheme (`htps`). Did you mean `http` or `https`? + E + end + + describe "checksum validation", :rubygems => ">= 2.3.0" do + it "raises when the checksum does not match" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + + expect(exitstatus).to eq(19) if exitstatus + expect(out). + to include("Bundler cannot continue installing rack (1.0.0)."). + and include("The checksum for the downloaded `rack-1.0.0.gem` does not match the checksum given by the server."). + and include("This means the contents of the downloaded gem is different from what was uploaded to the server, and could be a potential security issue."). + and include("To resolve this issue:"). + and include("1. delete the downloaded gem located at: `#{default_bundle_path}/gems/rack-1.0.0/rack-1.0.0.gem`"). + and include("2. run `bundle install`"). + and include("If you wish to continue installing the downloaded gem, and are certain it does not pose a security issue despite the mismatching checksum, do the following:"). + and include("1. run `bundle config disable_checksum_validation true` to turn off checksum verification"). + and include("2. run `bundle install`"). + and match(/\(More info: The expected SHA256 checksum was "#{"ab" * 22}", but the checksum for the downloaded gem was ".+?"\.\)/) + end + + it "raises when the checksum is the wrong length" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum", :env => { "BUNDLER_SPEC_RACK_CHECKSUM" => "checksum!" } + source "#{source_uri}" + gem "rack" + G + expect(exitstatus).to eq(5) if exitstatus + expect(out).to include("The given checksum for rack-1.0.0 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") + end + + it "does not raise when disable_checksum_validation is set" do + bundle! "config disable_checksum_validation true" + install_gemfile! <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + end + end + + it "works when cache dir is world-writable" do + install_gemfile! <<-G, :artifice => "compact_index" + File.umask(0000) + source "#{source_uri}" + gem "rack" + G + end + + it "doesn't explode when the API dependencies are wrong" do + install_gemfile <<-G, :artifice => "compact_index_wrong_dependencies", :env => { "DEBUG" => "true" } + source "#{source_uri}" + gem "rails" + G + deps = [Gem::Dependency.new("rake", "= 10.0.2"), + Gem::Dependency.new("actionpack", "= 2.3.2"), + Gem::Dependency.new("activerecord", "= 2.3.2"), + Gem::Dependency.new("actionmailer", "= 2.3.2"), + Gem::Dependency.new("activeresource", "= 2.3.2")] + expect(out).to include(<<-E.strip).and include("rails-2.3.2 from rubygems remote at #{source_uri}/ has either corrupted API or lockfile dependencies") +Bundler::APIResponseMismatchError: Downloading rails-2.3.2 revealed dependencies not in the API or the lockfile (#{deps.map(&:to_s).join(", ")}). +Either installing with `--full-index` or running `bundle update rails` should fix the problem. + E + end + + it "does not duplicate specs in the lockfile when updating and a dependency is not installed" do + install_gemfile! <<-G, :artifice => "compact_index" + source "#{source_uri}" do + gem "rails" + gem "activemerchant" + end + G + gem_command! :uninstall, "activemerchant" + bundle! "update rails", :artifice => "compact_index" + expect(lockfile.scan(/activemerchant \(/).size).to eq(1) + end +end diff --git a/spec/bundler/install/gems/dependency_api_spec.rb b/spec/bundler/install/gems/dependency_api_spec.rb new file mode 100644 index 00000000000000..3cb98db1ebd805 --- /dev/null +++ b/spec/bundler/install/gems/dependency_api_spec.rb @@ -0,0 +1,760 @@ +# frozen_string_literal: true + +RSpec.describe "gemcutter's dependency API" do + let(:source_hostname) { "localgemserver.test" } + let(:source_uri) { "http://#{source_hostname}" } + + it "should use the API" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should URI encode gem names" do + gemfile <<-G + source "#{source_uri}" + gem " sinatra" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("' sinatra' is not a valid gem name because it contains whitespace.") + end + + it "should handle nested dependencies" do + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}/...") + expect(the_bundle).to include_gems( + "rails 2.3.2", + "actionpack 2.3.2", + "activerecord 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2" + ) + end + + it "should handle multiple gem dependencies on the same gem" do + gemfile <<-G + source "#{source_uri}" + gem "net-sftp" + G + + bundle :install, :artifice => "endpoint" + expect(the_bundle).to include_gems "net-sftp 1.1.1" + end + + it "should use the endpoint when using --deployment" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + bundle :install, :artifice => "endpoint" + + bundle! :install, forgotten_command_line_options(:deployment => true, :path => "vendor/bundle").merge(:artifice => "endpoint") + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles git dependencies that are in rubygems" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + git "file:///#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + bundle :install, :artifice => "endpoint" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "handles git dependencies that are in rubygems using --deployment" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle :install, :artifice => "endpoint" + + bundle "install --deployment", :artifice => "endpoint" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "doesn't fail if you only have a git gem with no deps when using --deployment" do + build_git "foo" + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle "install", :artifice => "endpoint" + bundle! :install, forgotten_command_line_options(:deployment => true).merge(:artifice => "endpoint") + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "falls back when the API errors out" do + simulate_platform mswin + + gemfile <<-G + source "#{source_uri}" + gem "rcov" + G + + bundle :install, :artifice => "windows" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rcov 1.0.0" + end + + it "falls back when hitting the Gemcutter Dependency Limit" do + gemfile <<-G + source "#{source_uri}" + gem "activesupport" + gem "actionpack" + gem "actionmailer" + gem "activeresource" + gem "thin" + gem "rack" + gem "rails" + G + bundle :install, :artifice => "endpoint_fallback" + expect(out).to include("Fetching source index from #{source_uri}") + + expect(the_bundle).to include_gems( + "activesupport 2.3.2", + "actionpack 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2", + "thin 1.0.0", + "rack 1.0.0", + "rails 2.3.2" + ) + end + + it "falls back when Gemcutter API doesn't return proper Marshal format" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :verbose => true, :artifice => "endpoint_marshal_fail" + expect(out).to include("could not fetch from the dependency API, trying the full index") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "falls back when the API URL returns 403 Forbidden" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :verbose => true, :artifice => "endpoint_api_forbidden" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_host_redirect" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects without Net::HTTP::Persistent" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + FileUtils.mkdir_p lib_path + File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h| + h.write <<-H + module Kernel + alias require_without_disabled_net_http require + def require(*args) + raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty? + require_without_disabled_net_http(*args) + end + end + H + end + + bundle :install, :artifice => "endpoint_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + expect(out).to_not match(/Too many redirects/) + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "timeouts when Bundler::Fetcher redirects too much" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_redirect" + expect(out).to match(/Too many redirects/) + end + + context "when --full-index is specified" do + it "should use the modern index for install" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --full-index", :artifice => "endpoint" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the modern index for update" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! "update --full-index", :artifice => "endpoint", :all => bundle_update_requires_all? + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + it "fetches again when more dependencies are found in subsequent sources", :bundler => "< 3" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle :install, :artifice => "endpoint_extra" + expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources using blocks" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle :install, :artifice => "endpoint_extra" + expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0" + end + + it "fetches gem versions even when those gems are already installed" do + gemfile <<-G + source "#{source_uri}" + gem "rack", "1.0.0" + G + bundle :install, :artifice => "endpoint_extra_api" + + build_repo4 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + end + + gemfile <<-G + source "#{source_uri}" do; end + source "#{source_uri}/extra" + gem "rack", "1.2" + G + bundle :install, :artifice => "endpoint_extra_api" + expect(the_bundle).to include_gems "rack 1.2" + end + + it "considers all possible versions of dependencies from all api gem sources", :bundler => "< 3" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that + # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 + # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other + # repo and installs it. + build_repo4 do + build_gem "activesupport", "1.2.0" + build_gem "somegem", "1.0.0" do |s| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem 'somegem', '1.0.0' + G + + bundle! :install, :artifice => "endpoint_extra_api" + + expect(the_bundle).to include_gems "somegem 1.0.0" + expect(the_bundle).to include_gems "activesupport 1.2.3" + end + + it "considers all possible versions of dependencies from all api gem sources using blocks" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that + # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 + # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other + # repo and installs it. + build_repo4 do + build_gem "activesupport", "1.2.0" + build_gem "somegem", "1.0.0" do |s| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem 'somegem', '1.0.0' + end + G + + bundle :install, :artifice => "endpoint_extra_api" + + expect(the_bundle).to include_gems "somegem 1.0.0" + expect(the_bundle).to include_gems "activesupport 1.2.3" + end + + it "prints API output properly with back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle :install, :artifice => "endpoint_extra" + + expect(out).to include("Fetching gem metadata from http://localgemserver.test/.") + expect(out).to include("Fetching source index from http://localgemserver.test/extra") + end + + it "does not fetch every spec if the index of gems is large when doing back deps", :bundler => "< 3" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + # need to hit the limit + 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| + build_gem "gem#{i}" + end + + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle :install, :artifice => "endpoint_extra_missing" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "does not fetch every spec if the index of gems is large when doing back deps using blocks" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + # need to hit the limit + 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| + build_gem "gem#{i}" + end + + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle :install, :artifice => "endpoint_extra_missing" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "uses the endpoint if all sources support it" do + gemfile <<-G + source "#{source_uri}" + + gem 'foo' + G + + bundle :install, :artifice => "endpoint_api_missing" + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources using --deployment", :bundler => "< 3" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle :install, :artifice => "endpoint_extra" + + bundle "install --deployment", :artifice => "endpoint_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources using --deployment with blocks" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle :install, :artifice => "endpoint_extra" + + bundle "install --deployment", :artifice => "endpoint_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "does not refetch if the only unmet dependency is bundler" do + gemfile <<-G + source "#{source_uri}" + + gem "bundler_dep" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}") + end + + it "should install when EndpointSpecification has a bin dir owned by root", :sudo => true do + sudo "mkdir -p #{system_gem_path("bin")}" + sudo "chown -R root #{system_gem_path("bin")}" + + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + bundle :install, :artifice => "endpoint" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "installs the binstubs", :bundler => "< 3" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --binstubs", :artifice => "endpoint" + + gembin "rackup" + expect(out).to eq("1.0.0") + end + + it "installs the bins when using --path and uses autoclean", :bundler => "< 3" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle", :artifice => "endpoint" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "installs the bins when using --path and uses bundle clean", :bundler => "< 3" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle --no-clean", :artifice => "endpoint" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "prints post_install_messages" do + gemfile <<-G + source "#{source_uri}" + gem 'rack-obama' + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Post-install message from rack:") + end + + it "should display the post install message for a dependency" do + gemfile <<-G + source "#{source_uri}" + gem 'rack_middleware' + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + + context "when using basic authentication" do + let(:user) { "user" } + let(:password) { "pass" } + let(:basic_auth_source_uri) do + uri = URI.parse(source_uri) + uri.user = user + uri.password = password + + uri + end + + it "passes basic authentication details and strips out creds" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic authentication creds for modern index" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endopint_marshal_fail_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic auth creds when it can't reach the server" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_500" + expect(out).not_to include("#{user}:#{password}") + end + + it "strips http basic auth creds when warning about ambiguous sources", :bundler => "< 3" do + gemfile <<-G + source "#{basic_auth_source_uri}" + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_basic_authentication" + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not pass the user / password to different hosts on redirect" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_creds_diff_host" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + describe "with authentication details in bundle config" do + before do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + end + + it "reads authentication details by host name from bundle config" do + bundle "config #{source_hostname} #{user}:#{password}" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "reads authentication details by full url from bundle config" do + # The trailing slash is necessary here; Fetcher canonicalizes the URI. + bundle "config #{source_uri}/ #{user}:#{password}" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the API" do + bundle "config #{source_hostname} #{user}:#{password}" + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "prefers auth supplied in the source uri" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle "config #{source_hostname} otheruser:wrong" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "shows instructions if auth is not provided for the source" do + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(out).to include("bundle config #{source_hostname} username:password") + end + + it "fails if authentication has already been provided, but failed" do + bundle "config #{source_hostname} #{user}:wrong" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(out).to include("Bad username or password") + end + end + + describe "with no password" do + let(:password) { nil } + + it "passes basic authentication details" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + + context "when ruby is compiled without openssl" do + before do + # Install a monkeypatch that reproduces the effects of openssl being + # missing when the fetcher runs, as happens in real life. The reason + # we can't just overwrite openssl.rb is that Artifice uses it. + bundled_app("broken_ssl").mkpath + bundled_app("broken_ssl/openssl.rb").open("w") do |f| + f.write <<-RUBY + raise LoadError, "cannot load such file -- openssl" + RUBY + end + end + + it "explains what to do to get it" do + gemfile <<-G + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } + expect(out).to include("OpenSSL") + end + end + + context "when SSL certificate verification fails" do + it "explains what happened" do + # Install a monkeypatch that reproduces the effects of openssl raising + # a certificate validation error when RubyGems tries to connect. + gemfile <<-G + class Net::HTTP + def start + raise OpenSSL::SSL::SSLError, "certificate verify failed" + end + end + + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install + expect(out).to match(/could not verify the SSL certificate/i) + end + end + + context ".gemrc with sources is present" do + before do + File.open(home(".gemrc"), "w") do |file| + file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + end + end + + after do + home(".gemrc").rmtree + end + + it "uses other sources declared in the Gemfile" do + gemfile <<-G + source "#{source_uri}" + gem 'rack' + G + + bundle "install", :artifice => "endpoint_marshal_fail" + + expect(exitstatus).to eq(0) if exitstatus + end + end +end diff --git a/spec/bundler/install/gems/env_spec.rb b/spec/bundler/install/gems/env_spec.rb new file mode 100644 index 00000000000000..0dccbbfd246ebd --- /dev/null +++ b/spec/bundler/install/gems/env_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with ENV conditionals" do + describe "when just setting an ENV key as a string" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env "BUNDLER_TEST" do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set" do + ENV["BUNDLER_TEST"] = "1" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when just setting an ENV key as a symbol" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env :BUNDLER_TEST do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set" do + ENV["BUNDLER_TEST"] = "1" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when setting a string to match the env" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env "BUNDLER_TEST" => "foo" do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "excludes the gems when the ENV variable is set but does not match the condition" do + ENV["BUNDLER_TEST"] = "1" + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set and matches the condition" do + ENV["BUNDLER_TEST"] = "foo" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when setting a regex to match the env" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env "BUNDLER_TEST" => /foo/ do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "excludes the gems when the ENV variable is set but does not match the condition" do + ENV["BUNDLER_TEST"] = "fo" + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set and matches the condition" do + ENV["BUNDLER_TEST"] = "foobar" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end +end diff --git a/spec/bundler/install/gems/flex_spec.rb b/spec/bundler/install/gems/flex_spec.rb new file mode 100644 index 00000000000000..736f418ec70be2 --- /dev/null +++ b/spec/bundler/install/gems/flex_spec.rb @@ -0,0 +1,351 @@ +# frozen_string_literal: true + +RSpec.describe "bundle flex_install" do + it "installs the gems as expected" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to be_locked + end + + it "installs even when the lockfile is invalid" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to be_locked + + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', '1.0' + G + + bundle :install + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to be_locked + end + + it "keeps child dependencies at the same version" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + + update_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama", "1.0" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + end + + describe "adding new gems" do + it "installs added gems without updating previously installed gems" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.5' + G + + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + + it "keeps child dependencies pinned" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama" + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama" + gem "thin" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0", "thin 1.0" + end + end + + describe "removing gems" do + it "removes gems without changing the versions of remaining gems" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.5' + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.2' + G + + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.2" + end + + it "removes top level dependencies when removed from the Gemfile while leaving other dependencies intact" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.5' + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + G + + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "removes child dependencies" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack-obama' + gem 'activesupport' + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0", "activesupport 2.3.5" + + update_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'activesupport' + G + + expect(the_bundle).to include_gems "activesupport 2.3.5" + expect(the_bundle).not_to include_gems "rack-obama", "rack" + end + end + + describe "when Gemfile conflicts with lockfile" do + before(:each) do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack_middleware" + G + + expect(the_bundle).to include_gems "rack_middleware 1.0", "rack 0.9.1" + + build_repo2 + update_repo2 do + build_gem "rack-obama", "2.0" do |s| + s.add_dependency "rack", "=1.2" + end + build_gem "rack_middleware", "2.0" do |s| + s.add_dependency "rack", ">=1.0" + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama", "2.0" + gem "rack_middleware" + G + end + + it "does not install gems whose dependencies are not met" do + bundle :install + ruby <<-RUBY + require 'bundler/setup' + RUBY + expect(err).to match(/could not find gem 'rack-obama/i) + end + + it "suggests bundle update when the Gemfile requires different versions than the lock" do + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Bundler could not find compatible versions for gem "rack": + In snapshot (Gemfile.lock): + rack (= 0.9.1) + + In Gemfile: + rack-obama (= 2.0) was resolved to 2.0, which depends on + rack (= 1.2) + + rack_middleware was resolved to 1.0, which depends on + rack (= 0.9.1) + + Running `bundle update` will rebuild your snapshot from scratch, using only + the gems in your Gemfile, which may resolve the conflict. + E + + bundle :install, :retry => 0 + expect(last_command.bundler_err).to end_with(nice_error) + end + end + + describe "subtler cases" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + gem "rack-obama" + G + end + + it "does something" do + expect do + bundle "install" + end.not_to change { File.read(bundled_app("Gemfile.lock")) } + + expect(out).to include("rack = 0.9.1") + expect(out).to include("locked at 1.0.0") + expect(out).to include("bundle update rack") + end + + it "should work when you update" do + bundle "update rack" + end + end + + describe "when adding a new source" do + it "updates the lockfile", :bundler => "< 3" do + build_repo2 + install_gemfile! <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + G + install_gemfile! <<-G + source "file://localhost#{gem_repo1}" + source "file://localhost#{gem_repo2}" + gem "rack" + G + + lockfile_should_be <<-L + GEM + remote: file://localhost#{gem_repo1}/ + remote: file://localhost#{gem_repo2}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "updates the lockfile", :bundler => "3" do + build_repo2 + install_gemfile! <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + G + + install_gemfile! <<-G + source "file://localhost#{gem_repo1}" + source "file://localhost#{gem_repo2}" do + end + gem "rack" + G + + lockfile_should_be <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + GEM + remote: file://localhost#{gem_repo2}/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + # This was written to test github issue #636 + describe "when a locked child dependency conflicts" do + before(:each) do + build_repo2 do + build_gem "capybara", "0.3.9" do |s| + s.add_dependency "rack", ">= 1.0.0" + end + + build_gem "rack", "1.1.0" + build_gem "rails", "3.0.0.rc4" do |s| + s.add_dependency "rack", "~> 1.1.0" + end + + build_gem "rack", "1.2.1" + build_gem "rails", "3.0.0" do |s| + s.add_dependency "rack", "~> 1.2.1" + end + end + end + + it "prints the correct error message" do + # install Rails 3.0.0.rc + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0.0.rc4" + gem "capybara", "0.3.9" + G + + # upgrade Rails to 3.0.0 and then install again + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0.0" + gem "capybara", "0.3.9" + G + + expect(out).to include("Gemfile.lock") + end + end +end diff --git a/spec/bundler/install/gems/mirror_spec.rb b/spec/bundler/install/gems/mirror_spec.rb new file mode 100644 index 00000000000000..4c35b8f2061e56 --- /dev/null +++ b/spec/bundler/install/gems/mirror_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with a mirror configured" do + describe "when the mirror does not match the gem source" do + before :each do + gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + bundle "config --local mirror.http://gems.example.org http://gem-mirror.example.org" + end + + it "installs from the normal location" do + bundle :install + expect(out).to include(normalize_uri_file("Fetching source index from file://localhost#{gem_repo1}")) + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when the gem source matches a configured mirror" do + before :each do + gemfile <<-G + # This source is bogus and doesn't have the gem we're looking for + source "file://localhost#{gem_repo2}" + + gem "rack" + G + bundle "config --local mirror.file://localhost#{gem_repo2} file://localhost#{gem_repo1}" + end + + it "installs the gem from the mirror" do + bundle :install + expect(out).to include(normalize_uri_file("Fetching source index from file://localhost#{gem_repo1}")) + expect(out).not_to include(normalize_uri_file("Fetching source index from file://localhost#{gem_repo2}")) + expect(the_bundle).to include_gems "rack 1.0" + end + end +end diff --git a/spec/bundler/install/gems/native_extensions_spec.rb b/spec/bundler/install/gems/native_extensions_spec.rb new file mode 100644 index 00000000000000..ea616f60d39083 --- /dev/null +++ b/spec/bundler/install/gems/native_extensions_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +RSpec.describe "installing a gem with native extensions", :ruby_repo do + it "installs" do + build_repo2 do + build_gem "c_extension" do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-E + require "mkmf" + name = "c_extension_bundle" + dir_config(name) + raise "OMG" unless with_config("c_extension") == "hello" + create_makefile(name) + E + + s.write "ext/c_extension.c", <<-C + #include "ruby.h" + + VALUE c_extension_true(VALUE self) { + return Qtrue; + } + + void Init_c_extension_bundle() { + VALUE c_Extension = rb_define_class("CExtension", rb_cObject); + rb_define_method(c_Extension, "its_true", c_extension_true, 0); + } + C + + s.write "lib/c_extension.rb", <<-C + require "c_extension_bundle" + C + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "c_extension" + G + + bundle "config build.c_extension --with-c_extension=hello" + bundle "install" + + expect(out).not_to include("extconf.rb failed") + expect(out).to include("Installing c_extension 1.0 with native extensions") + + run "Bundler.require; puts CExtension.new.its_true" + expect(out).to eq("true") + end + + it "installs from git" do + build_git "c_extension" do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-E + require "mkmf" + name = "c_extension_bundle" + dir_config(name) + raise "OMG" unless with_config("c_extension") == "hello" + create_makefile(name) + E + + s.write "ext/c_extension.c", <<-C + #include "ruby.h" + + VALUE c_extension_true(VALUE self) { + return Qtrue; + } + + void Init_c_extension_bundle() { + VALUE c_Extension = rb_define_class("CExtension", rb_cObject); + rb_define_method(c_Extension, "its_true", c_extension_true, 0); + } + C + + s.write "lib/c_extension.rb", <<-C + require "c_extension_bundle" + C + end + + bundle! "config build.c_extension --with-c_extension=hello" + + install_gemfile! <<-G + gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump} + G + + expect(out).not_to include("extconf.rb failed") + + run! "Bundler.require; puts CExtension.new.its_true" + expect(out).to eq("true") + end +end diff --git a/spec/bundler/install/gems/post_install_spec.rb b/spec/bundler/install/gems/post_install_spec.rb new file mode 100644 index 00000000000000..c6e348fb653680 --- /dev/null +++ b/spec/bundler/install/gems/post_install_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "with gem sources" do + context "when gems include post install messages" do + it "should display the post-install messages after installing" do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + gem 'thin' + gem 'rack-obama' + G + + bundle :install + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + expect(out).to include("Post-install message from thin:") + expect(out).to include("Thin's post install message") + expect(out).to include("Post-install message from rack-obama:") + expect(out).to include("Rack-obama's post install message") + end + end + + context "when gems do not include post install messages" do + it "should not display any post-install messages" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when a dependecy includes a post install message" do + it "should display the post install message" do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack_middleware' + G + + bundle :install + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + end + end + + context "with git sources" do + context "when gems include post install messages" do + it "should display the post-install messages after installing" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") + end + + it "should display the post-install messages if repo is updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + bundle :install + + build_git "foo", "1.1" do |s| + s.post_install_message = "Foo's 1.1 post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.1")}' + G + bundle :install + + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's 1.1 post install message") + end + + it "should not display the post-install messages if repo is not updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when gems do not include post install messages" do + it "should not display any post-install messages" do + build_git "foo" do |s| + s.post_install_message = nil + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).not_to include("Post-install message") + end + end + end + + context "when ignore post-install messages for gem is set" do + it "doesn't display any post-install messages" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config ignore_messages.rack true" + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when ignore post-install messages for all gems" do + it "doesn't display any post-install messages" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config ignore_messages true" + + bundle :install + expect(out).not_to include("Post-install message") + end + end +end diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb new file mode 100644 index 00000000000000..01c03ac79349fe --- /dev/null +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with install-time dependencies" do + it "installs gems with implicit rake dependencies", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "with_implicit_rake_dep" + gem "another_implicit_rake_dep" + gem "rake" + G + + run <<-R + require 'implicit_rake_dep' + require 'another_implicit_rake_dep' + puts IMPLICIT_RAKE_DEP + puts ANOTHER_IMPLICIT_RAKE_DEP + R + expect(out).to eq("YES\nYES") + end + + it "installs gems with a dependency with no type" do + build_repo2 + + path = "#{gem_repo2}/#{Gem::MARSHAL_SPEC_DIR}/actionpack-2.3.2.gemspec.rz" + spec = Marshal.load(Bundler.rubygems.inflate(File.read(path))) + spec.dependencies.each do |d| + d.instance_variable_set(:@type, :fail) + end + File.open(path, "w") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "actionpack", "2.3.2" + G + + expect(the_bundle).to include_gems "actionpack 2.3.2", "activesupport 2.3.2" + end + + describe "with crazy rubygem plugin stuff" do + it "installs plugins" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_b" + G + + expect(the_bundle).to include_gems "net_b 1.0" + end + + it "installs plugins depended on by other plugins", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_a" + G + + expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0" + end + + it "installs multiple levels of dependencies", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + + expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0", "net_c 1.0", "net_d 1.0", "net_e 1.0" + end + + context "with ENV['DEBUG_RESOLVER'] set" do + it "produces debug output" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + + bundle :install, :env => { "DEBUG_RESOLVER" => "1" } + + expect(err).to include("Creating possibility state for net_c") + end + end + + context "with ENV['DEBUG_RESOLVER_TREE'] set" do + it "produces debug output" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + + bundle :install, :env => { "DEBUG_RESOLVER_TREE" => "1" } + + expect(err).to include(" net_b"). + and include("Starting resolution"). + and include("Finished resolution"). + and include("Attempting to activate") + end + end + end + + describe "when a required ruby version" do + context "allows only an older version" do + it "installs the older version" do + build_repo2 do + build_gem "rack", "9001.0.0" do |s| + s.required_ruby_version = "> 9000" + end + end + + install_gemfile <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo2 } + ruby "#{RUBY_VERSION}" + source "http://localgemserver.test/" + gem 'rack' + G + + expect(out).to_not include("rack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("rack 1.2") + end + end + + context "allows no gems" do + before do + build_repo2 do + build_gem "require_ruby" do |s| + s.required_ruby_version = "> 9000" + end + end + end + + let(:ruby_requirement) { %("#{RUBY_VERSION}") } + let(:error_message_requirement) { "~> #{RUBY_VERSION}.0" } + + shared_examples_for "ruby version conflicts" do + it "raises an error during resolution" do + install_gemfile <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo2 } + source "http://localgemserver.test/" + ruby #{ruby_requirement} + gem 'require_ruby' + G + + expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000") + + nice_error = strip_whitespace(<<-E).strip + Bundler could not find compatible versions for gem "ruby\0": + In Gemfile: + ruby\0 (#{error_message_requirement}) + + require_ruby was resolved to 1.0, which depends on + ruby\0 (> 9000) + + Could not find gem 'ruby\0 (> 9000)', which is required by gem 'require_ruby', in any of the relevant sources: + the local ruby installation + E + expect(last_command.bundler_err).to end_with(nice_error) + end + end + + it_behaves_like "ruby version conflicts" + + describe "with a < requirement" do + let(:ruby_requirement) { %("< 5000") } + let(:error_message_requirement) { "< 5000" } + + it_behaves_like "ruby version conflicts" + end + + describe "with a compound requirement" do + let(:reqs) { ["> 0.1", "< 5000"] } + let(:ruby_requirement) { reqs.map(&:dump).join(", ") } + let(:error_message_requirement) { Gem::Requirement.new(reqs).to_s } + + it_behaves_like "ruby version conflicts" + end + end + end + + describe "when a required rubygems version disallows a gem" do + it "does not try to install those gems" do + build_repo2 do + build_gem "require_rubygems" do |s| + s.required_rubygems_version = "> 9000" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'require_rubygems' + G + + expect(out).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000") + expect(out).to include("require_rubygems-1.0 requires rubygems version > 9000, which is incompatible with the current version, #{Gem::VERSION}") + end + end +end diff --git a/spec/bundler/install/gems/standalone_spec.rb b/spec/bundler/install/gems/standalone_spec.rb new file mode 100644 index 00000000000000..fa7a3bdc277db5 --- /dev/null +++ b/spec/bundler/install/gems/standalone_spec.rb @@ -0,0 +1,337 @@ +# frozen_string_literal: true + +RSpec.shared_examples "bundle install --standalone" do + shared_examples "common functionality" do + it "still makes the gems available to normal bundler" do + args = expected_gems.map {|k, v| "#{k} #{v}" } + expect(the_bundle).to include_gems(*args) + end + + it "generates a bundle/bundler/setup.rb" do + expect(bundled_app("bundle/bundler/setup.rb")).to exist + end + + it "makes the gems available without bundler" do + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" + end + Dir.chdir(bundled_app) do + ruby testrb, :no_lib => true + end + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "works on a different system" do + FileUtils.mv(bundled_app, "#{bundled_app}2") + + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" + end + Dir.chdir("#{bundled_app}2") do + ruby testrb, :no_lib => true + end + + expect(out).to eq(expected_gems.values.join("\n")) + end + end + + describe "with simple gems" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + + describe "with gems with native extension", :ruby_repo do + before do + install_gemfile <<-G, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + source "file://#{gem_repo1}" + gem "very_simple_binary" + G + end + + it "generates a bundle/bundler/setup.rb with the proper paths", :rubygems => "2.4" do + expected_path = bundled_app("bundle/bundler/setup.rb") + extension_line = File.read(expected_path).each_line.find {|line| line.include? "/extensions/" }.strip + expect(extension_line).to start_with '$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/' + expect(extension_line).to end_with '/very_simple_binary-1.0"' + end + end + + describe "with gem that has an invalid gemspec" do + before do + build_git "bar", :gemspec => false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + lib = File.expand_path('../lib/', __FILE__) + $:.unshift lib unless $:.include?(lib) + require 'bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'Anonymous' + s.require_path = [1,2] + end + G + end + install_gemfile <<-G, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + end + + it "outputs a helpful error message" do + expect(out).to include("You have one or more invalid gemspecs that need to be fixed.") + expect(out).to include("bar 1.0 has an invalid gemspec") + end + end + + describe "with a combination of gems and git repos" do + before do + build_git "devise", "1.0" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "devise", :git => "#{lib_path("devise-1.0")}" + G + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "devise" => "1.0", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + + describe "with groups" do + before do + build_git "devise", "1.0" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + group :test do + gem "rspec" + gem "rack-test" + end + G + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + + it "allows creating a standalone file with limited groups" do + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => "default") + + Dir.chdir(bundled_app) do + load_error_ruby <<-RUBY, "spec", :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + end + + expect(last_command.stdout).to eq("2.3.2") + expect(last_command.stderr).to eq("ZOMG LOAD ERROR") + end + + it "allows --without to limit the groups used in a standalone" do + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle"), :without => "test").merge(:standalone => true) + + Dir.chdir(bundled_app) do + load_error_ruby <<-RUBY, "spec", :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + end + + expect(last_command.stdout).to eq("2.3.2") + expect(last_command.stderr).to eq("ZOMG LOAD ERROR") + end + + it "allows --path to change the location of the standalone bundle", :bundler => "< 3" do + bundle! "install", forgotten_command_line_options(:path => "path/to/bundle").merge(:standalone => true) + + Dir.chdir(bundled_app) do + ruby <<-RUBY, :no_lib => true + $:.unshift File.expand_path("path/to/bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + RUBY + end + + expect(last_command.stdout).to eq("2.3.2") + end + + it "allows --path to change the location of the standalone bundle", :bundler => "3" do + bundle! "install", forgotten_command_line_options(:path => "path/to/bundle").merge(:standalone => true) + path = File.expand_path("path/to/bundle") + + Dir.chdir(bundled_app) do + ruby <<-RUBY, :no_lib => true + $:.unshift File.expand_path(#{path.dump}) + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + RUBY + end + + expect(last_command.stdout).to eq("2.3.2") + end + + it "allows remembered --without to limit the groups used in a standalone" do + bundle! :install, forgotten_command_line_options(:without => "test") + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + + Dir.chdir(bundled_app) do + load_error_ruby <<-RUBY, "spec", :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + end + + expect(last_command.stdout).to eq("2.3.2") + expect(last_command.stderr).to eq("ZOMG LOAD ERROR") + end + end + + describe "with gemcutter's dependency API" do + let(:source_uri) { "http://localgemserver.test" } + + describe "simple gems" do + before do + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true, :artifice => "endpoint") + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + end + + describe "with --binstubs", :bundler => "< 3" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true, :binstubs => true) + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + + it "creates stubs that use the standalone load path" do + Dir.chdir(bundled_app) do + expect(`bin/rails -v`.chomp).to eql "2.3.2" + end + end + + it "creates stubs that can be executed from anywhere" do + require "tmpdir" + Dir.chdir(Dir.tmpdir) do + sys_exec!(%(#{bundled_app("bin/rails")} -v)) + expect(out).to eq("2.3.2") + end + end + + it "creates stubs that can be symlinked" do + pending "File.symlink is unsupported on Windows" if Bundler::WINDOWS + + symlink_dir = tmp("symlink") + FileUtils.mkdir_p(symlink_dir) + symlink = File.join(symlink_dir, "rails") + + File.symlink(bundled_app("bin/rails"), symlink) + sys_exec!("#{symlink} -v") + expect(out).to eq("2.3.2") + end + + it "creates stubs with the correct load path" do + extension_line = File.read(bundled_app("bin/rails")).each_line.find {|line| line.include? "$:.unshift" }.strip + expect(extension_line).to eq %($:.unshift File.expand_path "../../bundle", path.realpath) + end + end +end + +RSpec.describe "bundle install --standalone" do + include_examples("bundle install --standalone") +end + +RSpec.describe "bundle install --standalone run in a subdirectory" do + before do + Dir.chdir(bundled_app("bob").tap(&:mkpath)) + end + + include_examples("bundle install --standalone") +end diff --git a/spec/bundler/install/gems/sudo_spec.rb b/spec/bundler/install/gems/sudo_spec.rb new file mode 100644 index 00000000000000..1781451c9886f9 --- /dev/null +++ b/spec/bundler/install/gems/sudo_spec.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +RSpec.describe "when using sudo", :sudo => true do + describe "and BUNDLE_PATH is writable" do + context "but BUNDLE_PATH/build_info is not writable" do + before do + bundle! "config path.system true" + subdir = system_gem_path("cache") + subdir.mkpath + sudo "chmod u-w #{subdir}" + end + + it "installs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(out).to_not match(/an error occurred/i) + expect(system_gem_path("cache/rack-1.0.0.gem")).to exist + expect(the_bundle).to include_gems "rack 1.0" + end + end + end + + describe "and GEM_HOME is owned by root" do + before :each do + bundle! "config path.system true" + chown_system_gems_to_root + end + + it "installs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + gem "thin" + G + + expect(system_gem_path("gems/rack-1.0.0")).to exist + expect(system_gem_path("gems/rack-1.0.0").stat.uid).to eq(0) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "installs rake and a gem dependent on rake in the same session" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "another_implicit_rake_dep" + G + bundle "install" + expect(system_gem_path("gems/another_implicit_rake_dep-1.0")).to exist + end + + it "installs when BUNDLE_PATH is owned by root" do + bundle! "config global_path_appends_ruby_scope false" # consistency in tests between 1.x and 2.x modes + + bundle_path = tmp("owned_by_root") + FileUtils.mkdir_p bundle_path + sudo "chown -R root #{bundle_path}" + + ENV["BUNDLE_PATH"] = bundle_path.to_s + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + expect(bundle_path.join("gems/rack-1.0.0")).to exist + expect(bundle_path.join("gems/rack-1.0.0").stat.uid).to eq(0) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "installs when BUNDLE_PATH does not exist" do + bundle! "config global_path_appends_ruby_scope false" # consistency in tests between 1.x and 2.x modes + + root_path = tmp("owned_by_root") + FileUtils.mkdir_p root_path + sudo "chown -R root #{root_path}" + bundle_path = root_path.join("does_not_exist") + + ENV["BUNDLE_PATH"] = bundle_path.to_s + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + expect(bundle_path.join("gems/rack-1.0.0")).to exist + expect(bundle_path.join("gems/rack-1.0.0").stat.uid).to eq(0) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "installs extensions/ compiled by RubyGems 2.2", :rubygems => "2.2" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "very_simple_binary" + G + + expect(system_gem_path("gems/very_simple_binary-1.0")).to exist + binary_glob = system_gem_path("extensions/*/*/very_simple_binary-1.0") + expect(Dir.glob(binary_glob).first).to be + end + end + + describe "and BUNDLE_PATH is not writable" do + before do + sudo "chmod ugo-w #{default_bundle_path}" + end + + it "installs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + expect(default_bundle_path("gems/rack-1.0.0")).to exist + expect(the_bundle).to include_gems "rack 1.0" + end + + it "cleans up the tmpdirs generated" do + require "tmpdir" + Dir.glob("#{Dir.tmpdir}/bundler*").each do |tmpdir| + FileUtils.remove_entry_secure(tmpdir) + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + tmpdirs = Dir.glob("#{Dir.tmpdir}/bundler*") + + expect(tmpdirs).to be_empty + end + end + + describe "and GEM_HOME is not writable" do + it "installs" do + bundle! "config path.system true" + gem_home = tmp("sudo_gem_home") + sudo "mkdir -p #{gem_home}" + sudo "chmod ugo-w #{gem_home}" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + bundle :install, :env => { "GEM_HOME" => gem_home.to_s, "GEM_PATH" => nil } + expect(gem_home.join("bin/rackup")).to exist + expect(the_bundle).to include_gems "rack 1.0", :env => { "GEM_HOME" => gem_home.to_s, "GEM_PATH" => nil } + end + end + + describe "and root runs install" do + let(:warning) { "Don't run Bundler as root." } + + before do + gemfile %(source "file://#{gem_repo1}") + end + + it "warns against that" do + bundle :install, :sudo => true + expect(out).to include(warning) + end + + context "when ENV['BUNDLE_SILENCE_ROOT_WARNING'] is set" do + it "skips the warning" do + bundle :install, :sudo => :preserve_env, :env => { "BUNDLE_SILENCE_ROOT_WARNING" => true } + expect(out).to_not include(warning) + end + end + + context "when silence_root_warning = false" do + it "warns against that" do + bundle :install, :sudo => true, :env => { "BUNDLE_SILENCE_ROOT_WARNING" => "false" } + expect(out).to include(warning) + end + end + end +end diff --git a/spec/bundler/install/gems/win32_spec.rb b/spec/bundler/install/gems/win32_spec.rb new file mode 100644 index 00000000000000..ad758b94fa8cea --- /dev/null +++ b/spec/bundler/install/gems/win32_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with win32-generated lockfile" do + it "should read lockfile" do + File.open(bundled_app("Gemfile.lock"), "wb") do |f| + f << "GEM\r\n" + f << " remote: file:#{gem_repo1}/\r\n" + f << " specs:\r\n" + f << "\r\n" + f << " rack (1.0.0)\r\n" + f << "\r\n" + f << "PLATFORMS\r\n" + f << " ruby\r\n" + f << "\r\n" + f << "DEPENDENCIES\r\n" + f << " rack\r\n" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + expect(exitstatus).to eq(0) if exitstatus + end +end diff --git a/spec/bundler/install/gemspecs_spec.rb b/spec/bundler/install/gemspecs_spec.rb new file mode 100644 index 00000000000000..666707b295fde0 --- /dev/null +++ b/spec/bundler/install/gemspecs_spec.rb @@ -0,0 +1,154 @@ +# encoding: utf-8 +# frozen_string_literal: true + +RSpec.describe "bundle install" do + describe "when a gem has a YAML gemspec" do + before :each do + build_repo2 do + build_gem "yaml_spec", :gemspec => :yaml + end + end + + it "still installs correctly" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "yaml_spec" + G + bundle :install + expect(err).to lack_errors + end + + it "still installs correctly when using path" do + build_lib "yaml_spec", :gemspec => :yaml + + install_gemfile <<-G + gem 'yaml_spec', :path => "#{lib_path("yaml_spec-1.0")}" + G + expect(err).to lack_errors + end + end + + it "should use gemspecs in the system cache when available" do + gemfile <<-G + source "http://localtestserver.gem" + gem 'rack' + G + + FileUtils.mkdir_p "#{default_bundle_path}/specifications" + File.open("#{default_bundle_path}/specifications/rack-1.0.0.gemspec", "w+") do |f| + spec = Gem::Specification.new do |s| + s.name = "rack" + s.version = "1.0.0" + s.add_runtime_dependency "activesupport", "2.3.2" + end + f.write spec.to_ruby + end + bundle :install, :artifice => "endpoint_marshal_fail" # force gemspec load + expect(the_bundle).to include_gems "activesupport 2.3.2" + end + + it "does not hang when gemspec has incompatible encoding" do + create_file "foo.gemspec", <<-G + Gem::Specification.new do |gem| + gem.name = "pry-byebug" + gem.version = "3.4.2" + gem.author = "David Rodriguez" + gem.summary = "Good stuff" + end + G + + install_gemfile <<-G, :env => { "LANG" => "C" } + gemspec + G + + expect(out).to include("Bundle complete!") + end + + it "reads gemspecs respecting their encoding" do + skip "Unicode is not supported on Ruby 1.x without extra work" if RUBY_VERSION < "2.0" + + create_file "version.rb", <<-RUBY + module Persistent💎 + VERSION = "0.0.1" + end + RUBY + + create_file "persistent-dmnd.gemspec", <<-G + require_relative "version" + + Gem::Specification.new do |gem| + gem.name = "persistent-dmnd" + gem.version = Persistent💎::VERSION + gem.author = "Ivo Anjo" + gem.summary = "Unscratchable stuff" + end + G + + install_gemfile <<-G + gemspec + G + + expect(out).to include("Bundle complete!") + end + + context "when ruby version is specified in gemspec and gemfile" do + it "installs when patch level is not specified and the version matches" do + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = "~> #{RUBY_VERSION}.0" + end + + install_gemfile <<-G + ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby' + gemspec + G + expect(the_bundle).to include_gems "foo 1.0" + end + + it "installs when patch level is specified and the version still matches the current version", + :if => RUBY_PATCHLEVEL >= 0 do + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = "#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" + end + + install_gemfile <<-G + ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{RUBY_PATCHLEVEL}' + gemspec + G + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fails and complains about patchlevel on patchlevel mismatch", + :if => RUBY_PATCHLEVEL >= 0 do + patchlevel = RUBY_PATCHLEVEL.to_i + 1 + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = "#{RUBY_VERSION}.#{patchlevel}" + end + + install_gemfile <<-G + ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{patchlevel}' + gemspec + G + + expect(out).to include("Ruby patchlevel") + expect(out).to include("but your Gemfile specified") + expect(exitstatus).to eq(18) if exitstatus + end + + it "fails and complains about version on version mismatch" do + version = Gem::Requirement.create(RUBY_VERSION).requirements.first.last.bump.version + + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = version + end + + install_gemfile <<-G + ruby '#{version}', :engine_version => '#{version}', :engine => 'ruby' + gemspec + G + + expect(out).to include("Ruby version") + expect(out).to include("but your Gemfile specified") + expect(exitstatus).to eq(18) if exitstatus + end + end +end diff --git a/spec/bundler/install/git_spec.rb b/spec/bundler/install/git_spec.rb new file mode 100644 index 00000000000000..48086a86c7a2d0 --- /dev/null +++ b/spec/bundler/install/git_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "git sources" do + it "displays the revision hash of the gem repository", :bundler => "< 3" do + build_git "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}" + G + + bundle! :install + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at master@#{revision_for(lib_path("foo"))[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", :source => "git@#{lib_path("foo")}" + end + + it "displays the ref of the gem repository when using branch~num as a ref", :bundler => "< 3" do + build_git "foo", "1.0", :path => lib_path("foo") + rev = revision_for(lib_path("foo"))[0..6] + update_git "foo", "2.0", :path => lib_path("foo"), :gemspec => true + rev2 = revision_for(lib_path("foo"))[0..6] + update_git "foo", "3.0", :path => lib_path("foo"), :gemspec => true + + install_gemfile! <<-G + gem "foo", :git => "#{lib_path("foo")}", :ref => "master~2" + G + + bundle! :install + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at master~2@#{rev})") + expect(the_bundle).to include_gems "foo 1.0", :source => "git@#{lib_path("foo")}" + + update_git "foo", "4.0", :path => lib_path("foo"), :gemspec => true + + bundle! :update, :all => bundle_update_requires_all? + expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at master~2@#{rev2})") + expect(the_bundle).to include_gems "foo 2.0", :source => "git@#{lib_path("foo")}" + end + + it "should allows git repos that are missing but not being installed" do + revision = build_git("foo").ref_for("HEAD") + + gemfile <<-G + gem "foo", :git => "file://#{lib_path("foo-1.0")}", :group => :development + G + + lockfile <<-L + GIT + remote: file://#{lib_path("foo-1.0")} + revision: #{revision} + specs: + foo (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + L + + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle", :without => "development") + + expect(out).to include("Bundle complete!") + end + end +end diff --git a/spec/bundler/install/global_cache_spec.rb b/spec/bundler/install/global_cache_spec.rb new file mode 100644 index 00000000000000..e41e7e0157692e --- /dev/null +++ b/spec/bundler/install/global_cache_spec.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +RSpec.describe "global gem caching" do + before { bundle! "config global_gem_cache true" } + + describe "using the cross-application user cache" do + let(:source) { "http://localgemserver.test" } + let(:source2) { "http://gemserver.example.org" } + + def source_global_cache(*segments) + home(".bundle", "cache", "gems", "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", *segments) + end + + def source2_global_cache(*segments) + home(".bundle", "cache", "gems", "gemserver.example.org.80.1ae1663619ffe0a3c9d97712f44c705b", *segments) + end + + it "caches gems into the global cache on download" do + install_gemfile! <<-G, :artifice => "compact_index" + source "#{source}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(source_global_cache("rack-1.0.0.gem")).to exist + end + + it "uses globally cached gems if they exist" do + source_global_cache.mkpath + FileUtils.cp(gem_repo1("gems/rack-1.0.0.gem"), source_global_cache("rack-1.0.0.gem")) + + install_gemfile! <<-G, :artifice => "compact_index_no_gem" + source "#{source}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + describe "when the same gem from different sources is installed" do + it "should use the appropriate one from the global cache" do + install_gemfile! <<-G, :artifice => "compact_index" + source "#{source}" + gem "rack" + G + + FileUtils.rm_r(default_bundle_path) + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(source_global_cache("rack-1.0.0.gem")).to exist + # rack 1.0.0 is not installed and it is in the global cache + + install_gemfile! <<-G, :artifice => "compact_index" + source "#{source2}" + gem "rack", "0.9.1" + G + + FileUtils.rm_r(default_bundle_path) + expect(the_bundle).not_to include_gems "rack 0.9.1" + expect(source2_global_cache("rack-0.9.1.gem")).to exist + # rack 0.9.1 is not installed and it is in the global cache + + gemfile <<-G + source "#{source}" + gem "rack", "1.0.0" + G + + bundle! :install, :artifice => "compact_index_no_gem" + # rack 1.0.0 is installed and rack 0.9.1 is not + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "rack 0.9.1" + FileUtils.rm_r(default_bundle_path) + + gemfile <<-G + source "#{source2}" + gem "rack", "0.9.1" + G + + bundle! :install, :artifice => "compact_index_no_gem" + # rack 0.9.1 is installed and rack 1.0.0 is not + expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).not_to include_gems "rack 1.0.0" + end + + it "should not install if the wrong source is provided" do + gemfile <<-G + source "#{source}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index" + FileUtils.rm_r(default_bundle_path) + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(source_global_cache("rack-1.0.0.gem")).to exist + # rack 1.0.0 is not installed and it is in the global cache + + gemfile <<-G + source "#{source2}" + gem "rack", "0.9.1" + G + + bundle! :install, :artifice => "compact_index" + FileUtils.rm_r(default_bundle_path) + expect(the_bundle).not_to include_gems "rack 0.9.1" + expect(source2_global_cache("rack-0.9.1.gem")).to exist + # rack 0.9.1 is not installed and it is in the global cache + + gemfile <<-G + source "#{source2}" + gem "rack", "1.0.0" + G + + expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source2_global_cache("rack-0.9.1.gem")).to exist + bundle :install, :artifice => "compact_index_no_gem" + expect(out).to include("Internal Server Error 500") + # rack 1.0.0 is not installed and rack 0.9.1 is not + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "rack 0.9.1" + + gemfile <<-G + source "#{source}" + gem "rack", "0.9.1" + G + + expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source2_global_cache("rack-0.9.1.gem")).to exist + bundle :install, :artifice => "compact_index_no_gem" + expect(out).to include("Internal Server Error 500") + # rack 0.9.1 is not installed and rack 1.0.0 is not + expect(the_bundle).not_to include_gems "rack 0.9.1" + expect(the_bundle).not_to include_gems "rack 1.0.0" + end + end + + describe "when installing gems from a different directory" do + it "uses the global cache as a source" do + install_gemfile! <<-G, :artifice => "compact_index" + source "#{source}" + gem "rack" + gem "activesupport" + G + + # Both gems are installed and in the global cache + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "activesupport 2.3.5" + expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + FileUtils.rm_r(default_bundle_path) + # Both gems are now only in the global cache + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + + install_gemfile! <<-G, :artifice => "compact_index_no_gem" + source "#{source}" + gem "rack" + G + + # rack is installed and both are in the global cache + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + + Dir.chdir bundled_app2 do + create_file bundled_app2("gems.rb"), <<-G + source "#{source}" + gem "activesupport" + G + + # Neither gem is installed and both are in the global cache + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + + # Install using the global cache instead of by downloading the .gem + # from the server + bundle! :install, :artifice => "compact_index_no_gem" + + # activesupport is installed and both are in the global cache + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "activesupport 2.3.5" + expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + end + end + end + end + + describe "extension caching", :ruby_repo, :rubygems => "2.2" do + it "works" do + build_git "very_simple_git_binary", &:add_c_extension + build_lib "very_simple_path_binary", &:add_c_extension + revision = revision_for(lib_path("very_simple_git_binary-1.0"))[0, 12] + + install_gemfile! <<-G + source "file:#{gem_repo1}" + + gem "very_simple_binary" + gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}" + gem "very_simple_path_binary", :path => "#{lib_path("very_simple_path_binary-1.0")}" + G + + gem_binary_cache = home(".bundle", "cache", "extensions", specific_local_platform.to_s, Bundler.ruby_scope, + Digest(:MD5).hexdigest("#{gem_repo1}/"), "very_simple_binary-1.0") + git_binary_cache = home(".bundle", "cache", "extensions", specific_local_platform.to_s, Bundler.ruby_scope, + "very_simple_git_binary-1.0-#{revision}", "very_simple_git_binary-1.0") + + cached_extensions = Pathname.glob(home(".bundle", "cache", "extensions", "*", "*", "*", "*", "*")).sort + expect(cached_extensions).to eq [gem_binary_cache, git_binary_cache].sort + + run! <<-R + require 'very_simple_binary_c'; puts ::VERY_SIMPLE_BINARY_IN_C + require 'very_simple_git_binary_c'; puts ::VERY_SIMPLE_GIT_BINARY_IN_C + R + expect(out).to eq "VERY_SIMPLE_BINARY_IN_C\nVERY_SIMPLE_GIT_BINARY_IN_C" + + FileUtils.rm Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")] + + gem_binary_cache.join("very_simple_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" } + git_binary_cache.join("very_simple_git_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" } + + bundle! "config --local path different_path" + bundle! :install + + expect(Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")]).to all(end_with(".rb")) + + run! <<-R + require 'very_simple_binary_c' + require 'very_simple_git_binary_c' + R + expect(out).to eq "very_simple_binary_c.rb\nvery_simple_git_binary_c.rb" + end + end +end diff --git a/spec/bundler/install/path_spec.rb b/spec/bundler/install/path_spec.rb new file mode 100644 index 00000000000000..94f38c9290b7d8 --- /dev/null +++ b/spec/bundler/install/path_spec.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + describe "with --path" do + before :each do + build_gem "rack", "1.0.0", :to_system => true do |s| + s.write "lib/rack.rb", "puts 'FAIL'" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "does not use available system gems with bundle --path vendor/bundle", :bundler => "< 3" do + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles paths with regex characters in them" do + dir = bundled_app("bun++dle") + dir.mkpath + + Dir.chdir(dir) do + bundle! :install, forgotten_command_line_options(:path => dir.join("vendor/bundle")) + expect(out).to include("installed into `./vendor/bundle`") + end + + dir.rmtree + end + + it "prints a warning to let the user know what has happened with bundle --path vendor/bundle" do + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + expect(out).to include("gems are installed into `./vendor/bundle`") + end + + it "disallows --path vendor/bundle --system", :bundler => "< 3" do + bundle "install --path vendor/bundle --system" + expect(out).to include("Please choose only one option.") + expect(exitstatus).to eq(15) if exitstatus + end + + it "remembers to disable system gems after the first time with bundle --path vendor/bundle", :bundler => "< 3" do + bundle "install --path vendor/bundle" + FileUtils.rm_rf bundled_app("vendor") + bundle "install" + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + context "with path_relative_to_cwd set to true" do + before { bundle! "config path_relative_to_cwd true" } + + it "installs the bundle relatively to current working directory", :bundler => "< 3" do + Dir.chdir(bundled_app.parent) do + bundle! "install --gemfile='#{bundled_app}/Gemfile' --path vendor/bundle" + expect(out).to include("installed into `./vendor/bundle`") + expect(bundled_app("../vendor/bundle")).to be_directory + end + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs the standalone bundle relative to the cwd" do + Dir.chdir(bundled_app.parent) do + bundle! :install, :gemfile => bundled_app("Gemfile"), :standalone => true + expect(out).to include("installed into `./bundled_app/bundle`") + expect(bundled_app("bundle")).to be_directory + expect(bundled_app("bundle/ruby")).to be_directory + end + + bundle! "config unset path" + + Dir.chdir(bundled_app("subdir").tap(&:mkpath)) do + bundle! :install, :gemfile => bundled_app("Gemfile"), :standalone => true + expect(out).to include("installed into `../bundle`") + expect(bundled_app("bundle")).to be_directory + expect(bundled_app("bundle/ruby")).to be_directory + end + end + end + end + + describe "when BUNDLE_PATH or the global path config is set" do + before :each do + build_lib "rack", "1.0.0", :to_system => true do |s| + s.write "lib/rack.rb", "raise 'FAIL'" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + def set_bundle_path(type, location) + if type == :env + ENV["BUNDLE_PATH"] = location + elsif type == :global + bundle! "config path #{location}", "no-color" => nil + end + end + + [:env, :global].each do |type| + context "when set via #{type}" do + it "installs gems to a path if one is specified" do + set_bundle_path(type, bundled_app("vendor2").to_s) + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(bundled_app("vendor2")).not_to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + context "with global_path_appends_ruby_scope set", :bundler => "3" do + it "installs gems to ." do + set_bundle_path(type, ".") + bundle! "config --global disable_shared_gems true" + + bundle! :install + + paths_to_exist = %w[cache/rack-1.0.0.gem gems/rack-1.0.0 specifications/rack-1.0.0.gemspec].map {|path| bundled_app(Bundler.ruby_scope, path) } + expect(paths_to_exist).to all exist + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems to the path" do + set_bundle_path(type, bundled_app("vendor").to_s) + + bundle! :install + + expect(bundled_app("vendor", Bundler.ruby_scope, "gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems to the path relative to root when relative" do + set_bundle_path(type, "vendor") + + FileUtils.mkdir_p bundled_app("lol") + Dir.chdir(bundled_app("lol")) do + bundle! :install + end + + expect(bundled_app("vendor", Bundler.ruby_scope, "gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with global_path_appends_ruby_scope unset", :bundler => "< 3" do + it "installs gems to ." do + set_bundle_path(type, ".") + bundle! "config --global disable_shared_gems true" + + bundle! :install + + expect([bundled_app("cache/rack-1.0.0.gem"), bundled_app("gems/rack-1.0.0"), bundled_app("specifications/rack-1.0.0.gemspec")]).to all exist + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems to BUNDLE_PATH with #{type}" do + set_bundle_path(type, bundled_app("vendor").to_s) + + bundle :install + + expect(bundled_app("vendor/gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems to BUNDLE_PATH relative to root when relative" do + set_bundle_path(type, "vendor") + + FileUtils.mkdir_p bundled_app("lol") + Dir.chdir(bundled_app("lol")) do + bundle :install + end + + expect(bundled_app("vendor/gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + it "installs gems to BUNDLE_PATH from .bundle/config" do + config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s + + bundle :install + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "sets BUNDLE_PATH as the first argument to bundle install" do + bundle! :install, forgotten_command_line_options(:path => "./vendor/bundle") + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "disables system gems when passing a path to install" do + # This is so that vendored gems can be distributed to others + build_gem "rack", "1.1.0", :to_system => true + bundle! :install, forgotten_command_line_options(:path => "./vendor/bundle") + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "re-installs gems whose extensions have been deleted", :ruby_repo, :rubygems => ">= 2.3" do + build_lib "very_simple_binary", "1.0.0", :to_system => true do |s| + s.write "lib/very_simple_binary.rb", "raise 'FAIL'" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "very_simple_binary" + G + + bundle! :install, forgotten_command_line_options(:path => "./vendor/bundle") + + expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory + expect(vendored_gems("extensions")).to be_directory + expect(the_bundle).to include_gems "very_simple_binary 1.0", :source => "remote1" + + vendored_gems("extensions").rmtree + + run "require 'very_simple_binary_c'" + expect(err).to include("Bundler::GemNotFound") + + bundle :install, forgotten_command_line_options(:path => "./vendor/bundle") + + expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory + expect(vendored_gems("extensions")).to be_directory + expect(the_bundle).to include_gems "very_simple_binary 1.0", :source => "remote1" + end + end + + describe "to a file" do + before do + in_app_root do + FileUtils.touch "bundle" + end + end + + it "reports the file exists" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, forgotten_command_line_options(:path => "bundle") + expect(out).to include("file already exists") + end + end +end diff --git a/spec/bundler/install/post_bundle_message_spec.rb b/spec/bundler/install/post_bundle_message_spec.rb new file mode 100644 index 00000000000000..394134f5230b70 --- /dev/null +++ b/spec/bundler/install/post_bundle_message_spec.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +RSpec.describe "post bundle message" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", "2.3.5", :group => [:emo, :test] + group :test do + gem "rspec" + end + gem "rack-obama", :group => :obama + G + end + + let(:bundle_path) { "./.bundle" } + let(:bundle_show_system_message) { "Use `bundle info [gemname]` to see where a bundled gem is installed." } + let(:bundle_show_path_message) { "Bundled gems are installed into `#{bundle_path}`" } + let(:bundle_complete_message) { "Bundle complete!" } + let(:bundle_updated_message) { "Bundle updated!" } + let(:installed_gems_stats) { "4 Gemfile dependencies, 5 gems now installed." } + let(:bundle_show_message) { Bundler::VERSION.split(".").first.to_i < 3 ? bundle_show_system_message : bundle_show_path_message } + + describe "for fresh bundle install" do + it "without any options" do + bundle :install + expect(out).to include(bundle_show_message) + expect(out).not_to include("Gems in the group") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without one group" do + bundle! :install, forgotten_command_line_options(:without => "emo") + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without two groups" do + bundle! :install, forgotten_command_line_options(:without => "emo test") + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include("4 Gemfile dependencies, 3 gems now installed.") + end + + it "with --without more groups" do + bundle! :install, forgotten_command_line_options(:without => "emo obama test") + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include("4 Gemfile dependencies, 2 gems now installed.") + end + + describe "with --path and" do + let(:bundle_path) { "./vendor" } + + it "without any options" do + bundle! :install, forgotten_command_line_options(:path => "vendor") + expect(out).to include(bundle_show_path_message) + expect(out).to_not include("Gems in the group") + expect(out).to include(bundle_complete_message) + end + + it "with --without one group" do + bundle! :install, forgotten_command_line_options(:without => "emo", :path => "vendor") + expect(out).to include(bundle_show_path_message) + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with --without two groups" do + bundle! :install, forgotten_command_line_options(:without => "emo test", :path => "vendor") + expect(out).to include(bundle_show_path_message) + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with --without more groups" do + bundle! :install, forgotten_command_line_options(:without => "emo obama test", :path => "vendor") + expect(out).to include(bundle_show_path_message) + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with an absolute --path inside the cwd" do + bundle! :install, forgotten_command_line_options(:path => bundled_app("cache")) + expect(out).to include("Bundled gems are installed into `./cache`") + expect(out).to_not include("Gems in the group") + expect(out).to include(bundle_complete_message) + end + + it "with an absolute --path outside the cwd" do + bundle! :install, forgotten_command_line_options(:path => tmp("not_bundled_app")) + expect(out).to include("Bundled gems are installed into `#{tmp("not_bundled_app")}`") + expect(out).to_not include("Gems in the group") + expect(out).to include(bundle_complete_message) + end + end + + describe "with misspelled or non-existent gem name" do + it "should report a helpful error message", :bundler => "< 3" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + gem "not-a-gem", :group => :development + G + expect(out).to include("Could not find gem 'not-a-gem' in any of the gem sources listed in your Gemfile.") + end + + it "should report a helpful error message", :bundler => "3" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + gem "not-a-gem", :group => :development + G + expect(out).to include normalize_uri_file(<<-EOS.strip) +Could not find gem 'not-a-gem' in rubygems repository file://localhost#{gem_repo1}/ or installed locally. +The source does not contain any versions of 'not-a-gem' + EOS + end + + it "should report a helpful error message with reference to cache if available" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + G + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + gem "not-a-gem", :group => :development + G + expect(out).to include("Could not find gem 'not-a-gem' in"). + and include("or in gems cached in vendor/cache.") + end + end + end + + describe "for second bundle install run" do + it "without any options" do + 2.times { bundle :install } + expect(out).to include(bundle_show_message) + expect(out).to_not include("Gems in the groups") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without one group" do + bundle! :install, forgotten_command_line_options(:without => "emo") + bundle! :install + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without two groups" do + bundle! :install, forgotten_command_line_options(:without => "emo test") + bundle! :install + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with --without more groups" do + bundle! :install, forgotten_command_line_options(:without => "emo obama test") + bundle :install + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_complete_message) + end + end + + describe "for bundle update" do + it "without any options" do + bundle! :update, :all => bundle_update_requires_all? + expect(out).not_to include("Gems in the groups") + expect(out).to include(bundle_updated_message) + end + + it "with --without one group" do + bundle! :install, forgotten_command_line_options(:without => "emo") + bundle! :update, :all => bundle_update_requires_all? + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_updated_message) + end + + it "with --without two groups" do + bundle! :install, forgotten_command_line_options(:without => "emo test") + bundle! :update, :all => bundle_update_requires_all? + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_updated_message) + end + + it "with --without more groups" do + bundle! :install, forgotten_command_line_options(:without => "emo obama test") + bundle! :update, :all => bundle_update_requires_all? + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_updated_message) + end + end +end diff --git a/spec/bundler/install/prereleases_spec.rb b/spec/bundler/install/prereleases_spec.rb new file mode 100644 index 00000000000000..7af8c3b3044973 --- /dev/null +++ b/spec/bundler/install/prereleases_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + describe "when prerelease gems are available" do + it "finds prereleases" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "not_released" + G + expect(the_bundle).to include_gems "not_released 1.0.pre" + end + + it "uses regular releases if available" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "has_prerelease" + G + expect(the_bundle).to include_gems "has_prerelease 1.0" + end + + it "uses prereleases if requested" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "has_prerelease", "1.1.pre" + G + expect(the_bundle).to include_gems "has_prerelease 1.1.pre" + end + end + + describe "when prerelease gems are not available" do + it "still works" do + build_repo3 + install_gemfile <<-G + source "file://#{gem_repo3}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0" + end + end +end diff --git a/spec/bundler/install/process_lock_spec.rb b/spec/bundler/install/process_lock_spec.rb new file mode 100644 index 00000000000000..be8fd04fddbb0f --- /dev/null +++ b/spec/bundler/install/process_lock_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.describe "process lock spec" do + describe "when an install operation is already holding a process lock" do + before { FileUtils.mkdir_p(default_bundle_path) } + + it "will not run a second concurrent bundle install until the lock is released" do + thread = Thread.new do + Bundler::ProcessLock.lock(default_bundle_path) do + sleep 1 # ignore quality_spec + expect(the_bundle).not_to include_gems "rack 1.0" + end + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + thread.join + expect(the_bundle).to include_gems "rack 1.0" + end + + context "when creating a lock raises Errno::ENOTSUP", :ruby => ">= 1.9" do + before { allow(File).to receive(:open).and_raise(Errno::ENOTSUP) } + + it "skips creating the lock file and yields" do + processed = false + Bundler::ProcessLock.lock(default_bundle_path) { processed = true } + + expect(processed).to eq true + end + end + end +end diff --git a/spec/bundler/install/redownload_spec.rb b/spec/bundler/install/redownload_spec.rb new file mode 100644 index 00000000000000..f9caeed58a5089 --- /dev/null +++ b/spec/bundler/install/redownload_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install", :bundler => "< 3", :ruby => ">= 2.0" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + before { bundle "config major_deprecations yes" } + + shared_examples_for "an option to force redownloading gems" do + it "re-installs installed gems" do + rack_lib = default_bundle_path("gems/rack-1.0.0/lib/rack.rb") + + bundle! :install + rack_lib.open("w") {|f| f.write("blah blah blah") } + bundle! :install, flag => true + + expect(out).to include "Installing rack 1.0.0" + expect(rack_lib.open(&:read)).to eq("RACK = '1.0.0'\n") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "works on first bundle install" do + bundle! :install, flag => true + + expect(out).to include "Installing rack 1.0.0" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + context "with a git gem" do + let!(:ref) { build_git("foo", "1.0").ref_for("HEAD", 11) } + + before do + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + end + + it "re-installs installed gems" do + foo_lib = default_bundle_path("bundler/gems/foo-1.0-#{ref}/lib/foo.rb") + + bundle! :install + foo_lib.open("w") {|f| f.write("blah blah blah") } + bundle! :install, flag => true + + expect(foo_lib.open(&:read)).to eq("FOO = '1.0'\n") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works on first bundle install" do + bundle! :install, flag => true + + expect(the_bundle).to include_gems "foo 1.0" + end + end + end + + describe "with --force" do + it_behaves_like "an option to force redownloading gems" do + let(:flag) { "force" } + end + + it "shows a deprecation when single flag passed" do + bundle! "install --force" + expect(out).to include "[DEPRECATED FOR 3.0] The `--force` option has been renamed to `--redownload`" + end + + it "shows a deprecation when multiple flags passed" do + bundle! "install --no-color --force" + expect(out).to include "[DEPRECATED FOR 3.0] The `--force` option has been renamed to `--redownload`" + end + end + + describe "with --redownload" do + it_behaves_like "an option to force redownloading gems" do + let(:flag) { "redownload" } + end + + it "does not show a deprecation when single flag passed" do + bundle! "install --redownload" + expect(out).not_to include "[DEPRECATED FOR 2.0] The `--force` option has been renamed to `--redownload`" + end + + it "does not show a deprecation when single multiple flags passed" do + bundle! "install --no-color --redownload" + expect(out).not_to include "[DEPRECATED FOR 2.0] The `--force` option has been renamed to `--redownload`" + end + end +end diff --git a/spec/bundler/install/security_policy_spec.rb b/spec/bundler/install/security_policy_spec.rb new file mode 100644 index 00000000000000..7be09d6bd4729b --- /dev/null +++ b/spec/bundler/install/security_policy_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "rubygems/security" + +# unfortunately, testing signed gems with a provided CA is extremely difficult +# as 'gem cert' is currently the only way to add CAs to the system. + +RSpec.describe "policies with unsigned gems" do + before do + build_security_repo + gemfile <<-G + source "file://#{security_repo}" + gem "rack" + gem "signed_gem" + G + end + + it "will work after you try to deploy without a lock" do + bundle "install --deployment" + bundle :install + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "rack 1.0", "signed_gem 1.0" + end + + it "will fail when given invalid security policy" do + bundle "install --trust-policy=InvalidPolicyName" + expect(out).to include("RubyGems doesn't know about trust policy") + end + + it "will fail with High Security setting due to presence of unsigned gem" do + bundle "install --trust-policy=HighSecurity" + expect(out).to include("security policy didn't allow") + end + + # This spec will fail on RubyGems 2 rc1 due to a bug in policy.rb. the bug is fixed in rc3. + it "will fail with Medium Security setting due to presence of unsigned gem", :unless => ENV["RGV"] == "v2.0.0.rc.1" do + bundle "install --trust-policy=MediumSecurity" + expect(out).to include("security policy didn't allow") + end + + it "will succeed with no policy" do + bundle "install" + expect(exitstatus).to eq(0) if exitstatus + end +end + +RSpec.describe "policies with signed gems and no CA" do + before do + build_security_repo + gemfile <<-G + source "file://#{security_repo}" + gem "signed_gem" + G + end + + it "will fail with High Security setting, gem is self-signed" do + bundle "install --trust-policy=HighSecurity" + expect(out).to include("security policy didn't allow") + end + + it "will fail with Medium Security setting, gem is self-signed" do + bundle "install --trust-policy=MediumSecurity" + expect(out).to include("security policy didn't allow") + end + + it "will succeed with Low Security setting, low security accepts self signed gem" do + bundle "install --trust-policy=LowSecurity" + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "signed_gem 1.0" + end + + it "will succeed with no policy" do + bundle "install" + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "signed_gem 1.0" + end +end diff --git a/spec/bundler/install/yanked_spec.rb b/spec/bundler/install/yanked_spec.rb new file mode 100644 index 00000000000000..7c4b98bfdf3924 --- /dev/null +++ b/spec/bundler/install/yanked_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.context "when installing a bundle that includes yanked gems" do + before(:each) do + build_repo4 do + build_gem "foo", "9.0.0" + end + end + + it "throws an error when the original gem version is yanked" do + lockfile <<-L + GEM + remote: file://#{gem_repo4} + specs: + foo (10.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo (= 10.0.0) + + L + + install_gemfile <<-G + source "file://#{gem_repo4}" + gem "foo", "10.0.0" + G + + expect(out).to include("Your bundle is locked to foo (10.0.0)") + end + + it "throws the original error when only the Gemfile specifies a gem version that doesn't exist" do + install_gemfile <<-G + source "file://#{gem_repo4}" + gem "foo", "10.0.0" + G + + expect(out).not_to include("Your bundle is locked to foo (10.0.0)") + expect(out).to include("Could not find gem 'foo (= 10.0.0)' in") + end +end + +RSpec.context "when using gem before installing" do + it "does not suggest the author has yanked the gem" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + lockfile <<-L + GEM + remote: file://#{gem_repo1} + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 0.9.1) + L + + bundle :list + + expect(out).to include("Could not find rack-0.9.1 in any of the sources") + expect(out).to_not include("Your bundle is locked to rack (0.9.1), but that version could not be found in any of the sources listed in your Gemfile.") + expect(out).to_not include("If you haven't changed sources, that means the author of rack (0.9.1) has removed it.") + expect(out).to_not include("You'll need to update your bundle to a different version of rack (0.9.1) that hasn't been removed in order to install.") + end +end diff --git a/spec/bundler/lock/git_spec.rb b/spec/bundler/lock/git_spec.rb new file mode 100644 index 00000000000000..14b80483eec012 --- /dev/null +++ b/spec/bundler/lock/git_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe "bundle lock with git gems" do + before :each do + build_git "foo" + + install_gemfile <<-G + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + end + + it "doesn't break right after running lock" do + expect(the_bundle).to include_gems "foo 1.0.0" + end + + it "locks a git source to the current ref" do + update_git "foo" + bundle :install + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "provides correct #full_gem_path" do + run <<-RUBY + puts Bundler.rubygems.find_name('foo').first.full_gem_path + RUBY + expect(out).to eq(bundle("info foo --path")) + end +end diff --git a/spec/bundler/lock/lockfile_bundler_1_spec.rb b/spec/bundler/lock/lockfile_bundler_1_spec.rb new file mode 100644 index 00000000000000..851fbea99a6dcd --- /dev/null +++ b/spec/bundler/lock/lockfile_bundler_1_spec.rb @@ -0,0 +1,1386 @@ +# frozen_string_literal: true + +RSpec.describe "the lockfile format", :bundler => "< 3" do + include Bundler::GemHelpers + + before { ENV["BUNDLER_SPEC_IGNORE_COMPATIBILITY_GUARD"] = "TRUE" } + + it "generates a simple lockfile for a single source, gem" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "updates the lockfile's bundler version if current ver. is newer" do + lockfile <<-L + GIT + remote: git://github.com/nex3/haml.git + revision: 8a2271f + specs: + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + omg! + rack + + BUNDLED WITH + 1.8.2 + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not update the lockfile's bundler version if nothing changed during bundle install", :ruby_repo do + version = "#{Bundler::VERSION.split(".").first}.0.0.0.a" + + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + #{version} + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + #{version} + G + end + + it "updates the lockfile's bundler version if not present" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack", "> 0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack (> 0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "outputs a warning if the current is older than lockfile's bundler version" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.1.0 + L + + simulate_bundler_version "9999999.0.0" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + end + + warning_message = "the running version of Bundler (9999999.0.0) is older " \ + "than the version that created the lockfile (9999999.1.0)" + expect(out.scan(warning_message).size).to eq(1) + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + #{specific_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.1.0 + G + end + + it "errors if the current is a major version older than lockfile's bundler version" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + expect(exitstatus > 0) if exitstatus + expect(out).to include("You must use Bundler 9999999 or greater with this lockfile.") + end + + it "shows a friendly error when running with a new bundler 2 lockfile" do + lockfile <<-L + GEM + remote: https://rails-assets.org/ + specs: + rails-assets-bootstrap (3.3.4) + rails-assets-jquery (>= 1.9.1) + rails-assets-jquery (2.1.4) + + GEM + remote: https://rubygems.org/ + specs: + rake (10.4.2) + + PLATFORMS + ruby + + DEPENDENCIES + rails-assets-bootstrap! + rake + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source 'https://rubygems.org' + gem 'rake' + + source 'https://rails-assets.org' do + gem 'rails-assets-bootstrap' + end + G + + expect(exitstatus > 0) if exitstatus + expect(out).to include("You must use Bundler 9999999 or greater with this lockfile.") + end + + it "warns when updating bundler major version" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + L + + simulate_bundler_version "9999999.0.0" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + end + + expect(out).to include("Warning: the lockfile is being updated to Bundler " \ + "9999999, after which you will be unable to return to Bundler 1.") + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + #{specific_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + G + end + + it "generates a simple lockfile for a single source, gem with dependencies" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a simple lockfile for a single source, gem with a version requirement" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack-obama", ">= 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile wihout credentials for a configured source" do + bundle "config http://localgemserver.test/ user:pass" + + install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + source "http://localgemserver.test/" + source "http://user:pass@othergemserver.test/" + + gem "rack-obama", ">= 1.0" + G + + lockfile_should_be <<-G + GEM + remote: http://localgemserver.test/ + remote: http://user:pass@othergemserver.test/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates lockfiles with multiple requirements" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "net-sftp" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + net-sftp (1.1.1) + net-ssh (>= 1.0.0, < 1.99.0) + net-ssh (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + net-sftp + + BUNDLED WITH + #{Bundler::VERSION} + G + + expect(the_bundle).to include_gems "net-sftp 1.1.1", "net-ssh 1.0.0" + end + + it "generates a simple lockfile for a single pinned source, gem with a version requirement" do + git = build_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("master")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not asplode when a platform specific dependency is present and the Gemfile has not been resolved on that platform" do + build_lib "omg", :path => lib_path("omg") + + gemfile <<-G + source "file://localhost#{gem_repo1}" + + platforms :#{not_local_tag} do + gem "omg", :path => "#{lib_path("omg")}" + end + + gem "rack" + G + + lockfile <<-L + GIT + remote: git://github.com/nex3/haml.git + revision: 8a2271f + specs: + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{not_local} + + DEPENDENCIES + omg! + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "serializes global git sources" do + git = build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("master")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile with a ref for a single pinned source, git gem with a branch requirement" do + git = build_git "foo" + update_git "foo", :branch => "omg" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("omg")} + branch: omg + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile with a ref for a single pinned source, git gem with a tag requirement" do + git = build_git "foo" + update_git "foo", :tag => "omg" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :tag => "omg" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("omg")} + tag: omg + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "serializes pinned path sources to the lockfile" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + lockfile_should_be <<-G + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "serializes pinned path sources to the lockfile even when packaging" do + build_lib "foo" + + install_gemfile! <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle! :package, forgotten_command_line_options([:all, :cache_all] => true) + bundle! :install, :local => true + + lockfile_should_be <<-G + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "sorts serialized sources by type" do + build_lib "foo" + bar = build_git "bar" + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + gem "foo", :path => "#{lib_path("foo-1.0")}" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("bar-1.0")} + revision: #{bar.ref_for("master")} + specs: + bar (1.0) + + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + bar! + foo! + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "lists gems alphabetically" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "thin" + gem "actionpack" + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + actionpack (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + rack (1.0.0) + rack-obama (1.0) + rack + thin (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + actionpack + rack-obama + thin + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "orders dependencies' dependencies in alphabetical order" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rails" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= 10.0.2) + rake (10.0.2) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rails + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "orders dependencies by version" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem 'double_deps' + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + double_deps (1.0) + net-ssh + net-ssh (>= 1.0.0) + net-ssh (1.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + double_deps + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add the :require option to the lockfile" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack-obama", ">= 1.0", :require => "rack/obama" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add the :group option to the lockfile" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack-obama", ">= 1.0", :group => :test + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in a relative fashion and in Gemfile dir" do + build_lib "foo", :path => bundled_app("foo") + + install_gemfile <<-G + path "foo" + gem "foo" + G + + lockfile_should_be <<-G + PATH + remote: foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in a relative fashion and is above Gemfile dir" do + build_lib "foo", :path => bundled_app(File.join("..", "foo")) + + install_gemfile <<-G + path "../foo" + gem "foo" + G + + lockfile_should_be <<-G + PATH + remote: ../foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in an absolute fashion but is relative" do + build_lib "foo", :path => bundled_app("foo") + + install_gemfile <<-G + path File.expand_path("../foo", __FILE__) + gem "foo" + G + + lockfile_should_be <<-G + PATH + remote: foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided for gemspec" do + build_lib("foo", :path => tmp.join("foo")) + + install_gemfile <<-G + gemspec :path => "../foo" + G + + lockfile_should_be <<-G + PATH + remote: ../foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "keeps existing platforms in the lockfile" do + lockfile <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + java + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + platforms = ["java", generic_local_platform.to_s].sort + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{platforms[0]} + #{platforms[1]} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "persists the spec's platform to the lockfile" do + build_gem "platform_specific", "1.0.0", :to_system => true do |s| + s.platform = Gem::Platform.new("universal-java-16") + end + + simulate_platform "universal-java-16" + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "platform_specific" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + platform_specific (1.0-java) + + PLATFORMS + java + + DEPENDENCIES + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate gems" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + G + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + gem "activesupport" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + activesupport (2.3.5) + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + activesupport + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies with versions" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack", "1.0" + gem "rack", "1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies in different groups" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack", "1.0", :group => :one + gem "rack", "1.0", :group => :two + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "raises if two different versions are used" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack", "1.0" + gem "rack", "1.1" + G + + expect(bundled_app("Gemfile.lock")).not_to exist + expect(out).to include "rack (= 1.0) and rack (= 1.1)" + end + + it "raises if two different sources are used" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack" + gem "rack", :git => "git://hubz.com" + G + + expect(bundled_app("Gemfile.lock")).not_to exist + expect(out).to include "rack (>= 0) should come from an unspecified source and git://hubz.com (at master)" + end + + it "works correctly with multiple version dependencies" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack (> 0.9, < 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "captures the Ruby version in the lockfile" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + ruby '#{RUBY_VERSION}' + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack (> 0.9, < 1.0) + + RUBY VERSION + ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + # Some versions of the Bundler 1.1 RC series introduced corrupted + # lockfiles. There were two major problems: + # + # * multiple copies of the same GIT section appeared in the lockfile + # * when this happened, those sections got multiple copies of gems + # in those sections. + it "fixes corrupted lockfiles" do + build_git "omg", :path => lib_path("omg") + revision = revision_for(lib_path("omg")) + + gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "omg", :git => "#{lib_path("omg")}", :branch => 'master' + G + + bundle "install --path vendor" + expect(the_bundle).to include_gems "omg 1.0" + + # Create a Gemfile.lock that has duplicate GIT sections + lockfile <<-L + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + + PLATFORMS + #{local} + + DEPENDENCIES + omg! + + BUNDLED WITH + #{Bundler::VERSION} + L + + FileUtils.rm_rf(bundled_app("vendor")) + bundle "install" + expect(the_bundle).to include_gems "omg 1.0" + + # Confirm that duplicate specs do not appear + lockfile_should_be(<<-L) + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + + PLATFORMS + #{local} + + DEPENDENCIES + omg! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a helpful error message when the lockfile is missing deps" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack_middleware (1.0) + + PLATFORMS + #{local} + + DEPENDENCIES + rack_middleware + L + + install_gemfile <<-G + source "file:#{gem_repo1}" + gem "rack_middleware" + G + + expect(out).to include("Downloading rack_middleware-1.0 revealed dependencies not in the API or the lockfile (#{Gem::Dependency.new("rack", "= 0.9.1")})."). + and include("Either installing with `--full-index` or running `bundle update rack_middleware` should fix the problem.") + end + + describe "a line ending" do + def set_lockfile_mtime_to_known_value + time = Time.local(2000, 1, 1, 0, 0, 0) + File.utime(time, time, bundled_app("Gemfile.lock")) + end + before(:each) do + build_repo2 + + install_gemfile <<-G + source "file://localhost#{gem_repo2}" + gem "rack" + G + set_lockfile_mtime_to_known_value + end + + it "generates Gemfile.lock with \\n line endings" do + expect(File.read(bundled_app("Gemfile.lock"))).not_to match("\r\n") + expect(the_bundle).to include_gems "rack 1.0" + end + + context "during updates" do + it "preserves Gemfile.lock \\n line endings" do + update_repo2 + + expect { bundle "update", :all => true }.to change { File.mtime(bundled_app("Gemfile.lock")) } + expect(File.read(bundled_app("Gemfile.lock"))).not_to match("\r\n") + expect(the_bundle).to include_gems "rack 1.2" + end + + it "preserves Gemfile.lock \\n\\r line endings" do + update_repo2 + win_lock = File.read(bundled_app("Gemfile.lock")).gsub(/\n/, "\r\n") + File.open(bundled_app("Gemfile.lock"), "wb") {|f| f.puts(win_lock) } + set_lockfile_mtime_to_known_value + + expect { bundle "update", :all => true }.to change { File.mtime(bundled_app("Gemfile.lock")) } + expect(File.read(bundled_app("Gemfile.lock"))).to match("\r\n") + expect(the_bundle).to include_gems "rack 1.2" + end + end + + context "when nothing changes" do + it "preserves Gemfile.lock \\n line endings" do + expect do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + RUBY + end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + + it "preserves Gemfile.lock \\n\\r line endings" do + win_lock = File.read(bundled_app("Gemfile.lock")).gsub(/\n/, "\r\n") + File.open(bundled_app("Gemfile.lock"), "wb") {|f| f.puts(win_lock) } + set_lockfile_mtime_to_known_value + + expect do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + RUBY + end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + end + end + + it "refuses to install if Gemfile.lock contains conflict markers" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + <<<<<<< + rack (1.0.0) + ======= + rack (1.0.1) + >>>>>>> + + PLATFORMS + ruby + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + + install_gemfile(<<-G) + source "file://localhost#{gem_repo1}" + gem "rack" + G + + expect(last_command.bundler_err).to match(/your Gemfile.lock contains merge conflicts/i) + expect(last_command.bundler_err).to match(/git checkout HEAD -- Gemfile.lock/i) + end +end diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb new file mode 100644 index 00000000000000..9e2fb3b2dd08ce --- /dev/null +++ b/spec/bundler/lock/lockfile_spec.rb @@ -0,0 +1,1425 @@ +# frozen_string_literal: true + +RSpec.describe "the lockfile format", :bundler => "3" do + include Bundler::GemHelpers + + before { ENV["BUNDLER_SPEC_IGNORE_COMPATIBILITY_GUARD"] = "TRUE" } + + it "generates a simple lockfile for a single source, gem" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "updates the lockfile's bundler version if current ver. is newer" do + lockfile <<-L + GIT + remote: git://github.com/nex3/haml.git + revision: 8a2271f + specs: + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + omg! + rack + + BUNDLED WITH + 1.8.2 + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not update the lockfile's bundler version if nothing changed during bundle install" do + version = "#{Bundler::VERSION.split(".").first}.0.0.0.a" + + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + #{version} + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + #{version} + G + end + + it "updates the lockfile's bundler version if not present" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack", "> 0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack (> 0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "outputs a warning if the current is older than lockfile's bundler version" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.1.0 + L + + simulate_bundler_version "9999999.0.0" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + + gem "rack" + G + end + + warning_message = "the running version of Bundler (9999999.0.0) is older " \ + "than the version that created the lockfile (9999999.1.0)" + expect(last_command.bundler_err).to include warning_message + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.1.0 + G + end + + it "errors if the current is a major version older than lockfile's bundler version" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack" + G + + expect(last_command).to be_failure + expect(last_command.bundler_err).to include("You must use Bundler 9999999 or greater with this lockfile.") + end + + it "shows a friendly error when running with a new bundler 2 lockfile" do + lockfile <<-L + GEM + remote: https://rails-assets.org/ + specs: + rails-assets-bootstrap (3.3.4) + rails-assets-jquery (>= 1.9.1) + rails-assets-jquery (2.1.4) + + GEM + remote: https://rubygems.org/ + specs: + rake (10.4.2) + + PLATFORMS + ruby + + DEPENDENCIES + rails-assets-bootstrap! + rake + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source 'https://rubygems.org' + gem 'rake' + + source 'https://rails-assets.org' do + gem 'rails-assets-bootstrap' + end + G + + expect(last_command).to be_failure + expect(out).to include("You must use Bundler 9999999 or greater with this lockfile.") + end + + it "warns when updating bundler major version" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + L + + simulate_bundler_version "9999999.0.0" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack" + G + end + + expect(out).to include("Warning: the lockfile is being updated to Bundler " \ + "9999999, after which you will be unable to return to Bundler 1.") + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + G + end + + it "generates a simple lockfile for a single source, gem with dependencies" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack-obama + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a simple lockfile for a single source, gem with a version requirement" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack-obama", ">= 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile without credentials for a configured source" do + bundle "config http://localgemserver.test/ user:pass" + + install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + source "http://localgemserver.test/" do + + end + + source "http://user:pass@othergemserver.test/" do + gem "rack-obama", ">= 1.0" + end + G + + lockfile_should_be <<-G + GEM + specs: + + GEM + remote: http://localgemserver.test/ + specs: + + GEM + remote: http://user:pass@othergemserver.test/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack-obama (>= 1.0)! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates lockfiles with multiple requirements" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "net-sftp" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + net-sftp (1.1.1) + net-ssh (>= 1.0.0, < 1.99.0) + net-ssh (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + net-sftp + + BUNDLED WITH + #{Bundler::VERSION} + G + + expect(the_bundle).to include_gems "net-sftp 1.1.1", "net-ssh 1.0.0" + end + + it "generates a simple lockfile for a single pinned source, gem with a version requirement", :bundler => "< 3" do + git = build_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("master")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a simple lockfile for a single pinned source, gem with a version requirement" do + git = build_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + lockfile_should_be <<-G + GEM + specs: + + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("master")} + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not asplode when a platform specific dependency is present and the Gemfile has not been resolved on that platform" do + build_lib "omg", :path => lib_path("omg") + + gemfile <<-G + source "file://localhost#{gem_repo1}/" + + platforms :#{not_local_tag} do + gem "omg", :path => "#{lib_path("omg")}" + end + + gem "rack" + G + + lockfile <<-L + GIT + remote: git://github.com/nex3/haml.git + revision: 8a2271f + specs: + + GEM + remote: file://localhost#{gem_repo1}// + specs: + rack (1.0.0) + + PLATFORMS + #{not_local} + + DEPENDENCIES + omg! + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle! "install" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "serializes global git sources" do + git = build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + lockfile_should_be <<-G + GEM + specs: + + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("master")} + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile with a ref for a single pinned source, git gem with a branch requirement" do + git = build_git "foo" + update_git "foo", :branch => "omg" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" + G + + lockfile_should_be <<-G + GEM + specs: + + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("omg")} + branch: omg + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile with a ref for a single pinned source, git gem with a tag requirement" do + git = build_git "foo" + update_git "foo", :tag => "omg" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :tag => "omg" + G + + lockfile_should_be <<-G + GEM + specs: + + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("omg")} + tag: omg + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "serializes pinned path sources to the lockfile" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + lockfile_should_be <<-G + GEM + specs: + + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "serializes pinned path sources to the lockfile even when packaging" do + build_lib "foo" + + install_gemfile! <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle! :package, forgotten_command_line_options([:all, :cache_all] => true) + bundle! :install, :local => true + + lockfile_should_be <<-G + GEM + specs: + + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "sorts serialized sources by type" do + build_lib "foo" + bar = build_git "bar" + + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack" + gem "foo", :path => "#{lib_path("foo-1.0")}" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + GIT + remote: #{lib_path("bar-1.0")} + revision: #{bar.ref_for("master")} + specs: + bar (1.0) + + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + bar! + foo! + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "lists gems alphabetically" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "thin" + gem "actionpack" + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + actionpack (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + rack (1.0.0) + rack-obama (1.0) + rack + thin (1.0) + rack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + actionpack + rack-obama + thin + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "orders dependencies' dependencies in alphabetical order" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rails" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= 10.0.2) + rake (10.0.2) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rails + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "orders dependencies by version" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem 'double_deps' + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + double_deps (1.0) + net-ssh + net-ssh (>= 1.0.0) + net-ssh (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + double_deps + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add the :require option to the lockfile" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack-obama", ">= 1.0", :require => "rack/obama" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add the :group option to the lockfile" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack-obama", ">= 1.0", :group => :test + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in a relative fashion and in Gemfile dir" do + build_lib "foo", :path => bundled_app("foo") + + install_gemfile <<-G + path "foo" do + gem "foo" + end + G + + lockfile_should_be <<-G + GEM + specs: + + PATH + remote: foo + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in a relative fashion and is above Gemfile dir" do + build_lib "foo", :path => bundled_app(File.join("..", "foo")) + + install_gemfile <<-G + path "../foo" do + gem "foo" + end + G + + lockfile_should_be <<-G + GEM + specs: + + PATH + remote: ../foo + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in an absolute fashion but is relative" do + build_lib "foo", :path => bundled_app("foo") + + install_gemfile <<-G + path File.expand_path("../foo", __FILE__) do + gem "foo" + end + G + + lockfile_should_be <<-G + GEM + specs: + + PATH + remote: foo + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided for gemspec" do + build_lib("foo", :path => tmp.join("foo")) + + install_gemfile <<-G + gemspec :path => "../foo" + G + + lockfile_should_be <<-G + GEM + specs: + + PATH + remote: ../foo + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "keeps existing platforms in the lockfile" do + lockfile <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + java + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms "java", generic_local_platform, specific_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "persists the spec's platform to the lockfile" do + build_repo2 do + build_gem "platform_specific", "1.0" do |s| + s.platform = Gem::Platform.new("universal-java-16") + end + end + + simulate_platform "universal-java-16" + + install_gemfile! <<-G + source "file://localhost#{gem_repo2}" + gem "platform_specific" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo2}/ + specs: + platform_specific (1.0-java) + platform_specific (1.0-universal-java-16) + + PLATFORMS + java + universal-java-16 + + DEPENDENCIES + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate gems" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack" + G + + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack" + gem "activesupport" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + activesupport (2.3.5) + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack" + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies with versions" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack", "1.0" + gem "rack", "1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies in different groups" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack", "1.0", :group => :one + gem "rack", "1.0", :group => :two + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "raises if two different versions are used" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack", "1.0" + gem "rack", "1.1" + G + + expect(bundled_app("Gemfile.lock")).not_to exist + expect(out).to include "rack (= 1.0) and rack (= 1.1)" + end + + it "raises if two different sources are used" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack" + gem "rack", :git => "git://hubz.com" + G + + expect(bundled_app("Gemfile.lock")).not_to exist + expect(out).to include "rack (>= 0) should come from an unspecified source and git://hubz.com (at master)" + end + + it "works correctly with multiple version dependencies" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack (> 0.9, < 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "captures the Ruby version in the lockfile" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}/" + ruby '#{RUBY_VERSION}' + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack (> 0.9, < 1.0) + + RUBY VERSION + ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + # Some versions of the Bundler 1.1 RC series introduced corrupted + # lockfiles. There were two major problems: + # + # * multiple copies of the same GIT section appeared in the lockfile + # * when this happened, those sections got multiple copies of gems + # in those sections. + it "fixes corrupted lockfiles" do + build_git "omg", :path => lib_path("omg") + revision = revision_for(lib_path("omg")) + + gemfile <<-G + source "file://localhost#{gem_repo1}/" + gem "omg", :git => "#{lib_path("omg")}", :branch => 'master' + G + + bundle! :install, forgotten_command_line_options(:path => "vendor") + expect(the_bundle).to include_gems "omg 1.0" + + # Create a Gemfile.lock that has duplicate GIT sections + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + omg! + + BUNDLED WITH + #{Bundler::VERSION} + L + + FileUtils.rm_rf(bundled_app("vendor")) + bundle "install" + expect(the_bundle).to include_gems "omg 1.0" + + # Confirm that duplicate specs do not appear + lockfile_should_be(<<-L) + GEM + remote: file://localhost#{gem_repo1}/ + specs: + + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + omg! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a helpful error message when the lockfile is missing deps" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack_middleware (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack_middleware + L + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "rack_middleware" + G + + expect(out).to include("Downloading rack_middleware-1.0 revealed dependencies not in the API or the lockfile (#{Gem::Dependency.new("rack", "= 0.9.1")})."). + and include("Either installing with `--full-index` or running `bundle update rack_middleware` should fix the problem.") + end + + describe "a line ending" do + def set_lockfile_mtime_to_known_value + time = Time.local(2000, 1, 1, 0, 0, 0) + File.utime(time, time, bundled_app("Gemfile.lock")) + end + before(:each) do + build_repo2 + + install_gemfile <<-G + source "file://localhost#{gem_repo2}" + gem "rack" + G + set_lockfile_mtime_to_known_value + end + + it "generates Gemfile.lock with \\n line endings" do + expect(File.read(bundled_app("Gemfile.lock"))).not_to match("\r\n") + expect(the_bundle).to include_gems "rack 1.0" + end + + context "during updates" do + it "preserves Gemfile.lock \\n line endings" do + update_repo2 + + expect { bundle "update", :all => true }.to change { File.mtime(bundled_app("Gemfile.lock")) } + expect(File.read(bundled_app("Gemfile.lock"))).not_to match("\r\n") + expect(the_bundle).to include_gems "rack 1.2" + end + + it "preserves Gemfile.lock \\n\\r line endings" do + update_repo2 + win_lock = File.read(bundled_app("Gemfile.lock")).gsub(/\n/, "\r\n") + File.open(bundled_app("Gemfile.lock"), "wb") {|f| f.puts(win_lock) } + set_lockfile_mtime_to_known_value + + expect { bundle "update", :all => true }.to change { File.mtime(bundled_app("Gemfile.lock")) } + expect(File.read(bundled_app("Gemfile.lock"))).to match("\r\n") + expect(the_bundle).to include_gems "rack 1.2" + end + end + + context "when nothing changes" do + it "preserves Gemfile.lock \\n line endings" do + expect do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + RUBY + end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + + it "preserves Gemfile.lock \\n\\r line endings" do + win_lock = File.read(bundled_app("Gemfile.lock")).gsub(/\n/, "\r\n") + File.open(bundled_app("Gemfile.lock"), "wb") {|f| f.puts(win_lock) } + set_lockfile_mtime_to_known_value + + expect do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + RUBY + end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + end + end + + it "refuses to install if Gemfile.lock contains conflict markers" do + lockfile <<-L + GEM + remote: file://localhost#{gem_repo1}// + specs: + <<<<<<< + rack (1.0.0) + ======= + rack (1.0.1) + >>>>>>> + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + + install_gemfile(<<-G) + source "file://localhost#{gem_repo1}/" + gem "rack" + G + + expect(last_command.bundler_err).to match(/your Gemfile.lock contains merge conflicts/i) + expect(last_command.bundler_err).to match(/git checkout HEAD -- Gemfile.lock/i) + end +end diff --git a/spec/bundler/other/bundle_ruby_spec.rb b/spec/bundler/other/bundle_ruby_spec.rb new file mode 100644 index 00000000000000..fbca31d0a0461d --- /dev/null +++ b/spec/bundler/other/bundle_ruby_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +RSpec.describe "bundle_ruby", :bundler => "< 3" do + context "without patchlevel" do + it "returns the ruby version" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :engine => 'ruby', :engine_version => '1.9.3' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3") + end + + it "engine defaults to MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3" + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3") + end + + it "handles jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'jruby', :engine_version => '1.6.5' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.8.7 (jruby 1.6.5)") + end + + it "handles rbx" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx', :engine_version => '1.2.4' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.8.7 (rbx 1.2.4)") + end + + it "handles truffleruby", :rubygems => ">= 2.1.0" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "2.5.1", :engine => 'truffleruby', :engine_version => '1.0.0-rc6' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 2.5.1 (truffleruby 1.0.0-rc6)") + end + + it "raises an error if engine is used but engine version is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx' + + gem "foo" + G + + bundle_ruby + expect(exitstatus).not_to eq(0) if exitstatus + + bundle_ruby + expect(out).to include("Please define :engine_version") + end + + it "raises an error if engine_version is used but engine is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine_version => '1.2.4' + + gem "foo" + G + + bundle_ruby + expect(exitstatus).not_to eq(0) if exitstatus + + bundle_ruby + expect(out).to include("Please define :engine") + end + + it "raises an error if engine version doesn't match ruby version for MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4' + + gem "foo" + G + + bundle_ruby + expect(exitstatus).not_to eq(0) if exitstatus + + bundle_ruby + expect(out).to include("ruby_version must match the :engine_version for MRI") + end + + it "should print if no ruby version is specified" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + + bundle_ruby + + expect(out).to include("No ruby version specified") + end + end + + context "when using patchlevel" do + it "returns the ruby version" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :patchlevel => '429', :engine => 'ruby', :engine_version => '1.9.3' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3p429") + end + + it "handles an engine" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :patchlevel => '392', :engine => 'jruby', :engine_version => '1.7.4' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3p392 (jruby 1.7.4)") + end + end +end diff --git a/spec/bundler/other/cli_dispatch_spec.rb b/spec/bundler/other/cli_dispatch_spec.rb new file mode 100644 index 00000000000000..d17819b3948eaa --- /dev/null +++ b/spec/bundler/other/cli_dispatch_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe "bundle command names" do + it "work when given fully" do + bundle "install" + expect(last_command.bundler_err).to eq("Could not locate Gemfile") + expect(last_command.stdboth).not_to include("Ambiguous command") + end + + it "work when not ambiguous" do + bundle "ins" + expect(last_command.bundler_err).to eq("Could not locate Gemfile") + expect(last_command.stdboth).not_to include("Ambiguous command") + end + + it "print a friendly error when ambiguous" do + bundle "in" + expect(last_command.bundler_err).to eq("Ambiguous command in matches [info, init, inject, install]") + end + + context "when cache_command_is_package is set" do + before { bundle! "config cache_command_is_package true" } + + it "dispatches `bundle cache` to the package command" do + bundle "cache --verbose" + expect(last_command.stdout).to start_with "Running `bundle package --verbose`" + end + end +end diff --git a/spec/bundler/other/compatibility_guard_spec.rb b/spec/bundler/other/compatibility_guard_spec.rb new file mode 100644 index 00000000000000..ac05ebd91807cc --- /dev/null +++ b/spec/bundler/other/compatibility_guard_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe "bundler compatibility guard" do + context "when the bundler version is 2+" do + before { simulate_bundler_version "2.0.a" } + + context "when running on Ruby < 2.3", :ruby => "< 2.3" do + before { simulate_rubygems_version "2.6.11" } + + it "raises a friendly error" do + bundle :version + expect(err).to eq("Bundler 2 requires Ruby 2.3 or later. Either install bundler 1 or update to a supported Ruby version.") + end + end + + context "when running on RubyGems < 2.5", :ruby => ">= 2.5" do + before { simulate_rubygems_version "1.3.6" } + + it "raises a friendly error" do + bundle :version + expect(err).to eq("Bundler 2 requires RubyGems 2.5 or later. Either install bundler 1 or update to a supported RubyGems version.") + end + end + end +end diff --git a/spec/bundler/other/ext_spec.rb b/spec/bundler/other/ext_spec.rb new file mode 100644 index 00000000000000..3f6f8b4928f900 --- /dev/null +++ b/spec/bundler/other/ext_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.describe "Gem::Specification#match_platform" do + it "does not match platforms other than the gem platform" do + darwin = gem "lol", "1.0", "platform_specific-1.0-x86-darwin-10" + expect(darwin.match_platform(pl("java"))).to eq(false) + end + + context "when platform is a string" do + it "matches when platform is a string" do + lazy_spec = Bundler::LazySpecification.new("lol", "1.0", "universal-mingw32") + expect(lazy_spec.match_platform(pl("x86-mingw32"))).to eq(true) + expect(lazy_spec.match_platform(pl("x64-mingw32"))).to eq(true) + end + end +end + +RSpec.describe "Bundler::GemHelpers#generic" do + include Bundler::GemHelpers + + it "converts non-windows platforms into ruby" do + expect(generic(pl("x86-darwin-10"))).to eq(pl("ruby")) + expect(generic(pl("ruby"))).to eq(pl("ruby")) + end + + it "converts java platform variants into java" do + expect(generic(pl("universal-java-17"))).to eq(pl("java")) + expect(generic(pl("java"))).to eq(pl("java")) + end + + it "converts mswin platform variants into x86-mswin32" do + expect(generic(pl("mswin32"))).to eq(pl("x86-mswin32")) + expect(generic(pl("i386-mswin32"))).to eq(pl("x86-mswin32")) + expect(generic(pl("x86-mswin32"))).to eq(pl("x86-mswin32")) + end + + it "converts 32-bit mingw platform variants into x86-mingw32" do + expect(generic(pl("mingw32"))).to eq(pl("x86-mingw32")) + expect(generic(pl("i386-mingw32"))).to eq(pl("x86-mingw32")) + expect(generic(pl("x86-mingw32"))).to eq(pl("x86-mingw32")) + end + + it "converts 64-bit mingw platform variants into x64-mingw32" do + expect(generic(pl("x64-mingw32"))).to eq(pl("x64-mingw32")) + expect(generic(pl("x86_64-mingw32"))).to eq(pl("x64-mingw32")) + end +end + +RSpec.describe "Gem::SourceIndex#refresh!" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "does not explode when called", :rubygems => "1.7" do + run "Gem.source_index.refresh!" + run "Gem::SourceIndex.new([]).refresh!" + end + + it "does not explode when called", :rubygems => "< 1.7" do + run "Gem.source_index.refresh!" + run "Gem::SourceIndex.from_gems_in([]).refresh!" + end +end diff --git a/spec/bundler/other/major_deprecation_spec.rb b/spec/bundler/other/major_deprecation_spec.rb new file mode 100644 index 00000000000000..50800dbb0c3a66 --- /dev/null +++ b/spec/bundler/other/major_deprecation_spec.rb @@ -0,0 +1,282 @@ +# frozen_string_literal: true + +RSpec.describe "major deprecations", :bundler => "< 3" do + let(:warnings) { last_command.bundler_err } # change to err in 2.0 + let(:warnings_without_version_messages) { warnings.gsub(/#{Spec::Matchers::MAJOR_DEPRECATION}Bundler will only support ruby(gems)? >= .*/, "") } + + context "in a .99 version" do + before do + simulate_bundler_version "1.99.1" + bundle "config --delete major_deprecations" + end + + it "prints major deprecations without being configured" do + ruby <<-R + require "bundler" + Bundler::SharedHelpers.major_deprecation(Bundler::VERSION) + R + + expect(warnings).to have_major_deprecation("1.99.1") + end + end + + before do + bundle "config major_deprecations true" + + create_file "gems.rb", <<-G + source "file:#{gem_repo1}" + ruby #{RUBY_VERSION.dump} + gem "rack" + G + bundle! "install" + end + + describe "bundle_ruby" do + it "prints a deprecation" do + bundle_ruby + warnings.gsub! "\nruby #{RUBY_VERSION}", "" + expect(warnings).to have_major_deprecation "the bundle_ruby executable has been removed in favor of `bundle platform --ruby`" + end + end + + describe "Bundler" do + describe ".clean_env" do + it "is deprecated in favor of .original_env" do + source = "Bundler.clean_env" + bundle "exec ruby -e #{source.dump}" + expect(warnings).to have_major_deprecation "`Bundler.clean_env` has weird edge cases, use `.original_env` instead" + end + end + + describe ".environment" do + it "is deprecated in favor of .load" do + source = "Bundler.environment" + bundle "exec ruby -e #{source.dump}" + expect(warnings).to have_major_deprecation "Bundler.environment has been removed in favor of Bundler.load" + end + end + + shared_examples_for "environmental deprecations" do |trigger| + describe "ruby version", :ruby => "< 2.0" do + it "requires a newer ruby version" do + instance_eval(&trigger) + expect(warnings).to have_major_deprecation "Bundler will only support ruby >= 2.0, you are running #{RUBY_VERSION}" + end + end + + describe "rubygems version", :rubygems => "< 2.0" do + it "requires a newer rubygems version" do + instance_eval(&trigger) + expect(warnings).to have_major_deprecation "Bundler will only support rubygems >= 2.0, you are running #{Gem::VERSION}" + end + end + end + + describe "-rbundler/setup" do + it_behaves_like "environmental deprecations", proc { ruby "require 'bundler/setup'" } + end + + describe "Bundler.setup" do + it_behaves_like "environmental deprecations", proc { ruby "require 'bundler'; Bundler.setup" } + end + + describe "bundle check" do + it_behaves_like "environmental deprecations", proc { bundle :check } + end + + describe "bundle update --quiet" do + it "does not print any deprecations" do + bundle :update, :quiet => true + expect(warnings_without_version_messages).not_to have_major_deprecation + end + end + + describe "bundle update" do + before do + create_file("gems.rb", "") + bundle! "install" + end + + it "warns when no options are given" do + bundle! "update" + expect(warnings).to have_major_deprecation a_string_including("Pass --all to `bundle update` to update everything") + end + + it "does not warn when --all is passed" do + bundle! "update --all" + expect(warnings_without_version_messages).not_to have_major_deprecation + end + end + + describe "bundle install --binstubs" do + it "should output a deprecation warning" do + gemfile <<-G + gem 'rack' + G + + bundle :install, :binstubs => true + expect(warnings).to have_major_deprecation a_string_including("The --binstubs option will be removed") + end + end + end + + context "when bundle is run" do + it "should not warn about gems.rb" do + create_file "gems.rb", <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + expect(warnings_without_version_messages).not_to have_major_deprecation + end + + it "should print a Gemfile deprecation warning" do + create_file "gems.rb" + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + expect(the_bundle).to include_gem "rack 1.0" + + expect(warnings).to have_major_deprecation a_string_including("gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock.") + end + + context "with flags" do + it "should print a deprecation warning about autoremembering flags" do + install_gemfile <<-G, :path => "vendor/bundle" + source "file://#{gem_repo1}" + gem "rack" + G + + expect(warnings).to have_major_deprecation a_string_including( + "flags passed to commands will no longer be automatically remembered." + ) + end + end + end + + context "when Bundler.setup is run in a ruby script" do + it "should print a single deprecation warning" do + create_file "gems.rb" + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack", :group => :test + G + + ruby <<-RUBY + require 'rubygems' + require 'bundler' + require 'bundler/vendored_thor' + + Bundler.ui = Bundler::UI::Shell.new + Bundler.setup + Bundler.setup + RUBY + + expect(warnings_without_version_messages).to have_major_deprecation("gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock.") + end + end + + context "when `bundler/deployment` is required in a ruby script" do + it "should print a capistrano deprecation warning" do + ruby(<<-RUBY) + require 'bundler/deployment' + RUBY + + expect(warnings).to have_major_deprecation("Bundler no longer integrates " \ + "with Capistrano, but Capistrano provides " \ + "its own integration with Bundler via the " \ + "capistrano-bundler gem. Use it instead.") + end + end + + describe Bundler::Dsl do + before do + @rubygems = double("rubygems") + allow(Bundler::Source::Rubygems).to receive(:new) { @rubygems } + end + + context "with github gems" do + it "warns about the https change" do + msg = <<-EOS +The :github git source is deprecated, and will be removed in Bundler 3.0. Change any "reponame" :github sources to "username/reponame". Add this code to the top of your Gemfile to ensure it continues to work: + + git_source(:github) {|repo_name| "https://github.com/\#{repo_name}.git" } + + EOS + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, msg) + subject.gem("sparks", :github => "indirect/sparks") + end + + it "upgrades to https on request" do + Bundler.settings.temporary "github.https" => true + msg = <<-EOS +The :github git source is deprecated, and will be removed in Bundler 3.0. Change any "reponame" :github sources to "username/reponame". Add this code to the top of your Gemfile to ensure it continues to work: + + git_source(:github) {|repo_name| "https://github.com/\#{repo_name}.git" } + + EOS + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, msg) + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, "The `github.https` setting will be removed") + subject.gem("sparks", :github => "indirect/sparks") + github_uri = "https://github.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + end + + context "with bitbucket gems" do + it "warns about removal" do + allow(Bundler.ui).to receive(:deprecate) + msg = <<-EOS +The :bitbucket git source is deprecated, and will be removed in Bundler 3.0. Add this code to the top of your Gemfile to ensure it continues to work: + + git_source(:bitbucket) do |repo_name| + user_name, repo_name = repo_name.split("/") + repo_name ||= user_name + "https://\#{user_name}@bitbucket.org/\#{user_name}/\#{repo_name}.git" + end + + EOS + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, msg) + subject.gem("not-really-a-gem", :bitbucket => "mcorp/flatlab-rails") + end + end + + context "with gist gems" do + it "warns about removal" do + allow(Bundler.ui).to receive(:deprecate) + msg = "The :gist git source is deprecated, and will be removed " \ + "in Bundler 3.0. Add this code to the top of your Gemfile to ensure it " \ + "continues to work:\n\n git_source(:gist) {|repo_name| " \ + "\"https://gist.github.com/\#{repo_name}.git\" }\n\n" + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, msg) + subject.gem("not-really-a-gem", :gist => "1234") + end + end + end + + context "bundle show" do + it "prints a deprecation warning" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle! :show + + warnings.gsub!(/gems included.*?\[DEPRECATED/im, "[DEPRECATED") + + expect(warnings).to have_major_deprecation a_string_including("use `bundle list` instead of `bundle show`") + end + end + + context "bundle console" do + it "prints a deprecation warning" do + bundle "console" + + expect(warnings).to have_major_deprecation \ + a_string_including("bundle console will be replaced by `bin/console` generated by `bundle gem `") + end + end +end diff --git a/spec/bundler/other/platform_spec.rb b/spec/bundler/other/platform_spec.rb new file mode 100644 index 00000000000000..6c59fd893c46d4 --- /dev/null +++ b/spec/bundler/other/platform_spec.rb @@ -0,0 +1,1312 @@ +# frozen_string_literal: true + +RSpec.describe "bundle platform" do + context "without flags" do + let(:bundle_platform_platforms_string) do + platforms = [rb] + platforms.unshift(specific_local_platform) if Bundler.feature_flag.bundler_3_mode? + platforms.map {|pl| "* #{pl}" }.join("\n") + end + + it "returns all the output" do + gemfile <<-G + source "file://#{gem_repo1}" + + #{ruby_version_correct} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +#{bundle_platform_platforms_string} + +Your Gemfile specifies a Ruby version requirement: +* ruby #{RUBY_VERSION} + +Your current platform satisfies the Ruby version requirement. +G + end + + it "returns all the output including the patchlevel" do + gemfile <<-G + source "file://#{gem_repo1}" + + #{ruby_version_correct_patchlevel} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +#{bundle_platform_platforms_string} + +Your Gemfile specifies a Ruby version requirement: +* ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + +Your current platform satisfies the Ruby version requirement. +G + end + + it "doesn't print ruby version requirement if it isn't specified" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +#{bundle_platform_platforms_string} + +Your Gemfile does not specify a Ruby version requirement. +G + end + + it "doesn't match the ruby version requirement" do + gemfile <<-G + source "file://#{gem_repo1}" + + #{ruby_version_incorrect} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +#{bundle_platform_platforms_string} + +Your Gemfile specifies a Ruby version requirement: +* ruby #{not_local_ruby_version} + +Your Ruby version is #{RUBY_VERSION}, but your Gemfile specified #{not_local_ruby_version} +G + end + end + + context "--ruby" do + it "returns ruby version when explicit" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :engine => 'ruby', :engine_version => '1.9.3' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.9.3") + end + + it "defaults to MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3" + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.9.3") + end + + it "handles jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'jruby', :engine_version => '1.6.5' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.8.7 (jruby 1.6.5)") + end + + it "handles rbx" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx', :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.8.7 (rbx 1.2.4)") + end + + it "handles truffleruby", :rubygems => ">= 2.1.0" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "2.5.1", :engine => 'truffleruby', :engine_version => '1.0.0-rc6' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 2.5.1 (truffleruby 1.0.0-rc6)") + end + + it "raises an error if engine is used but engine version is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx' + + gem "foo" + G + + bundle "platform" + + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "raises an error if engine_version is used but engine is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform" + + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "raises an error if engine version doesn't match ruby version for MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform" + + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "should print if no ruby version is specified" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("No ruby version specified") + end + + it "handles when there is a locked requirement" do + gemfile <<-G + ruby "< 1.8.7" + G + + lockfile <<-L + GEM + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + RUBY VERSION + ruby 1.0.0p127 + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle! "platform --ruby" + expect(out).to eq("ruby 1.0.0p127") + end + + it "handles when there is a requirement in the gemfile" do + gemfile <<-G + ruby ">= 1.8.7" + G + + bundle! "platform --ruby" + expect(out).to eq("ruby 1.8.7") + end + + it "handles when there are multiple requirements in the gemfile" do + gemfile <<-G + ruby ">= 1.8.7", "< 2.0.0" + G + + bundle! "platform --ruby" + expect(out).to eq("ruby 1.8.7") + end + end + + let(:ruby_version_correct) { "ruby \"#{RUBY_VERSION}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{local_engine_version}\"" } + let(:ruby_version_correct_engineless) { "ruby \"#{RUBY_VERSION}\"" } + let(:ruby_version_correct_patchlevel) { "#{ruby_version_correct}, :patchlevel => '#{RUBY_PATCHLEVEL}'" } + let(:ruby_version_incorrect) { "ruby \"#{not_local_ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_ruby_version}\"" } + let(:engine_incorrect) { "ruby \"#{RUBY_VERSION}\", :engine => \"#{not_local_tag}\", :engine_version => \"#{RUBY_VERSION}\"" } + let(:engine_version_incorrect) { "ruby \"#{RUBY_VERSION}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_engine_version}\"" } + let(:patchlevel_incorrect) { "#{ruby_version_correct}, :patchlevel => '#{not_local_patchlevel}'" } + let(:patchlevel_fixnum) { "#{ruby_version_correct}, :patchlevel => #{RUBY_PATCHLEVEL}1" } + + def should_be_ruby_version_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your Ruby version is #{RUBY_VERSION}, but your Gemfile specified #{not_local_ruby_version}") + end + + def should_be_engine_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your Ruby engine is #{local_ruby_engine}, but your Gemfile specified #{not_local_tag}") + end + + def should_be_engine_version_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your #{local_ruby_engine} version is #{local_engine_version}, but your Gemfile specified #{local_ruby_engine} #{not_local_engine_version}") + end + + def should_be_patchlevel_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your Ruby patchlevel is #{RUBY_PATCHLEVEL}, but your Gemfile specified #{not_local_patchlevel}") + end + + def should_be_patchlevel_fixnum + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("The Ruby patchlevel in your Gemfile must be a string") + end + + context "bundle install" do + it "installs fine when the ruby version matches" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct} + G + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "installs fine with any engine" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct_engineless} + G + + expect(bundled_app("Gemfile.lock")).to exist + end + end + + it "installs fine when the patchlevel matches" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct_patchlevel} + G + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "doesn't install when the ruby version doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_ruby_version_incorrect + end + + it "doesn't install when engine doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_incorrect + end + + it "doesn't install when engine version doesn't match" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_version_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_version_incorrect + end + end + + it "doesn't install when patchlevel doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_patchlevel_incorrect + end + end + + context "bundle check" do + it "checks fine when the ruby version matches" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct} + G + + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq("Resolving dependencies...\nThe Gemfile's dependencies are satisfied") + end + + it "checks fine with any engine" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct_engineless} + G + + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq("Resolving dependencies...\nThe Gemfile's dependencies are satisfied") + end + end + + it "fails when ruby version doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_incorrect} + G + + bundle :check + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_incorrect} + G + + bundle :check + should_be_engine_incorrect + end + + it "fails when engine version doesn't match" do + simulate_ruby_engine "ruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_version_incorrect} + G + + bundle :check + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle :check + should_be_patchlevel_incorrect + end + end + + context "bundle update" do + before do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + G + end + + it "updates successfully when the ruby version matches" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{ruby_version_correct} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update", :all => bundle_update_requires_all? + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + end + + it "updates fine with any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{ruby_version_correct_engineless} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update", :all => bundle_update_requires_all? + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + end + end + + it "fails when ruby version doesn't match" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{ruby_version_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, :all => bundle_update_requires_all? + should_be_ruby_version_incorrect + end + + it "fails when ruby engine doesn't match" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{engine_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, :all => bundle_update_requires_all? + should_be_engine_incorrect + end + + it "fails when ruby engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{engine_version_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, :all => bundle_update_requires_all? + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, :all => bundle_update_requires_all? + should_be_patchlevel_incorrect + end + end + + context "bundle info" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "prints path if ruby version is correct" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_correct} + G + + bundle "info rails --path" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints path if ruby version is correct for any engine" do + simulate_ruby_engine "jruby" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_correct_engineless} + G + + bundle "info rails --path" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + end + + it "fails if ruby version doesn't match", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_incorrect} + G + + bundle "show rails" + should_be_ruby_version_incorrect + end + + it "fails if engine doesn't match", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{engine_incorrect} + G + + bundle "show rails" + should_be_engine_incorrect + end + + it "fails if engine version doesn't match", :bundler => "< 3" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{engine_version_incorrect} + G + + bundle "show rails" + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "show rails" + should_be_patchlevel_incorrect + end + end + + context "bundle cache" do + before do + install_gemfile <<-G + source "file:#{gem_repo1}" + gem 'rack' + G + end + + it "copies the .gem file to vendor/cache when ruby version matches" do + gemfile <<-G + gem 'rack' + + #{ruby_version_correct} + G + + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "copies the .gem file to vendor/cache when ruby version matches for any engine" do + simulate_ruby_engine "jruby" do + install_gemfile! <<-G + source "file:#{gem_repo1}" + gem 'rack' + + #{ruby_version_correct_engineless} + G + + bundle! :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + end + + it "fails if the ruby version doesn't match" do + gemfile <<-G + gem 'rack' + + #{ruby_version_incorrect} + G + + bundle :cache + should_be_ruby_version_incorrect + end + + it "fails if the engine doesn't match" do + gemfile <<-G + gem 'rack' + + #{engine_incorrect} + G + + bundle :cache + should_be_engine_incorrect + end + + it "fails if the engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + gem 'rack' + + #{engine_version_incorrect} + G + + bundle :cache + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle :cache + should_be_patchlevel_incorrect + end + end + + context "bundle pack" do + before do + install_gemfile! <<-G + source "file:#{gem_repo1}" + gem 'rack' + G + end + + it "copies the .gem file to vendor/cache when ruby version matches" do + gemfile <<-G + gem 'rack' + + #{ruby_version_correct} + G + + bundle :pack + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "copies the .gem file to vendor/cache when ruby version matches any engine" do + simulate_ruby_engine "jruby" do + install_gemfile! <<-G + source "file:#{gem_repo1}" + gem 'rack' + + #{ruby_version_correct_engineless} + G + + bundle :pack + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + end + + it "fails if the ruby version doesn't match" do + gemfile <<-G + gem 'rack' + + #{ruby_version_incorrect} + G + + bundle :pack + should_be_ruby_version_incorrect + end + + it "fails if the engine doesn't match" do + gemfile <<-G + gem 'rack' + + #{engine_incorrect} + G + + bundle :pack + should_be_engine_incorrect + end + + it "fails if the engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + gem 'rack' + + #{engine_version_incorrect} + G + + bundle :pack + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle :pack + should_be_patchlevel_incorrect + end + end + + context "bundle exec" do + before do + ENV["BUNDLER_FORCE_TTY"] = "true" + system_gems "rack-1.0.0", "rack-0.9.1", :path => :bundle_path + end + + it "activates the correct gem when ruby version matches" do + gemfile <<-G + gem "rack", "0.9.1" + + #{ruby_version_correct} + G + + bundle "exec rackup" + expect(out).to eq("0.9.1") + end + + it "activates the correct gem when ruby version matches any engine" do + simulate_ruby_engine "jruby" do + system_gems "rack-1.0.0", "rack-0.9.1", :path => :bundle_path + gemfile <<-G + gem "rack", "0.9.1" + + #{ruby_version_correct_engineless} + G + + bundle "exec rackup" + expect(out).to eq("0.9.1") + end + end + + it "fails when the ruby version doesn't match" do + gemfile <<-G + gem "rack", "0.9.1" + + #{ruby_version_incorrect} + G + + bundle "exec rackup" + should_be_ruby_version_incorrect + end + + it "fails when the engine doesn't match" do + gemfile <<-G + gem "rack", "0.9.1" + + #{engine_incorrect} + G + + bundle "exec rackup" + should_be_engine_incorrect + end + + # it "fails when the engine version doesn't match" do + # simulate_ruby_engine "jruby" do + # gemfile <<-G + # gem "rack", "0.9.1" + # + # #{engine_version_incorrect} + # G + # + # bundle "exec rackup" + # should_be_engine_version_incorrect + # end + # end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle "exec rackup" + should_be_patchlevel_incorrect + end + end + + context "bundle console", :bundler => "< 3" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + G + end + + it "starts IRB with the default group loaded when ruby version matches" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{ruby_version_correct} + G + + bundle "console" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "starts IRB with the default group loaded when ruby version matches any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{ruby_version_correct_engineless} + G + + bundle "console" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + end + + it "fails when ruby version doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{ruby_version_incorrect} + G + + bundle "console" + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{engine_incorrect} + G + + bundle "console" + should_be_engine_incorrect + end + + it "fails when engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{engine_version_incorrect} + G + + bundle "console" + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{patchlevel_incorrect} + G + + bundle "console" + should_be_patchlevel_incorrect + end + end + + context "Bundler.setup" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack", :group => :test + G + + ENV["BUNDLER_FORCE_TTY"] = "true" + end + + it "makes a Gemfile.lock if setup succeeds" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{ruby_version_correct} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + run "1" + expect(bundled_app("Gemfile.lock")).to exist + end + + it "makes a Gemfile.lock if setup succeeds for any engine" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{ruby_version_correct_engineless} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + run "1" + expect(bundled_app("Gemfile.lock")).to exist + end + end + + it "fails when ruby version doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{ruby_version_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{engine_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_incorrect + end + + it "fails when engine version doesn't match" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{engine_version_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{patchlevel_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_patchlevel_incorrect + end + end + + context "bundle outdated" do + before do + build_repo2 do + build_git "foo", :path => lib_path("foo") + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + G + end + + it "returns list of outdated gems when the ruby version matches" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_correct} + G + + bundle "outdated" + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5") + expect(out).to include("foo (newest 1.0") + end + + it "returns list of outdated gems when the ruby version matches for any engine" do + simulate_ruby_engine "jruby" do + bundle! :install + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_correct_engineless} + G + + bundle "outdated" + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("foo (newest 1.0") + end + end + + it "fails when the ruby version doesn't match" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_incorrect} + G + + bundle "outdated" + should_be_ruby_version_incorrect + end + + it "fails when the engine doesn't match" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{engine_incorrect} + G + + bundle "outdated" + should_be_engine_incorrect + end + + it "fails when the engine version doesn't match" do + simulate_ruby_engine "jruby" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{engine_version_incorrect} + G + + bundle "outdated" + should_be_engine_version_incorrect + end + end + + it "fails when the patchlevel doesn't match" do + simulate_ruby_engine "jruby" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{patchlevel_incorrect} + G + + bundle "outdated" + should_be_patchlevel_incorrect + end + end + + it "fails when the patchlevel is a fixnum" do + simulate_ruby_engine "jruby" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{patchlevel_fixnum} + G + + bundle "outdated" + should_be_patchlevel_fixnum + end + end + end +end diff --git a/spec/bundler/other/ssl_cert_spec.rb b/spec/bundler/other/ssl_cert_spec.rb new file mode 100644 index 00000000000000..6d957276fc6f4b --- /dev/null +++ b/spec/bundler/other/ssl_cert_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "bundler/ssl_certs/certificate_manager" + +RSpec.describe "SSL Certificates", :rubygems_master do + hosts = %w[ + rubygems.org + index.rubygems.org + rubygems.global.ssl.fastly.net + staging.rubygems.org + ] + + hosts.each do |host| + it "can securely connect to #{host}", :realworld do + Bundler::SSLCerts::CertificateManager.new.connect_to(host) + end + end +end diff --git a/spec/bundler/plugins/command_spec.rb b/spec/bundler/plugins/command_spec.rb new file mode 100644 index 00000000000000..999d8b722b3b0e --- /dev/null +++ b/spec/bundler/plugins/command_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +RSpec.describe "command plugins" do + before do + build_repo2 do + build_plugin "command-mah" do |s| + s.write "plugins.rb", <<-RUBY + module Mah + class Plugin < Bundler::Plugin::API + command "mahcommand" # declares the command + + def exec(command, args) + puts "MahHello" + end + end + end + RUBY + end + end + + bundle "plugin install command-mah --source file://#{gem_repo2}" + end + + it "executes without arguments" do + expect(out).to include("Installed plugin command-mah") + + bundle "mahcommand" + expect(out).to eq("MahHello") + end + + it "accepts the arguments" do + build_repo2 do + build_plugin "the-echoer" do |s| + s.write "plugins.rb", <<-RUBY + module Resonance + class Echoer + # Another method to declare the command + Bundler::Plugin::API.command "echo", self + + def exec(command, args) + puts "You gave me \#{args.join(", ")}" + end + end + end + RUBY + end + end + + bundle "plugin install the-echoer --source file://#{gem_repo2}" + expect(out).to include("Installed plugin the-echoer") + + bundle "echo tacos tofu lasange" + expect(out).to eq("You gave me tacos, tofu, lasange") + end + + it "raises error on redeclaration of command" do + build_repo2 do + build_plugin "copycat" do |s| + s.write "plugins.rb", <<-RUBY + module CopyCat + class Cheater < Bundler::Plugin::API + command "mahcommand", self + + def exec(command, args) + end + end + end + RUBY + end + end + + bundle "plugin install copycat --source file://#{gem_repo2}" + + expect(out).not_to include("Installed plugin copycat") + + expect(out).to include("Failed to install plugin") + + expect(out).to include("Command(s) `mahcommand` declared by copycat are already registered.") + end +end diff --git a/spec/bundler/plugins/hook_spec.rb b/spec/bundler/plugins/hook_spec.rb new file mode 100644 index 00000000000000..53062095e25218 --- /dev/null +++ b/spec/bundler/plugins/hook_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +RSpec.describe "hook plugins" do + context "before-install-all hook" do + before do + build_repo2 do + build_plugin "before-install-all-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_INSTALL_ALL do |deps| + puts "gems to be installed \#{deps.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install before-install-all-plugin --source file://#{gem_repo2}" + end + + it "runs before all rubygems are installed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "rack" + G + + expect(out).to include "gems to be installed rake, rack" + end + end + + context "before-install hook" do + before do + build_repo2 do + build_plugin "before-install-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_INSTALL do |spec_install| + puts "installing gem \#{spec_install.name}" + end + RUBY + end + end + + bundle "plugin install before-install-plugin --source file://#{gem_repo2}" + end + + it "runs before each rubygem is installed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "rack" + G + + expect(out).to include "installing gem rake" + expect(out).to include "installing gem rack" + end + end + + context "after-install-all hook" do + before do + build_repo2 do + build_plugin "after-install-all-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_INSTALL_ALL do |deps| + puts "installed gems \#{deps.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install after-install-all-plugin --source file://#{gem_repo2}" + end + + it "runs after each rubygem is installed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "rack" + G + + expect(out).to include "installed gems rake, rack" + end + end + + context "after-install hook" do + before do + build_repo2 do + build_plugin "after-install-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_INSTALL do |spec_install| + puts "installed gem \#{spec_install.name} : \#{spec_install.state}" + end + RUBY + end + end + + bundle "plugin install after-install-plugin --source file://#{gem_repo2}" + end + + it "runs after each rubygem is installed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "rack" + G + + expect(out).to include "installed gem rake : installed" + expect(out).to include "installed gem rack : installed" + end + end +end diff --git a/spec/bundler/plugins/install_spec.rb b/spec/bundler/plugins/install_spec.rb new file mode 100644 index 00000000000000..9304d78062a8e8 --- /dev/null +++ b/spec/bundler/plugins/install_spec.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true + +RSpec.describe "bundler plugin install" do + before do + build_repo2 do + build_plugin "foo" + build_plugin "kung-foo" + end + end + + it "shows proper message when gem in not found in the source" do + bundle "plugin install no-foo --source file://#{gem_repo1}" + + expect(out).to include("Could not find") + plugin_should_not_be_installed("no-foo") + end + + it "installs from rubygems source" do + bundle "plugin install foo --source file://#{gem_repo2}" + + expect(out).to include("Installed plugin foo") + plugin_should_be_installed("foo") + end + + it "installs multiple plugins" do + bundle "plugin install foo kung-foo --source file://#{gem_repo2}" + + expect(out).to include("Installed plugin foo") + expect(out).to include("Installed plugin kung-foo") + + plugin_should_be_installed("foo", "kung-foo") + end + + it "uses the same version for multiple plugins" do + update_repo2 do + build_plugin "foo", "1.1" + build_plugin "kung-foo", "1.1" + end + + bundle "plugin install foo kung-foo --version '1.0' --source file://#{gem_repo2}" + + expect(out).to include("Installing foo 1.0") + expect(out).to include("Installing kung-foo 1.0") + plugin_should_be_installed("foo", "kung-foo") + end + + it "works with different load paths" do + build_repo2 do + build_plugin "testing" do |s| + s.write "plugins.rb", <<-RUBY + require "fubar" + class Test < Bundler::Plugin::API + command "check2" + + def exec(command, args) + puts "mate" + end + end + RUBY + s.require_paths = %w[lib src] + s.write("src/fubar.rb") + end + end + bundle "plugin install testing --source file://#{gem_repo2}" + + bundle "check2", "no-color" => false + expect(out).to eq("mate") + end + + context "malformatted plugin" do + it "fails when plugins.rb is missing" do + update_repo2 do + build_plugin "foo", "1.1" + build_plugin "kung-foo", "1.1" + end + + bundle "plugin install foo kung-foo --version '1.0' --source file://#{gem_repo2}" + + expect(out).to include("Installing foo 1.0") + expect(out).to include("Installing kung-foo 1.0") + plugin_should_be_installed("foo", "kung-foo") + + build_repo2 do + build_gem "charlie" + end + + bundle "plugin install charlie --source file://#{gem_repo2}" + + expect(out).to include("plugins.rb was not found") + + expect(global_plugin_gem("charlie-1.0")).not_to be_directory + + plugin_should_be_installed("foo", "kung-foo") + plugin_should_not_be_installed("charlie") + end + + it "fails when plugins.rb throws exception on load" do + build_repo2 do + build_plugin "chaplin" do |s| + s.write "plugins.rb", <<-RUBY + raise "I got you man" + RUBY + end + end + + bundle "plugin install chaplin --source file://#{gem_repo2}" + + expect(global_plugin_gem("chaplin-1.0")).not_to be_directory + + plugin_should_not_be_installed("chaplin") + end + end + + context "git plugins" do + it "installs form a git source" do + build_git "foo" do |s| + s.write "plugins.rb" + end + + bundle "plugin install foo --git file://#{lib_path("foo-1.0")}" + + expect(out).to include("Installed plugin foo") + plugin_should_be_installed("foo") + end + end + + context "Gemfile eval" do + it "installs plugins listed in gemfile" do + gemfile <<-G + source 'file://#{gem_repo2}' + plugin 'foo' + gem 'rack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installed plugin foo") + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("rack 1.0.0") + plugin_should_be_installed("foo") + end + + it "accepts plugin version" do + update_repo2 do + build_plugin "foo", "1.1.0" + end + + install_gemfile <<-G + source 'file://#{gem_repo2}' + plugin 'foo', "1.0" + G + + bundle "install" + + expect(out).to include("Installing foo 1.0") + + plugin_should_be_installed("foo") + + expect(out).to include("Bundle complete!") + end + + it "accepts git sources" do + build_git "ga-plugin" do |s| + s.write "plugins.rb" + end + + install_gemfile <<-G + plugin 'ga-plugin', :git => "#{lib_path("ga-plugin-1.0")}" + G + + expect(out).to include("Installed plugin ga-plugin") + plugin_should_be_installed("ga-plugin") + end + end + + context "inline gemfiles" do + it "installs the listed plugins" do + code = <<-RUBY + require "bundler/inline" + + gemfile do + source 'file://#{gem_repo2}' + plugin 'foo' + end + RUBY + + ruby code + expect(local_plugin_gem("foo-1.0", "plugins.rb")).to exist + end + end + + describe "local plugin" do + it "is installed when inside an app" do + gemfile "" + bundle "plugin install foo --source file://#{gem_repo2}" + + plugin_should_be_installed("foo") + expect(local_plugin_gem("foo-1.0")).to be_directory + end + + context "conflict with global plugin" do + before do + update_repo2 do + build_plugin "fubar" do |s| + s.write "plugins.rb", <<-RUBY + class Fubar < Bundler::Plugin::API + command "shout" + + def exec(command, args) + puts "local_one" + end + end + RUBY + end + end + + # inside the app + gemfile "source 'file://#{gem_repo2}'\nplugin 'fubar'" + bundle "install" + + update_repo2 do + build_plugin "fubar", "1.1" do |s| + s.write "plugins.rb", <<-RUBY + class Fubar < Bundler::Plugin::API + command "shout" + + def exec(command, args) + puts "global_one" + end + end + RUBY + end + end + + # outside the app + Dir.chdir tmp + bundle "plugin install fubar --source file://#{gem_repo2}" + end + + it "inside the app takes precedence over global plugin" do + Dir.chdir bundled_app + + bundle "shout" + expect(out).to eq("local_one") + end + + it "outside the app global plugin is used" do + Dir.chdir tmp + + bundle "shout" + expect(out).to eq("global_one") + end + end + end +end diff --git a/spec/bundler/plugins/source/example_spec.rb b/spec/bundler/plugins/source/example_spec.rb new file mode 100644 index 00000000000000..d9a3cd2b92feb3 --- /dev/null +++ b/spec/bundler/plugins/source/example_spec.rb @@ -0,0 +1,505 @@ +# frozen_string_literal: true + +RSpec.describe "real source plugins" do + context "with a minimal source plugin" do + before do + build_repo2 do + build_plugin "bundler-source-mpath" do |s| + s.write "plugins.rb", <<-RUBY + require "bundler/vendored_fileutils" + require "bundler-source-mpath" + + class MPath < Bundler::Plugin::API + source "mpath" + + attr_reader :path + + def initialize(opts) + super + + @path = Pathname.new options["uri"] + end + + def fetch_gemspec_files + @spec_files ||= begin + glob = "{,*,*/*}.gemspec" + if installed? + search_path = install_path + else + search_path = path + end + Dir["\#{search_path.to_s}/\#{glob}"] + end + end + + def install(spec, opts) + mkdir_p(install_path.parent) + FileUtils.cp_r(path, install_path) + + spec_path = install_path.join("\#{spec.full_name}.gemspec") + spec_path.open("wb") {|f| f.write spec.to_ruby } + spec.loaded_from = spec_path.to_s + + post_install(spec) + + nil + end + end + RUBY + end # build_plugin + end + + build_lib "a-path-gem" + + gemfile <<-G + source "file://localhost#{gem_repo2}" # plugin source + source "#{lib_path("a-path-gem-1.0")}", :type => :mpath do + gem "a-path-gem" + end + G + end + + it "installs" do + bundle "install" + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + + it "writes to lock file", :bundler => "< 3" do + bundle "install" + + lockfile_should_be <<-G + PLUGIN SOURCE + remote: #{lib_path("a-path-gem-1.0")} + type: mpath + specs: + a-path-gem (1.0) + + GEM + remote: file://localhost#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + a-path-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "writes to lock file", :bundler => "3" do + bundle "install" + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo2}/ + specs: + + PLUGIN SOURCE + remote: #{lib_path("a-path-gem-1.0")} + type: mpath + specs: + a-path-gem (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + a-path-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "provides correct #full_gem_path" do + bundle "install" + run <<-RUBY + puts Bundler.rubygems.find_name('a-path-gem').first.full_gem_path + RUBY + expect(out).to eq(bundle("info a-path-gem --path")) + end + + it "installs the gem executables" do + build_lib "gem-with-bin" do |s| + s.executables = ["foo"] + end + + install_gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source "#{lib_path("gem-with-bin-1.0")}", :type => :mpath do + gem "gem-with-bin" + end + G + + bundle "exec foo" + expect(out).to eq("1.0") + end + + describe "bundle cache/package" do + let(:uri_hash) { Digest(:SHA1).hexdigest(lib_path("a-path-gem-1.0").to_s) } + it "copies repository to vendor cache and uses it" do + bundle "install" + bundle :cache, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.git")).not_to exist + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("a-path-gem-1.0") + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + + it "copies repository to vendor cache and uses it even when installed with bundle --path" do + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + bundle! :cache, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist + + FileUtils.rm_rf lib_path("a-path-gem-1.0") + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + + it "bundler package copies repository to vendor cache" do + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + bundle! :package, forgotten_command_line_options([:all, :cache_all] => true) + + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist + + FileUtils.rm_rf lib_path("a-path-gem-1.0") + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + end + + context "with lockfile" do + before do + lockfile <<-G + PLUGIN SOURCE + remote: #{lib_path("a-path-gem-1.0")} + type: mpath + specs: + a-path-gem (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + a-path-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "installs" do + bundle! "install" + + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + end + end + + context "with a more elaborate source plugin" do + before do + build_repo2 do + build_plugin "bundler-source-gitp" do |s| + s.write "plugins.rb", <<-RUBY + class SPlugin < Bundler::Plugin::API + source "gitp" + + attr_reader :ref + + def initialize(opts) + super + + @ref = options["ref"] || options["branch"] || options["tag"] || "master" + @unlocked = false + end + + def eql?(other) + other.is_a?(self.class) && uri == other.uri && ref == other.ref + end + + alias_method :==, :eql? + + def fetch_gemspec_files + @spec_files ||= begin + glob = "{,*,*/*}.gemspec" + if !cached? + cache_repo + end + + if installed? && !@unlocked + path = install_path + else + path = cache_path + end + + Dir["\#{path}/\#{glob}"] + end + end + + def install(spec, opts) + mkdir_p(install_path.dirname) + rm_rf(install_path) + `git clone --no-checkout --quiet "\#{cache_path}" "\#{install_path}"` + Dir.chdir install_path do + `git reset --hard \#{revision}` + end + + spec_path = install_path.join("\#{spec.full_name}.gemspec") + spec_path.open("wb") {|f| f.write spec.to_ruby } + spec.loaded_from = spec_path.to_s + + post_install(spec) + + nil + end + + def options_to_lock + opts = {"revision" => revision} + opts["ref"] = ref if ref != "master" + opts + end + + def unlock! + @unlocked = true + @revision = latest_revision + end + + def app_cache_dirname + "\#{base_name}-\#{shortref_for_path(revision)}" + end + + private + + def cache_path + @cache_path ||= cache_dir.join("gitp", base_name) + end + + def cache_repo + `git clone --quiet \#{@options["uri"]} \#{cache_path}` + end + + def cached? + File.directory?(cache_path) + end + + def locked_revision + options["revision"] + end + + def revision + @revision ||= locked_revision || latest_revision + end + + def latest_revision + if !cached? || @unlocked + rm_rf(cache_path) + cache_repo + end + + Dir.chdir cache_path do + `git rev-parse --verify \#{@ref}`.strip + end + end + + def base_name + File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git") + end + + def shortref_for_path(ref) + ref[0..11] + end + + def install_path + @install_path ||= begin + git_scope = "\#{base_name}-\#{shortref_for_path(revision)}" + + path = gem_install_dir.join(git_scope) + + if !path.exist? && requires_sudo? + user_bundle_path.join(ruby_scope).join(git_scope) + else + path + end + end + end + + def installed? + File.directory?(install_path) + end + end + RUBY + end + end + + build_git "ma-gitp-gem" + + gemfile <<-G + source "file://localhost#{gem_repo2}" # plugin source + source "file://#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do + gem "ma-gitp-gem" + end + G + end + + it "handles the source option" do + bundle "install" + expect(out).to include("Bundle complete!") + expect(the_bundle).to include_gems("ma-gitp-gem 1.0") + end + + it "writes to lock file", :bundler => "< 3" do + revision = revision_for(lib_path("ma-gitp-gem-1.0")) + bundle "install" + + lockfile_should_be <<-G + PLUGIN SOURCE + remote: file://#{lib_path("ma-gitp-gem-1.0")} + type: gitp + revision: #{revision} + specs: + ma-gitp-gem (1.0) + + GEM + remote: file://localhost#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + ma-gitp-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "writes to lock file", :bundler => "3" do + revision = revision_for(lib_path("ma-gitp-gem-1.0")) + bundle "install" + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo2}/ + specs: + + PLUGIN SOURCE + remote: file://#{lib_path("ma-gitp-gem-1.0")} + type: gitp + revision: #{revision} + specs: + ma-gitp-gem (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ma-gitp-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + context "with lockfile" do + before do + revision = revision_for(lib_path("ma-gitp-gem-1.0")) + lockfile <<-G + PLUGIN SOURCE + remote: file://#{lib_path("ma-gitp-gem-1.0")} + type: gitp + revision: #{revision} + specs: + ma-gitp-gem (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + ma-gitp-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "installs" do + bundle "install" + expect(the_bundle).to include_gems("ma-gitp-gem 1.0") + end + + it "uses the locked ref" do + update_git "ma-gitp-gem" + bundle "install" + + run <<-RUBY + require 'ma-gitp-gem' + puts "WIN" unless defined?(MAGITPGEM_PREV_REF) + RUBY + expect(out).to eq("WIN") + end + + it "updates the deps on bundler update" do + update_git "ma-gitp-gem" + bundle "update ma-gitp-gem" + + run <<-RUBY + require 'ma-gitp-gem' + puts "WIN" if defined?(MAGITPGEM_PREV_REF) + RUBY + expect(out).to eq("WIN") + end + + it "updates the deps on change in gemfile" do + update_git "ma-gitp-gem", "1.1", :path => lib_path("ma-gitp-gem-1.0"), :gemspec => true + gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source "file://#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do + gem "ma-gitp-gem", "1.1" + end + G + bundle "install" + + expect(the_bundle).to include_gems("ma-gitp-gem 1.1") + end + end + + describe "bundle cache with gitp" do + it "copies repository to vendor cache and uses it" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source '#{lib_path("foo-1.0")}', :type => :gitp do + gem "foo" + end + G + + bundle :cache, forgotten_command_line_options([:all, :cache_all] => true) + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + end + end +end diff --git a/spec/bundler/plugins/source_spec.rb b/spec/bundler/plugins/source_spec.rb new file mode 100644 index 00000000000000..543e90eb600612 --- /dev/null +++ b/spec/bundler/plugins/source_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +RSpec.describe "bundler source plugin" do + describe "plugins dsl eval for #source with :type option" do + before do + update_repo2 do + build_plugin "bundler-source-psource" do |s| + s.write "plugins.rb", <<-RUBY + class OPSource < Bundler::Plugin::API + source "psource" + end + RUBY + end + end + end + + it "installs bundler-source-* gem when no handler for source is present" do + install_gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + + plugin_should_be_installed("bundler-source-psource") + end + + it "enables the plugin to require a lib path" do + update_repo2 do + build_plugin "bundler-source-psource" do |s| + s.write "plugins.rb", <<-RUBY + require "bundler-source-psource" + class PSource < Bundler::Plugin::API + source "psource" + end + RUBY + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + + expect(out).to include("Bundle complete!") + end + + context "with an explicit handler" do + before do + update_repo2 do + build_plugin "another-psource" do |s| + s.write "plugins.rb", <<-RUBY + class Cheater < Bundler::Plugin::API + source "psource" + end + RUBY + end + end + end + + context "explicit presence in gemfile" do + before do + install_gemfile <<-G + source "file://#{gem_repo2}" + + plugin "another-psource" + + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + end + + it "completes successfully" do + expect(out).to include("Bundle complete!") + end + + it "installs the explicit one" do + plugin_should_be_installed("another-psource") + end + + it "doesn't install the default one" do + plugin_should_not_be_installed("bundler-source-psource") + end + end + + context "explicit default source" do + before do + install_gemfile <<-G + source "file://#{gem_repo2}" + + plugin "bundler-source-psource" + + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + end + + it "completes successfully" do + expect(out).to include("Bundle complete!") + end + + it "installs the default one" do + plugin_should_be_installed("bundler-source-psource") + end + end + end + end +end diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb new file mode 100644 index 00000000000000..812acc344ff181 --- /dev/null +++ b/spec/bundler/quality_spec.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: true + +if defined?(Encoding) && Encoding.default_external.name != "UTF-8" + # Poor man's ruby -E UTF-8, since it works on 1.8.7 + Encoding.default_external = Encoding.find("UTF-8") +end + +RSpec.describe "The library itself" do + def check_for_debugging_mechanisms(filename) + debugging_mechanisms_regex = / + (binding\.pry)| + (debugger)| + (sleep\s*\(?\d+)| + (fit\s*\(?("|\w)) + /x + + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + if line =~ debugging_mechanisms_regex && !line.end_with?("# ignore quality_spec\n") + failing_lines << number + 1 + end + end + + return if failing_lines.empty? + "#{filename} has debugging mechanisms (like binding.pry, sleep, debugger, rspec focusing, etc.) on lines #{failing_lines.join(", ")}" + end + + def check_for_git_merge_conflicts(filename) + merge_conflicts_regex = / + <<<<<<<| + =======| + >>>>>>> + /x + + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + failing_lines << number + 1 if line =~ merge_conflicts_regex + end + + return if failing_lines.empty? + "#{filename} has unresolved git merge conflicts on lines #{failing_lines.join(", ")}" + end + + def check_for_tab_characters(filename) + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + failing_lines << number + 1 if line =~ /\t/ + end + + return if failing_lines.empty? + "#{filename} has tab characters on lines #{failing_lines.join(", ")}" + end + + def check_for_extra_spaces(filename) + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + next if line =~ /^\s+#.*\s+\n$/ + next if %w[LICENCE.md].include?(line) + failing_lines << number + 1 if line =~ /\s+\n$/ + end + + return if failing_lines.empty? + "#{filename} has spaces on the EOL on lines #{failing_lines.join(", ")}" + end + + def check_for_expendable_words(filename) + failing_line_message = [] + useless_words = %w[ + actually + basically + clearly + just + obviously + really + simply + ] + pattern = /\b#{Regexp.union(useless_words)}\b/i + + File.readlines(filename).each_with_index do |line, number| + next unless word_found = pattern.match(line) + failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Avoid using these kinds of weak modifiers." + end + + failing_line_message unless failing_line_message.empty? + end + + def check_for_specific_pronouns(filename) + failing_line_message = [] + specific_pronouns = /\b(he|she|his|hers|him|her|himself|herself)\b/i + + File.readlines(filename).each_with_index do |line, number| + next unless word_found = specific_pronouns.match(line) + failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Use more generic pronouns in documentation." + end + + failing_line_message unless failing_line_message.empty? + end + + RSpec::Matchers.define :be_well_formed do + match(&:empty?) + + failure_message do |actual| + actual.join("\n") + end + end + + it "has no malformed whitespace" do + exempt = /\.gitmodules|\.marshal|fixtures|vendor|ssl_certs|LICENSE|vcr_cassettes/ + error_messages = [] + Dir.chdir(root) do + lib_files = ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb spec/bundler` : `git ls-files -z -- lib` + lib_files.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_tab_characters(filename) + error_messages << check_for_extra_spaces(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "does not include any leftover debugging or development mechanisms" do + exempt = %r{quality_spec.rb|support/helpers|vcr_cassettes|\.md|\.ronn} + error_messages = [] + Dir.chdir(root) do + lib_files = ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb spec/bundler` : `git ls-files -z -- lib` + lib_files.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_debugging_mechanisms(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "does not include any unresolved merge conflicts" do + error_messages = [] + exempt = %r{lock/lockfile_(bundler_1_)?spec|quality_spec|vcr_cassettes|\.ronn|lockfile_parser\.rb} + Dir.chdir(root) do + lib_files = ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb spec/bundler` : `git ls-files -z -- lib` + lib_files.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_git_merge_conflicts(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "maintains language quality of the documentation" do + included = /ronn/ + error_messages = [] + Dir.chdir(root) do + `git ls-files -z -- man`.split("\x0").each do |filename| + next unless filename =~ included + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "maintains language quality of sentences used in source code" do + error_messages = [] + exempt = /vendor/ + Dir.chdir(root) do + lib_files = ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb` : `git ls-files -z -- lib` + lib_files.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "documents all used settings" do + exemptions = %w[ + auto_config_jobs + cache_command_is_package + console_command + deployment_means_frozen + forget_cli_options + gem.coc + gem.mit + inline + lockfile_upgrade_warning + lockfile_uses_separate_rubygems_sources + use_gem_version_promoter_for_major_updates + viz_command + ] + + all_settings = Hash.new {|h, k| h[k] = [] } + documented_settings = [] + + Bundler::Settings::BOOL_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::BOOL_KEYS" } + Bundler::Settings::NUMBER_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::NUMBER_KEYS" } + Bundler::Settings::ARRAY_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::ARRAY_KEYS" } + + Dir.chdir(root) do + key_pattern = /([a-z\._-]+)/i + lib_files = ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb` : `git ls-files -z -- lib` + lib_files.split("\x0").each do |filename| + File.readlines(filename).each_with_index do |line, number| + line.scan(/Bundler\.settings\[:#{key_pattern}\]/).flatten.each {|s| all_settings[s] << "referenced at `#{filename}:#{number.succ}`" } + end + end + documented_settings = File.read("man/bundle-config.ronn")[/LIST OF AVAILABLE KEYS.*/m].scan(/^\* `#{key_pattern}`/).flatten + end + + documented_settings.each do |s| + all_settings.delete(s) + expect(exemptions.delete(s)).to be_nil, "setting #{s} was exempted but was actually documented" + end + + exemptions.each do |s| + expect(all_settings.delete(s)).to be_truthy, "setting #{s} was exempted but unused" + end + error_messages = all_settings.map do |setting, refs| + "The `#{setting}` setting is undocumented\n\t- #{refs.join("\n\t- ")}\n" + end + + expect(error_messages.sort).to be_well_formed + + expect(documented_settings).to be_sorted + end + + it "can still be built" do + Dir.chdir(root) do + begin + gem_command! :build, gemspec + if Bundler.rubygems.provides?(">= 2.4") + # there's no way aroudn this warning + last_command.stderr.sub!(/^YAML safe loading.*/, "") + + # older rubygems have weird warnings, and we won't actually be using them + # to build the gem for releases anyways + expect(last_command.stderr).to be_empty, "bundler should build as a gem without warnings, but\n#{err}" + end + ensure + # clean up the .gem generated + path_prefix = ruby_core? ? "lib/" : "./" + FileUtils.rm("#{path_prefix}bundler-#{Bundler::VERSION}.gem") + end + end + end + + it "does not contain any warnings" do + Dir.chdir(root) do + exclusions = %w[ + lib/bundler/capistrano.rb + lib/bundler/deployment.rb + lib/bundler/gem_tasks.rb + lib/bundler/vlad.rb + lib/bundler/templates/gems.rb + ] + lib_files = ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb` : `git ls-files -z -- lib` + lib_files = lib_files.split("\x0").grep(/\.rb$/) - exclusions + lib_files.reject! {|f| f.start_with?("lib/bundler/vendor") } + lib_files.map! {|f| f.chomp(".rb") } + sys_exec!("ruby -w -Ilib") do |input, _, _| + lib_files.each do |f| + input.puts "require '#{f.sub(%r{\Alib/}, "")}'" + end + end + + expect(last_command.stdboth.split("\n")).to be_well_formed + end + end +end diff --git a/spec/bundler/realworld/dependency_api_spec.rb b/spec/bundler/realworld/dependency_api_spec.rb new file mode 100644 index 00000000000000..13527ce5d1edb5 --- /dev/null +++ b/spec/bundler/realworld/dependency_api_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.describe "gemcutter's dependency API", :realworld => true do + context "when Gemcutter API takes too long to respond" do + before do + require_rack + + port = find_unused_port + @server_uri = "http://127.0.0.1:#{port}" + + require File.expand_path("../../support/artifice/endpoint_timeout", __FILE__) + require "thread" + @t = Thread.new do + server = Rack::Server.start(:app => EndpointTimeout, + :Host => "0.0.0.0", + :Port => port, + :server => "webrick", + :AccessLog => [], + :Logger => Spec::SilentLogger.new) + server.start + end + @t.run + + wait_for_server("127.0.0.1", port) + bundle! "config timeout 1" + end + + after do + Artifice.deactivate + @t.kill + @t.join + end + + it "times out and falls back on the modern index" do + install_gemfile! <<-G, :artifice => nil + source "#{@server_uri}" + gem "rack" + G + + expect(out).to include("Fetching source index from #{@server_uri}/") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end +end diff --git a/spec/bundler/realworld/double_check_spec.rb b/spec/bundler/realworld/double_check_spec.rb new file mode 100644 index 00000000000000..94ab49ba2a672f --- /dev/null +++ b/spec/bundler/realworld/double_check_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.describe "double checking sources", :realworld => true do + it "finds already-installed gems", :ruby => ">= 2.2" do + create_file("rails.gemspec", <<-RUBY) + Gem::Specification.new do |s| + s.name = "rails" + s.version = "5.1.4" + s.summary = "" + s.description = "" + s.author = "" + s.add_dependency "actionpack", "5.1.4" + end + RUBY + + create_file("actionpack.gemspec", <<-RUBY) + Gem::Specification.new do |s| + s.name = "actionpack" + s.version = "5.1.4" + s.summary = "" + s.description = "" + s.author = "" + s.add_dependency "rack", "~> 2.0.0" + end + RUBY + + cmd = <<-RUBY + require "bundler" + require #{File.expand_path("../../support/artifice/vcr.rb", __FILE__).dump} + require "bundler/inline" + gemfile(true) do + source "https://rubygems.org" + gem "rails", path: "." + end + RUBY + + ruby! cmd + ruby! cmd + end +end diff --git a/spec/bundler/realworld/edgecases_spec.rb b/spec/bundler/realworld/edgecases_spec.rb new file mode 100644 index 00000000000000..bbfd0f68fd4636 --- /dev/null +++ b/spec/bundler/realworld/edgecases_spec.rb @@ -0,0 +1,382 @@ +# frozen_string_literal: true + +RSpec.describe "real world edgecases", :realworld => true, :sometimes => true do + def rubygems_version(name, requirement) + ruby! <<-RUBY + require #{File.expand_path("../../support/artifice/vcr.rb", __FILE__).dump} + require "bundler" + require "bundler/source/rubygems/remote" + require "bundler/fetcher" + source = Bundler::Source::Rubygems::Remote.new(URI("https://rubygems.org")) + fetcher = Bundler::Fetcher.new(source) + index = fetcher.specs([#{name.dump}], nil) + rubygem = index.search(Gem::Dependency.new(#{name.dump}, #{requirement.dump})).last + if rubygem.nil? + raise "Could not find #{name} (#{requirement}) on rubygems.org!\n" \ + "Found specs:\n\#{index.send(:specs).inspect}" + end + "#{name} (\#{rubygem.version})" + RUBY + end + + # there is no rbx-relative-require gem that will install on 1.9 + it "ignores extra gems with bad platforms", :ruby => "~> 1.8.7" do + gemfile <<-G + source "https://rubygems.org" + gem "linecache", "0.46" + G + bundle :lock + expect(err).to lack_errors + expect(exitstatus).to eq(0) if exitstatus + end + + # https://github.com/bundler/bundler/issues/1202 + it "bundle cache works with rubygems 1.3.7 and pre gems", + :ruby => "~> 1.8.7", :rubygems => "~> 1.3.7" do + install_gemfile <<-G + source "https://rubygems.org" + gem "rack", "1.3.0.beta2" + gem "will_paginate", "3.0.pre2" + G + bundle :cache + expect(out).not_to include("Removing outdated .gem files from vendor/cache") + end + + # https://github.com/bundler/bundler/issues/1486 + # this is a hash collision that only manifests on 1.8.7 + it "finds the correct child versions", :ruby => "~> 1.8.7" do + gemfile <<-G + source "https://rubygems.org" + + gem 'i18n', '~> 0.6.0' + gem 'activesupport', '~> 3.0.5' + gem 'activerecord', '~> 3.0.5' + gem 'builder', '~> 2.1.2' + G + bundle :lock + expect(lockfile).to include("activemodel (3.0.5)") + end + + it "resolves dependencies correctly", :ruby => "<= 1.9.3" do + gemfile <<-G + source "https://rubygems.org" + + gem 'rails', '~> 3.0' + gem 'capybara', '~> 2.2.0' + gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9 + G + bundle! :lock + expect(lockfile).to include(rubygems_version("rails", "~> 3.0")) + expect(lockfile).to include("capybara (2.2.1)") + end + + it "installs the latest version of gxapi_rails", :ruby => "<= 1.9.3" do + gemfile <<-G + source "https://rubygems.org" + + gem "sass-rails" + gem "rails", "~> 3" + gem "gxapi_rails", "< 0.1.0" # 0.1.0 was released way after the test was written + gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9 + G + bundle! :lock + expect(lockfile).to include("gxapi_rails (0.0.6)") + end + + it "installs the latest version of i18n" do + gemfile <<-G + source "https://rubygems.org" + + gem "i18n", "~> 0.6.0" + gem "activesupport", "~> 3.0" + gem "activerecord", "~> 3.0" + gem "builder", "~> 2.1.2" + G + bundle! :lock + expect(lockfile).to include(rubygems_version("i18n", "~> 0.6.0")) + expect(lockfile).to include(rubygems_version("activesupport", "~> 3.0")) + end + + it "is able to update a top-level dependency when there is a conflict on a shared transitive child", :ruby => "2.1" do + # from https://github.com/bundler/bundler/issues/5031 + + gemfile <<-G + source "https://rubygems.org" + gem 'rails', '~> 4.2.7.1' + gem 'paperclip', '~> 5.1.0' + G + + lockfile <<-L + GEM + remote: https://rubygems.org/ + specs: + actionmailer (4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.7.1) + actionview (= 4.2.7.1) + activesupport (= 4.2.7.1) + rack (~> 1.6) + rack-test (~> 0.6.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.7.1) + activesupport (= 4.2.7.1) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (4.2.7.1) + activesupport (= 4.2.7.1) + globalid (>= 0.3.0) + activemodel (4.2.7.1) + activesupport (= 4.2.7.1) + builder (~> 3.1) + activerecord (4.2.7.1) + activemodel (= 4.2.7.1) + activesupport (= 4.2.7.1) + arel (~> 6.0) + activesupport (4.2.7.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + arel (6.0.3) + builder (3.2.2) + climate_control (0.0.3) + activesupport (>= 3.0) + cocaine (0.5.8) + climate_control (>= 0.0.3, < 1.0) + concurrent-ruby (1.0.2) + erubis (2.7.0) + globalid (0.3.7) + activesupport (>= 4.1.0) + i18n (0.7.0) + json (1.8.3) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mimemagic (0.3.2) + mini_portile2 (2.1.0) + minitest (5.9.1) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + cocaine (~> 0.5.5) + mime-types + mimemagic (~> 0.3.0) + pkg-config (1.1.7) + rack (1.6.4) + rack-test (0.6.3) + rack (>= 1.0) + rails (4.2.7.1) + actionmailer (= 4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + activemodel (= 4.2.7.1) + activerecord (= 4.2.7.1) + activesupport (= 4.2.7.1) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.7.1) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (4.2.7.1) + actionpack (= 4.2.7.1) + activesupport (= 4.2.7.1) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (11.3.0) + sprockets (3.7.0) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (0.19.1) + thread_safe (0.3.5) + tzinfo (1.2.2) + thread_safe (~> 0.1) + + PLATFORMS + ruby + + DEPENDENCIES + paperclip (~> 5.1.0) + rails (~> 4.2.7.1) + L + + bundle! "lock --update paperclip" + + expect(lockfile).to include(rubygems_version("paperclip", "~> 5.1.0")) + end + + # https://github.com/bundler/bundler/issues/1500 + it "does not fail install because of gem plugins" do + realworld_system_gems("open_gem --version 1.4.2", "rake --version 0.9.2") + gemfile <<-G + source "https://rubygems.org" + + gem 'rack', '1.0.1' + G + + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + expect(err).not_to include("Could not find rake") + expect(err).to lack_errors + end + + it "checks out git repos when the lockfile is corrupted" do + gemfile <<-G + source "https://rubygems.org" + git_source(:github) {|repo| "https://github.com/\#{repo}.git" } + + gem 'activerecord', :github => 'carlhuda/rails-bundler-test', :branch => 'master' + gem 'activesupport', :github => 'carlhuda/rails-bundler-test', :branch => 'master' + gem 'actionpack', :github => 'carlhuda/rails-bundler-test', :branch => 'master' + G + + lockfile <<-L + GIT + remote: https://github.com/carlhuda/rails-bundler-test.git + revision: 369e28a87419565f1940815219ea9200474589d4 + branch: master + specs: + actionpack (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + activerecord (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activesupport (3.2.2) + i18n (~> 0.6) + multi_json (~> 1.0) + + GIT + remote: https://github.com/carlhuda/rails-bundler-test.git + revision: 369e28a87419565f1940815219ea9200474589d4 + branch: master + specs: + actionpack (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + activerecord (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activesupport (3.2.2) + i18n (~> 0.6) + multi_json (~> 1.0) + + GIT + remote: https://github.com/carlhuda/rails-bundler-test.git + revision: 369e28a87419565f1940815219ea9200474589d4 + branch: master + specs: + actionpack (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + activerecord (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activesupport (3.2.2) + i18n (~> 0.6) + multi_json (~> 1.0) + + GEM + remote: https://rubygems.org/ + specs: + arel (3.0.2) + builder (3.0.0) + erubis (2.7.0) + hike (1.2.1) + i18n (0.6.0) + journey (1.0.3) + multi_json (1.1.0) + rack (1.4.1) + rack-cache (1.2) + rack (>= 0.4) + rack-test (0.6.1) + rack (>= 1.0) + sprockets (2.1.2) + hike (~> 1.2) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + tilt (1.3.3) + tzinfo (0.3.32) + + PLATFORMS + ruby + + DEPENDENCIES + actionpack! + activerecord! + activesupport! + L + + bundle! :lock + expect(last_command.stderr).to lack_errors + end + + it "outputs a helpful error message when gems have invalid gemspecs" do + install_gemfile <<-G, :standalone => true + source 'https://rubygems.org' + gem "resque-scheduler", "2.2.0" + G + expect(out).to include("You have one or more invalid gemspecs that need to be fixed.") + expect(out).to include("resque-scheduler 2.2.0 has an invalid gemspec") + end +end diff --git a/spec/bundler/realworld/gemfile_source_header_spec.rb b/spec/bundler/realworld/gemfile_source_header_spec.rb new file mode 100644 index 00000000000000..59c1916874f024 --- /dev/null +++ b/spec/bundler/realworld/gemfile_source_header_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "thread" + +RSpec.describe "fetching dependencies with a mirrored source", :realworld => true, :rubygems => ">= 2.0" do + let(:mirror) { "https://server.example.org" } + let(:original) { "http://127.0.0.1:#{@port}" } + + before do + setup_server + bundle "config --local mirror.#{mirror} #{original}" + end + + after do + Artifice.deactivate + @t.kill + @t.join + end + + it "sets the 'X-Gemfile-Source' header and bundles successfully" do + gemfile <<-G + source "#{mirror}" + gem 'weakling' + G + + bundle :install, :artifice => nil + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + + private + + def setup_server + require_rack + @port = find_unused_port + @server_uri = "http://127.0.0.1:#{@port}" + + require File.expand_path("../../support/artifice/endpoint_mirror_source", __FILE__) + + @t = Thread.new do + Rack::Server.start(:app => EndpointMirrorSource, + :Host => "0.0.0.0", + :Port => @port, + :server => "webrick", + :AccessLog => [], + :Logger => Spec::SilentLogger.new) + end.run + + wait_for_server("127.0.0.1", @port) + end +end diff --git a/spec/bundler/realworld/mirror_probe_spec.rb b/spec/bundler/realworld/mirror_probe_spec.rb new file mode 100644 index 00000000000000..ab748863299989 --- /dev/null +++ b/spec/bundler/realworld/mirror_probe_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +require "thread" + +RSpec.describe "fetching dependencies with a not available mirror", :realworld => true do + let(:mirror) { @mirror_uri } + let(:original) { @server_uri } + let(:server_port) { @server_port } + let(:host) { "127.0.0.1" } + + before do + require_rack + setup_server + setup_mirror + end + + after do + Artifice.deactivate + @server_thread.kill + @server_thread.join + end + + context "with a specific fallback timeout" do + before do + global_config("BUNDLE_MIRROR__HTTP://127__0__0__1:#{server_port}/__FALLBACK_TIMEOUT/" => "true", + "BUNDLE_MIRROR__HTTP://127__0__0__1:#{server_port}/" => mirror) + end + + it "install a gem using the original uri when the mirror is not responding" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install, :artifice => nil + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + end + + context "with a global fallback timeout" do + before do + global_config("BUNDLE_MIRROR__ALL__FALLBACK_TIMEOUT/" => "1", + "BUNDLE_MIRROR__ALL" => mirror) + end + + it "install a gem using the original uri when the mirror is not responding" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install, :artifice => nil + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + end + + context "with a specific mirror without a fallback timeout" do + before do + global_config("BUNDLE_MIRROR__HTTP://127__0__0__1:#{server_port}/" => mirror) + end + + it "fails to install the gem with a timeout error" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install, :artifice => nil + + expect(out).to include("Fetching source index from #{mirror}") + expect(out).to include("Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Could not fetch specs from #{mirror}") + end + + it "prints each error and warning on a new line" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install, :artifice => nil + + expect(last_command.stdout).to include "Fetching source index from #{mirror}/" + expect(last_command.bundler_err).to include <<-EOS.strip +Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}/ +Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}/ +Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}/ +Could not fetch specs from #{mirror}/ + EOS + end + end + + context "with a global mirror without a fallback timeout" do + before do + global_config("BUNDLE_MIRROR__ALL" => mirror) + end + + it "fails to install the gem with a timeout error" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install, :artifice => nil + + expect(out).to include("Fetching source index from #{mirror}") + expect(out).to include("Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Could not fetch specs from #{mirror}") + end + end + + def setup_server + @server_port = find_unused_port + @server_uri = "http://#{host}:#{@server_port}" + + require File.expand_path("../../support/artifice/endpoint", __FILE__) + + @server_thread = Thread.new do + Rack::Server.start(:app => Endpoint, + :Host => host, + :Port => @server_port, + :server => "webrick", + :AccessLog => [], + :Logger => Spec::SilentLogger.new) + end.run + + wait_for_server(host, @server_port) + end + + def setup_mirror + mirror_port = find_unused_port + @mirror_uri = "http://#{host}:#{mirror_port}" + end +end diff --git a/spec/bundler/realworld/parallel_spec.rb b/spec/bundler/realworld/parallel_spec.rb new file mode 100644 index 00000000000000..ed4430c68b1908 --- /dev/null +++ b/spec/bundler/realworld/parallel_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +RSpec.describe "parallel", :realworld => true, :sometimes => true do + it "installs" do + gemfile <<-G + source "https://rubygems.org" + gem 'activesupport', '~> 3.2.13' + gem 'faker', '~> 1.1.2' + gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ + G + + bundle :install, :jobs => 4, :env => { "DEBUG" => "1" } + + if Bundler.rubygems.provides?(">= 2.1.0") + expect(out).to match(/[1-3]: /) + else + expect(out).to include("is not threadsafe") + end + + bundle "info activesupport --path" + expect(out).to match(/activesupport/) + + bundle "info faker --path" + expect(out).to match(/faker/) + end + + it "updates" do + install_gemfile <<-G + source "https://rubygems.org" + gem 'activesupport', '3.2.12' + gem 'faker', '~> 1.1.2' + G + + gemfile <<-G + source "https://rubygems.org" + gem 'activesupport', '~> 3.2.12' + gem 'faker', '~> 1.1.2' + gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ + G + + bundle :update, :jobs => 4, :env => { "DEBUG" => "1" }, :all => bundle_update_requires_all? + + if Bundler.rubygems.provides?(">= 2.1.0") + expect(out).to match(/[1-3]: /) + else + expect(out).to include("is not threadsafe") + end + + bundle "info activesupport --path" + expect(out).to match(/activesupport-3\.2\.\d+/) + + bundle "info faker --path" + expect(out).to match(/faker/) + end + + it "works with --standalone" do + gemfile <<-G, :standalone => true + source "https://rubygems.org" + gem "diff-lcs" + G + + bundle :install, :standalone => true, :jobs => 4 + + ruby <<-RUBY, :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "diff/lcs" + puts Diff::LCS + RUBY + + expect(out).to eq("Diff::LCS") + end +end diff --git a/spec/bundler/resolver/basic_spec.rb b/spec/bundler/resolver/basic_spec.rb new file mode 100644 index 00000000000000..c023f5d7aaf0e1 --- /dev/null +++ b/spec/bundler/resolver/basic_spec.rb @@ -0,0 +1,308 @@ +# frozen_string_literal: true + +RSpec.describe "Resolving" do + before :each do + @index = an_awesome_index + end + + it "resolves a single gem" do + dep "rack" + + should_resolve_as %w[rack-1.1] + end + + it "resolves a gem with dependencies" do + dep "actionpack" + + should_resolve_as %w[actionpack-2.3.5 activesupport-2.3.5 rack-1.0] + end + + it "resolves a conflicting index" do + @index = a_conflict_index + dep "my_app" + should_resolve_as %w[activemodel-3.2.11 builder-3.0.4 grape-0.2.6 my_app-1.0.0] + end + + it "resolves a complex conflicting index" do + @index = a_complex_conflict_index + dep "my_app" + should_resolve_as %w[a-1.4.0 b-0.3.5 c-3.2 d-0.9.8 my_app-1.1.0] + end + + it "resolves a index with conflict on child" do + @index = index_with_conflict_on_child + dep "chef_app" + should_resolve_as %w[berkshelf-2.0.7 chef-10.26 chef_app-1.0.0 json-1.7.7] + end + + it "prefers expicitly requested dependencies when resolving an index which would otherwise be ambiguous" do + @index = an_ambiguous_index + dep "a" + dep "b" + should_resolve_as %w[a-1.0.0 b-2.0.0 c-1.0.0 d-1.0.0] + end + + it "prefers non-prerelease resolutions in sort order" do + @index = optional_prereleases_index + dep "a" + dep "b" + should_resolve_as %w[a-1.0.0 b-1.5.0] + end + + it "resolves a index with root level conflict on child" do + @index = a_index_with_root_conflict_on_child + dep "i18n", "~> 0.4" + dep "activesupport", "~> 3.0" + dep "activerecord", "~> 3.0" + dep "builder", "~> 2.1.2" + should_resolve_as %w[activesupport-3.0.5 i18n-0.4.2 builder-2.1.2 activerecord-3.0.5 activemodel-3.0.5] + end + + it "resolves a gem specified with a pre-release version" do + dep "activesupport", "~> 3.0.0.beta" + dep "activemerchant" + should_resolve_as %w[activemerchant-2.3.5 activesupport-3.0.0.beta1] + end + + it "doesn't select a pre-release if not specified in the Gemfile" do + dep "activesupport" + dep "reform" + should_resolve_as %w[reform-1.0.0 activesupport-2.3.5] + end + + it "doesn't select a pre-release for sub-dependencies" do + dep "reform" + should_resolve_as %w[reform-1.0.0 activesupport-2.3.5] + end + + it "selects a pre-release for sub-dependencies if it's the only option" do + dep "need-pre" + should_resolve_as %w[need-pre-1.0.0 activesupport-3.0.0.beta1] + end + + it "selects a pre-release if it's specified in the Gemfile" do + dep "activesupport", "= 3.0.0.beta" + dep "actionpack" + + should_resolve_as %w[activesupport-3.0.0.beta actionpack-3.0.0.beta rack-1.1 rack-mount-0.6] + end + + it "prefers non-pre-releases when doing conservative updates" do + @index = build_index do + gem "mail", "2.7.0" + gem "mail", "2.7.1.rc1" + gem "RubyGems\0", Gem::VERSION + end + dep "mail" + @locked = locked ["mail", "2.7.0"] + @base = locked + should_conservative_resolve_and_include [:patch], [], ["mail-2.7.0"] + end + + it "raises an exception if a child dependency is not resolved" do + @index = a_unresovable_child_index + dep "chef_app_error" + expect do + resolve + end.to raise_error(Bundler::VersionConflict) + end + + it "raises an exception with the minimal set of conflicting dependencies" do + @index = build_index do + %w[0.9 1.0 2.0].each {|v| gem("a", v) } + gem("b", "1.0") { dep "a", ">= 2" } + gem("c", "1.0") { dep "a", "< 1" } + end + dep "a" + dep "b" + dep "c" + expect do + resolve + end.to raise_error(Bundler::VersionConflict, <<-E.strip) +Bundler could not find compatible versions for gem "a": + In Gemfile: + b was resolved to 1.0, which depends on + a (>= 2) + + c was resolved to 1.0, which depends on + a (< 1) + E + end + + it "should throw error in case of circular dependencies" do + @index = a_circular_index + dep "circular_app" + + expect do + resolve + end.to raise_error(Bundler::CyclicDependencyError, /please remove either gem 'bar' or gem 'foo'/i) + end + + # Issue #3459 + it "should install the latest possible version of a direct requirement with no constraints given" do + @index = a_complicated_index + dep "foo" + should_resolve_and_include %w[foo-3.0.5] + end + + # Issue #3459 + it "should install the latest possible version of a direct requirement with constraints given" do + @index = a_complicated_index + dep "foo", ">= 3.0.0" + should_resolve_and_include %w[foo-3.0.5] + end + + it "takes into account required_ruby_version" do + @index = build_index do + gem "foo", "1.0.0" do + dep "bar", ">= 0" + end + + gem "foo", "2.0.0" do |s| + dep "bar", ">= 0" + s.required_ruby_version = "~> 2.0.0" + end + + gem "bar", "1.0.0" + + gem "bar", "2.0.0" do |s| + s.required_ruby_version = "~> 2.0.0" + end + + gem "ruby\0", "1.8.7" + end + dep "foo" + dep "ruby\0", "1.8.7" + + deps = [] + @deps.each do |d| + deps << Bundler::DepProxy.new(d, "ruby") + end + + should_resolve_and_include %w[foo-1.0.0 bar-1.0.0], [[]] + end + + context "conservative" do + before :each do + @index = build_index do + gem("foo", "1.3.7") { dep "bar", "~> 2.0" } + gem("foo", "1.3.8") { dep "bar", "~> 2.0" } + gem("foo", "1.4.3") { dep "bar", "~> 2.0" } + gem("foo", "1.4.4") { dep "bar", "~> 2.0" } + gem("foo", "1.4.5") { dep "bar", "~> 2.1" } + gem("foo", "1.5.0") { dep "bar", "~> 2.1" } + gem("foo", "1.5.1") { dep "bar", "~> 3.0" } + gem("foo", "2.0.0") { dep "bar", "~> 3.0" } + gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] + end + dep "foo" + + # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile + @base = Bundler::SpecSet.new([]) + + # locked represents versions in lockfile + @locked = locked(%w[foo 1.4.3], %w[bar 2.0.3]) + end + + it "resolves all gems to latest patch" do + # strict is not set, so bar goes up a minor version due to dependency from foo 1.4.5 + should_conservative_resolve_and_include :patch, [], %w[foo-1.4.5 bar-2.1.1] + end + + it "resolves all gems to latest patch strict" do + # strict is set, so foo can only go up to 1.4.4 to avoid bar going up a minor version, and bar can go up to 2.0.5 + should_conservative_resolve_and_include [:patch, :strict], [], %w[foo-1.4.4 bar-2.0.5] + end + + it "resolves foo only to latest patch - same dependency case" do + @locked = locked(%w[foo 1.3.7], %w[bar 2.0.3]) + # bar is locked, and the lock holds here because the dependency on bar doesn't change on the matching foo version. + should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.3.8 bar-2.0.3] + end + + it "resolves foo only to latest patch - changing dependency not declared case" do + # foo is the only gem being requested for update, therefore bar is locked, but bar is NOT + # declared as a dependency in the Gemfile. In this case, locks don't apply to _changing_ + # dependencies and since the dependency of the selected foo gem changes, the latest matching + # dependency of "bar", "~> 2.1" -- bar-2.1.1 -- is selected. This is not a bug and follows + # the long-standing documented Conservative Updating behavior of bundle install. + # http://bundler.io/v1.12/man/bundle-install.1.html#CONSERVATIVE-UPDATING + should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.4.5 bar-2.1.1] + end + + it "resolves foo only to latest patch - changing dependency declared case" do + # bar is locked AND a declared dependency in the Gemfile, so it will not move, and therefore + # foo can only move up to 1.4.4. + @base << build_spec("bar", "2.0.3").first + should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.4.4 bar-2.0.3] + end + + it "resolves foo only to latest patch strict" do + # adding strict helps solve the possibly unexpected behavior of bar changing in the prior test case, + # because no versions will be returned for bar ~> 2.1, so the engine falls back to ~> 2.0 (turn on + # debugging to see this happen). + should_conservative_resolve_and_include [:patch, :strict], ["foo"], %w[foo-1.4.4 bar-2.0.3] + end + + it "resolves bar only to latest patch" do + # bar is locked, so foo can only go up to 1.4.4 + should_conservative_resolve_and_include :patch, ["bar"], %w[foo-1.4.3 bar-2.0.5] + end + + it "resolves all gems to latest minor" do + # strict is not set, so bar goes up a major version due to dependency from foo 1.4.5 + should_conservative_resolve_and_include :minor, [], %w[foo-1.5.1 bar-3.0.0] + end + + it "resolves all gems to latest minor strict" do + # strict is set, so foo can only go up to 1.5.0 to avoid bar going up a major version + should_conservative_resolve_and_include [:minor, :strict], [], %w[foo-1.5.0 bar-2.1.1] + end + + it "resolves all gems to latest major" do + should_conservative_resolve_and_include :major, [], %w[foo-2.0.0 bar-3.0.0] + end + + it "resolves all gems to latest major strict" do + should_conservative_resolve_and_include [:major, :strict], [], %w[foo-2.0.0 bar-3.0.0] + end + + # Why would this happen in real life? If bar 2.2 has a bug that the author of foo wants to bypass + # by reverting the dependency, the author of foo could release a new gem with an older requirement. + context "revert to previous" do + before :each do + @index = build_index do + gem("foo", "1.4.3") { dep "bar", "~> 2.2" } + gem("foo", "1.4.4") { dep "bar", "~> 2.1.0" } + gem("foo", "1.5.0") { dep "bar", "~> 2.0.0" } + gem "bar", %w[2.0.5 2.1.1 2.2.3] + end + dep "foo" + + # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile + @base = Bundler::SpecSet.new([]) + + # locked represents versions in lockfile + @locked = locked(%w[foo 1.4.3], %w[bar 2.2.3]) + end + + it "could revert to a previous version level patch" do + should_conservative_resolve_and_include :patch, [], %w[foo-1.4.4 bar-2.1.1] + end + + it "cannot revert to a previous version in strict mode level patch" do + # fall back to the locked resolution since strict means we can't regress either version + should_conservative_resolve_and_include [:patch, :strict], [], %w[foo-1.4.3 bar-2.2.3] + end + + it "could revert to a previous version level minor" do + should_conservative_resolve_and_include :minor, [], %w[foo-1.5.0 bar-2.0.5] + end + + it "cannot revert to a previous version in strict mode level minor" do + # fall back to the locked resolution since strict means we can't regress either version + should_conservative_resolve_and_include [:minor, :strict], [], %w[foo-1.4.3 bar-2.2.3] + end + end + end +end diff --git a/spec/bundler/resolver/platform_spec.rb b/spec/bundler/resolver/platform_spec.rb new file mode 100644 index 00000000000000..fee0cf1f1cb8a6 --- /dev/null +++ b/spec/bundler/resolver/platform_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +RSpec.describe "Resolving platform craziness" do + describe "with cross-platform gems" do + before :each do + @index = an_awesome_index + end + + it "resolves a simple multi platform gem" do + dep "nokogiri" + platforms "ruby", "java" + + should_resolve_as %w[nokogiri-1.4.2 nokogiri-1.4.2-java weakling-0.0.3] + end + + it "doesn't pull gems that don't exist for the current platform" do + dep "nokogiri" + platforms "ruby" + + should_resolve_as %w[nokogiri-1.4.2] + end + + it "doesn't pull gems when the version is available for all requested platforms" do + dep "nokogiri" + platforms "mswin32" + + should_resolve_as %w[nokogiri-1.4.2.1-x86-mswin32] + end + end + + describe "with mingw32" do + before :each do + @index = build_index do + platforms "mingw32 mswin32 x64-mingw32" do |platform| + gem "thin", "1.2.7", platform + end + gem "win32-api", "1.5.1", "universal-mingw32" + end + end + + it "finds mswin gems" do + # win32 is hardcoded to get CPU x86 in rubygems + platforms "mswin32" + dep "thin" + should_resolve_as %w[thin-1.2.7-x86-mswin32] + end + + it "finds mingw gems" do + # mingw is _not_ hardcoded to add CPU x86 in rubygems + platforms "x86-mingw32" + dep "thin" + should_resolve_as %w[thin-1.2.7-mingw32] + end + + it "finds x64-mingw gems" do + platforms "x64-mingw32" + dep "thin" + should_resolve_as %w[thin-1.2.7-x64-mingw32] + end + + it "finds universal-mingw gems on x86-mingw" do + platform "x86-mingw32" + dep "win32-api" + should_resolve_as %w[win32-api-1.5.1-universal-mingw32] + end + + it "finds universal-mingw gems on x64-mingw" do + platform "x64-mingw32" + dep "win32-api" + should_resolve_as %w[win32-api-1.5.1-universal-mingw32] + end + end + + describe "with conflicting cases" do + before :each do + @index = build_index do + gem "foo", "1.0.0" do + dep "bar", ">= 0" + end + + gem "bar", "1.0.0" do + dep "baz", "~> 1.0.0" + end + + gem "bar", "1.0.0", "java" do + dep "baz", " ~> 1.1.0" + end + + gem "baz", %w[1.0.0 1.1.0 1.2.0] + end + end + + it "reports on the conflict" do + platforms "ruby", "java" + dep "foo" + + should_conflict_on "baz" + end + end +end diff --git a/spec/bundler/runtime/executable_spec.rb b/spec/bundler/runtime/executable_spec.rb new file mode 100644 index 00000000000000..7ba510a50966e3 --- /dev/null +++ b/spec/bundler/runtime/executable_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +RSpec.describe "Running bin/* commands" do + before :each do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "runs the bundled command when in the bundle" do + bundle! "binstubs rack" + + build_gem "rack", "2.0", :to_system => true do |s| + s.executables = "rackup" + end + + gembin "rackup" + expect(out).to eq("1.0.0") + end + + it "allows the location of the gem stubs to be specified" do + bundle! "binstubs rack", :path => "gbin" + + expect(bundled_app("bin")).not_to exist + expect(bundled_app("gbin/rackup")).to exist + + gembin bundled_app("gbin/rackup") + expect(out).to eq("1.0.0") + end + + it "allows absolute paths as a specification of where to install bin stubs" do + bundle! "binstubs rack", :path => tmp("bin") + + gembin tmp("bin/rackup") + expect(out).to eq("1.0.0") + end + + it "uses the default ruby install name when shebang is not specified" do + bundle! "binstubs rack" + expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]}\n") + end + + it "allows the name of the shebang executable to be specified" do + bundle! "binstubs rack", :shebang => "ruby-foo" + expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env ruby-foo\n") + end + + it "runs the bundled command when out of the bundle" do + bundle! "binstubs rack" + + build_gem "rack", "2.0", :to_system => true do |s| + s.executables = "rackup" + end + + Dir.chdir(tmp) do + gembin "rackup" + expect(out).to eq("1.0.0") + end + end + + it "works with gems in path" do + build_lib "rack", :path => lib_path("rack") do |s| + s.executables = "rackup" + end + + gemfile <<-G + gem "rack", :path => "#{lib_path("rack")}" + G + + bundle! "binstubs rack" + + build_gem "rack", "2.0", :to_system => true do |s| + s.executables = "rackup" + end + + gembin "rackup" + expect(out).to eq("1.0") + end + + it "creates a bundle binstub" do + build_gem "bundler", Bundler::VERSION, :to_system => true do |s| + s.executables = "bundle" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "bundler" + G + + bundle! "binstubs bundler" + + expect(bundled_app("bin/bundle")).to exist + end + + it "does not generate bin stubs if the option was not specified" do + bundle! "install" + + expect(bundled_app("bin/rackup")).not_to exist + end + + it "allows you to stop installing binstubs", :bundler => "< 3" do + bundle! "install --binstubs bin/" + bundled_app("bin/rackup").rmtree + bundle! "install --binstubs \"\"" + + expect(bundled_app("bin/rackup")).not_to exist + + bundle! "config bin" + expect(out).to include("You have not configured a value for `bin`") + end + + it "remembers that the option was specified", :bundler => "< 3" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + bundle! :install, forgotten_command_line_options([:binstubs, :bin] => "bin") + + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + gem "rack" + G + + bundle "install" + + expect(bundled_app("bin/rackup")).to exist + end + + it "rewrites bins on --binstubs (to maintain backwards compatibility)", :bundler => "< 2" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle! :install, forgotten_command_line_options([:binstubs, :bin] => "bin") + + File.open(bundled_app("bin/rackup"), "wb") do |file| + file.print "OMG" + end + + bundle "install" + + expect(bundled_app("bin/rackup").read).to_not eq("OMG") + end + + it "rewrites bins on binstubs (to maintain backwards compatibility)" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + create_file("bin/rackup", "OMG") + + bundle! "binstubs rack" + + expect(bundled_app("bin/rackup").read).to_not eq("OMG") + end + + it "use BUNDLE_GEMFILE gemfile for binstub" do + # context with bin/bunlder w/ default Gemfile + bundle! "binstubs bundler" + + # generate other Gemfile with executable gem + build_repo2 do + build_gem("bindir") {|s| s.executables = "foo" } + end + + create_file("OtherGemfile", <<-G) + source "file://#{gem_repo2}" + gem 'bindir' + G + + # generate binstub for executable from non default Gemfile (other then bin/bundler version) + ENV["BUNDLE_GEMFILE"] = "OtherGemfile" + bundle "install" + bundle! "binstubs bindir" + + # remove user settings + ENV["BUNDLE_GEMFILE"] = nil + + # run binstub for non default Gemfile + gembin "foo" + + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq("1.0") + end +end diff --git a/spec/bundler/runtime/gem_tasks_spec.rb b/spec/bundler/runtime/gem_tasks_spec.rb new file mode 100644 index 00000000000000..de72869dc320b8 --- /dev/null +++ b/spec/bundler/runtime/gem_tasks_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.describe "require 'bundler/gem_tasks'", :ruby_repo do + before :each do + bundled_app("foo.gemspec").open("w") do |f| + f.write <<-GEMSPEC + Gem::Specification.new do |s| + s.name = "foo" + end + GEMSPEC + end + bundled_app("Rakefile").open("w") do |f| + f.write <<-RAKEFILE + $:.unshift("#{bundler_path}") + require "bundler/gem_tasks" + RAKEFILE + end + end + + it "includes the relevant tasks" do + with_gem_path_as(Spec::Path.base_system_gems.to_s) do + sys_exec "#{rake} -T" + end + + expect(err).to eq("") + expected_tasks = [ + "rake build", + "rake clean", + "rake clobber", + "rake install", + "rake release[remote]", + ] + tasks = out.lines.to_a.map {|s| s.split("#").first.strip } + expect(tasks & expected_tasks).to eq(expected_tasks) + expect(exitstatus).to eq(0) if exitstatus + end + + it "adds 'pkg' to rake/clean's CLOBBER" do + with_gem_path_as(Spec::Path.base_system_gems.to_s) do + sys_exec! %(#{rake} -e 'load "Rakefile"; puts CLOBBER.inspect') + end + expect(last_command.stdout).to eq '["pkg"]' + end +end diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb new file mode 100644 index 00000000000000..18ca246199c056 --- /dev/null +++ b/spec/bundler/runtime/inline_spec.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +RSpec.describe "bundler/inline#gemfile" do + def script(code, options = {}) + requires = ["bundler/inline"] + requires.unshift File.expand_path("../../support/artifice/" + options.delete(:artifice) + ".rb", __FILE__) if options.key?(:artifice) + requires = requires.map {|r| "require '#{r}'" }.join("\n") + @out = ruby("#{requires}\n\n" + code, options) + end + + before :each do + build_lib "one", "1.0.0" do |s| + s.write "lib/baz.rb", "puts 'baz'" + s.write "lib/qux.rb", "puts 'qux'" + end + + build_lib "two", "1.0.0" do |s| + s.write "lib/two.rb", "puts 'two'" + s.add_dependency "three", "= 1.0.0" + end + + build_lib "three", "1.0.0" do |s| + s.write "lib/three.rb", "puts 'three'" + s.add_dependency "seven", "= 1.0.0" + end + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + + build_lib "five", "1.0.0", :no_default => true do |s| + s.write "lib/mofive.rb", "puts 'five'" + end + + build_lib "six", "1.0.0" do |s| + s.write "lib/six.rb", "puts 'six'" + end + + build_lib "seven", "1.0.0" do |s| + s.write "lib/seven.rb", "puts 'seven'" + end + + build_lib "eight", "1.0.0" do |s| + s.write "lib/eight.rb", "puts 'eight'" + end + end + + it "requires the gems" do + script <<-RUBY + gemfile do + path "#{lib_path}" do + gem "two" + end + end + RUBY + + expect(out).to eq("two") + expect(exitstatus).to be_zero if exitstatus + + script <<-RUBY + gemfile do + path "#{lib_path}" do + gem "eleven" + end + end + + puts "success" + RUBY + + expect(err).to include "Could not find gem 'eleven'" + expect(out).not_to include "success" + + script <<-RUBY + gemfile(true) do + source "file://#{gem_repo1}" + gem "rack" + end + RUBY + + expect(out).to include("Rack's post install message") + expect(exitstatus).to be_zero if exitstatus + + script <<-RUBY, :artifice => "endpoint" + gemfile(true) do + source "https://notaserver.com" + gem "activesupport", :require => true + end + RUBY + + expect(out).to include("Installing activesupport") + err.gsub! %r{.*lib/sinatra/base\.rb:\d+: warning: constant ::Fixnum is deprecated$}, "" + err.strip! + expect(err).to lack_errors + expect(exitstatus).to be_zero if exitstatus + end + + it "lets me use my own ui object" do + script <<-RUBY, :artifice => "endpoint" + require 'bundler' + class MyBundlerUI < Bundler::UI::Silent + def confirm(msg, newline = nil) + puts "CONFIRMED!" + end + end + gemfile(true, :ui => MyBundlerUI.new) do + source "https://notaserver.com" + gem "activesupport", :require => true + end + RUBY + + expect(out).to eq("CONFIRMED!\nCONFIRMED!") + expect(exitstatus).to be_zero if exitstatus + end + + it "raises an exception if passed unknown arguments" do + script <<-RUBY + gemfile(true, :arglebargle => true) do + path "#{lib_path}" + gem "two" + end + + puts "success" + RUBY + expect(err).to include "Unknown options: arglebargle" + expect(out).not_to include "success" + end + + it "does not mutate the option argument" do + script <<-RUBY + require 'bundler' + options = { :ui => Bundler::UI::Shell.new } + gemfile(false, options) do + path "#{lib_path}" do + gem "two" + end + end + puts "OKAY" if options.key?(:ui) + RUBY + + expect(out).to match("OKAY") + expect(exitstatus).to be_zero if exitstatus + end + + it "installs quietly if necessary when the install option is not set" do + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" + end + + puts RACK + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs quietly from git if necessary when the install option is not set" do + build_git "foo", "1.0.0" + baz_ref = build_git("baz", "2.0.0").ref_for("HEAD") + script <<-RUBY + gemfile do + gem "foo", :git => #{lib_path("foo-1.0.0").to_s.dump} + gem "baz", :git => #{lib_path("baz-2.0.0").to_s.dump}, :ref => #{baz_ref.dump} + end + + puts FOO + puts BAZ + RUBY + + expect(out).to eq("1.0.0\n2.0.0") + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "allows calling gemfile twice" do + script <<-RUBY + gemfile do + path "#{lib_path}" do + gem "two" + end + end + + gemfile do + path "#{lib_path}" do + gem "four" + end + end + RUBY + + expect(out).to eq("two\nfour") + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs inline gems when a Gemfile.lock is present" do + gemfile <<-G + source "https://notaserver.com" + gem "rake" + G + + lockfile <<-G + GEM + remote: https://rubygems.org/ + specs: + rake (11.3.0) + + PLATFORMS + ruby + + DEPENDENCIES + rake + + BUNDLED WITH + 1.13.6 + G + + in_app_root do + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" + end + + puts RACK + RUBY + end + + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs inline gems when BUNDLE_GEMFILE is set to an empty string" do + ENV["BUNDLE_GEMFILE"] = "" + + in_app_root do + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" + end + + puts RACK + RUBY + end + + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs inline gems when BUNDLE_BIN is set" do + ENV["BUNDLE_BIN"] = "/usr/local/bundle/bin" + + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" # has the rackup executable + end + + puts RACK + RUBY + expect(last_command).to be_success + expect(last_command.stdout).to eq "1.0.0" + end +end diff --git a/spec/bundler/runtime/load_spec.rb b/spec/bundler/runtime/load_spec.rb new file mode 100644 index 00000000000000..b74dbde3f6f943 --- /dev/null +++ b/spec/bundler/runtime/load_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +RSpec.describe "Bundler.load" do + describe "with a gemfile" do + before(:each) do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "provides a list of the env dependencies" do + expect(Bundler.load.dependencies).to have_dep("rack", ">= 0") + end + + it "provides a list of the resolved gems" do + expect(Bundler.load.gems).to have_gem("rack-1.0.0", "bundler-#{Bundler::VERSION}") + end + + it "ignores blank BUNDLE_GEMFILEs" do + expect do + ENV["BUNDLE_GEMFILE"] = "" + Bundler.load + end.not_to raise_error + end + end + + describe "with a gems.rb file" do + before(:each) do + create_file "gems.rb", <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle! :install + end + + it "provides a list of the env dependencies" do + expect(Bundler.load.dependencies).to have_dep("rack", ">= 0") + end + + it "provides a list of the resolved gems" do + expect(Bundler.load.gems).to have_gem("rack-1.0.0", "bundler-#{Bundler::VERSION}") + end + end + + describe "without a gemfile" do + it "raises an exception if the default gemfile is not found" do + expect do + Bundler.load + end.to raise_error(Bundler::GemfileNotFound, /could not locate gemfile/i) + end + + it "raises an exception if a specified gemfile is not found" do + expect do + ENV["BUNDLE_GEMFILE"] = "omg.rb" + Bundler.load + end.to raise_error(Bundler::GemfileNotFound, /omg\.rb/) + end + + it "does not find a Gemfile above the testing directory" do + bundler_gemfile = tmp.join("../Gemfile") + unless File.exist?(bundler_gemfile) + FileUtils.touch(bundler_gemfile) + @remove_bundler_gemfile = true + end + begin + expect { Bundler.load }.to raise_error(Bundler::GemfileNotFound) + ensure + bundler_gemfile.rmtree if @remove_bundler_gemfile + end + end + end + + describe "when called twice" do + it "doesn't try to load the runtime twice" do + install_gemfile! <<-G + source "file:#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + G + + ruby! <<-RUBY + require "bundler" + Bundler.setup :default + Bundler.require :default + puts RACK + begin + require "activesupport" + rescue LoadError + puts "no activesupport" + end + RUBY + + expect(out.split("\n")).to eq(["1.0.0", "no activesupport"]) + end + end + + describe "not hurting brittle rubygems" do + it "does not inject #source into the generated YAML of the gem specs" do + install_gemfile! <<-G + source "file:#{gem_repo1}" + gem "activerecord" + G + + Bundler.load.specs.each do |spec| + expect(spec.to_yaml).not_to match(/^\s+source:/) + expect(spec.to_yaml).not_to match(/^\s+groups:/) + end + end + end +end diff --git a/spec/bundler/runtime/platform_spec.rb b/spec/bundler/runtime/platform_spec.rb new file mode 100644 index 00000000000000..eecf162427cddf --- /dev/null +++ b/spec/bundler/runtime/platform_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +RSpec.describe "Bundler.setup with multi platform stuff" do + it "raises a friendly error when gems are missing locally" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0) + + PLATFORMS + #{local_tag} + + DEPENDENCIES + rack + G + + ruby <<-R + begin + require 'bundler' + Bundler.setup + rescue Bundler::GemNotFound => e + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "will resolve correctly on the current platform when the lockfile was targeted for a different one" do + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + nokogiri (1.4.2-java) + weakling (= 0.0.3) + weakling (0.0.3) + + PLATFORMS + java + + DEPENDENCIES + nokogiri + G + + simulate_platform "x86-darwin-10" + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "nokogiri" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "will add the resolve for the current platform" do + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + nokogiri (1.4.2-java) + weakling (= 0.0.3) + weakling (0.0.3) + + PLATFORMS + java + + DEPENDENCIES + nokogiri + G + + simulate_platform "x86-darwin-100" + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "nokogiri" + gem "platform_specific" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 x86-darwin-100" + end + + it "allows specifying only-ruby-platform" do + simulate_platform "java" + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "nokogiri" + gem "platform_specific" + G + + bundle! "config force_ruby_platform true" + + bundle! "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 RUBY" + end + + it "allows specifying only-ruby-platform on windows with dependency platforms" do + simulate_windows do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platforms => [:mingw, :mswin, :x64_mingw, :jruby] + gem "platform_specific" + G + + bundle! "config force_ruby_platform true" + + bundle! "install" + + expect(the_bundle).to include_gems "platform_specific 1.0 RUBY" + end + end + + it "recovers when the lockfile is missing a platform-specific gem" do + build_repo2 do + build_gem "requires_platform_specific" do |s| + s.add_dependency "platform_specific" + end + end + simulate_windows x64_mingw do + lockfile <<-L + GEM + remote: file:#{gem_repo2}/ + specs: + platform_specific (1.0-x86-mingw32) + requires_platform_specific (1.0) + platform_specific + + PLATFORMS + x64-mingw32 + x86-mingw32 + + DEPENDENCIES + requires_platform_specific + L + + install_gemfile! <<-G, :verbose => true + source "file://#{gem_repo2}" + gem "requires_platform_specific" + G + + expect(the_bundle).to include_gem "platform_specific 1.0 x64-mingw32" + end + end +end diff --git a/spec/bundler/runtime/require_spec.rb b/spec/bundler/runtime/require_spec.rb new file mode 100644 index 00000000000000..0484e38845f1ec --- /dev/null +++ b/spec/bundler/runtime/require_spec.rb @@ -0,0 +1,452 @@ +# frozen_string_literal: true + +RSpec.describe "Bundler.require" do + before :each do + build_lib "one", "1.0.0" do |s| + s.write "lib/baz.rb", "puts 'baz'" + s.write "lib/qux.rb", "puts 'qux'" + end + + build_lib "two", "1.0.0" do |s| + s.write "lib/two.rb", "puts 'two'" + s.add_dependency "three", "= 1.0.0" + end + + build_lib "three", "1.0.0" do |s| + s.write "lib/three.rb", "puts 'three'" + s.add_dependency "seven", "= 1.0.0" + end + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + + build_lib "five", "1.0.0", :no_default => true do |s| + s.write "lib/mofive.rb", "puts 'five'" + end + + build_lib "six", "1.0.0" do |s| + s.write "lib/six.rb", "puts 'six'" + end + + build_lib "seven", "1.0.0" do |s| + s.write "lib/seven.rb", "puts 'seven'" + end + + build_lib "eight", "1.0.0" do |s| + s.write "lib/eight.rb", "puts 'eight'" + end + + build_lib "nine", "1.0.0" do |s| + s.write "lib/nine.rb", "puts 'nine'" + end + + build_lib "ten", "1.0.0" do |s| + s.write "lib/ten.rb", "puts 'ten'" + end + + gemfile <<-G + path "#{lib_path}" do + gem "one", :group => :bar, :require => %w[baz qux] + gem "two" + gem "three", :group => :not + gem "four", :require => false + gem "five" + gem "six", :group => "string" + gem "seven", :group => :not + gem "eight", :require => true, :group => :require_true + env "BUNDLER_TEST" => "nine" do + gem "nine", :require => true + end + gem "ten", :install_if => lambda { ENV["BUNDLER_TEST"] == "ten" } + end + G + end + + it "requires the gems" do + # default group + run "Bundler.require" + expect(out).to eq("two") + + # specific group + run "Bundler.require(:bar)" + expect(out).to eq("baz\nqux") + + # default and specific group + run "Bundler.require(:default, :bar)" + expect(out).to eq("baz\nqux\ntwo") + + # specific group given as a string + run "Bundler.require('bar')" + expect(out).to eq("baz\nqux") + + # specific group declared as a string + run "Bundler.require(:string)" + expect(out).to eq("six") + + # required in resolver order instead of gemfile order + run("Bundler.require(:not)") + expect(out.split("\n").sort).to eq(%w[seven three]) + + # test require: true + run "Bundler.require(:require_true)" + expect(out).to eq("eight") + end + + it "allows requiring gems with non standard names explicitly" do + run "Bundler.require ; require 'mofive'" + expect(out).to eq("two\nfive") + end + + it "allows requiring gems which are scoped by env" do + ENV["BUNDLER_TEST"] = "nine" + run "Bundler.require" + expect(out).to eq("two\nnine") + end + + it "allows requiring gems which are scoped by install_if" do + ENV["BUNDLER_TEST"] = "ten" + run "Bundler.require" + expect(out).to eq("two\nten") + end + + it "raises an exception if a require is specified but the file does not exist" do + gemfile <<-G + path "#{lib_path}" do + gem "two", :require => 'fail' + end + G + + load_error_run <<-R, "fail" + Bundler.require + R + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "displays a helpful message if the required gem throws an error" do + build_lib "faulty", "1.0.0" do |s| + s.write "lib/faulty.rb", "raise RuntimeError.new(\"Gem Internal Error Message\")" + end + + gemfile <<-G + path "#{lib_path}" do + gem "faulty" + end + G + + run "Bundler.require" + expect(err).to match("error while trying to load the gem 'faulty'") + expect(err).to match("Gem Internal Error Message") + end + + it "doesn't swallow the error when the library has an unrelated error" do + build_lib "loadfuuu", "1.0.0" do |s| + s.write "lib/loadfuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")" + end + + gemfile <<-G + path "#{lib_path}" do + gem "loadfuuu" + end + G + + cmd = <<-RUBY + begin + Bundler.require + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR: \#{e.message}" + end + RUBY + run(cmd) + + expect(err).to eq_err("ZOMG LOAD ERROR: cannot load such file -- load-bar") + end + + describe "with namespaced gems" do + before :each do + build_lib "jquery-rails", "1.0.0" do |s| + s.write "lib/jquery/rails.rb", "puts 'jquery/rails'" + end + lib_path("jquery-rails-1.0.0/lib/jquery-rails.rb").rmtree + end + + it "requires gem names that are namespaced" do + gemfile <<-G + path '#{lib_path}' do + gem 'jquery-rails' + end + G + + run "Bundler.require" + expect(out).to eq("jquery/rails") + end + + it "silently passes if the require fails" do + build_lib "bcrypt-ruby", "1.0.0", :no_default => true do |s| + s.write "lib/brcrypt.rb", "BCrypt = '1.0.0'" + end + gemfile <<-G + path "#{lib_path}" do + gem "bcrypt-ruby" + end + G + + cmd = <<-RUBY + require 'bundler' + Bundler.require + RUBY + ruby(cmd) + + expect(err).to lack_errors + end + + it "does not mangle explicitly given requires" do + gemfile <<-G + path "#{lib_path}" do + gem 'jquery-rails', :require => 'jquery-rails' + end + G + + load_error_run <<-R, "jquery-rails" + Bundler.require + R + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "handles the case where regex fails" do + build_lib "load-fuuu", "1.0.0" do |s| + s.write "lib/load-fuuu.rb", "raise LoadError.new(\"Could not open library 'libfuuu-1.0': libfuuu-1.0: cannot open shared object file: No such file or directory.\")" + end + + gemfile <<-G + path "#{lib_path}" do + gem "load-fuuu" + end + G + + cmd = <<-RUBY + begin + Bundler.require + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR" if e.message.include?("Could not open library 'libfuuu-1.0'") + end + RUBY + run(cmd) + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "doesn't swallow the error when the library has an unrelated error" do + build_lib "load-fuuu", "1.0.0" do |s| + s.write "lib/load/fuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")" + end + lib_path("load-fuuu-1.0.0/lib/load-fuuu.rb").rmtree + + gemfile <<-G + path "#{lib_path}" do + gem "load-fuuu" + end + G + + cmd = <<-RUBY + begin + Bundler.require + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR: \#{e.message}" + end + RUBY + run(cmd) + + expect(err).to eq_err("ZOMG LOAD ERROR: cannot load such file -- load-bar") + end + end + + describe "using bundle exec" do + it "requires the locked gems" do + bundle "exec ruby -e 'Bundler.require'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to eq("two") + + bundle "exec ruby -e 'Bundler.require(:bar)'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to eq("baz\nqux") + + bundle "exec ruby -e 'Bundler.require(:default, :bar)'", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to eq("baz\nqux\ntwo") + end + end + + describe "order" do + before(:each) do + build_lib "one", "1.0.0" do |s| + s.write "lib/one.rb", <<-ONE + if defined?(Two) + Two.two + else + puts "two_not_loaded" + end + puts 'one' + ONE + end + + build_lib "two", "1.0.0" do |s| + s.write "lib/two.rb", <<-TWO + module Two + def self.two + puts 'module_two' + end + end + puts 'two' + TWO + end + end + + it "works when the gems are in the Gemfile in the correct order" do + gemfile <<-G + path "#{lib_path}" do + gem "two" + gem "one" + end + G + + run "Bundler.require" + expect(out).to eq("two\nmodule_two\none") + end + + describe "a gem with different requires for different envs" do + before(:each) do + build_gem "multi_gem", :to_bundle => true do |s| + s.write "lib/one.rb", "puts 'ONE'" + s.write "lib/two.rb", "puts 'TWO'" + end + + install_gemfile <<-G + gem "multi_gem", :require => "one", :group => :one + gem "multi_gem", :require => "two", :group => :two + G + end + + it "requires both with Bundler.require(both)" do + run "Bundler.require(:one, :two)" + expect(out).to eq("ONE\nTWO") + end + + it "requires one with Bundler.require(:one)" do + run "Bundler.require(:one)" + expect(out).to eq("ONE") + end + + it "requires :two with Bundler.require(:two)" do + run "Bundler.require(:two)" + expect(out).to eq("TWO") + end + end + + it "fails when the gems are in the Gemfile in the wrong order" do + gemfile <<-G + path "#{lib_path}" do + gem "one" + gem "two" + end + G + + run "Bundler.require" + expect(out).to eq("two_not_loaded\none\ntwo") + end + + describe "with busted gems" do + it "should be busted" do + build_gem "busted_require", :to_bundle => true do |s| + s.write "lib/busted_require.rb", "require 'no_such_file_omg'" + end + + install_gemfile <<-G + gem "busted_require" + G + + load_error_run <<-R, "no_such_file_omg" + Bundler.require + R + expect(err).to eq_err("ZOMG LOAD ERROR") + end + end + end + + it "does not load rubygems gemspecs that are used", :rubygems => ">= 2.5.2" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + run! <<-R + path = File.join(Gem.dir, "specifications", "rack-1.0.0.gemspec") + contents = File.read(path) + contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join + File.open(path, "w") do |f| + f.write contents + end + R + + run! <<-R + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end + + it "does not load git gemspecs that are used", :rubygems => ">= 2.5.2" do + build_git "foo" + + install_gemfile! <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run! <<-R + path = Gem.loaded_specs["foo"].loaded_from + contents = File.read(path) + contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join + File.open(path, "w") do |f| + f.write contents + end + R + + run! <<-R + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end +end + +RSpec.describe "Bundler.require with platform specific dependencies" do + it "does not require the gems that are pinned to other platforms" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + platforms :#{not_local_tag} do + gem "fail", :require => "omgomg" + end + + gem "rack", "1.0.0" + G + + run "Bundler.require" + expect(err).to lack_errors + end + + it "requires gems pinned to multiple platforms, including the current one" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + platforms :#{not_local_tag}, :#{local_tag} do + gem "rack", :require => "rack" + end + G + + run "Bundler.require; puts RACK" + + expect(out).to eq("1.0.0") + expect(err).to lack_errors + end +end diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb new file mode 100644 index 00000000000000..15dd1fe19034e7 --- /dev/null +++ b/spec/bundler/runtime/setup_spec.rb @@ -0,0 +1,1445 @@ +# frozen_string_literal: true + +RSpec.describe "Bundler.setup" do + describe "with no arguments" do + it "makes all groups available" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :group => :test + G + + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + + require 'rack' + puts RACK + RUBY + expect(err).to lack_errors + expect(out).to eq("1.0.0") + end + end + + describe "when called with groups" do + before(:each) do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack", :group => :test + G + end + + it "doesn't make all groups available" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup(:default) + + begin + require 'rack' + rescue LoadError + puts "WIN" + end + RUBY + expect(err).to lack_errors + expect(out).to eq("WIN") + end + + it "accepts string for group name" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup(:default, 'test') + + require 'rack' + puts RACK + RUBY + expect(err).to lack_errors + expect(out).to eq("1.0.0") + end + + it "leaves all groups available if they were already" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + Bundler.setup(:default) + + require 'rack' + puts RACK + RUBY + expect(err).to lack_errors + expect(out).to eq("1.0.0") + end + + it "leaves :default available if setup is called twice" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup(:default) + Bundler.setup(:default, :test) + + begin + require 'yard' + puts "WIN" + rescue LoadError + puts "FAIL" + end + RUBY + expect(err).to lack_errors + expect(out).to match("WIN") + end + + it "handles multiple non-additive invocations" do + ruby <<-RUBY + require 'bundler' + Bundler.setup(:default, :test) + Bundler.setup(:default) + require 'rack' + + puts "FAIL" + RUBY + + expect(err).to match("rack") + expect(err).to match("LoadError") + expect(out).not_to match("FAIL") + end + end + + context "load order" do + def clean_load_path(lp) + without_bundler_load_path = ruby!("puts $LOAD_PATH").split("\n") + lp = lp - [ + bundler_path.to_s, + bundler_path.join("gems/bundler-#{Bundler::VERSION}/lib").to_s, + tmp("rubygems/lib").to_s, + root.join("../lib").expand_path.to_s, + ] - without_bundler_load_path + lp.map! {|p| p.sub(/^#{Regexp.union system_gem_path.to_s, default_bundle_path.to_s}/i, "") } + end + + it "puts loaded gems after -I and RUBYLIB", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ENV["RUBYOPT"] = "-Idash_i_dir" + ENV["RUBYLIB"] = "rubylib_dir" + + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + puts $LOAD_PATH + RUBY + + load_path = out.split("\n") + rack_load_order = load_path.index {|path| path.include?("rack") } + + expect(err).to eq("") + expect(load_path[1]).to include "dash_i_dir" + expect(load_path[2]).to include "rubylib_dir" + expect(rack_load_order).to be > 0 + end + + it "orders the load path correctly when there are dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + ruby! <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + puts $LOAD_PATH + RUBY + + load_path = clean_load_path(out.split("\n")) + + unless Bundler.load.specs["bundler"].empty? + load_path.delete_if {|path| path =~ /bundler/ } + end + + expect(load_path).to start_with( + "/gems/rails-2.3.2/lib", + "/gems/activeresource-2.3.2/lib", + "/gems/activerecord-2.3.2/lib", + "/gems/actionpack-2.3.2/lib", + "/gems/actionmailer-2.3.2/lib", + "/gems/activesupport-2.3.2/lib", + "/gems/rake-10.0.2/lib" + ) + end + + it "falls back to order the load path alphabetically for backwards compatibility" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "weakling" + gem "duradura" + gem "terranova" + G + + ruby! <<-RUBY + require 'rubygems' + require 'bundler/setup' + puts $LOAD_PATH + RUBY + + load_path = clean_load_path(out.split("\n")) + + expect(load_path).to start_with( + "/gems/weakling-0.0.3/lib", + "/gems/terranova-8/lib", + "/gems/duradura-7.0/lib" + ) + end + end + + it "raises if the Gemfile was not yet installed" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ruby <<-R + require 'rubygems' + require 'bundler' + + begin + Bundler.setup + puts "FAIL" + rescue Bundler::GemNotFound + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "doesn't create a Gemfile.lock if the setup fails" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ruby <<-R + require 'rubygems' + require 'bundler' + + Bundler.setup + R + + expect(bundled_app("Gemfile.lock")).not_to exist + end + + it "doesn't change the Gemfile.lock if the setup fails" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + lockfile = File.read(bundled_app("Gemfile.lock")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "nosuchgem", "10.0" + G + + ruby <<-R + require 'rubygems' + require 'bundler' + + Bundler.setup + R + + expect(File.read(bundled_app("Gemfile.lock"))).to eq(lockfile) + end + + it "makes a Gemfile.lock if setup succeeds" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + File.read(bundled_app("Gemfile.lock")) + + FileUtils.rm(bundled_app("Gemfile.lock")) + + run "1" + expect(bundled_app("Gemfile.lock")).to exist + end + + describe "$BUNDLE_GEMFILE" do + context "user provides an absolute path" do + it "uses BUNDLE_GEMFILE to locate the gemfile if present" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile bundled_app("4realz"), <<-G + source "file://#{gem_repo1}" + gem "activesupport", "2.3.5" + G + + ENV["BUNDLE_GEMFILE"] = bundled_app("4realz").to_s + bundle :install + + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + end + + context "an absolute path is not provided" do + it "uses BUNDLE_GEMFILE to locate the gemfile if present" do + gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle "install" + bundle "install --deployment" + + ENV["BUNDLE_GEMFILE"] = "Gemfile" + ruby <<-R + require 'rubygems' + require 'bundler' + + begin + Bundler.setup + puts "WIN" + rescue ArgumentError => e + puts "FAIL" + end + R + + expect(out).to eq("WIN") + end + end + end + + it "prioritizes gems in BUNDLE_PATH over gems in GEM_HOME" do + ENV["BUNDLE_PATH"] = bundled_app(".bundle").to_s + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + G + + build_gem "rack", "1.0", :to_system => true do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + describe "integrate with rubygems" do + describe "by replacing #gem" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + end + + it "replaces #gem but raises when the gem is missing" do + run <<-R + begin + gem "activesupport" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "version_requirement is now deprecated in rubygems 1.4.0+ when gem is missing" do + run <<-R + begin + gem "activesupport" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(err).to lack_errors + end + + it "replaces #gem but raises when the version is wrong" do + run <<-R + begin + gem "rack", "1.0.0" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "version_requirement is now deprecated in rubygems 1.4.0+ when the version is wrong" do + run <<-R + begin + gem "rack", "1.0.0" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(err).to lack_errors + end + end + + describe "by hiding system gems" do + before :each do + system_gems "activesupport-2.3.5" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + G + end + + it "removes system gems from Gem.source_index" do + run "require 'yard'" + expect(out).to eq("bundler-#{Bundler::VERSION}\nyard-1.0") + end + + context "when the ruby stdlib is a substring of Gem.path" do + it "does not reject the stdlib from $LOAD_PATH" do + substring = "/" + $LOAD_PATH.find {|p| p =~ /vendor_ruby/ }.split("/")[2] + run "puts 'worked!'", :env => { "GEM_PATH" => substring } + expect(out).to eq("worked!") + end + end + end + end + + describe "with paths" do + it "activates the gems in the path source" do + system_gems "rack-1.0.0" + + build_lib "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "puts 'WIN'" + end + + gemfile <<-G + source "file://#{gem_repo1}" + path "#{lib_path("rack-1.0.0")}" do + gem "rack" + end + G + + run "require 'rack'" + expect(out).to eq("WIN") + end + end + + describe "with git" do + before do + build_git "rack", "1.0.0" + + gemfile <<-G + gem "rack", :git => "#{lib_path("rack-1.0.0")}" + G + end + + it "provides a useful exception when the git repo is not checked out yet" do + run "1" + expect(err).to match(/the git source #{lib_path('rack-1.0.0')} is not yet checked out. Please run `bundle install`/i) + end + + it "does not hit the git binary if the lockfile is available and up to date" do + bundle "install" + + break_git! + + ruby <<-R + require 'rubygems' + require 'bundler' + + begin + Bundler.setup + puts "WIN" + rescue Exception => e + puts "FAIL" + end + R + + expect(out).to eq("WIN") + end + + it "provides a good exception if the lockfile is unavailable" do + bundle "install" + + FileUtils.rm(bundled_app("Gemfile.lock")) + + break_git! + + ruby <<-R + require "rubygems" + require "bundler" + + begin + Bundler.setup + puts "FAIL" + rescue Bundler::GitError => e + puts e.message + end + R + + run "puts 'FAIL'" + + expect(err).not_to include "This is not the git you are looking for" + end + + it "works even when the cache directory has been deleted" do + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + FileUtils.rm_rf vendored_gems("cache") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not randomly change the path when specifying --path and the bundle directory becomes read only" do + bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + + with_read_only("**/*") do + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + it "finds git gem when default bundle path becomes read only" do + bundle "install" + + with_read_only("#{Bundler.bundle_path}/**/*") do + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + + describe "when specifying local override" do + it "explodes if given path does not exist on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle! :install + + FileUtils.rm_rf(lib_path("local-rack")) + run "require 'rack'" + expect(err).to match(/Cannot use local override for rack-0.8 because #{Regexp.escape(lib_path('local-rack').to_s)} does not exist/) + end + + it "explodes if branch is not given on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle! :install + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + run "require 'rack'" + expect(err).to match(/because :branch is not specified in Gemfile/) + end + + it "explodes on different branches on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle! :install + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "changed" + G + + run "require 'rack'" + expect(err).to match(/is using branch master but Gemfile specifies changed/) + end + + it "explodes on refs with different branches on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :ref => "master", :branch => "master" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :ref => "master", :branch => "nonexistant" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + run "require 'rack'" + expect(err).to match(/is using branch master but Gemfile specifies nonexistant/) + end + end + + describe "when excluding groups" do + it "doesn't change the resolve if --without is used" do + install_gemfile <<-G, forgotten_command_line_options(:without => :rails) + source "file://#{gem_repo1}" + gem "activesupport" + + group :rails do + gem "rails", "2.3.2" + end + G + + install_gems "activesupport-2.3.5" + + expect(the_bundle).to include_gems "activesupport 2.3.2", :groups => :default + end + + it "remembers --without and does not bail on bare Bundler.setup" do + install_gemfile <<-G, forgotten_command_line_options(:without => :rails) + source "file://#{gem_repo1}" + gem "activesupport" + + group :rails do + gem "rails", "2.3.2" + end + G + + install_gems "activesupport-2.3.5" + + expect(the_bundle).to include_gems "activesupport 2.3.2" + end + + it "remembers --without and does not include groups passed to Bundler.setup" do + install_gemfile <<-G, forgotten_command_line_options(:without => :rails) + source "file://#{gem_repo1}" + gem "activesupport" + + group :rack do + gem "rack" + end + + group :rails do + gem "rails", "2.3.2" + end + G + + expect(the_bundle).not_to include_gems "activesupport 2.3.2", :groups => :rack + expect(the_bundle).to include_gems "rack 1.0.0", :groups => :rack + end + end + + # Unfortunately, gem_prelude does not record the information about + # activated gems, so this test cannot work on 1.9 :( + if RUBY_VERSION < "1.9" + describe "preactivated gems" do + it "raises an exception if a pre activated gem conflicts with the bundle" do + system_gems "thin-1.0", "rack-1.0.0" + build_gem "thin", "1.1", :to_system => true do |s| + s.add_dependency "rack" + end + + gemfile <<-G + gem "thin", "1.0" + G + + ruby <<-R + require 'rubygems' + gem "thin" + require 'bundler' + begin + Bundler.setup + puts "FAIL" + rescue Gem::LoadError => e + puts e.message + end + R + + expect(out).to eq("You have already activated thin 1.1, but your Gemfile requires thin 1.0. Prepending `bundle exec` to your command may solve this.") + end + + it "version_requirement is now deprecated in rubygems 1.4.0+" do + system_gems "thin-1.0", "rack-1.0.0" + build_gem "thin", "1.1", :to_system => true do |s| + s.add_dependency "rack" + end + + gemfile <<-G + gem "thin", "1.0" + G + + ruby <<-R + require 'rubygems' + gem "thin" + require 'bundler' + begin + Bundler.setup + puts "FAIL" + rescue Gem::LoadError => e + puts e.message + end + R + + expect(err).to lack_errors + end + end + end + + # RubyGems returns loaded_from as a string + it "has loaded_from as a string on all specs" do + build_git "foo" + build_git "no-gemspec", :gemspec => false + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "foo", :git => "#{lib_path("foo-1.0")}" + gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}" + G + + run <<-R + Gem.loaded_specs.each do |n, s| + puts "FAIL" unless s.loaded_from.is_a?(String) + end + R + + expect(out).to be_empty + end + + it "does not load all gemspecs", :rubygems => ">= 2.3" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + run! <<-R + File.open(File.join(Gem.dir, "specifications", "broken.gemspec"), "w") do |f| + f.write <<-RUBY +# -*- encoding: utf-8 -*- +# stub: broken 1.0.0 ruby lib + +Gem::Specification.new do |s| + s.name = "broken" + s.version = "1.0.0" + raise "BROKEN GEMSPEC" +end + RUBY + end + R + + run! <<-R + puts "WIN" + R + + expect(out).to eq("WIN") + end + + it "ignores empty gem paths" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ENV["GEM_HOME"] = "" + bundle %(exec ruby -e "require 'set'"), :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + + expect(err).to lack_errors + end + + describe "$MANPATH" do + before do + build_repo4 do + build_gem "with_man" do |s| + s.write("man/man1/page.1", "MANPAGE") + end + end + end + + context "when the user has one set" do + before { ENV["MANPATH"] = "/foo:" } + + it "adds the gem's man dir to the MANPATH" do + install_gemfile! <<-G + source "file:#{gem_repo4}" + gem "with_man" + G + + run! "puts ENV['MANPATH']" + expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}:/foo") + end + end + + context "when the user does not have one set" do + before { ENV.delete("MANPATH") } + + it "adds the gem's man dir to the MANPATH" do + install_gemfile! <<-G + source "file:#{gem_repo4}" + gem "with_man" + G + + run! "puts ENV['MANPATH']" + expect(out).to eq(default_bundle_path("gems/with_man-1.0/man").to_s) + end + end + end + + it "should prepend gemspec require paths to $LOAD_PATH in order" do + update_repo2 do + build_gem("requirepaths") do |s| + s.write("lib/rq.rb", "puts 'yay'") + s.write("src/rq.rb", "puts 'nooo'") + s.require_paths = %w[lib src] + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "requirepaths", :require => nil + G + + run "require 'rq'" + expect(out).to eq("yay") + end + + it "should clean $LOAD_PATH properly", :ruby_repo do + gem_name = "very_simple_binary" + full_gem_name = gem_name + "-1.0" + ext_dir = File.join(tmp "extenstions", full_gem_name) + + install_gem full_gem_name + + install_gemfile <<-G + source "file://#{gem_repo1}" + G + + ruby <<-R + if Gem::Specification.method_defined? :extension_dir + s = Gem::Specification.find_by_name '#{gem_name}' + s.extension_dir = '#{ext_dir}' + + # Don't build extensions. + s.class.send(:define_method, :build_extensions) { nil } + end + + require 'bundler' + gem '#{gem_name}' + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 + + Bundler.setup + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0 + R + + expect(out).to eq("true\ntrue") + end + + context "with bundler is located in symlinked GEM_HOME" do + let(:gem_home) { Dir.mktmpdir } + let(:symlinked_gem_home) { Tempfile.new("gem_home") } + let(:bundler_dir) { ruby_core? ? File.expand_path("../../../..", __FILE__) : File.expand_path("../../..", __FILE__) } + let(:bundler_lib) { File.join(bundler_dir, "lib") } + + before do + FileUtils.ln_sf(gem_home, symlinked_gem_home.path) + gems_dir = File.join(gem_home, "gems") + specifications_dir = File.join(gem_home, "specifications") + Dir.mkdir(gems_dir) + Dir.mkdir(specifications_dir) + + FileUtils.ln_s(bundler_dir, File.join(gems_dir, "bundler-#{Bundler::VERSION}")) + + gemspec_file = ruby_core? ? "#{bundler_dir}/lib/bundler.gemspec" : "#{bundler_dir}/bundler.gemspec" + gemspec = File.read(gemspec_file). + sub("Bundler::VERSION", %("#{Bundler::VERSION}")) + gemspec = gemspec.lines.reject {|line| line =~ %r{lib/bundler/version} }.join + + File.open(File.join(specifications_dir, "bundler.gemspec"), "wb") do |f| + f.write(gemspec) + end + end + + it "should succesfully require 'bundler/setup'", :ruby_repo do + install_gemfile "" + + ENV["GEM_PATH"] = symlinked_gem_home.path + + ruby <<-R + if $LOAD_PATH.include?("#{bundler_lib}") + # We should use bundler from GEM_PATH for this test, so we should + # remove path to the bundler source tree + $LOAD_PATH.delete("#{bundler_lib}") + else + raise "We don't have #{bundler_lib} in $LOAD_PATH" + end + puts (require 'bundler/setup') + R + + expect(out).to eql("true") + end + end + + it "stubs out Gem.refresh so it does not reveal system gems" do + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + run <<-R + puts Bundler.rubygems.find_name("rack").inspect + Gem.refresh + puts Bundler.rubygems.find_name("rack").inspect + R + + expect(out).to eq("[]\n[]") + end + + describe "when a vendored gem specification uses the :path option" do + it "should resolve paths relative to the Gemfile" do + path = bundled_app(File.join("vendor", "foo")) + build_lib "foo", :path => path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(path, "foo.gemspec")) + + install_gemfile <<-G + gem 'foo', '1.2.3', :path => 'vendor/foo' + G + + Dir.chdir(bundled_app.parent) do + run <<-R, :env => { "BUNDLE_GEMFILE" => bundled_app("Gemfile") } + require 'foo' + R + end + expect(err).to lack_errors + end + + it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do + relative_path = File.join("vendor", Dir.pwd[1..-1], "foo") + absolute_path = bundled_app(relative_path) + FileUtils.mkdir_p(absolute_path) + build_lib "foo", :path => absolute_path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(absolute_path, "foo.gemspec")) + + gemfile <<-G + gem 'foo', '1.2.3', :path => '#{relative_path}' + G + + bundle :install + + Dir.chdir(bundled_app.parent) do + run <<-R, :env => { "BUNDLE_GEMFILE" => bundled_app("Gemfile") } + require 'foo' + R + end + + expect(err).to lack_errors + end + end + + describe "with git gems that don't have gemspecs" do + before :each do + build_git "no-gemspec", :gemspec => false + + install_gemfile <<-G + gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}" + G + end + + it "loads the library via a virtual spec" do + run <<-R + require 'no-gemspec' + puts NOGEMSPEC + R + + expect(out).to eq("1.0") + end + end + + describe "with bundled and system gems" do + before :each do + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "activesupport", "2.3.5" + G + end + + it "does not pull in system gems" do + run <<-R + require 'rubygems' + + begin; + require 'rack' + rescue LoadError + puts 'WIN' + end + R + + expect(out).to eq("WIN") + end + + it "provides a gem method" do + run <<-R + gem 'activesupport' + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(out).to eq("2.3.5") + end + + it "raises an exception if gem is used to invoke a system gem not in the bundle" do + run <<-R + begin + gem 'rack' + rescue LoadError => e + puts e.message + end + R + + expect(out).to eq("rack is not part of the bundle. Add it to your Gemfile.") + end + + it "sets GEM_HOME appropriately" do + run "puts ENV['GEM_HOME']" + expect(out).to eq(default_bundle_path.to_s) + end + end + + describe "with system gems in the bundle" do + before :each do + bundle! "config path.system true" + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + gem "activesupport", "2.3.5" + G + end + + it "sets GEM_PATH appropriately" do + run "puts Gem.path" + paths = out.split("\n") + expect(paths).to include(system_gem_path.to_s) + end + end + + describe "with a gemspec that requires other files" do + before :each do + build_git "bar", :gemspec => false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + lib = File.expand_path('../lib/', __FILE__) + $:.unshift lib unless $:.include?(lib) + require 'bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'no one' + end + G + end + + gemfile <<-G + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + end + + it "evals each gemspec in the context of its parent directory" do + bundle :install + run "require 'bar'; puts BAR" + expect(out).to eq("1.0") + end + + it "error intelligently if the gemspec has a LoadError" do + ref = update_git "bar", :gemspec => false do |s| + s.write "bar.gemspec", "require 'foobarbaz'" + end.ref_for("HEAD") + bundle :install + + expect(out.lines.map(&:chomp)).to include( + a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"), + RUBY_VERSION >= "1.9" ? a_string_starting_with("Does it try to require a relative path? That's been removed in Ruby 1.9.") : "", + " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1", + " > require 'foobarbaz'" + ) + end + + it "evals each gemspec with a binding from the top level" do + bundle "install" + + ruby <<-RUBY + require 'bundler' + def Bundler.require(path) + raise "LOSE" + end + Bundler.load + RUBY + + expect(err).to lack_errors + expect(out).to eq("") + end + end + + describe "when Bundler is bundled" do + it "doesn't blow up" do + install_gemfile <<-G + gem "bundler", :path => "#{File.expand_path("..", lib)}" + G + + bundle %(exec ruby -e "require 'bundler'; Bundler.setup"), :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(err).to lack_errors + end + end + + describe "when BUNDLED WITH" do + def lock_with(bundler_version = nil) + lock = <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + L + + if bundler_version + lock += "\n BUNDLED WITH\n #{bundler_version}\n" + end + + lock + end + + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "is not present" do + it "does not change the lock" do + lockfile lock_with(nil) + ruby "require 'bundler/setup'" + lockfile_should_be lock_with(nil) + end + end + + context "is newer" do + it "does not change the lock or warn" do + lockfile lock_with(Bundler::VERSION.succ) + ruby "require 'bundler/setup'" + expect(out).to eq("") + expect(err).to eq("") + lockfile_should_be lock_with(Bundler::VERSION.succ) + end + end + + context "is older" do + it "does not change the lock" do + lockfile lock_with("1.10.1") + ruby "require 'bundler/setup'" + lockfile_should_be lock_with("1.10.1") + end + end + end + + describe "when RUBY VERSION" do + let(:ruby_version) { nil } + + def lock_with(ruby_version = nil) + lock = <<-L + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rack + L + + if ruby_version + lock += "\n RUBY VERSION\n ruby #{ruby_version}\n" + end + + lock += <<-L + + BUNDLED WITH + #{Bundler::VERSION} + L + + normalize_uri_file(lock) + end + + before do + install_gemfile <<-G + ruby ">= 0" + source "file://localhost#{gem_repo1}" + gem "rack" + G + lockfile lock_with(ruby_version) + end + + context "is not present" do + it "does not change the lock" do + expect { ruby! "require 'bundler/setup'" }.not_to change { lockfile } + end + end + + context "is newer" do + let(:ruby_version) { "5.5.5" } + it "does not change the lock or warn" do + expect { ruby! "require 'bundler/setup'" }.not_to change { lockfile } + expect(out).to eq("") + expect(err).to eq("") + end + end + + context "is older" do + let(:ruby_version) { "1.0.0" } + it "does not change the lock" do + expect { ruby! "require 'bundler/setup'" }.not_to change { lockfile } + end + end + end + + describe "with gemified standard libraries" do + it "does not load Psych", :ruby => "~> 2.2" do + gemfile "" + ruby <<-RUBY + require 'bundler/setup' + puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined" + require 'psych' + puts Psych::VERSION + RUBY + pre_bundler, post_bundler = out.split("\n") + expect(pre_bundler).to eq("undefined") + expect(post_bundler).to match(/\d+\.\d+\.\d+/) + end + + it "does not load openssl" do + install_gemfile! "" + ruby! <<-RUBY + require "bundler/setup" + puts defined?(OpenSSL) || "undefined" + require "openssl" + puts defined?(OpenSSL) || "undefined" + RUBY + expect(out).to eq("undefined\nconstant") + end + + describe "default gem activation" do + let(:exemptions) do + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("2.7") || ENV["RGV"] == "master" + [] + else + %w[io-console openssl] + end << "bundler" + end + + let(:activation_warning_hack) { strip_whitespace(<<-RUBY) } + require #{spec_dir.join("support/hax").to_s.dump} + require "rubygems" + + if Gem::Specification.instance_methods.map(&:to_sym).include?(:activate) + Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate) + Gem::Specification.send(:define_method, :activate) do + unless #{exemptions.inspect}.include?(name) + warn '-' * 80 + warn "activating \#{full_name}" + warn *caller + warn '*' * 80 + end + bundler_spec_activate + end + end + RUBY + + let(:activation_warning_hack_rubyopt) do + create_file("activation_warning_hack.rb", activation_warning_hack) + "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}" + end + + let(:code) { strip_whitespace(<<-RUBY) } + require "bundler/setup" + require "pp" + loaded_specs = Gem.loaded_specs.dup + #{exemptions.inspect}.each {|s| loaded_specs.delete(s) } + pp loaded_specs + + # not a default gem, but harmful to have loaded + open_uri = $LOADED_FEATURES.grep(/open.uri/) + unless open_uri.empty? + warn "open_uri: \#{open_uri}" + end + RUBY + + it "activates no gems with -rbundler/setup" do + install_gemfile! "" + ruby! code, :env => { :RUBYOPT => activation_warning_hack_rubyopt } + expect(last_command.stdout).to eq("{}") + end + + it "activates no gems with bundle exec" do + install_gemfile! "" + # ensure we clean out the default gems, bceause bundler's allowed to be activated + create_file("script.rb", code) + bundle! "exec ruby ./script.rb", :env => { :RUBYOPT => activation_warning_hack_rubyopt + " -rbundler/setup" } + expect(last_command.stdout).to eq("{}") + end + + it "activates no gems with bundle exec that is loaded" do + # TODO: remove once https://github.com/erikhuda/thor/pull/539 is released + exemptions << "io-console" + + install_gemfile! "" + create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}") + FileUtils.chmod(0o777, bundled_app("script.rb")) + bundle! "exec ./script.rb", :artifice => nil, :env => { :RUBYOPT => activation_warning_hack_rubyopt } + expect(last_command.stdout).to eq("{}") + end + + let(:default_gems) do + ruby!(<<-RUBY).split("\n") + if Gem::Specification.is_a?(Enumerable) + puts Gem::Specification.select(&:default_gem?).map(&:name) + end + RUBY + end + + it "activates newer versions of default gems" do + build_repo4 do + default_gems.each do |g| + build_gem g, "999999" + end + end + + default_gems.reject! {|g| exemptions.include?(g) } + + install_gemfile! <<-G + source "file:#{gem_repo4}" + #{default_gems}.each do |g| + gem g, "999999" + end + G + + expect(the_bundle).to include_gems(*default_gems.map {|g| "#{g} 999999" }) + end + + it "activates older versions of default gems" do + build_repo4 do + default_gems.each do |g| + build_gem g, "0.0.0.a" + end + end + + default_gems.reject! {|g| exemptions.include?(g) } + + install_gemfile! <<-G + source "file:#{gem_repo4}" + #{default_gems}.each do |g| + gem g, "0.0.0.a" + end + G + + expect(the_bundle).to include_gems(*default_gems.map {|g| "#{g} 0.0.0.a" }) + end + end + end + + describe "after setup" do + it "allows calling #gem on random objects", :bundler => "< 3" do + install_gemfile <<-G + source "file:#{gem_repo1}" + gem "rack" + G + + ruby! <<-RUBY + require "bundler/setup" + Object.new.gem "rack" + puts Gem.loaded_specs["rack"].full_name + RUBY + + expect(out).to eq("rack-1.0.0") + end + + it "keeps Kernel#gem private", :bundler => "3" do + install_gemfile! <<-G + source "file:#{gem_repo1}" + gem "rack" + G + + ruby <<-RUBY + require "bundler/setup" + Object.new.gem "rack" + puts "FAIL" + RUBY + + expect(last_command.stdboth).not_to include "FAIL" + expect(last_command.stderr).to include "private method `gem'" + end + + it "keeps Kernel#require private" do + install_gemfile! <<-G + source "file:#{gem_repo1}" + gem "rack" + G + + ruby <<-RUBY + require "bundler/setup" + Object.new.require "rack" + puts "FAIL" + RUBY + + expect(last_command.stdboth).not_to include "FAIL" + expect(last_command.stderr).to include "private method `require'" + end + end +end diff --git a/spec/bundler/runtime/with_clean_env_spec.rb b/spec/bundler/runtime/with_clean_env_spec.rb new file mode 100644 index 00000000000000..da8e37b45d967d --- /dev/null +++ b/spec/bundler/runtime/with_clean_env_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +RSpec.describe "Bundler.with_env helpers" do + def bundle_exec_ruby!(code, *args) + opts = args.last.is_a?(Hash) ? args.pop : {} + env = opts[:env] ||= {} + env[:RUBYOPT] ||= "-r#{spec_dir.join("support/hax")}" + args.push opts + bundle! "exec '#{Gem.ruby}' -e #{code}", *args + end + + describe "Bundler.original_env" do + before do + bundle "config path vendor/bundle" + gemfile "" + bundle "install" + end + + it "should return the PATH present before bundle was activated" do + code = "print Bundler.original_env['PATH']" + path = `getconf PATH`.strip + "#{File::PATH_SEPARATOR}/foo" + with_path_as(path) do + bundle_exec_ruby!(code.dump) + expect(last_command.stdboth).to eq(path) + end + end + + it "should return the GEM_PATH present before bundle was activated" do + code = "print Bundler.original_env['GEM_PATH']" + gem_path = ENV["GEM_PATH"] + ":/foo" + with_gem_path_as(gem_path) do + bundle_exec_ruby!(code.dump) + expect(last_command.stdboth).to eq(gem_path) + end + end + + it "works with nested bundle exec invocations", :ruby_repo do + create_file("exe.rb", <<-'RB') + count = ARGV.first.to_i + exit if count < 0 + STDERR.puts "#{count} #{ENV["PATH"].end_with?(":/foo")}" + if count == 2 + ENV["PATH"] = "#{ENV["PATH"]}:/foo" + end + exec(Gem.ruby, __FILE__, (count - 1).to_s) + RB + path = `getconf PATH`.strip + File::PATH_SEPARATOR + File.dirname(Gem.ruby) + with_path_as(path) do + bundle! "exec '#{Gem.ruby}' #{bundled_app("exe.rb")} 2", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + end + expect(err).to eq <<-EOS.strip +2 false +1 true +0 true + EOS + end + + it "removes variables that bundler added", :ruby_repo do + original = ruby!('puts ENV.to_a.map {|e| e.join("=") }.sort.join("\n")', :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" }) + code = 'puts Bundler.original_env.to_a.map {|e| e.join("=") }.sort.join("\n")' + bundle! "exec '#{Gem.ruby}' -e #{code.dump}", :env => { :RUBYOPT => "-r#{spec_dir.join("support/hax")}" } + expect(out).to eq original + end + end + + describe "Bundler.clean_env", :bundler => "< 3" do + before do + bundle "config path vendor/bundle" + gemfile "" + bundle "install" + end + + it "should delete BUNDLE_PATH" do + code = "print Bundler.clean_env.has_key?('BUNDLE_PATH')" + ENV["BUNDLE_PATH"] = "./foo" + bundle_exec_ruby! code.dump + expect(last_command.stdboth).to eq "false" + end + + it "should remove '-rbundler/setup' from RUBYOPT" do + code = "print Bundler.clean_env['RUBYOPT']" + ENV["RUBYOPT"] = "-W2 -rbundler/setup" + bundle_exec_ruby! code.dump + expect(last_command.stdboth).not_to include("-rbundler/setup") + end + + it "should clean up RUBYLIB", :ruby_repo do + code = "print Bundler.clean_env['RUBYLIB']" + ENV["RUBYLIB"] = root.join("lib").to_s + File::PATH_SEPARATOR + "/foo" + bundle_exec_ruby! code.dump + expect(last_command.stdboth).to eq("/foo") + end + + it "should restore the original MANPATH" do + code = "print Bundler.clean_env['MANPATH']" + ENV["MANPATH"] = "/foo" + ENV["BUNDLER_ORIG_MANPATH"] = "/foo-original" + bundle_exec_ruby! code.dump + expect(last_command.stdboth).to eq("/foo-original") + end + end + + describe "Bundler.with_original_env" do + it "should set ENV to original_env in the block" do + expected = Bundler.original_env + actual = Bundler.with_original_env { ENV.to_hash } + expect(actual).to eq(expected) + end + + it "should restore the environment after execution" do + Bundler.with_original_env do + ENV["FOO"] = "hello" + end + + expect(ENV).not_to have_key("FOO") + end + end + + describe "Bundler.with_clean_env", :bundler => "< 3" do + it "should set ENV to clean_env in the block" do + expected = Bundler.clean_env + actual = Bundler.with_clean_env { ENV.to_hash } + expect(actual).to eq(expected) + end + + it "should restore the environment after execution" do + Bundler.with_clean_env do + ENV["FOO"] = "hello" + end + + expect(ENV).not_to have_key("FOO") + end + end + + describe "Bundler.clean_system", :ruby => ">= 1.9", :bundler => "< 3" do + it "runs system inside with_clean_env" do + Bundler.clean_system(%(echo 'if [ "$BUNDLE_PATH" = "" ]; then exit 42; else exit 1; fi' | /bin/sh)) + expect($?.exitstatus).to eq(42) + end + end + + describe "Bundler.clean_exec", :ruby => ">= 1.9", :bundler => "< 3" do + it "runs exec inside with_clean_env" do + pid = Kernel.fork do + Bundler.clean_exec(%(echo 'if [ "$BUNDLE_PATH" = "" ]; then exit 42; else exit 1; fi' | /bin/sh)) + end + Process.wait(pid) + expect($?.exitstatus).to eq(42) + end + end +end diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb new file mode 100644 index 00000000000000..c7614e1c4355d5 --- /dev/null +++ b/spec/bundler/spec_helper.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +$:.unshift File.expand_path("..", __FILE__) +$:.unshift File.expand_path("../../lib", __FILE__) + +require "rubygems" +module Gem + if defined?(@path_to_default_spec_map) + @path_to_default_spec_map.delete_if do |_path, spec| + spec.name == "bundler" + end + end +end + +begin + require File.expand_path("../support/path.rb", __FILE__) + spec = Gem::Specification.load(Spec::Path.gemspec.to_s) + rspec = spec.dependencies.find {|d| d.name == "rspec" } + gem "rspec", rspec.requirement.to_s + require "rspec" + require "diff/lcs" +rescue LoadError + abort "Run rake spec:deps to install development dependencies" +end + +require "bundler/psyched_yaml" +require "bundler/vendored_fileutils" +require "uri" +require "digest" + +# Delete the default copy of Bundler that RVM installs for us when running in CI +require "fileutils" +if ENV.select {|k, _v| k =~ /TRAVIS/ }.any? && Gem::Version.new(Gem::VERSION) > Gem::Version.new("2.0") + Dir.glob(File.join(Gem::Specification.default_specifications_dir, "bundler*.gemspec")).each do |file| + FileUtils.rm_rf(file) + end + + Dir.glob(File.join(RbConfig::CONFIG["sitelibdir"], "bundler*")).each do |file| + FileUtils.rm_rf(file) + end +end + +if File.expand_path(__FILE__) =~ %r{([^\w/\.:\-])} + abort "The bundler specs cannot be run from a path that contains special characters (particularly #{$1.inspect})" +end + +require "bundler" + +Dir["#{File.expand_path("../support", __FILE__)}/*.rb"].each do |file| + file = file.gsub(%r{\A#{Regexp.escape File.expand_path("..", __FILE__)}/}, "") + require file unless file.end_with?("hax.rb") +end + +$debug = false + +Spec::Manpages.setup +Spec::Rubygems.setup +FileUtils.rm_rf(Spec::Path.gem_repo1) +ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -r#{Spec::Path.spec_dir}/support/hax.rb" +ENV["BUNDLE_SPEC_RUN"] = "true" + +# Don't wrap output in tests +ENV["THOR_COLUMNS"] = "10000" + +Spec::CodeClimate.setup + +module Gem + def self.ruby=(ruby) + @ruby = ruby + end +end + +RSpec.configure do |config| + config.include Spec::Builders + config.include Spec::Helpers + config.include Spec::Indexes + config.include Spec::Matchers + config.include Spec::Path + config.include Spec::Rubygems + config.include Spec::Platforms + config.include Spec::Sudo + config.include Spec::Permissions + + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + config.disable_monkey_patching! + + # Since failures cause us to keep a bunch of long strings in memory, stop + # once we have a large number of failures (indicative of core pieces of + # bundler being broken) so that running the full test suite doesn't take + # forever due to memory constraints + config.fail_fast ||= 25 if ENV["CI"] + + if ENV["BUNDLER_SUDO_TESTS"] && Spec::Sudo.present? + config.filter_run :sudo => true + else + config.filter_run_excluding :sudo => true + end + + if ENV["BUNDLER_REALWORLD_TESTS"] + config.filter_run :realworld => true + else + config.filter_run_excluding :realworld => true + end + + git_version = Bundler::Source::Git::GitProxy.new(nil, nil, nil).version + + config.filter_run_excluding :ruby => LessThanProc.with(RUBY_VERSION) + config.filter_run_excluding :rubygems => LessThanProc.with(Gem::VERSION) + config.filter_run_excluding :git => LessThanProc.with(git_version) + config.filter_run_excluding :rubygems_master => (ENV["RGV"] != "master") + config.filter_run_excluding :bundler => LessThanProc.with(Bundler::VERSION.split(".")[0, 2].join(".")) + config.filter_run_excluding :ruby_repo => !(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]).nil? + + config.filter_run_when_matching :focus unless ENV["CI"] + + original_wd = Dir.pwd + original_env = ENV.to_hash.delete_if {|k, _v| k.start_with?(Bundler::EnvironmentPreserver::BUNDLER_PREFIX) } + + config.expect_with :rspec do |c| + c.syntax = :expect + end + + config.around :each do |example| + if ENV["BUNDLE_RUBY"] + orig_ruby = Gem.ruby + Gem.ruby = ENV["BUNDLE_RUBY"] + end + example.run + Gem.ruby = orig_ruby if ENV["BUNDLE_RUBY"] + end + + config.before :suite do + if ENV["BUNDLE_RUBY"] + FileUtils.cp_r Spec::Path.bindir, File.join(Spec::Path.root, "lib", "exe") + end + end + + config.before :all do + build_repo1 + end + + config.before :each do + reset! + system_gems [] + in_app_root + @command_executions = [] + end + + config.after :each do |example| + all_output = @command_executions.map(&:to_s_verbose).join("\n\n") + if example.exception && !all_output.empty? + warn all_output unless config.formatters.grep(RSpec::Core::Formatters::DocumentationFormatter).empty? + message = example.exception.message + "\n\nCommands:\n#{all_output}" + (class << example.exception; self; end).send(:define_method, :message) do + message + end + end + + Dir.chdir(original_wd) + ENV.replace(original_env) + end + + config.after :suite do + if ENV["BUNDLE_RUBY"] + FileUtils.rm_rf File.join(Spec::Path.root, "lib", "exe") + end + end +end diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb new file mode 100644 index 00000000000000..01e8eb78373441 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/compact_index*/lib")].first.to_s +require "compact_index" + +class CompactIndexAPI < Endpoint + helpers do + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.open(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")).read)) + end + + def etag_response + response_body = yield + checksum = Digest(:MD5).hexdigest(response_body) + return if not_modified?(checksum) + headers "ETag" => quote(checksum) + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + rescue => e + puts e + puts e.backtrace + raise + end + + def not_modified?(checksum) + etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) + + return unless etags.include?(checksum) + headers "ETag" => quote(checksum) + status 304 + body "" + end + + def requested_range_for(response_body) + ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) + + if ranges + status 206 + body ranges.map! {|range| slice_body(response_body, range) }.join + else + status 200 + body response_body + end + end + + def quote(string) + %("#{string}") + end + + def parse_etags(value) + value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] + end + + def slice_body(body, range) + if body.respond_to?(:byteslice) + body.byteslice(range) + else # pre-1.9.3 + body.unpack("@#{range.first}a#{range.end + 1}").first + end + end + + def gems(gem_repo = GEM_REPO) + @gems ||= {} + @gems[gem_repo] ||= begin + specs = Bundler::Deprecate.skip_during do + %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| + load_spec(name, version, platform, gem_repo) + end + end.flatten + end + + specs.group_by(&:name).map do |name, versions| + gem_versions = versions.map do |spec| + deps = spec.dependencies.select {|d| d.type == :runtime }.map do |d| + reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") + CompactIndex::Dependency.new(d.name, reqs) + end + checksum = begin + Digest::SHA256.file("#{GEM_REPO}/gems/#{spec.original_name}.gem").base64digest + rescue + nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version, spec.required_rubygems_version) + end + CompactIndex::Gem.new(name, gem_versions) + end + end + end + end + + get "/names" do + etag_response do + CompactIndex.names(gems.map(&:name)) + end + end + + get "/versions" do + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end + + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +Artifice.activate_with(CompactIndexAPI) diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb new file mode 100644 index 00000000000000..d4e68c38e8366d --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexApiMissing < CompactIndexAPI + get "/fetch/actual/gem/:id" do + $stderr.puts params[:id] + if params[:id] == "rack-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexApiMissing) diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb new file mode 100644 index 00000000000000..97aa6cbd84f5e8 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(CompactIndexBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb new file mode 100644 index 00000000000000..62feb9f1644e88 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexChecksumMismatch < CompactIndexAPI + get "/versions" do + headers "ETag" => quote("123") + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + body "" + end +end + +Artifice.activate_with(CompactIndexChecksumMismatch) diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb new file mode 100644 index 00000000000000..972ecb88b7504f --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexConcurrentDownload < CompactIndexAPI + get "/versions" do + versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") + + # Verify the original (empty) content hasn't been deleted, e.g. on a retry + File.read(versions) == "" || raise("Original file should be present and empty") + + # Verify this is only requested once for a partial download + env["HTTP_RANGE"] || raise("Missing Range header for expected partial download") + + # Overwrite the file in parallel, which should be then overwritten + # after a successful download to prevent corruption + File.open(versions, "w") {|f| f.puts "another process" } + + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end +end + +Artifice.activate_with(CompactIndexConcurrentDownload) diff --git a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb new file mode 100644 index 00000000000000..0d349bcc1ed3ff --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexCredsDiffHost < CompactIndexAPI + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass] + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.com/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.read("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexCredsDiffHost) diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb new file mode 100644 index 00000000000000..84d1859235d41b --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexExtra < CompactIndexAPI + get "/extra/versions" do + halt 404 + end + + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo2}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(CompactIndexExtra) diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb new file mode 100644 index 00000000000000..903aa900fb7f28 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexExtraApi < CompactIndexAPI + get "/extra/names" do + etag_response do + CompactIndex.names(gems(gem_repo4).map(&:name)) + end + end + + get "/extra/versions" do + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems(gem_repo4)) + file.contents + end + end + + get "/extra/info/:name" do + etag_response do + gem = gems(gem_repo4).find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo4}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(CompactIndexExtraApi) diff --git a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb new file mode 100644 index 00000000000000..e72040f60490be --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index_extra_api", __FILE__) + +Artifice.deactivate + +class CompactIndexExtraAPIMissing < CompactIndexExtraApi + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexExtraAPIMissing) diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb new file mode 100644 index 00000000000000..67a9d23691a168 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index_extra", __FILE__) + +Artifice.deactivate + +class CompactIndexExtraMissing < CompactIndexExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexExtraMissing) diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb new file mode 100644 index 00000000000000..0a4dfdb2e83176 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexForbidden < CompactIndexAPI + get "/versions" do + halt 403 + end +end + +Artifice.activate_with(CompactIndexForbidden) diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb new file mode 100644 index 00000000000000..ab371117deb72c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexHostRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(CompactIndexHostRedirect) diff --git a/spec/bundler/support/artifice/compact_index_no_gem.rb b/spec/bundler/support/artifice/compact_index_no_gem.rb new file mode 100644 index 00000000000000..01c5be1b3d5739 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_no_gem.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexNoGem < CompactIndexAPI + get "/gems/:id" do + halt 500 + end +end + +Artifice.activate_with(CompactIndexNoGem) diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb new file mode 100644 index 00000000000000..eaedff51056ada --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexPartialUpdate < CompactIndexAPI + # Stub the server to never return 304s. This simulates the behaviour of + # Fastly / Rubygems ignoring ETag headers. + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.read(cached_versions_path).start_with?("created_at: ") + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env["HTTP_RANGE"] == "bytes=#{File.read(cached_versions_path).bytesize - 1}-" + raise("Range header should be present, and start from the index of the final byte of the cache.") + end + + etag_response do + # Return the exact contents of the cache. + File.read(cached_versions_path) + end + end +end + +Artifice.activate_with(CompactIndexPartialUpdate) diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb new file mode 100644 index 00000000000000..487be4771a3e7c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexRangeNotSatisfiable < CompactIndexAPI + get "/versions" do + if env["HTTP_RANGE"] + status 416 + else + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end + end + + get "/info/:name" do + if env["HTTP_RANGE"] + status 416 + else + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + end +end + +Artifice.activate_with(CompactIndexRangeNotSatisfiable) diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb new file mode 100644 index 00000000000000..e83451b5b67106 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(CompactIndexRedirect) diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb new file mode 100644 index 00000000000000..abbf3258e7853a --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexStrictBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 403, "Authentication failed" + end + end +end + +Artifice.activate_with(CompactIndexStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb new file mode 100644 index 00000000000000..7e1d3686e2ab64 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexWrongDependencies < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + gem.versions.each {|gv| gv.dependencies.clear } if gem + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +Artifice.activate_with(CompactIndexWrongDependencies) diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb new file mode 100644 index 00000000000000..db4d8e397425e5 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexWrongGemChecksum < CompactIndexAPI + get "/info/:name" do + etag_response do + name = params[:name] + gem = gems.find {|g| g.name == name } + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } + versions = gem ? gem.versions : [] + versions.each {|v| v.checksum = checksum } + CompactIndex.info(versions) + end + end +end + +Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb new file mode 100644 index 00000000000000..12a6fa153f5c9d --- /dev/null +++ b/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint_marshal_fail", __FILE__) + +Artifice.deactivate + +class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(EndpointMarshalFailBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb new file mode 100644 index 00000000000000..9a0cfae8a28e04 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require File.expand_path("../../path.rb", __FILE__) +require Spec::Path.root.join("lib/bundler/deprecate") +include Spec::Path + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,rack,tilt,sinatra}-*/lib")].map(&:to_s)) +require "artifice" +require "sinatra/base" + +ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant +ALL_REQUESTS_MUTEX = Mutex.new + +at_exit do + if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] + expected = expected.split("\n").sort + actual = ALL_REQUESTS.sort + + unless expected == actual + raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" + end + end +end + +class Endpoint < Sinatra::Base + def self.all_requests + @all_requests ||= [] + end + + GEM_REPO = Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + set :raise_errors, true + set :show_exceptions, false + + def call!(*) + super.tap do + ALL_REQUESTS_MUTEX.synchronize do + ALL_REQUESTS << @request.url + end + end + end + + helpers do + def dependencies_for(gem_names, gem_repo = GEM_REPO) + return [] if gem_names.nil? || gem_names.empty? + + require "rubygems" + require "bundler" + Bundler::Deprecate.skip_during do + all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read) + end.inject(:+) + + all_specs.map do |name, version, platform| + spec = load_spec(name, version, platform, gem_repo) + next unless gem_names.include?(spec.name) + { + :name => spec.name, + :number => spec.version.version, + :platform => spec.platform.to_s, + :dependencies => spec.dependencies.select {|dep| dep.type == :runtime }.map do |dep| + [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] + end + } + end.compact + end + end + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.open(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")).read)) + end + end + + get "/quick/Marshal.4.8/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/fetch/actual/gem/:id" do + File.read("#{GEM_REPO}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/gems/:id" do + File.read("#{GEM_REPO}/gems/#{params[:id]}") + end + + get "/api/v1/dependencies" do + Marshal.dump(dependencies_for(params[:gems])) + end + + get "/specs.4.8.gz" do + File.read("#{GEM_REPO}/specs.4.8.gz") + end + + get "/prerelease_specs.4.8.gz" do + File.read("#{GEM_REPO}/prerelease_specs.4.8.gz") + end +end + +Artifice.activate_with(Endpoint) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb new file mode 100644 index 00000000000000..202ccfc8297778 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require File.expand_path("../../path.rb", __FILE__) +include Spec::Path + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,rack,tilt,sinatra}-*/lib")].map(&:to_s)) + +require "artifice" +require "sinatra/base" + +Artifice.deactivate + +class Endpoint500 < Sinatra::Base + before do + halt 500 + end +end + +Artifice.activate_with(Endpoint500) diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb new file mode 100644 index 00000000000000..bb89747adcfa1f --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointApiForbidden < Endpoint + get "/api/v1/dependencies" do + halt 403 + end +end + +Artifice.activate_with(EndpointApiForbidden) diff --git a/spec/bundler/support/artifice/endpoint_api_missing.rb b/spec/bundler/support/artifice/endpoint_api_missing.rb new file mode 100644 index 00000000000000..95db8e2a7e6965 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_missing.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointApiMissing < Endpoint + get "/fetch/actual/gem/:id" do + $stderr.puts params[:id] + if params[:id] == "rack-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointApiMissing) diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb new file mode 100644 index 00000000000000..223671bc299832 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(EndpointBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb new file mode 100644 index 00000000000000..925954b12d9f29 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointCredsDiffHost < Endpoint + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass] + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.com/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.read("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointCredsDiffHost) diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb new file mode 100644 index 00000000000000..422f65401ba435 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointExtra < Endpoint + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo2}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(EndpointExtra) diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb new file mode 100644 index 00000000000000..62e2c2bb93291e --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointExtraApi < Endpoint + get "/extra/api/v1/dependencies" do + deps = dependencies_for(params[:gems], gem_repo4) + Marshal.dump(deps) + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo4}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(EndpointExtraApi) diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb new file mode 100644 index 00000000000000..038a12610a490d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint_extra", __FILE__) + +Artifice.deactivate + +class EndpointExtraMissing < EndpointExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointExtraMissing) diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb new file mode 100644 index 00000000000000..554c08f0a20960 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointFallback < Endpoint + DEPENDENCY_LIMIT = 60 + + get "/api/v1/dependencies" do + if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT + Marshal.dump(dependencies_for(params[:gems])) + else + halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems" + end + end +end + +Artifice.activate_with(EndpointFallback) diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb new file mode 100644 index 00000000000000..cda5664be2445f --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointHostRedirect < Endpoint + get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(EndpointHostRedirect) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb new file mode 100644 index 00000000000000..2a5dcdc2fd19e4 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint_fallback", __FILE__) + +Artifice.deactivate + +class EndpointMarshalFail < EndpointFallback + get "/api/v1/dependencies" do + "f0283y01hasf" + end +end + +Artifice.activate_with(EndpointMarshalFail) diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb new file mode 100644 index 00000000000000..64452f198d2905 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +class EndpointMirrorSource < Endpoint + get "/gems/:id" do + if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" + File.read("#{gem_repo1}/gems/#{params[:id]}") + else + halt 500 + end + end +end + +Artifice.activate_with(EndpointMirrorSource) diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb new file mode 100644 index 00000000000000..ebf01458ba9182 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointRedirect < Endpoint + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(EndpointRedirect) diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb new file mode 100644 index 00000000000000..905a519f3f2ba1 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointStrictBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 403, "Authentication failed" + end + end +end + +Artifice.activate_with(EndpointStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb new file mode 100644 index 00000000000000..3f60471c907d03 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require File.expand_path("../endpoint_fallback", __FILE__) + +Artifice.deactivate + +class EndpointTimeout < EndpointFallback + SLEEP_TIMEOUT = 3 + + get "/api/v1/dependencies" do + sleep(SLEEP_TIMEOUT) + end +end + +Artifice.activate_with(EndpointTimeout) diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb new file mode 100644 index 00000000000000..1059c6df4e8598 --- /dev/null +++ b/spec/bundler/support/artifice/fail.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "net/http" +begin + require "net/https" +rescue LoadError + nil # net/https or openssl +end + +# We can't use artifice here because it uses rack + +module Artifice; end # for < 2.0, Net::HTTP::Persistent::SSLReuse + +class Fail < Net::HTTP + # Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Net::HTTP, we must set it + @newimpl = true + + def request(req, body = nil, &block) + raise(exception(req)) + end + + # Ensure we don't start a connect here + def connect + end + + def exception(req) + name = ENV.fetch("BUNDLER_SPEC_EXCEPTION") { "Errno::ENETUNREACH" } + const = name.split("::").reduce(Object) {|mod, sym| mod.const_get(sym) } + const.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") + end +end + +# Replace Net::HTTP with our failing subclass +::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, ::Fail) +end diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb new file mode 100644 index 00000000000000..edd2f49a916a21 --- /dev/null +++ b/spec/bundler/support/artifice/vcr.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require "net/http" +if RUBY_VERSION < "1.9" + begin + require "net/https" + rescue LoadError + nil # net/https or openssl + end +end # but only for 1.8 + +CASSETTE_PATH = File.expand_path("../vcr_cassettes", __FILE__) +CASSETTE_NAME = ENV.fetch("BUNDLER_SPEC_VCR_CASSETTE_NAME") { "realworld" } + +class BundlerVCRHTTP < Net::HTTP + class RequestHandler + attr_reader :http, :request, :body, :response_block + def initialize(http, request, body = nil, &response_block) + @http = http + @request = request + @body = body + @response_block = response_block + end + + def handle_request + handler = self + request.instance_eval do + @__vcr_request_handler = handler + end + + if recorded_response? + recorded_response + else + record_response + end + end + + def recorded_response? + return true if ENV["BUNDLER_SPEC_PRE_RECORDED"] + return false if ENV["BUNDLER_SPEC_FORCE_RECORD"] + request_pair_paths.all? {|f| File.exist?(f) } + end + + def recorded_response + File.open(request_pair_paths.last, "rb:ASCII-8BIT") do |response_file| + response_io = ::Net::BufferedIO.new(response_file) + ::Net::HTTPResponse.read_new(response_io).tap do |response| + response.decode_content = request.decode_content if request.respond_to?(:decode_content) + response.uri = request.uri if request.respond_to?(:uri) + + response.reading_body(response_io, request.response_body_permitted?) do + response_block.call(response) if response_block + end + end + end + end + + def record_response + request_path, response_path = *request_pair_paths + + @recording = true + + response = http.request_without_vcr(request, body, &response_block) + @recording = false + unless @recording + FileUtils.mkdir_p(File.dirname(request_path)) + binwrite(request_path, request_to_string(request)) + binwrite(response_path, response_to_string(response)) + end + response + end + + def key + [request["host"] || http.address, request.path, request.method].compact + end + + def file_name_for_key(key) + key.join("/").gsub(/[\:*?"<>|]/, "-") + end + + def request_pair_paths + %w[request response].map do |kind| + File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key + [kind])) + end + end + + def read_stored_request(path) + contents = File.read(path) + headers = {} + method = nil + path = nil + contents.lines.grep(/^> /).each do |line| + if line =~ /^> (GET|HEAD|POST|PATCH|PUT|DELETE) (.*)/ + method = $1 + path = $2.strip + elsif line =~ /^> (.*?): (.*)/ + headers[$1] = $2 + end + end + body = contents =~ /^([^>].*)/m && $1 + Net::HTTP.const_get(method.capitalize).new(path, headers).tap {|r| r.body = body if body } + end + + def request_to_string(request) + request_string = [] + request_string << "> #{request.method.upcase} #{request.path}" + request.to_hash.each do |key, value| + request_string << "> #{key}: #{Array(value).first}" + end + request << "" << request.body if request.body + request_string.join("\n") + end + + def response_to_string(response) + headers = response.to_hash + body = response.body + + response_string = [] + response_string << "HTTP/1.1 #{response.code} #{response.message}" + + headers["content-length"] = [body.bytesize.to_s] if body + + headers.each do |header, value| + response_string << "#{header}: #{value.join(", ")}" + end + + response_string << "" << body + + response_string = response_string.join("\n") + if response_string.respond_to?(:force_encoding) + response_string.force_encoding("ASCII-8BIT") + else + response_string + end + end + + def binwrite(path, contents) + File.open(path, "wb:ASCII-8BIT") {|f| f.write(contents) } + end + end + + def request_with_vcr(request, *args, &block) + handler = request.instance_eval do + remove_instance_variable(:@__vcr_request_handler) if defined?(@__vcr_request_handler) + end || RequestHandler.new(self, request, *args, &block) + + handler.handle_request + end + + alias_method :request_without_vcr, :request + alias_method :request, :request_with_vcr +end + +# Replace Net::HTTP with our VCR subclass +::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, BundlerVCRHTTP) +end diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb new file mode 100644 index 00000000000000..f39b2c6d53dc3e --- /dev/null +++ b/spec/bundler/support/artifice/windows.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require File.expand_path("../../path.rb", __FILE__) +include Spec::Path + +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/artifice*/lib")].first.to_s +$LOAD_PATH.unshift(*Dir[base_system_gems.join("gems/rack-*/lib")]) +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/tilt*/lib")].first.to_s +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/sinatra*/lib")].first.to_s +require "artifice" +require "sinatra/base" + +Artifice.deactivate + +class Windows < Sinatra::Base + set :raise_errors, true + set :show_exceptions, false + + helpers do + def gem_repo + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + end + end + + files = ["specs.4.8.gz", + "prerelease_specs.4.8.gz", + "quick/Marshal.4.8/rcov-1.0-mswin32.gemspec.rz", + "gems/rcov-1.0-mswin32.gem"] + + files.each do |file| + get "/#{file}" do + File.read gem_repo.join(file) + end + end + + get "/gems/rcov-1.0-x86-mswin32.gem" do + halt 404 + end + + get "/api/v1/dependencies" do + halt 404 + end + + get "/versions" do + halt 500 + end +end + +Artifice.activate_with(Windows) diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb new file mode 100644 index 00000000000000..97134a045a73cc --- /dev/null +++ b/spec/bundler/support/builders.rb @@ -0,0 +1,819 @@ +# frozen_string_literal: true + +require "bundler/shared_helpers" +require "shellwords" + +module Spec + module Builders + def self.constantize(name) + name.delete("-").upcase + end + + def v(version) + Gem::Version.new(version) + end + + def pl(platform) + Gem::Platform.new(platform) + end + + def build_repo1 + build_repo gem_repo1 do + build_gem "rack", %w[0.9.1 1.0.0] do |s| + s.executables = "rackup" + s.post_install_message = "Rack's post install message" + end + + build_gem "thin" do |s| + s.add_dependency "rack" + s.post_install_message = "Thin's post install message" + end + + build_gem "rack-obama" do |s| + s.add_dependency "rack" + s.post_install_message = "Rack-obama's post install message" + end + + build_gem "rack_middleware", "1.0" do |s| + s.add_dependency "rack", "0.9.1" + end + + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.add_dependency "rake", "10.0.2" + s.add_dependency "actionpack", "2.3.2" + s.add_dependency "activerecord", "2.3.2" + s.add_dependency "actionmailer", "2.3.2" + s.add_dependency "activeresource", "2.3.2" + end + build_gem "actionpack", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activerecord", ["2.3.1", "2.3.2"] do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "actionmailer", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activeresource", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activesupport", %w[1.2.3 2.3.2 2.3.5] + + build_gem "activemerchant" do |s| + s.add_dependency "activesupport", ">= 2.0.0" + end + + build_gem "rails_fail" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + + build_gem "missing_dep" do |s| + s.add_dependency "not_here" + end + + build_gem "rspec", "1.2.7", :no_default => true do |s| + s.write "lib/spec.rb", "SPEC = '1.2.7'" + end + + build_gem "rack-test", :no_default => true do |s| + s.write "lib/rack/test.rb", "RACK_TEST = '1.0'" + end + + build_gem "platform_specific" do |s| + s.platform = Bundler.local_platform + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Bundler.local_platform}'" + end + + build_gem "platform_specific" do |s| + s.platform = "java" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 JAVA'" + end + + build_gem "platform_specific" do |s| + s.platform = "ruby" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 RUBY'" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-mswin32" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 MSWIN'" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-mingw32" + end + + build_gem "platform_specific" do |s| + s.platform = "x64-mingw32" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-darwin-100" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 x86-darwin-100'" + end + + build_gem "only_java", "1.0" do |s| + s.platform = "java" + s.write "lib/only_java.rb", "ONLY_JAVA = '1.0.0 JAVA'" + end + + build_gem "only_java", "1.1" do |s| + s.platform = "java" + s.write "lib/only_java.rb", "ONLY_JAVA = '1.1.0 JAVA'" + end + + build_gem "nokogiri", "1.4.2" + build_gem "nokogiri", "1.4.2" do |s| + s.platform = "java" + s.write "lib/nokogiri.rb", "NOKOGIRI = '1.4.2 JAVA'" + s.add_dependency "weakling", ">= 0.0.3" + end + + build_gem "laduradura", "5.15.2" + build_gem "laduradura", "5.15.2" do |s| + s.platform = "java" + s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" + end + build_gem "laduradura", "5.15.3" do |s| + s.platform = "java" + s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" + end + + build_gem "weakling", "0.0.3" + + build_gem "terranova", "8" + + build_gem "duradura", "7.0" + + build_gem "multiple_versioned_deps" do |s| + s.add_dependency "weakling", ">= 0.0.1", "< 0.1" + end + + build_gem "not_released", "1.0.pre" + + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + + build_gem "with_development_dependency" do |s| + s.add_development_dependency "activesupport", "= 2.3.5" + end + + build_gem "with_license" do |s| + s.license = "MIT" + end + + build_gem "with_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/implicit_rake_dep.rb", "w") do |f| + f.puts "IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + build_gem "another_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/another_implicit_rake_dep.rb", "w") do |f| + f.puts "ANOTHER_IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + build_gem "very_simple_binary", &:add_c_extension + build_gem "simple_binary", &:add_c_extension + + build_gem "bundler", "0.9" do |s| + s.executables = "bundle" + s.write "bin/bundle", "puts 'FAIL'" + end + + # The bundler 0.8 gem has a rubygems plugin that always loads :( + build_gem "bundler", "0.8.1" do |s| + s.write "lib/bundler/omg.rb", "" + s.write "lib/rubygems_plugin.rb", "require 'bundler/omg' ; puts 'FAIL'" + end + + build_gem "bundler_dep" do |s| + s.add_dependency "bundler" + end + + # The yard gem iterates over Gem.source_index looking for plugins + build_gem "yard" do |s| + s.write "lib/yard.rb", <<-Y + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("1.8.10") + specs = Gem::Specification + else + specs = Gem.source_index.find_name('') + end + specs.sort_by(&:name).each do |gem| + puts gem.full_name + end + Y + end + + # The rcov gem is platform mswin32, but has no arch + build_gem "rcov" do |s| + s.platform = Gem::Platform.new([nil, "mswin32", nil]) + s.write "lib/rcov.rb", "RCOV = '1.0.0'" + end + + build_gem "net-ssh" + build_gem "net-sftp", "1.1.1" do |s| + s.add_dependency "net-ssh", ">= 1.0.0", "< 1.99.0" + end + + # Test complicated gem dependencies for install + build_gem "net_a" do |s| + s.add_dependency "net_b" + s.add_dependency "net_build_extensions" + end + + build_gem "net_b" + + build_gem "net_build_extensions" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/net_build_extensions.rb", "w") do |f| + f.puts "NET_BUILD_EXTENSIONS = 'YES'" + end + end + RUBY + end + + build_gem "net_c" do |s| + s.add_dependency "net_a" + s.add_dependency "net_d" + end + + build_gem "net_d" + + build_gem "net_e" do |s| + s.add_dependency "net_d" + end + + # Capistrano did this (at least until version 2.5.10) + # RubyGems 2.2 doesn't allow the specifying of a dependency twice + # See https://github.com/rubygems/rubygems/commit/03dbac93a3396a80db258d9bc63500333c25bd2f + build_gem "double_deps", "1.0", :skip_validation => true do |s| + s.add_dependency "net-ssh", ">= 1.0.0" + s.add_dependency "net-ssh" + end + + build_gem "foo" + + # A minimal fake pry console + build_gem "pry" do |s| + s.write "lib/pry.rb", <<-RUBY + class Pry + class << self + def toplevel_binding + unless defined?(@toplevel_binding) && @toplevel_binding + TOPLEVEL_BINDING.eval %{ + def self.__pry__; binding; end + Pry.instance_variable_set(:@toplevel_binding, __pry__) + class << self; undef __pry__; end + } + end + @toplevel_binding.eval('private') + @toplevel_binding + end + + def __pry__ + while line = gets + begin + puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1') + rescue Exception => e + puts "\#{e.class}: \#{e.message}" + puts e.backtrace.first + end + end + end + alias start __pry__ + end + end + RUBY + end + end + end + + def build_repo2(&blk) + FileUtils.rm_rf gem_repo2 + FileUtils.cp_r gem_repo1, gem_repo2 + update_repo2(&blk) if block_given? + end + + def build_repo3 + build_repo gem_repo3 do + build_gem "rack" + end + FileUtils.rm_rf Dir[gem_repo3("prerelease*")] + end + + # A repo that has no pre-installed gems included. (The caller completely + # determines the contents with the block.) + def build_repo4(&blk) + FileUtils.rm_rf gem_repo4 + build_repo(gem_repo4, &blk) + end + + def update_repo4(&blk) + update_repo(gem_repo4, &blk) + end + + def update_repo2 + update_repo gem_repo2 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + yield if block_given? + end + end + + def build_security_repo + build_repo security_repo do + build_gem "rack" + + build_gem "signed_gem" do |s| + cert = "signing-cert.pem" + pkey = "signing-pkey.pem" + s.write cert, TEST_CERT + s.write pkey, TEST_PKEY + s.signing_key = pkey + s.cert_chain = [cert] + end + end + end + + def build_repo(path, &blk) + return if File.directory?(path) + rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + + if rake_path.nil? + Spec::Path.base_system_gems.rmtree + Spec::Rubygems.setup + rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + end + + if rake_path + FileUtils.mkdir_p("#{path}/gems") + FileUtils.cp rake_path, "#{path}/gems/" + else + abort "Your test gems are missing! Run `rm -rf #{tmp}` and try again." + end + + update_repo(path, &blk) + end + + def update_repo(path) + if path == gem_repo1 && caller.first.split(" ").last == "`build_repo`" + raise "Updating gem_repo1 is unsupported -- use gem_repo2 instead" + end + return unless block_given? + @_build_path = "#{path}/gems" + @_build_repo = File.basename(path) + yield + with_gem_path_as Path.base_system_gems do + Dir.chdir(path) { gem_command! :generate_index } + end + ensure + @_build_path = nil + @_build_repo = nil + end + + def build_index(&block) + index = Bundler::Index.new + IndexBuilder.run(index, &block) if block_given? + index + end + + def build_spec(name, version = "0.0.1", platform = nil, &block) + Array(version).map do |v| + Gem::Specification.new do |s| + s.name = name + s.version = Gem::Version.new(v) + s.platform = platform + s.authors = ["no one in particular"] + s.summary = "a gemspec used only for testing" + DepBuilder.run(s, &block) if block_given? + end + end + end + + def build_dep(name, requirements = Gem::Requirement.default, type = :runtime) + Bundler::Dependency.new(name, :version => requirements) + end + + def build_lib(name, *args, &blk) + build_with(LibBuilder, name, args, &blk) + end + + def build_gem(name, *args, &blk) + build_with(GemBuilder, name, args, &blk) + end + + def build_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + builder = opts[:bare] ? GitBareBuilder : GitBuilder + spec = build_with(builder, name, args, &block) + GitReader.new(opts[:path] || lib_path(spec.full_name)) + end + + def update_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + spec = build_with(GitUpdater, name, args, &block) + GitReader.new(opts[:path] || lib_path(spec.full_name)) + end + + def build_plugin(name, *args, &blk) + build_with(PluginBuilder, name, args, &blk) + end + + private + + def build_with(builder, name, args, &blk) + @_build_path ||= nil + @_build_repo ||= nil + options = args.last.is_a?(Hash) ? args.pop : {} + versions = args.last || "1.0" + spec = nil + + options[:path] ||= @_build_path + options[:source] ||= @_build_repo + + Array(versions).each do |version| + spec = builder.new(self, name, version) + spec.authors = ["no one"] if !spec.authors || spec.authors.empty? + yield spec if block_given? + spec._build(options) + end + + spec + end + + class IndexBuilder + include Builders + + def self.run(index, &block) + new(index).run(&block) + end + + def initialize(index) + @index = index + end + + def run(&block) + instance_eval(&block) + end + + def gem(*args, &block) + build_spec(*args, &block).each do |s| + @index << s + end + end + + def platforms(platforms) + platforms.split(/\s+/).each do |platform| + platform.gsub!(/^(mswin32)$/, 'x86-\1') + yield Gem::Platform.new(platform) + end + end + + def versions(versions) + versions.split(/\s+/).each {|version| yield v(version) } + end + end + + class DepBuilder + include Builders + + def self.run(spec, &block) + new(spec).run(&block) + end + + def initialize(spec) + @spec = spec + end + + def run(&block) + instance_eval(&block) + end + + def runtime(name, requirements) + @spec.add_runtime_dependency(name, requirements) + end + + def development(name, requirements) + @spec.add_development_dependency(name, requirements) + end + + def required_ruby_version=(*reqs) + @spec.required_ruby_version = *reqs + end + + alias_method :dep, :runtime + end + + class LibBuilder + def initialize(context, name, version) + @context = context + @name = name + @spec = Gem::Specification.new do |s| + s.name = name + s.version = version + s.summary = "This is just a fake gem for testing" + s.description = "This is a completely fake gem, for testing purposes." + s.author = "no one" + s.email = "foo@bar.baz" + s.homepage = "http://example.com" + s.license = "MIT" + end + @files = {} + end + + def method_missing(*args, &blk) + @spec.send(*args, &blk) + end + + def write(file, source = "") + @files[file] = source + end + + def executables=(val) + @spec.executables = Array(val) + @spec.executables.each do |file| + executable = "#{@spec.bindir}/#{file}" + shebang = if Bundler.current_ruby.jruby? + "#!/usr/bin/env jruby\n" + else + "#!/usr/bin/env ruby\n" + end + @spec.files << executable + write executable, "#{shebang}require '#{@name}' ; puts #{Builders.constantize(@name)}" + end + end + + def add_c_extension + require_paths << "ext" + extensions << "ext/extconf.rb" + write "ext/extconf.rb", <<-RUBY + require "mkmf" + + + # exit 1 unless with_config("simple") + + extension_name = "#{name}_c" + if extra_lib_dir = with_config("ext-lib") + # add extra libpath if --with-ext-lib is + # passed in as a build_arg + dir_config extension_name, nil, extra_lib_dir + else + dir_config extension_name + end + create_makefile extension_name + RUBY + write "ext/#{name}.c", <<-C + #include "ruby.h" + + void Init_#{name}_c() { + rb_define_module("#{Builders.constantize(name)}_IN_C"); + } + C + end + + def _build(options) + path = options[:path] || _default_path + + if options[:rubygems_version] + @spec.rubygems_version = options[:rubygems_version] + def @spec.mark_version; end + + def @spec.validate(*); end + end + + case options[:gemspec] + when false + # do nothing + when :yaml + @files["#{name}.gemspec"] = @spec.to_yaml + else + @files["#{name}.gemspec"] = @spec.to_ruby + end + + unless options[:no_default] + gem_source = options[:source] || "path@#{path}" + @files = _default_files. + merge("lib/#{name}/source.rb" => "#{Builders.constantize(name)}_SOURCE = #{gem_source.to_s.dump}"). + merge(@files) + end + + @spec.authors = ["no one"] + + @files.each do |file, source| + file = Pathname.new(path).join(file) + FileUtils.mkdir_p(file.dirname) + File.open(file, "w") {|f| f.puts source } + end + @spec.files = @files.keys + path + end + + def _default_files + @_default_files ||= begin + platform_string = " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY + { "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" } + end + end + + def _default_path + @context.tmp("libs", @spec.full_name) + end + end + + class GitBuilder < LibBuilder + def _build(options) + path = options[:path] || _default_path + source = options[:source] || "git@#{path}" + super(options.merge(:path => path, :source => source)) + Dir.chdir(path) do + `git init` + `git add *` + `git config user.email "lol@wut.com"` + `git config user.name "lolwut"` + `git commit -m 'OMG INITIAL COMMIT'` + end + end + end + + class GitBareBuilder < LibBuilder + def _build(options) + path = options[:path] || _default_path + super(options.merge(:path => path)) + Dir.chdir(path) do + `git init --bare` + end + end + end + + class GitUpdater < LibBuilder + def silently(str) + `#{str} 2>#{Bundler::NULL}` + end + + def _build(options) + libpath = options[:path] || _default_path + update_gemspec = options[:gemspec] || false + source = options[:source] || "git@#{libpath}" + + Dir.chdir(libpath) do + silently "git checkout master" + + if branch = options[:branch] + raise "You can't specify `master` as the branch" if branch == "master" + escaped_branch = Shellwords.shellescape(branch) + + if `git branch | grep #{escaped_branch}`.empty? + silently("git branch #{escaped_branch}") + end + + silently("git checkout #{escaped_branch}") + elsif tag = options[:tag] + `git tag #{Shellwords.shellescape(tag)}` + elsif options[:remote] + silently("git remote add origin file://#{options[:remote]}") + elsif options[:push] + silently("git push origin #{options[:push]}") + end + + current_ref = `git rev-parse HEAD`.strip + _default_files.keys.each do |path| + _default_files[path] += "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'" + end + super(options.merge(:path => libpath, :gemspec => update_gemspec, :source => source)) + `git add *` + `git commit -m "BUMP"` + end + end + end + + class GitReader + attr_reader :path + + def initialize(path) + @path = path + end + + def ref_for(ref, len = nil) + ref = git "rev-parse #{ref}" + ref = ref[0..len] if len + ref + end + + private + + def git(cmd) + Bundler::SharedHelpers.with_clean_git_env do + Dir.chdir(@path) { `git #{cmd}`.strip } + end + end + end + + class GemBuilder < LibBuilder + def _build(opts) + lib_path = super(opts.merge(:path => @context.tmp(".tmp/#{@spec.full_name}"), :no_default => opts[:no_default])) + destination = opts[:path] || _default_path + Dir.chdir(lib_path) do + FileUtils.mkdir_p(destination) + + @spec.authors = ["that guy"] if !@spec.authors || @spec.authors.empty? + + Bundler.rubygems.build(@spec, opts[:skip_validation]) + end + gem_path = File.expand_path("#{@spec.full_name}.gem", lib_path) + if opts[:to_system] + @context.system_gems gem_path, :keep_path => true + elsif opts[:to_bundle] + @context.system_gems gem_path, :path => :bundle_path, :keep_path => true + else + FileUtils.mv(gem_path, destination) + end + end + + def _default_path + @context.gem_repo1("gems") + end + end + + class PluginBuilder < GemBuilder + def _default_files + @_default_files ||= super.merge("plugins.rb" => "") + end + end + + TEST_CERT = <<-CERT.gsub(/^\s*/, "") + -----BEGIN CERTIFICATE----- + MIIDMjCCAhqgAwIBAgIBATANBgkqhkiG9w0BAQUFADAnMQwwCgYDVQQDDAN5b3Ux + FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy + NTAwMTIyM1owJzEMMAoGA1UEAwwDeW91MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs + ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANlvFdpN43c4DMS9Jo06 + m0a7k3bQ3HWQ1yrYhZMi77F1F73NpBknYHIzDktQpGn6hs/4QFJT4m4zNEBF47UL + jHU5nTK5rjkS3niGYUjvh3ZEzVeo9zHUlD/UwflDo4ALl3TSo2KY/KdPS/UTdLXL + ajkQvaVJtEDgBPE3DPhlj5whp+Ik3mDHej7qpV6F502leAwYaFyOtlEG/ZGNG+nZ + L0clH0j77HpP42AylHDi+vakEM3xcjo9BeWQ6Vkboic93c9RTt6CWBWxMQP7Nol1 + MOebz9XOSQclxpxWteXNfPRtMdAhmRl76SMI8ywzThNPpa4EH/yz34ftebVOgKyM + nd0CAwEAAaNpMGcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFA7D + n9qo0np23qi3aOYuAAPn/5IdMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYGA1Ud + EgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBBQUAA4IBAQA7Gyk62sWOUX/N + vk4tJrgKESph6Ns8+E36A7n3jt8zCep8ldzMvwTWquf9iqhsC68FilEoaDnUlWw7 + d6oNuaFkv7zfrWGLlvqQJC+cu2X5EpcCksg5oRp8VNbwJysJ6JgwosxzROII8eXc + R+j1j6mDvQYqig2QOnzf480pjaqbP+tspfDFZbhKPrgM3Blrb3ZYuFpv4zkqI7aB + 6fuk2DUhNO1CuwrJA84TqC+jGo73bDKaT5hrIDiaJRrN5+zcWja2uEWrj5jSbep4 + oXdEdyH73hOHMBP40uds3PqnUsxEJhzjB2sCCe1geV24kw9J4m7EQXPVkUKDgKrt + LlpDmOoo + -----END CERTIFICATE----- + CERT + + TEST_PKEY = <<-PKEY.gsub(/^\s*/, "") + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA2W8V2k3jdzgMxL0mjTqbRruTdtDcdZDXKtiFkyLvsXUXvc2k + GSdgcjMOS1CkafqGz/hAUlPibjM0QEXjtQuMdTmdMrmuORLeeIZhSO+HdkTNV6j3 + MdSUP9TB+UOjgAuXdNKjYpj8p09L9RN0tctqORC9pUm0QOAE8TcM+GWPnCGn4iTe + YMd6PuqlXoXnTaV4DBhoXI62UQb9kY0b6dkvRyUfSPvsek/jYDKUcOL69qQQzfFy + Oj0F5ZDpWRuiJz3dz1FO3oJYFbExA/s2iXUw55vP1c5JByXGnFa15c189G0x0CGZ + GXvpIwjzLDNOE0+lrgQf/LPfh+15tU6ArIyd3QIDAQABAoIBACbDqz20TS1gDMa2 + gj0DidNedbflHKjJHdNBru7Ad8NHgOgR1YO2hXdWquG6itVqGMbTF4SV9/R1pIcg + 7qvEV1I+50u31tvOBWOvcYCzU48+TO2n7gowQA3xPHPYHzog1uu48fAOHl0lwgD7 + av9OOK3b0jO5pC08wyTOD73pPWU0NrkTh2+N364leIi1pNuI1z4V+nEuIIm7XpVd + 5V4sXidMTiEMJwE6baEDfTjHKaoRndXrrPo3ryIXmcX7Ag1SwAQwF5fBCRToCgIx + dszEZB1bJD5gA6r+eGnJLB/F60nK607az5o3EdguoB2LKa6q6krpaRCmZU5svvoF + J7xgBPECgYEA8RIzHAQ3zbaibKdnllBLIgsqGdSzebTLKheFuigRotEV3Or/z5Lg + k/nVnThWVkTOSRqXTNpJAME6a4KTdcVSxYP+SdZVO1esazHrGb7xPVb7MWSE1cqp + WEk3Yy8OUOPoPQMc4dyGzd30Mi8IBB6gnFIYOTrpUo0XtkBv8rGGhfsCgYEA5uYn + 6QgL4NqNT84IXylmMb5ia3iBt6lhxI/A28CDtQvfScl4eYK0IjBwdfG6E1vJgyzg + nJzv3xEVo9bz+Kq7CcThWpK5JQaPnsV0Q74Wjk0ShHet15txOdJuKImnh5F6lylC + GTLR9gnptytfMH/uuw4ws0Q2kcg4l5NHKOWOnAcCgYEAvAwIVkhsB0n59Wu4gCZu + FUZENxYWUk/XUyQ6KnZrG2ih90xQ8+iMyqFOIm/52R2fFKNrdoWoALC6E3ct8+ZS + pMRLrelFXx8K3it4SwMJR2H8XBEfFW4bH0UtsW7Zafv+AunUs9LETP5gKG1LgXsq + qgXX43yy2LQ61O365YPZfdUCgYBVbTvA3MhARbvYldrFEnUL3GtfZbNgdxuD9Mee + xig0eJMBIrgfBLuOlqtVB70XYnM4xAbKCso4loKSHnofO1N99siFkRlM2JOUY2tz + kMWZmmxKdFjuF0WZ5f/5oYxI/QsFGC+rUQEbbWl56mMKd5qkvEhKWudxoklF0yiV + ufC8SwKBgDWb8iWqWN5a/kfvKoxFcDM74UHk/SeKMGAL+ujKLf58F+CbweM5pX9C + EUsxeoUEraVWTiyFVNqD81rCdceus9TdBj0ZIK1vUttaRZyrMAwF0uQSfjtxsOpd + l69BkyvzjgDPkmOHVGiSZDLi3YDvypbUpo6LOy4v5rVg5U2F/A0v + -----END RSA PRIVATE KEY----- + PKEY + end +end diff --git a/spec/bundler/support/code_climate.rb b/spec/bundler/support/code_climate.rb new file mode 100644 index 00000000000000..a15442cabe6cc4 --- /dev/null +++ b/spec/bundler/support/code_climate.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Spec + module CodeClimate + def self.setup + require "codeclimate-test-reporter" + ::CodeClimate::TestReporter.start + configure_exclusions + rescue LoadError + # it's fine if CodeClimate isn't set up + nil + end + + def self.configure_exclusions + SimpleCov.start do + add_filter "/bin/" + add_filter "/lib/bundler/man/" + add_filter "/lib/bundler/vendor/" + add_filter "/man/" + add_filter "/pkg/" + add_filter "/spec/" + add_filter "/tmp/" + end + end + end +end diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb new file mode 100644 index 00000000000000..556285ac523b26 --- /dev/null +++ b/spec/bundler/support/command_execution.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "support/helpers" +require "support/path" + +module Spec + CommandExecution = Struct.new(:command, :working_directory, :exitstatus, :stdout, :stderr) do + include RSpec::Matchers::Composable + + def to_s + c = Shellwords.shellsplit(command.strip).map {|s| s.include?("\n") ? " \\\n <= 100 + acc + " \\\n " + elem + else + concat + end + end + "$ #{c.strip}" + end + alias_method :inspect, :to_s + + def stdboth + @stdboth ||= [stderr, stdout].join("\n").strip + end + + def bundler_err + if Bundler::VERSION.start_with?("1.") + stdout + else + stderr + end + end + + def to_s_verbose + [ + to_s, + stdout, + stderr, + exitstatus ? "# $? => #{exitstatus}" : "", + ].reject(&:empty?).join("\n") + end + + def success? + return true unless exitstatus + exitstatus == 0 + end + + def failure? + return true unless exitstatus + exitstatus > 0 + end + end +end diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb new file mode 100644 index 00000000000000..b14e4a5943c1a2 --- /dev/null +++ b/spec/bundler/support/hax.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "rubygems" + +module Gem + if version = ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] + remove_const(:VERSION) if const_defined?(:VERSION) + VERSION = version + end + + class Platform + @local = new(ENV["BUNDLER_SPEC_PLATFORM"]) if ENV["BUNDLER_SPEC_PLATFORM"] + end + @platforms = [Gem::Platform::RUBY, Gem::Platform.local] + + if defined?(@path_to_default_spec_map) && !ENV["BUNDLER_SPEC_KEEP_DEFAULT_BUNDLER_GEM"] + @path_to_default_spec_map.delete_if do |_path, spec| + spec.name == "bundler" + end + end +end + +if ENV["BUNDLER_SPEC_VERSION"] + module Bundler + remove_const(:VERSION) if const_defined?(:VERSION) + VERSION = ENV["BUNDLER_SPEC_VERSION"].dup + end +end + +if ENV["BUNDLER_SPEC_WINDOWS"] == "true" + require "bundler/constants" + + module Bundler + remove_const :WINDOWS if defined?(WINDOWS) + WINDOWS = true + end +end + +class Object + if ENV["BUNDLER_SPEC_RUBY_ENGINE"] + if defined?(RUBY_ENGINE) && RUBY_ENGINE != "jruby" && ENV["BUNDLER_SPEC_RUBY_ENGINE"] == "jruby" + begin + # this has to be done up front because psych will try to load a .jar + # if it thinks its on jruby + require "psych" + rescue LoadError + nil + end + end + + remove_const :RUBY_ENGINE if defined?(RUBY_ENGINE) + RUBY_ENGINE = ENV["BUNDLER_SPEC_RUBY_ENGINE"] + + if RUBY_ENGINE == "jruby" + remove_const :JRUBY_VERSION if defined?(JRUBY_VERSION) + JRUBY_VERSION = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + end + end +end + +if ENV["BUNDLER_SPEC_IGNORE_COMPATIBILITY_GUARD"] + $LOADED_FEATURES << File.expand_path("../../../bundler/compatibility_guard.rb", __FILE__) + $LOADED_FEATURES << File.expand_path("../../../bundler/compatibility_guard", __FILE__) + $LOADED_FEATURES << "bundler/compatibility_guard.rb" + $LOADED_FEATURES << "bundler/compatibility_guard" + require "bundler/compatibility_guard" +end diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb new file mode 100644 index 00000000000000..181dac3220706c --- /dev/null +++ b/spec/bundler/support/helpers.rb @@ -0,0 +1,600 @@ +# frozen_string_literal: true + +require "open3" + +module Spec + module Helpers + def reset! + Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir| + next if %w[base remote1 gems rubygems . ..].include?(File.basename(dir)) + if ENV["BUNDLER_SUDO_TESTS"] + `sudo rm -rf "#{dir}"` + else + FileUtils.rm_rf(dir) + end + end + FileUtils.mkdir_p(home) + FileUtils.mkdir_p(tmpdir) + Bundler.reset! + Bundler.ui = nil + Bundler.ui # force it to initialize + end + + def self.bang(method) + define_method("#{method}!") do |*args, &blk| + send(method, *args, &blk).tap do + unless last_command.success? + raise RuntimeError, + "Invoking #{method}!(#{args.map(&:inspect).join(", ")}) failed:\n#{last_command.stdboth}", + caller.drop_while {|bt| bt.start_with?(__FILE__) } + end + end + end + end + + def the_bundle(*args) + TheBundle.new(*args) + end + + def last_command + @command_executions.last || raise("There is no last command") + end + + def out + last_command.stdboth + end + + def err + last_command.stderr + end + + def exitstatus + last_command.exitstatus + end + + def bundle_update_requires_all? + Bundler::VERSION.start_with?("2.") ? nil : true + end + + def in_app_root(&blk) + Dir.chdir(bundled_app, &blk) + end + + def in_app_root2(&blk) + Dir.chdir(bundled_app2, &blk) + end + + def in_app_root_custom(root, &blk) + Dir.chdir(root, &blk) + end + + def run(cmd, *args) + opts = args.last.is_a?(Hash) ? args.pop : {} + groups = args.map(&:inspect).join(", ") + setup = "require 'rubygems' ; require 'bundler' ; Bundler.setup(#{groups})\n" + ruby(setup + cmd, opts) + end + bang :run + + def load_error_run(ruby, name, *args) + cmd = <<-RUBY + begin + #{ruby} + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR" if e.message.include?("-- #{name}") + end + RUBY + opts = args.last.is_a?(Hash) ? args.pop : {} + args += [opts] + run(cmd, *args) + end + + def lib + root.join("lib") + end + + def spec + spec_dir.to_s + end + + def bundle(cmd, options = {}) + with_sudo = options.delete(:sudo) + sudo = with_sudo == :preserve_env ? "sudo -E" : "sudo" if with_sudo + + bundle_bin = options.delete("bundle_bin") || bindir.join("bundle") + + if system_bundler = options.delete(:system_bundler) + bundle_bin = "-S bundle" + end + + env = options.delete(:env) || {} + env["PATH"].gsub!("#{Path.root}/exe", "") if env["PATH"] && system_bundler + + requires = options.delete(:requires) || [] + requires << "support/hax" + + artifice = options.delete(:artifice) do + if RSpec.current_example.metadata[:realworld] + "vcr" + else + "fail" + end + end + if artifice + requires << File.expand_path("../artifice/#{artifice}", __FILE__) + end + + requires_str = requires.map {|r| "-r#{r}" }.join(" ") + + load_path = [] + load_path << lib unless system_bundler + load_path << spec + load_path_str = "-I#{load_path.join(File::PATH_SEPARATOR)}" + + env = env.map {|k, v| "#{k}='#{v}'" }.join(" ") + + args = options.map do |k, v| + case v + when nil + next + when true + " --#{k}" + when false + " --no-#{k}" + else + " --#{k} #{v}" + end + end.join + + cmd = "#{env} #{sudo} #{Gem.ruby} #{load_path_str} #{requires_str} #{bundle_bin} #{cmd}#{args}" + sys_exec(cmd) {|i, o, thr| yield i, o, thr if block_given? } + end + bang :bundle + + def forgotten_command_line_options(options) + remembered = Bundler.bundler_major_version < 3 + options = options.map do |k, v| + k = Array(k)[remembered ? 0 : -1] + v = '""' if v && v.to_s.empty? + [k, v] + end + return Hash[options] if remembered + options.each do |k, v| + if v.nil? + bundle! "config --delete #{k}" + else + bundle! "config --local #{k} #{v}" + end + end + {} + end + + def bundler(cmd, options = {}) + options["bundle_bin"] = bindir.join("bundler") + bundle(cmd, options) + end + + def bundle_ruby(options = {}) + options["bundle_bin"] = bindir.join("bundle_ruby") + bundle("", options) + end + + def ruby(ruby, options = {}) + env = (options.delete(:env) || {}).map {|k, v| "#{k}='#{v}' " }.join + ruby = ruby.gsub(/["`\$]/) {|m| "\\#{m}" } + lib_option = options[:no_lib] ? "" : " -I#{lib}" + sys_exec(%(#{env}#{Gem.ruby}#{lib_option} -e "#{ruby}")) + end + bang :ruby + + def load_error_ruby(ruby, name, opts = {}) + ruby(<<-R) + begin + #{ruby} + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR"# if e.message.include?("-- #{name}") + end + R + end + + def gembin(cmd) + lib = File.expand_path("../../../lib", __FILE__) + old = ENV["RUBYOPT"] + ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -I#{lib}" + cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/") + sys_exec(cmd.to_s) + ensure + ENV["RUBYOPT"] = old + end + + def gem_command(command, args = "", options = {}) + if command == :exec && !options[:no_quote] + args = args.gsub(/(?=")/, "\\") + args = %("#{args}") + end + gem = ENV["BUNDLE_GEM"] || "#{Gem.ruby} -rrubygems -S gem --backtrace" + sys_exec("#{gem} #{command} #{args}") + end + bang :gem_command + + def rake + "#{Gem.ruby} -S #{ENV["GEM_PATH"]}/bin/rake" + end + + def sys_exec(cmd) + command_execution = CommandExecution.new(cmd.to_s, Dir.pwd) + + Open3.popen3(cmd.to_s) do |stdin, stdout, stderr, wait_thr| + yield stdin, stdout, wait_thr if block_given? + stdin.close + + command_execution.exitstatus = wait_thr && wait_thr.value.exitstatus + command_execution.stdout = Thread.new { stdout.read }.value.strip + command_execution.stderr = Thread.new { stderr.read }.value.strip + end + + (@command_executions ||= []) << command_execution + + command_execution.stdout + end + bang :sys_exec + + def config(config = nil, path = bundled_app(".bundle/config")) + return YAML.load_file(path) unless config + FileUtils.mkdir_p(File.dirname(path)) + File.open(path, "w") do |f| + f.puts config.to_yaml + end + config + end + + def global_config(config = nil) + config(config, home(".bundle/config")) + end + + def create_file(*args) + path = bundled_app(args.shift) + path = args.shift if args.first.is_a?(Pathname) + str = args.shift || "" + path.dirname.mkpath + File.open(path.to_s, "w") do |f| + f.puts strip_whitespace(str) + end + end + + def gemfile(*args) + if args.empty? + File.open("Gemfile", "r", &:read) + else + create_file("Gemfile", *args) + end + end + + def lockfile(*args) + if args.empty? + File.open("Gemfile.lock", "r", &:read) + else + create_file("Gemfile.lock", *args) + end + end + + def strip_whitespace(str) + # Trim the leading spaces + spaces = str[/\A\s+/, 0] || "" + str.gsub(/^#{spaces}/, "") + end + + def normalize_uri_file(str) + # URI::File of Ruby 2.6 normalize localhost variable with file protocol. + if defined?(URI::File) + str.gsub(%r{file:\/\/localhost}, "file://") + else + str + end + end + + def install_gemfile(*args) + gemfile(*args) + opts = args.last.is_a?(Hash) ? args.last : {} + opts[:retry] ||= 0 + bundle :install, opts + end + bang :install_gemfile + + def lock_gemfile(*args) + gemfile(*args) + opts = args.last.is_a?(Hash) ? args.last : {} + opts[:retry] ||= 0 + bundle :lock, opts + end + + def install_gems(*gems) + options = gems.last.is_a?(Hash) ? gems.pop : {} + gem_repo = options.fetch(:gem_repo) { gem_repo1 } + gems.each do |g| + path = if g == :bundler + Dir.chdir(root) { gem_command! :build, gemspec.to_s } + bundler_path = if ruby_core? + root + "lib/bundler-#{Bundler::VERSION}.gem" + else + root + "bundler-#{Bundler::VERSION}.gem" + end + elsif g.to_s =~ %r{\A/.*\.gem\z} + g + else + "#{gem_repo}/gems/#{g}.gem" + end + + raise "OMG `#{path}` does not exist!" unless File.exist?(path) + + if Gem::VERSION < "2.0.0" + gem_command! :install, "--no-rdoc --no-ri --ignore-dependencies '#{path}'" + else + gem_command! :install, "--no-document --ignore-dependencies '#{path}'" + end + bundler_path && bundler_path.rmtree + end + end + + alias_method :install_gem, :install_gems + + def with_gem_path_as(path) + backup = ENV.to_hash + ENV["GEM_HOME"] = path.to_s + ENV["GEM_PATH"] = path.to_s + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + yield + ensure + ENV.replace(backup) + end + + def with_path_as(path) + backup = ENV.to_hash + ENV["PATH"] = path.to_s + ENV["BUNDLER_ORIG_PATH"] = nil + yield + ensure + ENV.replace(backup) + end + + def with_path_added(path) + with_path_as(path.to_s + ":" + ENV["PATH"]) do + yield + end + end + + def break_git! + FileUtils.mkdir_p(tmp("broken_path")) + File.open(tmp("broken_path/git"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nSTDERR.puts 'This is not the git you are looking for'\nexit 1" + end + + ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}" + end + + def with_fake_man + FileUtils.mkdir_p(tmp("fake_man")) + File.open(tmp("fake_man/man"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nputs ARGV.inspect\n" + end + with_path_added(tmp("fake_man")) { yield } + end + + def system_gems(*gems) + opts = gems.last.is_a?(Hash) ? gems.last : {} + path = opts.fetch(:path, system_gem_path) + if path == :bundle_path + path = ruby!(<<-RUBY) + require "bundler" + begin + puts Bundler.bundle_path + rescue Bundler::GemfileNotFound + ENV["BUNDLE_GEMFILE"] = "Gemfile" + retry + end + + RUBY + end + gems = gems.flatten + + unless opts[:keep_path] + FileUtils.rm_rf(path) + FileUtils.mkdir_p(path) + end + + Gem.clear_paths + + env_backup = ENV.to_hash + ENV["GEM_HOME"] = path.to_s + ENV["GEM_PATH"] = path.to_s + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + + install_gems(*gems) + return unless block_given? + begin + yield + ensure + ENV.replace(env_backup) + end + end + + def realworld_system_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf(system_gem_path) + FileUtils.mkdir_p(system_gem_path) + + Gem.clear_paths + + gem_home = ENV["GEM_HOME"] + gem_path = ENV["GEM_PATH"] + path = ENV["PATH"] + ENV["GEM_HOME"] = system_gem_path.to_s + ENV["GEM_PATH"] = system_gem_path.to_s + + gems.each do |gem| + gem_command :install, "--no-rdoc --no-ri #{gem}" + end + return unless block_given? + begin + yield + ensure + ENV["GEM_HOME"] = gem_home + ENV["GEM_PATH"] = gem_path + ENV["PATH"] = path + end + end + + def cache_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf("#{bundled_app}/vendor/cache") + FileUtils.mkdir_p("#{bundled_app}/vendor/cache") + + gems.each do |g| + path = "#{gem_repo1}/gems/#{g}.gem" + raise "OMG `#{path}` does not exist!" unless File.exist?(path) + FileUtils.cp(path, "#{bundled_app}/vendor/cache") + end + end + + def simulate_new_machine + system_gems [] + FileUtils.rm_rf system_gem_path + FileUtils.rm_rf bundled_app(".bundle") + end + + def simulate_platform(platform) + old = ENV["BUNDLER_SPEC_PLATFORM"] + ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s + yield if block_given? + ensure + ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given? + end + + def simulate_ruby_version(version) + return if version == RUBY_VERSION + old = ENV["BUNDLER_SPEC_RUBY_VERSION"] + ENV["BUNDLER_SPEC_RUBY_VERSION"] = version + yield if block_given? + ensure + ENV["BUNDLER_SPEC_RUBY_VERSION"] = old if block_given? + end + + def simulate_ruby_engine(engine, version = "1.6.0") + return if engine == local_ruby_engine + + old = ENV["BUNDLER_SPEC_RUBY_ENGINE"] + ENV["BUNDLER_SPEC_RUBY_ENGINE"] = engine + old_version = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = version + yield if block_given? + ensure + ENV["BUNDLER_SPEC_RUBY_ENGINE"] = old if block_given? + ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = old_version if block_given? + end + + def simulate_bundler_version(version) + old = ENV["BUNDLER_SPEC_VERSION"] + ENV["BUNDLER_SPEC_VERSION"] = version.to_s + yield if block_given? + ensure + ENV["BUNDLER_SPEC_VERSION"] = old if block_given? + end + + def simulate_rubygems_version(version) + old = ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] + ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] = version.to_s + yield if block_given? + ensure + ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] = old if block_given? + end + + def simulate_windows(platform = mswin) + old = ENV["BUNDLER_SPEC_WINDOWS"] + ENV["BUNDLER_SPEC_WINDOWS"] = "true" + simulate_platform platform do + yield + end + ensure + ENV["BUNDLER_SPEC_WINDOWS"] = old + end + + def revision_for(path) + Dir.chdir(path) { `git rev-parse HEAD`.strip } + end + + def capture_output + capture(:stdout) + end + + def with_read_only(pattern) + chmod = lambda do |dirmode, filemode| + lambda do |f| + mode = File.directory?(f) ? dirmode : filemode + File.chmod(mode, f) + end + end + + Dir[pattern].each(&chmod[0o555, 0o444]) + yield + ensure + Dir[pattern].each(&chmod[0o755, 0o644]) + end + + def process_file(pathname) + changed_lines = pathname.readlines.map do |line| + yield line + end + File.open(pathname, "w") {|file| file.puts(changed_lines.join) } + end + + def with_env_vars(env_hash, &block) + current_values = {} + env_hash.each do |k, v| + current_values[k] = ENV[k] + ENV[k] = v + end + block.call if block_given? + env_hash.each do |k, _| + ENV[k] = current_values[k] + end + end + + def require_rack + # need to hack, so we can require rack + old_gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = Spec::Path.base_system_gems.to_s + require "rack" + ENV["GEM_HOME"] = old_gem_home + end + + def wait_for_server(host, port, seconds = 15) + tries = 0 + sleep 0.5 + TCPSocket.new(host, port) + rescue => e + raise(e) if tries > (seconds * 2) + tries += 1 + retry + end + + def find_unused_port + port = 21_453 + begin + port += 1 while TCPSocket.new("127.0.0.1", port) + rescue + false + end + port + end + + def bundler_fileutils + if RUBY_VERSION >= "2.4" + ::Bundler::FileUtils + else + ::FileUtils + end + end + end +end diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb new file mode 100644 index 00000000000000..69f8d9f679f02e --- /dev/null +++ b/spec/bundler/support/indexes.rb @@ -0,0 +1,421 @@ +# frozen_string_literal: true + +module Spec + module Indexes + def dep(name, reqs = nil) + @deps ||= [] + @deps << Bundler::Dependency.new(name, reqs) + end + + def platform(*args) + @platforms ||= [] + @platforms.concat args.map {|p| Gem::Platform.new(p) } + end + + alias_method :platforms, :platform + + def resolve(args = []) + @platforms ||= ["ruby"] + deps = [] + default_source = instance_double("Bundler::Source::Rubygems", :specs => @index) + source_requirements = { :default => default_source } + @deps.each do |d| + @platforms.each do |p| + source_requirements[d.name] = d.source = default_source + deps << Bundler::DepProxy.new(d, p) + end + end + source_requirements ||= {} + Bundler::Resolver.resolve(deps, @index, source_requirements, *args) + end + + def should_resolve_as(specs) + got = resolve + got = got.map(&:full_name).sort + expect(got).to eq(specs.sort) + end + + def should_resolve_and_include(specs, args = []) + got = resolve(args) + got = got.map(&:full_name).sort + specs.each do |s| + expect(got).to include(s) + end + end + + def should_conflict_on(names) + got = resolve + flunk "The resolve succeeded with: #{got.map(&:full_name).sort.inspect}" + rescue Bundler::VersionConflict => e + expect(Array(names).sort).to eq(e.conflicts.sort) + end + + def gem(*args, &blk) + build_spec(*args, &blk).first + end + + def locked(*args) + Bundler::SpecSet.new(args.map do |name, version| + gem(name, version) + end) + end + + def should_conservative_resolve_and_include(opts, unlock, specs) + # empty unlock means unlock all + opts = Array(opts) + search = Bundler::GemVersionPromoter.new(@locked, unlock).tap do |s| + s.level = opts.first + s.strict = opts.include?(:strict) + s.prerelease_specified = Hash[@deps.map {|d| [d.name, d.requirement.prerelease?] }] + end + should_resolve_and_include specs, [@base, search] + end + + def an_awesome_index + build_index do + gem "rack", %w[0.8 0.9 0.9.1 0.9.2 1.0 1.1] + gem "rack-mount", %w[0.4 0.5 0.5.1 0.5.2 0.6] + + # --- Pre-release support + gem "rubygems\0", ["1.3.2"] + + # --- Rails + versions "1.2.3 2.2.3 2.3.5 3.0.0.beta 3.0.0.beta1" do |version| + gem "activesupport", version + gem "actionpack", version do + dep "activesupport", version + if version >= v("3.0.0.beta") + dep "rack", "~> 1.1" + dep "rack-mount", ">= 0.5" + elsif version > v("2.3") then dep "rack", "~> 1.0.0" + elsif version > v("2.0.0") then dep "rack", "~> 0.9.0" + end + end + gem "activerecord", version do + dep "activesupport", version + dep "arel", ">= 0.2" if version >= v("3.0.0.beta") + end + gem "actionmailer", version do + dep "activesupport", version + dep "actionmailer", version + end + if version < v("3.0.0.beta") + gem "railties", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + end + else + gem "railties", version + gem "rails", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + dep "railties", version + end + end + end + + versions "1.0 1.2 1.2.1 1.2.2 1.3 1.3.0.1 1.3.5 1.4.0 1.4.2 1.4.2.1" do |version| + platforms "ruby java mswin32 mingw32 x64-mingw32" do |platform| + next if version == v("1.4.2.1") && platform != pl("x86-mswin32") + next if version == v("1.4.2") && platform == pl("x86-mswin32") + gem "nokogiri", version, platform do + dep "weakling", ">= 0.0.3" if platform =~ pl("java") + end + end + end + + versions "0.0.1 0.0.2 0.0.3" do |version| + gem "weakling", version + end + + # --- Rails related + versions "1.2.3 2.2.3 2.3.5" do |version| + gem "activemerchant", version do + dep "activesupport", ">= #{version}" + end + end + + gem "reform", ["1.0.0"] do + dep "activesupport", ">= 1.0.0.beta1" + end + + gem "need-pre", ["1.0.0"] do + dep "activesupport", "~> 3.0.0.beta1" + end + end + end + + # Builder 3.1.4 will activate first, but if all + # goes well, it should resolve to 3.0.4 + def a_conflict_index + build_index do + gem "builder", %w[3.0.4 3.1.4] + gem("grape", "0.2.6") do + dep "builder", ">= 0" + end + + versions "3.2.8 3.2.9 3.2.10 3.2.11" do |version| + gem("activemodel", version) do + dep "builder", "~> 3.0.0" + end + end + + gem("my_app", "1.0.0") do + dep "activemodel", ">= 0" + dep "grape", ">= 0" + end + end + end + + def a_complex_conflict_index + build_index do + gem("a", %w[1.0.2 1.1.4 1.2.0 1.4.0]) do + dep "d", ">= 0" + end + + gem("d", %w[1.3.0 1.4.1]) do + dep "x", ">= 0" + end + + gem "d", "0.9.8" + + gem("b", "0.3.4") do + dep "a", ">= 1.5.0" + end + + gem("b", "0.3.5") do + dep "a", ">= 1.2" + end + + gem("b", "0.3.3") do + dep "a", "> 1.0" + end + + versions "3.2 3.3" do |version| + gem("c", version) do + dep "a", "~> 1.0" + end + end + + gem("my_app", "1.3.0") do + dep "c", ">= 4.0" + dep "b", ">= 0" + end + + gem("my_app", "1.2.0") do + dep "c", "~> 3.3.0" + dep "b", "0.3.4" + end + + gem("my_app", "1.1.0") do + dep "c", "~> 3.2.0" + dep "b", "0.3.5" + end + end + end + + def index_with_conflict_on_child + build_index do + gem "json", %w[1.6.5 1.7.7 1.8.0] + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + # Issue #3459 + def a_complicated_index + build_index do + gem "foo", %w[3.0.0 3.0.5] do + dep "qux", ["~> 3.1"] + dep "baz", ["< 9.0", ">= 5.0"] + dep "bar", ["~> 1.0"] + dep "grault", ["~> 3.1"] + end + + gem "foo", "1.2.1" do + dep "baz", ["~> 4.2"] + dep "bar", ["~> 1.0"] + dep "qux", ["~> 3.1"] + dep "grault", ["~> 2.0"] + end + + gem "bar", "1.0.5" do + dep "grault", ["~> 3.1"] + dep "baz", ["< 9", ">= 4.2"] + end + + gem "bar", "1.0.3" do + dep "baz", ["< 9", ">= 4.2"] + dep "grault", ["~> 2.0"] + end + + gem "baz", "8.2.10" do + dep "grault", ["~> 3.0"] + dep "garply", [">= 0.5.1", "~> 0.5"] + end + + gem "baz", "5.0.2" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "baz", "4.2.0" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "grault", %w[2.6.3 3.1.1] + + gem "garply", "0.5.1" do + dep "waldo", ["~> 0.1.3"] + end + + gem "waldo", "0.1.5" do + dep "plugh", ["~> 0.6.0"] + end + + gem "plugh", %w[0.6.3 0.6.11 0.7.0] + + gem "qux", "3.2.21" do + dep "plugh", [">= 0.6.4", "~> 0.6"] + dep "corge", ["~> 1.0"] + end + + gem "corge", "1.10.1" + end + end + + def a_unresovable_child_index + build_index do + gem "json", %w[1.8.0] + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app_error", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + def a_index_with_root_conflict_on_child + build_index do + gem "builder", %w[2.1.2 3.0.1 3.1.3] + gem "i18n", %w[0.4.1 0.4.2] + + gem "activesupport", %w[3.0.0 3.0.1 3.0.5 3.1.7] + + gem("activemodel", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4" + end + + gem("activemodel", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4.1" + end + + gem("activemodel", "3.1.3") do + dep "activesupport", "= 3.1.3" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.5" + end + + gem("activerecord", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "activemodel", "= 3.0.0" + end + + gem("activerecord", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "activemodel", "= 3.0.5" + end + + gem("activerecord", "3.0.9") do + dep "activesupport", "= 3.1.5" + dep "activemodel", "= 3.1.5" + end + end + end + + def a_circular_index + build_index do + gem "rack", "1.0.1" + gem("foo", "0.2.6") do + dep "bar", ">= 0" + end + + gem("bar", "1.0.0") do + dep "foo", ">= 0" + end + + gem("circular_app", "1.0.0") do + dep "foo", ">= 0" + dep "bar", ">= 0" + end + end + end + + def an_ambiguous_index + build_index do + gem("a", "1.0.0") do + dep "c", ">= 0" + end + + gem("b", %w[0.5.0 1.0.0]) + + gem("b", "2.0.0") do + dep "c", "< 2.0.0" + end + + gem("c", "1.0.0") do + dep "d", "1.0.0" + end + + gem("c", "2.0.0") do + dep "d", "2.0.0" + end + + gem("d", %w[1.0.0 2.0.0]) + end + end + + def optional_prereleases_index + build_index do + gem("a", %w[1.0.0]) + + gem("a", "2.0.0") do + dep "b", ">= 2.0.0.pre" + end + + gem("b", %w[0.9.0 1.5.0 2.0.0.pre]) + + # --- Pre-release support + gem "rubygems\0", ["1.3.2"] + end + end + end +end diff --git a/spec/bundler/support/less_than_proc.rb b/spec/bundler/support/less_than_proc.rb new file mode 100644 index 00000000000000..ddac5458b71b1b --- /dev/null +++ b/spec/bundler/support/less_than_proc.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class LessThanProc < Proc + attr_accessor :present + + def self.with(present) + provided = Gem::Version.new(present.dup) + new do |required| + if required =~ /[=><~]/ + !Gem::Requirement.new(required).satisfied_by?(provided) + else + provided < Gem::Version.new(required) + end + end.tap {|l| l.present = present } + end + + def inspect + "\"=< #{present}\"" + end +end diff --git a/spec/bundler/support/manpages.rb b/spec/bundler/support/manpages.rb new file mode 100644 index 00000000000000..ce1f72cc497ade --- /dev/null +++ b/spec/bundler/support/manpages.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Spec + module Manpages + def self.setup + man_path = Spec::Path.root.join("man") + return if man_path.children(false).select {|file| file.extname == ".ronn" }.all? do |man| + Dir[man_path.join("#{man.to_s[0..-6]}*.txt").to_s].any? + end + + system(Spec::Path.root.join("bin", "rake").to_s, "man:build") || raise("Failed building man pages") + end + end +end diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb new file mode 100644 index 00000000000000..8e17be3a024608 --- /dev/null +++ b/spec/bundler/support/matchers.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +require "forwardable" +require "support/the_bundle" +module Spec + module Matchers + extend RSpec::Matchers + + class Precondition + include RSpec::Matchers::Composable + extend Forwardable + def_delegators :failing_matcher, + :failure_message, + :actual, + :description, + :diffable?, + :expected, + :failure_message_when_negated + + def initialize(matcher, preconditions) + @matcher = with_matchers_cloned(matcher) + @preconditions = with_matchers_cloned(preconditions) + @failure_index = nil + end + + def matches?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + @matcher.matches?(target, &blk) + end + + def does_not_match?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + if @matcher.respond_to?(:does_not_match?) + @matcher.does_not_match?(target, &blk) + else + !@matcher.matches?(target, &blk) + end + end + + def expects_call_stack_jump? + @matcher.expects_call_stack_jump? || @preconditions.any?(&:expects_call_stack_jump) + end + + def supports_block_expectations? + @matcher.supports_block_expectations? || @preconditions.any?(&:supports_block_expectations) + end + + def failing_matcher + @failure_index ? @preconditions[@failure_index] : @matcher + end + end + + def self.define_compound_matcher(matcher, preconditions, &declarations) + raise "Must have preconditions to define a compound matcher" if preconditions.empty? + define_method(matcher) do |*expected, &block_arg| + Precondition.new( + RSpec::Matchers::DSL::Matcher.new(matcher, declarations, self, *expected, &block_arg), + preconditions + ) + end + end + + MAJOR_DEPRECATION = /^\[DEPRECATED FOR 2\.0\]\s*/ + + RSpec::Matchers.define :lack_errors do + diffable + match do |actual| + actual.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") == "" + end + end + + RSpec::Matchers.define :eq_err do |expected| + diffable + match do |actual| + actual.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") == expected + end + end + + RSpec::Matchers.define :have_major_deprecation do |expected| + diffable + match do |actual| + deprecations = actual.split(MAJOR_DEPRECATION) + + return !expected.nil? if deprecations.size <= 1 + return true if expected.nil? + + deprecations.any? do |d| + !d.empty? && values_match?(expected, d.strip) + end + end + end + + RSpec::Matchers.define :have_dep do |*args| + dep = Bundler::Dependency.new(*args) + + match do |actual| + actual.length == 1 && actual.all? {|d| d == dep } + end + end + + RSpec::Matchers.define :have_gem do |*args| + match do |actual| + actual.length == args.length && actual.all? {|a| args.include?(a.full_name) } + end + end + + RSpec::Matchers.define :have_rubyopts do |*args| + args = args.flatten + args = args.first.split(/\s+/) if args.size == 1 + + match do |actual| + actual = actual.split(/\s+/) if actual.is_a?(String) + args.all? {|arg| actual.include?(arg) } && actual.uniq.size == actual.size + end + end + + RSpec::Matchers.define :be_sorted do + diffable + attr_reader :expected + match do |actual| + expected = block_arg ? actual.sort_by(&block_arg) : actual.sort + actual.==(expected).tap do + # HACK: since rspec won't show a diff when everything is a string + differ = RSpec::Support::Differ.new + @actual = differ.send(:object_to_string, actual) + @expected = differ.send(:object_to_string, expected) + end + end + end + + define_compound_matcher :read_as, [exist] do |file_contents| + diffable + + match do |actual| + @actual = Bundler.read_file(actual) + values_match?(file_contents, @actual) + end + end + + def indent(string, padding = 4, indent_character = " ") + string.to_s.gsub(/^/, indent_character * padding).gsub("\t", " ") + end + + define_compound_matcher :include_gems, [be_an_instance_of(Spec::TheBundle)] do |*names| + match do + opts = names.last.is_a?(Hash) ? names.pop : {} + source = opts.delete(:source) + groups = Array(opts[:groups]) + groups << opts + @errors = names.map do |name| + name, version, platform = name.split(/\s+/) + version_const = name == "bundler" ? "Bundler::VERSION" : Spec::Builders.constantize(name) + begin + run! "require '#{name}.rb'; puts #{version_const}", *groups + rescue => e + next "#{name} is not installed:\n#{indent(e)}" + end + last_command.stdout.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + actual_version, actual_platform = last_command.stdout.strip.split(/\s+/, 2) + unless Gem::Version.new(actual_version) == Gem::Version.new(version) + next "#{name} was expected to be at version #{version} but was #{actual_version}" + end + unless actual_platform == platform + next "#{name} was expected to be of platform #{platform} but was #{actual_platform}" + end + next unless source + begin + source_const = "#{Spec::Builders.constantize(name)}_SOURCE" + run! "require '#{name}/source'; puts #{source_const}", *groups + rescue + next "#{name} does not have a source defined:\n#{indent(e)}" + end + last_command.stdout.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + unless last_command.stdout.strip == source + next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{out}`" + end + end.compact + + @errors.empty? + end + + match_when_negated do + opts = names.last.is_a?(Hash) ? names.pop : {} + groups = Array(opts[:groups]) || [] + @errors = names.map do |name| + name, version = name.split(/\s+/, 2) + begin + run <<-R, *(groups + [opts]) + begin + require '#{name}' + puts #{Spec::Builders.constantize(name)} + rescue LoadError, NameError + puts "WIN" + end + R + rescue => e + next "checking for #{name} failed:\n#{e}" + end + next if last_command.stdout == "WIN" + next "expected #{name} to not be installed, but it was" if version.nil? + if Gem::Version.new(last_command.stdout) == Gem::Version.new(version) + next "expected #{name} (#{version}) not to be installed, but it was" + end + end.compact + + @errors.empty? + end + + failure_message do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + + failure_message_when_negated do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + end + RSpec::Matchers.define_negated_matcher :not_include_gems, :include_gems + RSpec::Matchers.alias_matcher :include_gem, :include_gems + + def have_lockfile(expected) + read_as(strip_whitespace(expected)) + end + + def plugin_should_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).to be_installed(name) + path = Pathname.new(Bundler::Plugin.installed?(name)) + expect(path + "plugins.rb").to exist + end + end + + def plugin_should_not_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).not_to be_installed(name) + end + end + + def lockfile_should_be(expected) + expect(bundled_app("Gemfile.lock")).to read_as(normalize_uri_file(strip_whitespace(expected))) + end + + def gemfile_should_be(expected) + expect(bundled_app("Gemfile")).to read_as(strip_whitespace(expected)) + end + end +end diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb new file mode 100644 index 00000000000000..03a96893f37bc0 --- /dev/null +++ b/spec/bundler/support/path.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require "pathname" + +module Spec + module Path + def root + @root ||= Pathname.new(ruby_core? ? "../../../.." : "../../..").expand_path(__FILE__) + end + + def gemspec + @gemspec ||= root.join(ruby_core? ? "lib/bundler.gemspec" : "bundler.gemspec") + end + + def bindir + @bindir ||= root.join(ruby_core? ? "libexec" : "exe") + end + + def spec_dir + @spec_dir ||= root.join(ruby_core? ? "spec/bundler" : "spec") + end + + def tmp(*path) + root.join("tmp", *path) + end + + def home(*path) + tmp.join("home", *path) + end + + def default_bundle_path(*path) + if Bundler::VERSION.split(".").first.to_i < 3 + system_gem_path(*path) + else + bundled_app(*[".bundle", ENV.fetch("BUNDLER_SPEC_RUBY_ENGINE", Gem.ruby_engine), Gem::ConfigMap[:ruby_version], *path].compact) + end + end + + def bundled_app(*path) + root = tmp.join("bundled_app") + FileUtils.mkdir_p(root) + root.join(*path) + end + + alias_method :bundled_app1, :bundled_app + + def bundled_app2(*path) + root = tmp.join("bundled_app2") + FileUtils.mkdir_p(root) + root.join(*path) + end + + def vendored_gems(path = nil) + bundled_app(*["vendor/bundle", Gem.ruby_engine, Gem::ConfigMap[:ruby_version], path].compact) + end + + def cached_gem(path) + bundled_app("vendor/cache/#{path}.gem") + end + + def base_system_gems + tmp.join("gems/base") + end + + def gem_repo1(*args) + tmp("gems/remote1", *args) + end + + def gem_repo_missing(*args) + tmp("gems/missing", *args) + end + + def gem_repo2(*args) + tmp("gems/remote2", *args) + end + + def gem_repo3(*args) + tmp("gems/remote3", *args) + end + + def gem_repo4(*args) + tmp("gems/remote4", *args) + end + + def security_repo(*args) + tmp("gems/security_repo", *args) + end + + def system_gem_path(*path) + tmp("gems/system", *path) + end + + def lib_path(*args) + tmp("libs", *args) + end + + def bundler_path + Pathname.new(File.expand_path(root.join("lib"), __FILE__)) + end + + def global_plugin_gem(*args) + home ".bundle", "plugin", "gems", *args + end + + def local_plugin_gem(*args) + bundled_app ".bundle", "plugin", "gems", *args + end + + def tmpdir(*args) + tmp "tmpdir", *args + end + + def ruby_core? + # avoid to wornings + @ruby_core ||= nil + + if @ruby_core.nil? + @ruby_core = true & (ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + else + @ruby_core + end + end + + extend self + end +end diff --git a/spec/bundler/support/permissions.rb b/spec/bundler/support/permissions.rb new file mode 100644 index 00000000000000..b21ce3848d437f --- /dev/null +++ b/spec/bundler/support/permissions.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Spec + module Permissions + def with_umask(new_umask) + old_umask = File.umask(new_umask) + yield if block_given? + ensure + File.umask(old_umask) + end + end +end diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb new file mode 100644 index 00000000000000..950311d20e4966 --- /dev/null +++ b/spec/bundler/support/platforms.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Spec + module Platforms + include Bundler::GemHelpers + + def rb + Gem::Platform::RUBY + end + + def mac + Gem::Platform.new("x86-darwin-10") + end + + def x64_mac + Gem::Platform.new("x86_64-darwin-15") + end + + def java + Gem::Platform.new([nil, "java", nil]) + end + + def linux + Gem::Platform.new(["x86", "linux", nil]) + end + + def mswin + Gem::Platform.new(["x86", "mswin32", nil]) + end + + def mingw + Gem::Platform.new(["x86", "mingw32", nil]) + end + + def x64_mingw + Gem::Platform.new(["x64", "mingw32", nil]) + end + + def all_platforms + [rb, java, linux, mswin, mingw, x64_mingw] + end + + def local + generic_local_platform + end + + def specific_local_platform + Bundler.local_platform + end + + def not_local + all_platforms.find {|p| p != generic_local_platform } + end + + def local_tag + if RUBY_PLATFORM == "java" + :jruby + else + :ruby + end + end + + def not_local_tag + [:ruby, :jruby].find {|tag| tag != local_tag } + end + + def local_ruby_engine + ENV["BUNDLER_SPEC_RUBY_ENGINE"] || (defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby") + end + + def local_engine_version + return ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] if ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + + case local_ruby_engine + when "ruby" + RUBY_VERSION + when "rbx" + Rubinius::VERSION + when "jruby" + JRUBY_VERSION + else + RUBY_ENGINE_VERSION + end + end + + def not_local_engine_version + case not_local_tag + when :ruby + not_local_ruby_version + when :jruby + "1.6.1" + end + end + + def not_local_ruby_version + "1.12" + end + + def not_local_patchlevel + 9999 + end + + def lockfile_platforms(*platforms) + platforms = local_platforms if platforms.empty? + platforms.map(&:to_s).sort.join("\n ") + end + + def local_platforms + if Bundler::VERSION.split(".").first.to_i > 2 + [local, specific_local_platform] + else + [local] + end + end + end +end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb new file mode 100644 index 00000000000000..c18f7650fc3399 --- /dev/null +++ b/spec/bundler/support/rubygems_ext.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "rubygems/user_interaction" +require "support/path" unless defined?(Spec::Path) + +module Spec + module Rubygems + DEPS = begin + deps = { + # rack 2.x requires Ruby version >= 2.2.2. + # artifice doesn't support rack 2.x now. + # TODO: revert to `< 2` once https://github.com/rack/rack/issues/1168 is + # addressed + "rack" => "1.6.6", + # rack-test 0.7.0 dropped 1.8.7 support + # https://github.com/rack-test/rack-test/issues/193#issuecomment-314230318 + "rack-test" => "< 0.7.0", + "artifice" => "~> 0.6.0", + "compact_index" => "~> 0.11.0", + "sinatra" => "~> 1.4.7", + # Rake version has to be consistent for tests to pass + "rake" => "10.0.2", + # 3.0.0 breaks 1.9.2 specs + "builder" => "2.1.2", + } + # ruby-graphviz is used by the viz tests + deps["ruby-graphviz"] = nil if RUBY_VERSION >= "1.9.3" + deps + end + + def self.setup + Gem.clear_paths + + ENV["BUNDLE_PATH"] = nil + ENV["GEM_HOME"] = ENV["GEM_PATH"] = Path.base_system_gems.to_s + ENV["PATH"] = [Path.bindir, "#{Path.system_gem_path}/bin", ENV["PATH"]].join(File::PATH_SEPARATOR) + + manifest = DEPS.to_a.sort_by(&:first).map {|k, v| "#{k} => #{v}\n" } + manifest_path = "#{Path.base_system_gems}/manifest.txt" + # it's OK if there are extra gems + if !File.exist?(manifest_path) || !(manifest - File.readlines(manifest_path)).empty? + FileUtils.rm_rf(Path.base_system_gems) + FileUtils.mkdir_p(Path.base_system_gems) + puts "installing gems for the tests to use..." + install_gems(DEPS) + File.open(manifest_path, "w") {|f| f << manifest.join } + end + + ENV["HOME"] = Path.home.to_s + ENV["TMPDIR"] = Path.tmpdir.to_s + + Gem::DefaultUserInteraction.ui = Gem::SilentUI.new + end + + def self.install_gems(gems) + reqs, no_reqs = gems.partition {|_, req| !req.nil? && !req.split(" ").empty? } + # TODO: remove when we drop ruby 1.8.7-2.2.2 support + reqs = reqs.sort_by {|name, _| name == "rack" ? 0 : 1 }.sort_by {|name, _| name =~ /rack/ ? 0 : 1 } + no_reqs.map!(&:first) + reqs.map! {|name, req| "'#{name}:#{req}'" } + deps = reqs.concat(no_reqs).join(" ") + cmd = if Gem::VERSION < "2.0.0" + "gem install #{deps} --no-rdoc --no-ri --conservative" + else + "gem install #{deps} --no-document --conservative" + end + puts cmd + system(cmd) || raise("Installing gems #{deps} for the tests to use failed!") + end + end +end diff --git a/spec/bundler/support/silent_logger.rb b/spec/bundler/support/silent_logger.rb new file mode 100644 index 00000000000000..8665beb2c9f380 --- /dev/null +++ b/spec/bundler/support/silent_logger.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "logger" +module Spec + class SilentLogger + (::Logger.instance_methods - Object.instance_methods).each do |logger_instance_method| + define_method(logger_instance_method) {|*args, &blk| } + end + end +end diff --git a/spec/bundler/support/sometimes.rb b/spec/bundler/support/sometimes.rb new file mode 100644 index 00000000000000..65a95ed59c95ac --- /dev/null +++ b/spec/bundler/support/sometimes.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Sometimes + def run_with_retries(example_to_run, retries) + example = RSpec.current_example + example.metadata[:retries] ||= retries + + retries.times do |t| + example.metadata[:retried] = t + 1 + example.instance_variable_set(:@exception, nil) + example_to_run.run + break unless example.exception + end + + if e = example.exception + new_exception = e.exception(e.message + "[Retried #{retries} times]") + new_exception.set_backtrace e.backtrace + example.instance_variable_set(:@exception, new_exception) + end + end +end + +RSpec.configure do |config| + config.include Sometimes + config.alias_example_to :sometimes, :sometimes => true + config.add_setting :sometimes_retry_count, :default => 5 + + config.around(:each, :sometimes => true) do |example| + retries = example.metadata[:retries] || RSpec.configuration.sometimes_retry_count + run_with_retries(example, retries) + end + + config.after(:suite) do + message = proc do |color, text| + colored = RSpec::Core::Formatters::ConsoleCodes.wrap(text, color) + notification = RSpec::Core::Notifications::MessageNotification.new(colored) + formatter = RSpec.configuration.formatters.first + formatter.message(notification) if formatter.respond_to?(:message) + end + + retried_examples = RSpec.world.example_groups.map do |g| + g.descendants.map do |d| + d.filtered_examples.select do |e| + e.metadata[:sometimes] && e.metadata.fetch(:retried, 1) > 1 + end + end + end.flatten + + message.call(retried_examples.empty? ? :green : :yellow, "\n\nRetried examples: #{retried_examples.count}") + + retried_examples.each do |e| + message.call(:cyan, " #{e.full_description}") + path = RSpec::Core::Metadata.relative_path(e.location) + message.call(:cyan, " [#{e.metadata[:retried]}/#{e.metadata[:retries]}] " + path) + end + end +end diff --git a/spec/bundler/support/streams.rb b/spec/bundler/support/streams.rb new file mode 100644 index 00000000000000..a947eebf6fece3 --- /dev/null +++ b/spec/bundler/support/streams.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "stringio" + +def capture(*args) + opts = args.pop if args.last.is_a?(Hash) + opts ||= {} + + args.map!(&:to_s) + begin + result = StringIO.new + result.close if opts[:closed] + args.each {|stream| eval "$#{stream} = result" } + yield + ensure + args.each {|stream| eval("$#{stream} = #{stream.upcase}") } + end + result.string +end diff --git a/spec/bundler/support/sudo.rb b/spec/bundler/support/sudo.rb new file mode 100644 index 00000000000000..04e944394526ed --- /dev/null +++ b/spec/bundler/support/sudo.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Spec + module Sudo + def self.present? + @which_sudo ||= Bundler.which("sudo") + end + + def sudo(cmd) + raise "sudo not present" unless Sudo.present? + sys_exec("sudo #{cmd}") + end + + def chown_system_gems_to_root + sudo "chown -R root #{system_gem_path}" + end + end +end diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb new file mode 100644 index 00000000000000..c994eaae788181 --- /dev/null +++ b/spec/bundler/support/the_bundle.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "support/helpers" +require "support/path" + +module Spec + class TheBundle + include Spec::Helpers + include Spec::Path + + attr_accessor :bundle_dir + + def initialize(opts = {}) + opts = opts.dup + @bundle_dir = Pathname.new(opts.delete(:bundle_dir) { bundled_app }) + raise "Too many options! #{opts}" unless opts.empty? + end + + def to_s + "the bundle" + end + alias_method :inspect, :to_s + + def locked? + lockfile.file? + end + + def lockfile + bundle_dir.join("Gemfile.lock") + end + + def locked_gems + raise "Cannot read lockfile if it doesn't exist" unless locked? + Bundler::LockfileParser.new(lockfile.read) + end + end +end diff --git a/spec/bundler/update/gemfile_spec.rb b/spec/bundler/update/gemfile_spec.rb new file mode 100644 index 00000000000000..f59f3a2d322204 --- /dev/null +++ b/spec/bundler/update/gemfile_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + context "with --gemfile" do + it "finds the gemfile" do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle! :install, :gemfile => bundled_app("NotGemfile") + bundle! :update, :gemfile => bundled_app("NotGemfile"), :all => bundle_update_requires_all? + + # Specify BUNDLE_GEMFILE for `the_bundle` + # to retrieve the proper Gemfile + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with gemfile set via config" do + before do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle "config --local gemfile #{bundled_app("NotGemfile")}" + bundle! :install + end + + it "uses the gemfile to update" do + bundle! "update", :all => bundle_update_requires_all? + bundle "list" + + expect(out).to include("rack (1.0.0)") + end + + it "uses the gemfile while in a subdirectory" do + bundled_app("subdir").mkpath + Dir.chdir(bundled_app("subdir")) do + bundle! "update", :all => bundle_update_requires_all? + bundle "list" + + expect(out).to include("rack (1.0.0)") + end + end + end + + context "with prefer_gems_rb set" do + before { bundle! "config prefer_gems_rb true" } + + it "prefers gems.rb to Gemfile" do + create_file("gems.rb", "gem 'bundler'") + create_file("Gemfile", "raise 'wrong Gemfile!'") + + bundle! :install + bundle! :update, :all => bundle_update_requires_all? + + expect(bundled_app("gems.rb")).to be_file + expect(bundled_app("Gemfile.lock")).not_to be_file + + expect(the_bundle).to include_gem "bundler #{Bundler::VERSION}" + end + end +end diff --git a/spec/bundler/update/gems/post_install_spec.rb b/spec/bundler/update/gems/post_install_spec.rb new file mode 100644 index 00000000000000..2fb3547806f080 --- /dev/null +++ b/spec/bundler/update/gems/post_install_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + let(:config) {} + + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', "< 1.0" + gem 'thin' + G + + bundle! "config #{config}" if config + + bundle! :install + end + + shared_examples "a config observer" do + context "when ignore post-install messages for gem is set" do + let(:config) { "ignore_messages.rack true" } + + it "doesn't display gem's post-install message" do + expect(out).not_to include("Rack's post install message") + end + end + + context "when ignore post-install messages for all gems" do + let(:config) { "ignore_messages true" } + + it "doesn't display any post-install messages" do + expect(out).not_to include("Post-install message") + end + end + end + + shared_examples "a post-install message outputter" do + it "should display post-install messages for updated gems" do + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + + it "should not display the post-install message for non-updated gems" do + expect(out).not_to include("Thin's post install message") + end + end + + context "when listed gem is updated" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + gem 'thin' + G + + bundle! :update, :all => bundle_update_requires_all? + end + + it_behaves_like "a post-install message outputter" + it_behaves_like "a config observer" + end + + context "when dependency triggers update" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack-obama' + gem 'thin' + G + + bundle! :update, :all => bundle_update_requires_all? + end + + it_behaves_like "a post-install message outputter" + it_behaves_like "a config observer" + end +end diff --git a/spec/bundler/update/git_spec.rb b/spec/bundler/update/git_spec.rb new file mode 100644 index 00000000000000..d97760e84b5cad --- /dev/null +++ b/spec/bundler/update/git_spec.rb @@ -0,0 +1,374 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + describe "git sources" do + it "floats on a branch when :branch is used" do + build_git "foo", "1.0" + update_git "foo", :branch => "omg" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :branch => "omg" do + gem 'foo' + end + G + + update_git "foo", :branch => "omg" do |s| + s.write "lib/foo.rb", "FOO = '1.1'" + end + + bundle "update", :all => bundle_update_requires_all? + + expect(the_bundle).to include_gems "foo 1.1" + end + + it "updates correctly when you have like craziness" do + build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") + build_git "rails", "3.0", :path => lib_path("rails") do |s| + s.add_dependency "activesupport", "= 3.0" + end + + install_gemfile! <<-G + gem "rails", :git => "#{lib_path("rails")}" + G + + bundle! "update rails" + expect(the_bundle).to include_gems "rails 3.0", "activesupport 3.0" + end + + it "floats on a branch when :branch is used and the source is specified in the update" do + build_git "foo", "1.0", :path => lib_path("foo") + update_git "foo", :branch => "omg", :path => lib_path("foo") + + install_gemfile <<-G + git "#{lib_path("foo")}", :branch => "omg" do + gem 'foo' + end + G + + update_git "foo", :branch => "omg", :path => lib_path("foo") do |s| + s.write "lib/foo.rb", "FOO = '1.1'" + end + + bundle "update --source foo" + + expect(the_bundle).to include_gems "foo 1.1" + end + + it "floats on master when updating all gems that are pinned to the source even if you have child dependencies" do + build_git "foo", :path => lib_path("foo") + build_gem "bar", :to_bundle => true do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}" + gem "bar" + G + + update_git "foo", :path => lib_path("foo") do |s| + s.write "lib/foo.rb", "FOO = '1.1'" + end + + bundle "update foo" + + expect(the_bundle).to include_gems "foo 1.1" + end + + it "notices when you change the repo url in the Gemfile" do + build_git "foo", :path => lib_path("foo_one") + build_git "foo", :path => lib_path("foo_two") + + install_gemfile <<-G + gem "foo", "1.0", :git => "#{lib_path("foo_one")}" + G + + FileUtils.rm_rf lib_path("foo_one") + + install_gemfile <<-G + gem "foo", "1.0", :git => "#{lib_path("foo_two")}" + G + + expect(err).to lack_errors + expect(out).to include("Fetching #{lib_path}/foo_two") + expect(out).to include("Bundle complete!") + end + + it "fetches tags from the remote" do + build_git "foo" + @remote = build_git("bar", :bare => true) + update_git "foo", :remote => @remote.path + update_git "foo", :push => "master" + + install_gemfile <<-G + gem 'foo', :git => "#{@remote.path}" + G + + # Create a new tag on the remote that needs fetching + update_git "foo", :tag => "fubar" + update_git "foo", :push => "fubar" + + gemfile <<-G + gem 'foo', :git => "#{@remote.path}", :tag => "fubar" + G + + bundle "update", :all => bundle_update_requires_all? + expect(exitstatus).to eq(0) if exitstatus + end + + describe "with submodules" do + before :each do + build_repo4 do + build_gem "submodule" do |s| + s.write "lib/submodule.rb", "puts 'GEM'" + end + end + + build_git "submodule", "1.0" do |s| + s.write "lib/submodule.rb", "puts 'GIT'" + end + + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + end + + it "it unlocks the source when submodules are added to a git source" do + install_gemfile <<-G + source "file:#{gem_repo4}" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + + run "require 'submodule'" + expect(out).to eq("GEM") + + install_gemfile <<-G + source "file:#{gem_repo4}" + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + run "require 'submodule'" + expect(out).to eq("GIT") + end + + it "unlocks the source when submodules are removed from git source", :git => ">= 2.9.0" do + install_gemfile! <<-G + source "file:#{gem_repo4}" + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + run! "require 'submodule'" + expect(out).to eq("GIT") + + install_gemfile! <<-G + source "file:#{gem_repo4}" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + + run! "require 'submodule'" + expect(out).to eq("GEM") + end + end + + it "errors with a message when the .git repo is gone" do + build_git "foo", "1.0" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + lib_path("foo-1.0").join(".git").rmtree + + bundle :update, :all => bundle_update_requires_all? + expect(last_command.bundler_err).to include(lib_path("foo-1.0").to_s). + and match(/Git error: command `git fetch.+has failed/) + end + + it "should not explode on invalid revision on update of gem by name" do + build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle "update rack" + expect(out).to include("Bundle updated!") + end + + it "shows the previous version of the gem" do + build_git "rails", "3.0", :path => lib_path("rails") + + install_gemfile <<-G + gem "rails", :git => "#{lib_path("rails")}" + G + + lockfile <<-G + GIT + remote: #{lib_path("rails")} + specs: + rails (2.3.2) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rails! + G + + bundle "update", :all => bundle_update_requires_all? + expect(out).to include("Using rails 3.0 (was 2.3.2) from #{lib_path("rails")} (at master@#{revision_for(lib_path("rails"))[0..6]})") + end + end + + describe "with --source flag" do + before :each do + build_repo2 + @git = build_git "foo", :path => lib_path("foo") do |s| + s.executables = "foobar" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + git "#{lib_path("foo")}" do + gem 'foo' + end + gem 'rack' + G + end + + it "updates the source" do + update_git "foo", :path => @git.path + + bundle "update --source foo" + + in_app_root do + run <<-RUBY + require 'foo' + puts "WIN" if defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + end + + it "unlocks gems that were originally pulled in by the source" do + update_git "foo", "2.0", :path => @git.path + + bundle "update --source foo" + expect(the_bundle).to include_gems "foo 2.0" + end + + it "leaves all other gems frozen" do + update_repo2 + update_git "foo", :path => @git.path + + bundle "update --source foo" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + context "when the gem and the repository have different names" do + before :each do + build_repo2 + @git = build_git "foo", :path => lib_path("bar") + + install_gemfile <<-G + source "file://localhost#{gem_repo2}" + git "#{lib_path("bar")}" do + gem 'foo' + end + gem 'rack' + G + end + + it "the --source flag updates version of gems that were originally pulled in by the source", :bundler => "> 3" do + spec_lines = lib_path("bar/foo.gemspec").read.split("\n") + spec_lines[5] = "s.version = '2.0'" + + update_git "foo", "2.0", :path => @git.path do |s| + s.write "foo.gemspec", spec_lines.join("\n") + end + + ref = @git.ref_for "master" + + bundle "update --source bar" + + lockfile_should_be <<-G + GIT + remote: #{@git.path} + revision: #{ref} + specs: + foo (2.0) + + GEM + remote: file://localhost#{gem_repo2}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "the --source flag updates version of gems that were originally pulled in by the source", :bundler => "3" do + spec_lines = lib_path("bar/foo.gemspec").read.split("\n") + spec_lines[5] = "s.version = '2.0'" + + update_git "foo", "2.0", :path => @git.path do |s| + s.write "foo.gemspec", spec_lines.join("\n") + end + + ref = @git.ref_for "master" + + bundle "update --source bar" + + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo2}/ + specs: + rack (1.0.0) + + GIT + remote: #{@git.path} + revision: #{ref} + specs: + foo (2.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + end +end diff --git a/spec/bundler/update/path_spec.rb b/spec/bundler/update/path_spec.rb new file mode 100644 index 00000000000000..38c125e04b26ca --- /dev/null +++ b/spec/bundler/update/path_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe "path sources" do + describe "bundle update --source" do + it "shows the previous version of the gem when updated from path source" do + build_lib "activesupport", "2.3.5", :path => lib_path("rails/activesupport") + + install_gemfile <<-G + gem "activesupport", :path => "#{lib_path("rails/activesupport")}" + G + + build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") + + bundle "update --source activesupport" + expect(out).to include("Using activesupport 3.0 (was 2.3.5) from source at `#{lib_path("rails/activesupport")}`") + end + end +end diff --git a/spec/bundler/update/redownload_spec.rb b/spec/bundler/update/redownload_spec.rb new file mode 100644 index 00000000000000..1bbc3a66fc8089 --- /dev/null +++ b/spec/bundler/update/redownload_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update", :bundler => "< 3", :ruby => ">= 2.0" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + before { bundle "config major_deprecations yes" } + + describe "with --force" do + it "shows a deprecation when single flag passed" do + bundle! "update rack --force" + expect(out).to include "[DEPRECATED FOR 3.0] The `--force` option has been renamed to `--redownload`" + end + + it "shows a deprecation when multiple flags passed" do + bundle! "update rack --no-color --force" + expect(out).to include "[DEPRECATED FOR 3.0] The `--force` option has been renamed to `--redownload`" + end + end + + describe "with --redownload" do + it "does not show a deprecation when single flag passed" do + bundle! "update rack --redownload" + expect(out).not_to include "[DEPRECATED FOR 3.0] The `--force` option has been renamed to `--redownload`" + end + + it "does not show a deprecation when single multiple flags passed" do + bundle! "update rack --no-color --redownload" + expect(out).not_to include "[DEPRECATED FOR 3.0] The `--force` option has been renamed to `--redownload`" + end + end +end diff --git a/spec/mspec/lib/mspec/matchers/block_caller.rb b/spec/mspec/lib/mspec/matchers/block_caller.rb index 4149586747d385..30fab4fc682eb4 100644 --- a/spec/mspec/lib/mspec/matchers/block_caller.rb +++ b/spec/mspec/lib/mspec/matchers/block_caller.rb @@ -1,26 +1,24 @@ class BlockingMatcher def matches?(block) - started = false - blocking = true - - thread = Thread.new do - started = true + t = Thread.new do block.call - - blocking = false end - Thread.pass while !started - - # Wait until the Thread status is "sleep" (then it's blocking) - # or nil (the Thread finished execution, it did not block) - while status = thread.status and status != "sleep" - Thread.pass + loop do + case t.status + when "sleep" # blocked + t.kill + t.join + return true + when false # terminated normally, so never blocked + t.join + return false + when nil # terminated exceptionally + t.value + else + Thread.pass + end end - thread.kill - thread.join - - blocking end def failure_message @@ -33,7 +31,7 @@ def negative_failure_message end module MSpecMatchers - private def block_caller(timeout = 0.1) + private def block_caller BlockingMatcher.new end end diff --git a/spec/ruby/README.md b/spec/ruby/README.md index f2e18d5800070d..eef4e9ca0b44dc 100644 --- a/spec/ruby/README.md +++ b/spec/ruby/README.md @@ -88,9 +88,9 @@ In similar fashion, the following commands run the respective specs: $ ../mspec/bin/mspec :library $ ../mspec/bin/mspec :capi -### Contributing +### Contributing and Writing Specs -See [CONTRIBUTING.md](https://github.com/ruby/spec/blob/master/CONTRIBUTING.md). +See [CONTRIBUTING.md](https://github.com/ruby/spec/blob/master/CONTRIBUTING.md) for documentation about contributing and writing specs (guards, matchers, etc). ### Socket specs from rubysl-socket diff --git a/spec/ruby/appveyor.yml b/spec/ruby/appveyor.yml index 88dd7530706eac..588172ee3d4e63 100644 --- a/spec/ruby/appveyor.yml +++ b/spec/ruby/appveyor.yml @@ -1,6 +1,6 @@ --- version: "{build}" -clone_depth: 5 +clone_depth: 100 init: # To avoid duplicated executables in PATH, see https://github.com/ruby/spec/pull/468 - set PATH=C:\Ruby%ruby_version%\bin;C:\Program Files\7-Zip;C:\Program Files\AppVeyor\BuildAgent;C:\Program Files\Git\cmd;C:\Windows\system32;C:\Program Files;C:\Windows diff --git a/spec/ruby/core/dir/home_spec.rb b/spec/ruby/core/dir/home_spec.rb index b57e16ec941c15..8180ce38be2acd 100644 --- a/spec/ruby/core/dir/home_spec.rb +++ b/spec/ruby/core/dir/home_spec.rb @@ -15,8 +15,8 @@ end platform_is_not :windows do - it "returns the named user's home directory as a string if called with an argument" do - Dir.home(ENV['USER']).should == ENV['HOME'] + it "returns the named user's home directory, from the user database, as a string if called with an argument" do + Dir.home(ENV['USER']).should == `echo ~#{ENV['USER']}`.chomp end end diff --git a/spec/ruby/core/file/expand_path_spec.rb b/spec/ruby/core/file/expand_path_spec.rb index 99279aec853f99..b7363fefa96c29 100644 --- a/spec/ruby/core/file/expand_path_spec.rb +++ b/spec/ruby/core/file/expand_path_spec.rb @@ -88,7 +88,8 @@ platform_is_not :windows do before do - @home = ENV['HOME'].chomp('/') + @var_home = ENV['HOME'].chomp('/') + @db_home = `echo ~#{ENV['USER']}`.chomp end # FIXME: these are insane! @@ -107,9 +108,9 @@ File.expand_path('./////').should == Dir.pwd File.expand_path('.').should == Dir.pwd File.expand_path(Dir.pwd).should == Dir.pwd - File.expand_path('~/').should == @home - File.expand_path('~/..badfilename').should == "#{@home}/..badfilename" - File.expand_path('~/a','~/b').should == "#{@home}/a" + File.expand_path('~/').should == @var_home + File.expand_path('~/..badfilename').should == "#{@var_home}/..badfilename" + File.expand_path('~/a','~/b').should == "#{@var_home}/a" File.expand_path('..').should == File.dirname(Dir.pwd) end @@ -126,8 +127,11 @@ end it "expands ~ENV['USER'] to the user's home directory" do - File.expand_path("~#{ENV['USER']}").should == @home - File.expand_path("~#{ENV['USER']}/a").should == "#{@home}/a" + File.expand_path("~#{ENV['USER']}").should == @db_home + end + + it "expands ~ENV['USER']/a to a in the user's home directory" do + File.expand_path("~#{ENV['USER']}/a").should == "#{@db_home}/a" end it "does not expand ~ENV['USER'] when it's not at the start" do @@ -135,7 +139,7 @@ end it "expands ../foo with ~/dir as base dir to /path/to/user/home/foo" do - File.expand_path('../foo', '~/dir').should == "#{@home}/foo" + File.expand_path('../foo', '~/dir').should == "#{@var_home}/foo" end end @@ -239,4 +243,19 @@ lambda { File.expand_path("~") }.should raise_error(ArgumentError) end end + + describe "File.expand_path with a non-absolute HOME" do + before :each do + @home = ENV["HOME"] + end + + after :each do + ENV["HOME"] = @home + end + + it "raises an ArgumentError" do + ENV["HOME"] = "non-absolute" + lambda { File.expand_path("~") }.should raise_error(ArgumentError, 'non-absolute home') + end + end end diff --git a/spec/ruby/core/gc/count_spec.rb b/spec/ruby/core/gc/count_spec.rb index 11648ac9be92f3..af2fbbe713d364 100644 --- a/spec/ruby/core/gc/count_spec.rb +++ b/spec/ruby/core/gc/count_spec.rb @@ -4,4 +4,14 @@ it "returns an integer" do GC.count.should be_kind_of(Integer) end + + it "increases as collections are run" do + count_before = GC.count + i = 0 + while GC.count <= count_before and i < 10 + GC.start + i += 1 + end + GC.count.should > count_before + end end diff --git a/spec/ruby/core/hash/shift_spec.rb b/spec/ruby/core/hash/shift_spec.rb index 9421a7753771cb..8cf3f4025a3b82 100644 --- a/spec/ruby/core/hash/shift_spec.rb +++ b/spec/ruby/core/hash/shift_spec.rb @@ -61,4 +61,19 @@ def h.default(key) lambda { HashSpecs.frozen_hash.shift }.should raise_error(frozen_error_class) lambda { HashSpecs.empty_frozen_hash.shift }.should raise_error(frozen_error_class) end + + it "works when the hash is at capacity" do + # We try a wide range of sizes in hopes that this will cover all implementationss base Hash size. + results = [] + 1.upto(100) do |n| + h = {} + n.times do |i| + h[i] = i + end + h.shift + results << h.size + end + + results.should == 0.upto(99).to_a + end end diff --git a/spec/ruby/core/integer/gcd_spec.rb b/spec/ruby/core/integer/gcd_spec.rb index 69f711d4c5d511..238bf66cc021d0 100644 --- a/spec/ruby/core/integer/gcd_spec.rb +++ b/spec/ruby/core/integer/gcd_spec.rb @@ -43,6 +43,17 @@ bignum.gcd(99).should == 99 end + it "doesn't cause an integer overflow" do + [2 ** (1.size * 8 - 2), 0x8000000000000000].each do |max| + [max - 1, max, max + 1].each do |num| + num.gcd(num).should == num + (-num).gcd(num).should == num + (-num).gcd(-num).should == num + num.gcd(-num).should == num + end + end + end + it "raises an ArgumentError if not given an argument" do lambda { 12.gcd }.should raise_error(ArgumentError) end diff --git a/spec/ruby/core/io/initialize_spec.rb b/spec/ruby/core/io/initialize_spec.rb index 8fb606d282f822..b1b720f040d8a5 100644 --- a/spec/ruby/core/io/initialize_spec.rb +++ b/spec/ruby/core/io/initialize_spec.rb @@ -13,12 +13,16 @@ rm_r @name end - it "reassociates the IO instance with the new descriptor when passed a Fixnum" do - fd = new_fd @name, "r:utf-8" - @io.send :initialize, fd, 'r' - @io.fileno.should == fd - # initialize has closed the old descriptor - lambda { IO.for_fd(@fd).close }.should raise_error(Errno::EBADF) + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1469621 + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1454818 + without_feature :mjit do # with RubyVM::MJIT.enabled?, this randomly fails for now + it "reassociates the IO instance with the new descriptor when passed a Fixnum" do + fd = new_fd @name, "r:utf-8" + @io.send :initialize, fd, 'r' + @io.fileno.should == fd + # initialize has closed the old descriptor + lambda { IO.for_fd(@fd).close }.should raise_error(Errno::EBADF) + end end it "calls #to_int to coerce the object passed as an fd" do diff --git a/spec/ruby/core/io/read_nonblock_spec.rb b/spec/ruby/core/io/read_nonblock_spec.rb index e224707e38453a..3c02f662f600b7 100644 --- a/spec/ruby/core/io/read_nonblock_spec.rb +++ b/spec/ruby/core/io/read_nonblock_spec.rb @@ -44,7 +44,6 @@ platform_is_not :windows do it 'sets the IO in nonblock mode' do require 'io/nonblock' - @read.nonblock?.should == false @write.write "abc" @read.read_nonblock(1).should == "a" @read.nonblock?.should == true diff --git a/spec/ruby/core/io/reopen_spec.rb b/spec/ruby/core/io/reopen_spec.rb index 0237004edc7e88..60dda726892534 100644 --- a/spec/ruby/core/io/reopen_spec.rb +++ b/spec/ruby/core/io/reopen_spec.rb @@ -145,17 +145,21 @@ File.read(@other_name).should == "new data" end - it "closes the file descriptor obtained by opening the new file" do - @io = new_io @name, "w" + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1461550 + # http://ci.rvm.jp/results/trunk-mjit-wait@silicon-docker/1448152 + without_feature :mjit do # with RubyVM::MJIT.enabled?, this randomly fails for now + it "closes the file descriptor obtained by opening the new file" do + @io = new_io @name, "w" - @other_io = File.open @other_name, "w" - max = @other_io.fileno - @other_io.close + @other_io = File.open @other_name, "w" + max = @other_io.fileno + @other_io.close - @io.reopen @other_name + @io.reopen @other_name - @other_io = File.open @other_name, "w" - @other_io.fileno.should == max + @other_io = File.open @other_name, "w" + @other_io.fileno.should == max + end end it "creates the file if it doesn't exist if the IO is opened in write mode" do diff --git a/spec/ruby/core/io/syswrite_spec.rb b/spec/ruby/core/io/syswrite_spec.rb index 1b1477bc6ee851..a4dc8328aac49c 100644 --- a/spec/ruby/core/io/syswrite_spec.rb +++ b/spec/ruby/core/io/syswrite_spec.rb @@ -55,7 +55,7 @@ r, w = IO.pipe begin w.nonblock = true - larger_than_pipe_capacity = 100 * 1024 + larger_than_pipe_capacity = 2 * 1024 * 1024 written = w.syswrite("a"*larger_than_pipe_capacity) written.should > 0 written.should < larger_than_pipe_capacity diff --git a/spec/ruby/core/io/ungetbyte_spec.rb b/spec/ruby/core/io/ungetbyte_spec.rb index c22661582b15b9..2639d24c45031a 100644 --- a/spec/ruby/core/io/ungetbyte_spec.rb +++ b/spec/ruby/core/io/ungetbyte_spec.rb @@ -36,9 +36,25 @@ @io.getbyte.should == 97 end - it "puts back one byte for an Integer argument" do - @io.ungetbyte(4095).should be_nil - @io.getbyte.should == 255 + ruby_version_is ''...'2.6' do + it "puts back one byte for a Fixnum argument..." do + @io.ungetbyte(4095).should be_nil + @io.getbyte.should == 255 + end + + it "... but not for Bignum argument (eh?)" do + lambda { + @io.ungetbyte(0x4f7574206f6620636861722072616e6765) + }.should raise_error(TypeError) + end + end + + ruby_version_is '2.6' do + it "is an RangeError if the integer is not in 8bit" do + for i in [4095, 0x4f7574206f6620636861722072616e6765] do + lambda { @io.ungetbyte(i) }.should raise_error(RangeError) + end + end end it "raises an IOError if the IO is closed" do diff --git a/spec/ruby/core/io/write_nonblock_spec.rb b/spec/ruby/core/io/write_nonblock_spec.rb index b0da9b7e1142c9..285d1af376b7b6 100644 --- a/spec/ruby/core/io/write_nonblock_spec.rb +++ b/spec/ruby/core/io/write_nonblock_spec.rb @@ -76,7 +76,6 @@ platform_is_not :windows do it 'sets the IO in nonblock mode' do require 'io/nonblock' - @write.nonblock?.should == false @write.write_nonblock('a') @write.nonblock?.should == true end diff --git a/spec/ruby/core/module/refine_spec.rb b/spec/ruby/core/module/refine_spec.rb index 287aa601a9e147..97ea0a706d6a54 100644 --- a/spec/ruby/core/module/refine_spec.rb +++ b/spec/ruby/core/module/refine_spec.rb @@ -425,6 +425,24 @@ def to_s end end + ruby_version_is "2.6" do + it "is honored by Kernel#public_send" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.public_send :foo + end + + result.should == "foo from refinement" + end + end + ruby_version_is "" ... "2.5" do it "is not honored by string interpolation" do refinement = Module.new do @@ -506,21 +524,42 @@ def foo; end }.should raise_error(NameError, /undefined method `foo'/) end - it "is not honored by Kernel#respond_to?" do - klass = Class.new - refinement = Module.new do - refine klass do - def foo; end + ruby_version_is "" ... "2.6" do + it "is not honored by Kernel#respond_to?" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end end - end - result = nil - Module.new do - using refinement - result = klass.new.respond_to?(:foo) + result = nil + Module.new do + using refinement + result = klass.new.respond_to?(:foo) + end + + result.should == false end + end + + ruby_version_is "2.6" do + it "is honored by Kernel#respond_to?" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end + end - result.should == false + result = nil + Module.new do + using refinement + result = klass.new.respond_to?(:foo) + end + + result.should == true + end end end @@ -678,4 +717,19 @@ def to_json_format result.should == "hello from refinement" end end + + it 'does not list methods defined only in refinement' do + refine_object = Module.new do + refine Object do + def refinement_only_method + end + end + end + spec = self + klass = Class.new { instance_methods.should_not spec.send(:include, :refinement_only_method) } + instance = klass.new + instance.methods.should_not include :refinement_only_method + instance.respond_to?(:refinement_only_method).should == false + -> { instance.method :refinement_only_method }.should raise_error(NameError) + end end diff --git a/spec/ruby/core/mutex/lock_spec.rb b/spec/ruby/core/mutex/lock_spec.rb index a272b16a324715..a1a2972d8c7611 100644 --- a/spec/ruby/core/mutex/lock_spec.rb +++ b/spec/ruby/core/mutex/lock_spec.rb @@ -11,22 +11,15 @@ m.unlock end - it "waits if the lock is not available" do + it "blocks the caller if already locked" do m = Mutex.new - m.lock + lambda { m.lock }.should block_caller + end - th = Thread.new do - m.lock - ScratchPad.record :after_lock - end - - Thread.pass while th.status and th.status != "sleep" - - ScratchPad.recorded.should be_nil - m.unlock - th.join - ScratchPad.recorded.should == :after_lock + it "does not block the caller if not locked" do + m = Mutex.new + lambda { m.lock }.should_not block_caller end # Unable to find a specific ticket but behavior change may be diff --git a/spec/ruby/core/mutex/synchronize_spec.rb b/spec/ruby/core/mutex/synchronize_spec.rb index 206b14e828c557..e3dad508b823f5 100644 --- a/spec/ruby/core/mutex/synchronize_spec.rb +++ b/spec/ruby/core/mutex/synchronize_spec.rb @@ -24,4 +24,43 @@ th.join m1.locked?.should be_false end + + it "blocks the caller if already locked" do + m = Mutex.new + m.lock + lambda { m.synchronize { } }.should block_caller + end + + it "does not block the caller if not locked" do + m = Mutex.new + lambda { m.synchronize { } }.should_not block_caller + end + + it "blocks the caller if another thread is also in the synchronize block" do + m = Mutex.new + q1 = Queue.new + q2 = Queue.new + + t = Thread.new { + m.synchronize { + q1.push :ready + q2.pop + } + } + + q1.pop.should == :ready + + lambda { m.synchronize { } }.should block_caller + + q2.push :done + t.join + end + + it "is not recursive" do + m = Mutex.new + + m.synchronize do + lambda { m.synchronize { } }.should raise_error(ThreadError) + end + end end diff --git a/spec/ruby/core/process/clock_gettime_spec.rb b/spec/ruby/core/process/clock_gettime_spec.rb new file mode 100644 index 00000000000000..165f0db730a4bd --- /dev/null +++ b/spec/ruby/core/process/clock_gettime_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Process.clock_gettime" do + describe 'time units' do + it 'handles a fixed set of time units' do + [:nanosecond, :microsecond, :millisecond, :second].each do |unit| + Process.clock_gettime(Process::CLOCK_MONOTONIC, unit).should be_kind_of(Integer) + end + + [:float_microsecond, :float_millisecond, :float_second].each do |unit| + Process.clock_gettime(Process::CLOCK_MONOTONIC, unit).should be_an_instance_of(Float) + end + end + + it 'raises an ArgumentError for an invalid time unit' do + lambda { Process.clock_gettime(Process::CLOCK_MONOTONIC, :bad) }.should raise_error(ArgumentError) + end + + it 'defaults to :float_second' do + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) + + t1.should be_an_instance_of(Float) + t2.should be_close(t1, 2.0) # 2.0 is chosen arbitrarily to allow for time skew without admitting failure cases, which would be off by an order of magnitude. + end + + it 'uses the default time unit (:float_second) when passed nil' do + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC, nil) + t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) + + t1.should be_an_instance_of(Float) + t2.should be_close(t1, 2.0) # 2.0 is chosen arbitrarily to allow for time skew without admitting failure cases, which would be off by an order of magnitude. + end + end +end diff --git a/spec/ruby/core/queue/append_spec.rb b/spec/ruby/core/queue/append_spec.rb new file mode 100644 index 00000000000000..34165a7506244f --- /dev/null +++ b/spec/ruby/core/queue/append_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' + +describe "Queue#<<" do + it_behaves_like :queue_enq, :<<, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/clear_spec.rb b/spec/ruby/core/queue/clear_spec.rb new file mode 100644 index 00000000000000..3245e4cb83af9e --- /dev/null +++ b/spec/ruby/core/queue/clear_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/clear' + +describe "Queue#clear" do + it_behaves_like :queue_clear, :clear, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/close_spec.rb b/spec/ruby/core/queue/close_spec.rb new file mode 100644 index 00000000000000..c0d774cd74a3a1 --- /dev/null +++ b/spec/ruby/core/queue/close_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/close' + +describe "Queue#close" do + it_behaves_like :queue_close, :close, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/closed_spec.rb b/spec/ruby/core/queue/closed_spec.rb new file mode 100644 index 00000000000000..10d552996da9b3 --- /dev/null +++ b/spec/ruby/core/queue/closed_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/closed' + +describe "Queue#closed?" do + it_behaves_like :queue_closed?, :closed?, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/deq_spec.rb b/spec/ruby/core/queue/deq_spec.rb new file mode 100644 index 00000000000000..9510978eacced8 --- /dev/null +++ b/spec/ruby/core/queue/deq_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' + +describe "Queue#deq" do + it_behaves_like :queue_deq, :deq, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/empty_spec.rb b/spec/ruby/core/queue/empty_spec.rb new file mode 100644 index 00000000000000..55ca777466832d --- /dev/null +++ b/spec/ruby/core/queue/empty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/empty' + +describe "Queue#empty?" do + it_behaves_like :queue_empty?, :empty?, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/enq_spec.rb b/spec/ruby/core/queue/enq_spec.rb new file mode 100644 index 00000000000000..c69c496fbcbb43 --- /dev/null +++ b/spec/ruby/core/queue/enq_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' + +describe "Queue#enq" do + it_behaves_like :queue_enq, :enq, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/length_spec.rb b/spec/ruby/core/queue/length_spec.rb new file mode 100644 index 00000000000000..25399b2b76e662 --- /dev/null +++ b/spec/ruby/core/queue/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "Queue#length" do + it_behaves_like :queue_length, :length, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/num_waiting_spec.rb b/spec/ruby/core/queue/num_waiting_spec.rb new file mode 100644 index 00000000000000..edc0c37a82237b --- /dev/null +++ b/spec/ruby/core/queue/num_waiting_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/num_waiting' + +describe "Queue#num_waiting" do + it_behaves_like :queue_num_waiting, :num_waiting, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/pop_spec.rb b/spec/ruby/core/queue/pop_spec.rb new file mode 100644 index 00000000000000..1ce92316850533 --- /dev/null +++ b/spec/ruby/core/queue/pop_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' + +describe "Queue#pop" do + it_behaves_like :queue_deq, :pop, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/push_spec.rb b/spec/ruby/core/queue/push_spec.rb new file mode 100644 index 00000000000000..e936f9d2822a51 --- /dev/null +++ b/spec/ruby/core/queue/push_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' + +describe "Queue#push" do + it_behaves_like :queue_enq, :push, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/shift_spec.rb b/spec/ruby/core/queue/shift_spec.rb new file mode 100644 index 00000000000000..f84058e1df0c02 --- /dev/null +++ b/spec/ruby/core/queue/shift_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' + +describe "Queue#shift" do + it_behaves_like :queue_deq, :shift, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/size_spec.rb b/spec/ruby/core/queue/size_spec.rb new file mode 100644 index 00000000000000..f528dfe797c92c --- /dev/null +++ b/spec/ruby/core/queue/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "Queue#size" do + it_behaves_like :queue_length, :size, -> { Queue.new } +end diff --git a/spec/ruby/core/signal/list_spec.rb b/spec/ruby/core/signal/list_spec.rb index 2af9c505975ac5..56ad6828fef643 100644 --- a/spec/ruby/core/signal/list_spec.rb +++ b/spec/ruby/core/signal/list_spec.rb @@ -61,4 +61,8 @@ it "includes the EXIT key with a value of zero" do Signal.list["EXIT"].should == 0 end + + it "includes the KILL key with a value of nine" do + Signal.list["KILL"].should == 9 + end end diff --git a/spec/ruby/core/sizedqueue/append_spec.rb b/spec/ruby/core/sizedqueue/append_spec.rb new file mode 100644 index 00000000000000..ff96b46d2c0557 --- /dev/null +++ b/spec/ruby/core/sizedqueue/append_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' +require_relative '../../shared/sizedqueue/enque' + +describe "SizedQueue#<<" do + it_behaves_like :queue_enq, :<<, -> { SizedQueue.new(10) } +end + +describe "SizedQueue#<<" do + it_behaves_like :sizedqueue_enq, :<<, ->(n) { SizedQueue.new(n) } +end diff --git a/spec/ruby/core/sizedqueue/clear_spec.rb b/spec/ruby/core/sizedqueue/clear_spec.rb new file mode 100644 index 00000000000000..abae01c6c06ae9 --- /dev/null +++ b/spec/ruby/core/sizedqueue/clear_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/clear' + +describe "SizedQueue#clear" do + it_behaves_like :queue_clear, :clear, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/library/thread/sizedqueue/close_spec.rb b/spec/ruby/core/sizedqueue/close_spec.rb similarity index 51% rename from spec/ruby/library/thread/sizedqueue/close_spec.rb rename to spec/ruby/core/sizedqueue/close_spec.rb index 3222772f61a26d..0e0af851cb55b3 100644 --- a/spec/ruby/library/thread/sizedqueue/close_spec.rb +++ b/spec/ruby/core/sizedqueue/close_spec.rb @@ -1,6 +1,5 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/close' +require_relative '../../spec_helper' +require_relative '../../shared/queue/close' describe "SizedQueue#close" do it_behaves_like :queue_close, :close, -> { SizedQueue.new(10) } diff --git a/spec/ruby/library/thread/sizedqueue/closed_spec.rb b/spec/ruby/core/sizedqueue/closed_spec.rb similarity index 52% rename from spec/ruby/library/thread/sizedqueue/closed_spec.rb rename to spec/ruby/core/sizedqueue/closed_spec.rb index 9e41b896402ef8..4b90da1faa741d 100644 --- a/spec/ruby/library/thread/sizedqueue/closed_spec.rb +++ b/spec/ruby/core/sizedqueue/closed_spec.rb @@ -1,6 +1,5 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/closed' +require_relative '../../spec_helper' +require_relative '../../shared/queue/closed' describe "SizedQueue#closed?" do it_behaves_like :queue_closed?, :closed?, -> { SizedQueue.new(10) } diff --git a/spec/ruby/core/sizedqueue/deq_spec.rb b/spec/ruby/core/sizedqueue/deq_spec.rb new file mode 100644 index 00000000000000..5e1bd9f7467b4c --- /dev/null +++ b/spec/ruby/core/sizedqueue/deq_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' + +describe "SizedQueue#deq" do + it_behaves_like :queue_deq, :deq, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/empty_spec.rb b/spec/ruby/core/sizedqueue/empty_spec.rb new file mode 100644 index 00000000000000..9b0d4ff013c25c --- /dev/null +++ b/spec/ruby/core/sizedqueue/empty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/empty' + +describe "SizedQueue#empty?" do + it_behaves_like :queue_empty?, :empty?, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/enq_spec.rb b/spec/ruby/core/sizedqueue/enq_spec.rb new file mode 100644 index 00000000000000..11c65ec14d5864 --- /dev/null +++ b/spec/ruby/core/sizedqueue/enq_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' +require_relative '../../shared/sizedqueue/enque' + +describe "SizedQueue#enq" do + it_behaves_like :queue_enq, :enq, -> { SizedQueue.new(10) } +end + +describe "SizedQueue#enq" do + it_behaves_like :sizedqueue_enq, :enq, ->(n) { SizedQueue.new(n) } +end diff --git a/spec/ruby/core/sizedqueue/length_spec.rb b/spec/ruby/core/sizedqueue/length_spec.rb new file mode 100644 index 00000000000000..b93e7f8997f22e --- /dev/null +++ b/spec/ruby/core/sizedqueue/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "SizedQueue#length" do + it_behaves_like :queue_length, :length, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/max_spec.rb b/spec/ruby/core/sizedqueue/max_spec.rb new file mode 100644 index 00000000000000..b65a67eeb0a2c1 --- /dev/null +++ b/spec/ruby/core/sizedqueue/max_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/sizedqueue/max' + +describe "SizedQueue#max" do + it_behaves_like :sizedqueue_max, :max, ->(n) { SizedQueue.new(n) } +end + +describe "SizedQueue#max=" do + it_behaves_like :sizedqueue_max=, :max=, ->(n) { SizedQueue.new(n) } +end diff --git a/spec/ruby/core/sizedqueue/new_spec.rb b/spec/ruby/core/sizedqueue/new_spec.rb new file mode 100644 index 00000000000000..8febbfa63bf262 --- /dev/null +++ b/spec/ruby/core/sizedqueue/new_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/sizedqueue/new' + +describe "SizedQueue.new" do + it_behaves_like :sizedqueue_new, :new, ->(*n) { SizedQueue.new(*n) } +end diff --git a/spec/ruby/core/sizedqueue/num_waiting_spec.rb b/spec/ruby/core/sizedqueue/num_waiting_spec.rb new file mode 100644 index 00000000000000..cbbbb2d0621f7f --- /dev/null +++ b/spec/ruby/core/sizedqueue/num_waiting_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/sizedqueue/num_waiting' + +describe "SizedQueue#num_waiting" do + it_behaves_like :sizedqueue_num_waiting, :new, ->(n) { SizedQueue.new(n) } +end diff --git a/spec/ruby/core/sizedqueue/pop_spec.rb b/spec/ruby/core/sizedqueue/pop_spec.rb new file mode 100644 index 00000000000000..a0cf6f509ce504 --- /dev/null +++ b/spec/ruby/core/sizedqueue/pop_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' + +describe "SizedQueue#pop" do + it_behaves_like :queue_deq, :pop, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/push_spec.rb b/spec/ruby/core/sizedqueue/push_spec.rb new file mode 100644 index 00000000000000..5f92c5a2b7e642 --- /dev/null +++ b/spec/ruby/core/sizedqueue/push_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' +require_relative '../../shared/sizedqueue/enque' + +describe "SizedQueue#push" do + it_behaves_like :queue_enq, :push, -> { SizedQueue.new(10) } +end + +describe "SizedQueue#push" do + it_behaves_like :sizedqueue_enq, :push, ->(n) { SizedQueue.new(n) } +end diff --git a/spec/ruby/core/sizedqueue/shift_spec.rb b/spec/ruby/core/sizedqueue/shift_spec.rb new file mode 100644 index 00000000000000..5138e682584d78 --- /dev/null +++ b/spec/ruby/core/sizedqueue/shift_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' + +describe "SizedQueue#shift" do + it_behaves_like :queue_deq, :shift, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/size_spec.rb b/spec/ruby/core/sizedqueue/size_spec.rb new file mode 100644 index 00000000000000..dfa76faabeb33b --- /dev/null +++ b/spec/ruby/core/sizedqueue/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "SizedQueue#size" do + it_behaves_like :queue_length, :size, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/struct/new_spec.rb b/spec/ruby/core/struct/new_spec.rb index 7c33489779cd9e..01231e85fb6332 100644 --- a/spec/ruby/core/struct/new_spec.rb +++ b/spec/ruby/core/struct/new_spec.rb @@ -130,6 +130,17 @@ def platform it "fails with too many arguments" do lambda { StructClasses::Ruby.new('2.0', 'i686', true) }.should raise_error(ArgumentError) end + + it "passes a hash as a normal argument" do + type = Struct.new(:args) + + obj = type.new(keyword: :arg) + obj2 = type.new(*[{keyword: :arg}]) + + obj.should == obj2 + obj.args.should == {keyword: :arg} + obj2.args.should == {keyword: :arg} + end end ruby_version_is "2.5" do @@ -163,6 +174,12 @@ def platform @struct_with_kwa.new("elefant", 4) }.should raise_error(ArgumentError, /wrong number of arguments/) end + + it "raises ArgumentError when passed a single non-hash argument" do + -> { + @struct_with_kwa.new("elefant") + }.should raise_error(ArgumentError, /wrong number of arguments/) + end end end diff --git a/spec/ruby/core/thread/exclusive_spec.rb b/spec/ruby/core/thread/exclusive_spec.rb index 522a43cac5a800..9de427fb520fa1 100644 --- a/spec/ruby/core/thread/exclusive_spec.rb +++ b/spec/ruby/core/thread/exclusive_spec.rb @@ -14,5 +14,29 @@ Thread.exclusive { :result }.should == :result end - it "needs to be reviewed for spec completeness" + it "blocks the caller if another thread is also in an exclusive block" do + m = Mutex.new + q1 = Queue.new + q2 = Queue.new + + t = Thread.new { + Thread.exclusive { + q1.push :ready + q2.pop + } + } + + q1.pop.should == :ready + + lambda { Thread.exclusive { } }.should block_caller + + q2.push :done + t.join + end + + it "is not recursive" do + Thread.exclusive do + lambda { Thread.exclusive { } }.should raise_error(ThreadError) + end + end end diff --git a/spec/ruby/core/thread/raise_spec.rb b/spec/ruby/core/thread/raise_spec.rb index 0b18369cb9f985..38571854efb726 100644 --- a/spec/ruby/core/thread/raise_spec.rb +++ b/spec/ruby/core/thread/raise_spec.rb @@ -78,6 +78,30 @@ end -> { t.value }.should raise_error(RuntimeError) end + + it "re-raises a previously rescued exception without overwriting the backtrace" do + t = Thread.new do + -> { # To make sure there is at least one entry in the call stack + begin + sleep + rescue => e + e + end + }.call + end + + ThreadSpecs.spin_until_sleeping(t) + + begin + initial_raise_line = __LINE__; raise 'raised' + rescue => raised + raise_again_line = __LINE__; t.raise raised + raised_again = t.value + + raised_again.backtrace.first.should include("#{__FILE__}:#{initial_raise_line}:") + raised_again.backtrace.first.should_not include("#{__FILE__}:#{raise_again_line}:") + end + end end describe "Thread#raise on a running thread" do diff --git a/spec/ruby/core/time/fixtures/classes.rb b/spec/ruby/core/time/fixtures/classes.rb index 328f9160f6e1c9..d89e4911c8e4c9 100644 --- a/spec/ruby/core/time/fixtures/classes.rb +++ b/spec/ruby/core/time/fixtures/classes.rb @@ -9,4 +9,18 @@ class << self end end + Timezone = Struct.new(:name, :abbr, :offset) + class Timezone + def utc_offset(t = nil) + offset + end + + def local_to_utc(t) + t - utc_offset(t) + end + + def utc_to_local(t) + t + utc_offset(t) + end + end end diff --git a/spec/ruby/core/time/new_spec.rb b/spec/ruby/core/time/new_spec.rb index eced8a75fb7d7a..8d32c4e492f22d 100644 --- a/spec/ruby/core/time/new_spec.rb +++ b/spec/ruby/core/time/new_spec.rb @@ -117,8 +117,7 @@ ruby_version_is "2.6" do describe "Time.new with a timezone argument" do it "returns a Time correspoinding to UTC time returned by local_to_utc" do - zone = mock('timezone') - zone.should_receive(:local_to_utc).and_return(Time::TM.new(2000, 1, 1, 6, 30, 0)) + zone = TimeSpecs::Timezone.new("Asia/Colombo", "MMT", (5*3600+30*60)) t = Time.new(2000, 1, 1, 12, 0, 0, zone) t.to_a[0, 6].should == [0, 0, 12, 1, 1, 2000] t.utc_offset.should == 19800 diff --git a/spec/ruby/core/time/nsec_spec.rb b/spec/ruby/core/time/nsec_spec.rb index fb3df6394ca921..9338eb435ad89c 100644 --- a/spec/ruby/core/time/nsec_spec.rb +++ b/spec/ruby/core/time/nsec_spec.rb @@ -24,4 +24,8 @@ it "returns the nanoseconds part of a Time constructed with an Rational number of microseconds" do Time.at(0, Rational(99, 10)).nsec.should == 9900 end + + it "returns a positive value for dates before the epoch" do + Time.utc(1969, 11, 12, 13, 18, 57, 404240).nsec.should == 404240000 + end end diff --git a/spec/ruby/core/time/shared/time_params.rb b/spec/ruby/core/time/shared/time_params.rb index 120d8d3af1c1ea..39245116b05e8a 100644 --- a/spec/ruby/core/time/shared/time_params.rb +++ b/spec/ruby/core/time/shared/time_params.rb @@ -230,6 +230,10 @@ t.usec.should == 123 end + it "raises an ArgumentError for out of range microsecond" do + lambda { Time.send(@method, 2000, 1, 1, 20, 15, 1, 1000000) }.should raise_error(ArgumentError) + end + it "handles fractional microseconds as a Float" do t = Time.send(@method, 2000, 1, 1, 20, 15, 1, 1.75) t.usec.should == 1 diff --git a/spec/ruby/core/time/usec_spec.rb b/spec/ruby/core/time/usec_spec.rb index 44cd2047e76ead..6ea52f5e79949c 100644 --- a/spec/ruby/core/time/usec_spec.rb +++ b/spec/ruby/core/time/usec_spec.rb @@ -36,4 +36,8 @@ it "returns the microseconds for time created by Time#local" do Time.local(1,2,3,4,5,Rational(6.78)).usec.should == 780000 end + + it "returns a positive value for dates before the epoch" do + Time.utc(1969, 11, 12, 13, 18, 57, 404240).usec.should == 404240 + end end diff --git a/spec/ruby/core/warning/warn_spec.rb b/spec/ruby/core/warning/warn_spec.rb index 9ebe018d8f883e..2844d97e7671ee 100644 --- a/spec/ruby/core/warning/warn_spec.rb +++ b/spec/ruby/core/warning/warn_spec.rb @@ -8,6 +8,14 @@ }.should complain("Chunky bacon!") end + it "does not add a newline" do + ruby_exe("Warning.warn('test')", args: "2>&1").should == "test" + end + + it "returns nil" do + ruby_exe("p Warning.warn('test')", args: "2>&1").should == "testnil\n" + end + it "extends itself" do Warning.singleton_class.ancestors.should include(Warning) end diff --git a/spec/ruby/language/def_spec.rb b/spec/ruby/language/def_spec.rb index 95469546c7e58b..dee60c9adfb768 100644 --- a/spec/ruby/language/def_spec.rb +++ b/spec/ruby/language/def_spec.rb @@ -79,6 +79,18 @@ def respond_to_missing? end end +describe "An instance method" do + it "raises an error with too few arguments" do + def foo(a, b); end + lambda { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2)') + end + + it "raises an error with too many arguments" do + def foo(a); end + lambda { foo 1, 2 }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)') + end +end + describe "An instance method definition with a splat" do it "accepts an unnamed '*' argument" do def foo(*); end; @@ -106,7 +118,7 @@ def foo(a, b, c, d, e, *f); [a, b, c, d, e, f]; end it "requires the presence of any arguments that precede the *" do def foo(a, b, *c); end - lambda { foo 1 }.should raise_error(ArgumentError) + lambda { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2+)') end end @@ -139,7 +151,7 @@ def foo(a = 1, *b) def foo(a, b = 2) [a,b] end - lambda { foo }.should raise_error(ArgumentError) + lambda { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1..2)') foo(1).should == [1, 2] end @@ -147,7 +159,7 @@ def foo(a, b = 2) def foo(a, b = 2, *c) [a,b,c] end - lambda { foo }.should raise_error(ArgumentError) + lambda { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1+)') foo(1).should == [1,2,[]] end @@ -717,7 +729,7 @@ def foo(a=b=c={}) end it "only allows overriding the default value of the first such parameter in each set" do - lambda { foo(1,2) }.should raise_error(ArgumentError) + lambda { foo(1,2) }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0..1)') end def bar(a=b=c=1,d=2) @@ -728,7 +740,7 @@ def bar(a=b=c=1,d=2) bar.should == [1,1,1,2] bar(3).should == [3,nil,nil,2] bar(3,4).should == [3,nil,nil,4] - lambda { bar(3,4,5) }.should raise_error(ArgumentError) + lambda { bar(3,4,5) }.should raise_error(ArgumentError, 'wrong number of arguments (given 3, expected 0..2)') end end diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb index 54f0c6ccf8c689..c5612ed19f8eb4 100644 --- a/spec/ruby/language/method_spec.rb +++ b/spec/ruby/language/method_spec.rb @@ -1269,7 +1269,7 @@ def m(a, b=1, *c, d, e:, f: 2, g:, **k, &l) def m(a, b = nil, c = nil, d, e: nil, **f) [a, b, c, d, e, f] end - ruby + ruby result = m(1, 2) result.should == [1, nil, nil, 2, nil, {}] @@ -1281,6 +1281,19 @@ def m(a, b = nil, c = nil, d, e: nil, **f) result.should == [1, nil, nil, {foo: :bar}, nil, {}] end end + + context "assigns keyword arguments from a passed Hash without modifying it" do + evaluate <<-ruby do + def m(a: nil); a; end + ruby + + options = {a: 1}.freeze + lambda do + m(options).should == 1 + end.should_not raise_error + options.should == {a: 1} + end + end end describe "A method call with a space between method name and parentheses" do diff --git a/spec/ruby/library/erb/new_spec.rb b/spec/ruby/library/erb/new_spec.rb index 0497bb7e156351..f1e5cd299e7b52 100644 --- a/spec/ruby/library/erb/new_spec.rb +++ b/spec/ruby/library/erb/new_spec.rb @@ -31,11 +31,19 @@ it "compiles eRuby script into ruby code when trim mode is 0 or not specified" do expected = "
    \n\n\n\n
  • 1
  • \n\n\n\n
  • 2
  • \n\n\n\n
  • 3
  • \n\n\n
\n" - [0, '', nil].each do |trim_mode| + [0, nil].each do |trim_mode| ERBSpecs.new_erb(@eruby_str, trim_mode: trim_mode).result.should == expected end end + ruby_version_is "2.6" do + it "warns invalid trim_mode" do + lambda do + ERBSpecs.new_erb(@eruby_str, trim_mode: '') + end.should complain(/Invalid ERB trim mode/) + end + end + it "removes '\n' when trim_mode is 1 or '>'" do expected = "
    \n
  • 1
  • \n
  • 2
  • \n
  • 3
  • \n
\n" [1, '>'].each do |trim_mode| diff --git a/spec/ruby/library/matrix/reflexive_spec.rb b/spec/ruby/library/matrix/reflexive_spec.rb deleted file mode 100644 index 380fbb7ed6d7db..00000000000000 --- a/spec/ruby/library/matrix/reflexive_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require 'matrix' - -ruby_version_is '2.6' do - describe "Matrix.reflexive?" do - it "returns true for a reflexive Matrix" do - Matrix[[1, 2, 3], [4, 1, 3], [5, 3, 1]].reflexive?.should be_true - end - - it "returns true for a 0x0 empty matrix" do - Matrix.empty.reflexive?.should be_true - end - - it "returns false for a non-reflexive Matrix" do - Matrix[[1, 1],[2, 2]].reflexive?.should be_false - end - - it "raises an error for non-square matrices" do - [ - Matrix[[0], [0]], - Matrix[[0, 0]], - Matrix.empty(0, 2), - Matrix.empty(2, 0), - ].each do |rectangular_matrix| - lambda { - rectangular_matrix.reflexive? - }.should raise_error(Matrix::ErrDimensionMismatch) - end - end - end -end diff --git a/spec/ruby/library/set/initialize_spec.rb b/spec/ruby/library/set/initialize_spec.rb index 11a1095fd703f9..887cae45fcb69b 100644 --- a/spec/ruby/library/set/initialize_spec.rb +++ b/spec/ruby/library/set/initialize_spec.rb @@ -14,6 +14,14 @@ s.should include(3) end + it "should initialize with empty array and set" do + s = Set.new([]) + s.size.should eql(0) + + s = Set.new({}) + s.size.should eql(0) + end + it "preprocesses all elements by a passed block before adding to self" do s = Set.new([1, 2, 3]) { |x| x * x } s.size.should eql(3) @@ -21,4 +29,20 @@ s.should include(4) s.should include(9) end + + it "should initialize with empty array and block" do + s = Set.new([]) { |x| x * x } + s.size.should eql(0) + end + + it "should initialize with empty set and block" do + s = Set.new(Set.new) { |x| x * x } + s.size.should eql(0) + end + + it "should initialize with just block" do + s = Set.new { |x| x * x } + s.size.should eql(0) + s.should eql(Set.new) + end end diff --git a/spec/ruby/library/socket/socket/accept_nonblock_spec.rb b/spec/ruby/library/socket/socket/accept_nonblock_spec.rb index 7ad32fa16e10a9..3ef219ed05a9c3 100644 --- a/spec/ruby/library/socket/socket/accept_nonblock_spec.rb +++ b/spec/ruby/library/socket/socket/accept_nonblock_spec.rb @@ -87,7 +87,7 @@ @client.connect(addr) - platform_is(:darwin, :freebsd) { IO.select([@server]) } + platform_is(:darwin, :freebsd, :solaris) { IO.select([@server]) } end after do diff --git a/spec/ruby/library/socket/socket/connect_nonblock_spec.rb b/spec/ruby/library/socket/socket/connect_nonblock_spec.rb index 0012b5ada77e3a..c56bebee623977 100644 --- a/spec/ruby/library/socket/socket/connect_nonblock_spec.rb +++ b/spec/ruby/library/socket/socket/connect_nonblock_spec.rb @@ -99,19 +99,43 @@ end end - platform_is_not :freebsd, :solaris do - describe 'using a STREAM socket' do - before do - @server = Socket.new(family, :STREAM) - @client = Socket.new(family, :STREAM) - @sockaddr = Socket.sockaddr_in(0, ip_address) + describe 'using a STREAM socket' do + before do + @server = Socket.new(family, :STREAM) + @client = Socket.new(family, :STREAM) + @sockaddr = Socket.sockaddr_in(0, ip_address) + end + + after do + @client.close + @server.close + end + + platform_is_not :windows do + it 'raises Errno::EISCONN when already connected' do + @server.listen(1) + @client.connect(@server.getsockname).should == 0 + + lambda { + @client.connect_nonblock(@server.getsockname) + + # A second call needed if non-blocking sockets become default + # XXX honestly I don't expect any real code to care about this spec + # as it's too implementation-dependent and checking for connect() + # errors is futile anyways because of TOCTOU + @client.connect_nonblock(@server.getsockname) + }.should raise_error(Errno::EISCONN) end - after do - @client.close - @server.close + it 'returns 0 when already connected in exceptionless mode' do + @server.listen(1) + @client.connect(@server.getsockname).should == 0 + + @client.connect_nonblock(@server.getsockname, exception: false).should == 0 end + end + platform_is_not :freebsd, :solaris do it 'raises IO:EINPROGRESSWaitWritable when the connection would block' do @server.bind(@sockaddr) diff --git a/spec/ruby/library/socket/socket/connect_spec.rb b/spec/ruby/library/socket/socket/connect_spec.rb index e26bf39cbb84de..df5cc5bf348785 100644 --- a/spec/ruby/library/socket/socket/connect_spec.rb +++ b/spec/ruby/library/socket/socket/connect_spec.rb @@ -34,6 +34,12 @@ lambda { @client.connect(@server.getsockname) + + # A second call needed if non-blocking sockets become default + # XXX honestly I don't expect any real code to care about this spec + # as it's too implementation-dependent and checking for connect() + # errors is futile anyways because of TOCTOU + @client.connect(@server.getsockname) }.should raise_error(Errno::EISCONN) end diff --git a/spec/ruby/library/socket/socket/getnameinfo_spec.rb b/spec/ruby/library/socket/socket/getnameinfo_spec.rb index 1e58205f255ea8..fbbbcb53c5e0a5 100644 --- a/spec/ruby/library/socket/socket/getnameinfo_spec.rb +++ b/spec/ruby/library/socket/socket/getnameinfo_spec.rb @@ -105,15 +105,6 @@ def should_be_valid_dns_name(name) lambda { Socket.getnameinfo([family_name]) }.should raise_error(ArgumentError) end - describe 'without custom flags' do - it 'returns an Array containing the hostname and service name' do - array = Socket.getnameinfo(@addr) - array.should be_an_instance_of(Array) - array[0].should include(@hostname) - array[1].should == 'ftp' - end - end - platform_is_not :windows do describe 'using NI_NUMERICHOST as the flag' do it 'returns an Array containing the numeric hostname and service name' do diff --git a/spec/ruby/library/tempfile/open_spec.rb b/spec/ruby/library/tempfile/open_spec.rb index 062d1a3fc22a7d..c4c3d91051757b 100644 --- a/spec/ruby/library/tempfile/open_spec.rb +++ b/spec/ruby/library/tempfile/open_spec.rb @@ -41,6 +41,14 @@ Tempfile.open(["specs", ".tt"]) { |tempfile| @tempfile = tempfile } @tempfile.path.should =~ /specs.*\.tt$/ end + + it "passes the third argument (options) to open" do + Tempfile.open("specs", Dir.tmpdir, encoding: "IBM037:IBM037", binmode: true) do |tempfile| + @tempfile = tempfile + tempfile.external_encoding.should == Encoding.find("IBM037") + tempfile.binmode?.should be_true + end + end end describe "Tempfile.open when passed a block" do diff --git a/spec/ruby/library/thread/exclusive_spec.rb b/spec/ruby/library/thread/exclusive_spec.rb deleted file mode 100644 index 2be2346761ca5f..00000000000000 --- a/spec/ruby/library/thread/exclusive_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require_relative '../../spec_helper' -require 'thread' - -describe "Thread.exclusive" do - before :each do - ScratchPad.clear - end - - it "returns the result of yielding" do - Thread.exclusive { :result }.should == :result - end -end diff --git a/spec/ruby/library/thread/queue/append_spec.rb b/spec/ruby/library/thread/queue/append_spec.rb deleted file mode 100644 index 09b9fc0cfa170e..00000000000000 --- a/spec/ruby/library/thread/queue/append_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/enque' - -describe "Thread::Queue#<<" do - it_behaves_like :queue_enq, :<<, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/clear_spec.rb b/spec/ruby/library/thread/queue/clear_spec.rb deleted file mode 100644 index f4ec45add681dd..00000000000000 --- a/spec/ruby/library/thread/queue/clear_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/clear' - -describe "Thread::Queue#clear" do - it_behaves_like :queue_clear, :clear, -> { Queue.new } - - # TODO: test for atomicity of Queue#clear -end diff --git a/spec/ruby/library/thread/queue/close_spec.rb b/spec/ruby/library/thread/queue/close_spec.rb deleted file mode 100644 index 32bc0a32d7bdfc..00000000000000 --- a/spec/ruby/library/thread/queue/close_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/close' - -describe "Queue#close" do - it_behaves_like :queue_close, :close, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/closed_spec.rb b/spec/ruby/library/thread/queue/closed_spec.rb deleted file mode 100644 index df3596b14b1450..00000000000000 --- a/spec/ruby/library/thread/queue/closed_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/closed' - -describe "Queue#closed?" do - it_behaves_like :queue_closed?, :closed?, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/deq_spec.rb b/spec/ruby/library/thread/queue/deq_spec.rb deleted file mode 100644 index 176dc3620ed73f..00000000000000 --- a/spec/ruby/library/thread/queue/deq_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/deque' - -describe "Thread::Queue#deq" do - it_behaves_like :queue_deq, :deq, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/empty_spec.rb b/spec/ruby/library/thread/queue/empty_spec.rb deleted file mode 100644 index 6854d311553b9e..00000000000000 --- a/spec/ruby/library/thread/queue/empty_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/empty' - -describe "Thread::Queue#empty?" do - it_behaves_like :queue_empty?, :empty?, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/enq_spec.rb b/spec/ruby/library/thread/queue/enq_spec.rb deleted file mode 100644 index 01d3a48738e766..00000000000000 --- a/spec/ruby/library/thread/queue/enq_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/enque' - -describe "Thread::Queue#enq" do - it_behaves_like :queue_enq, :enq, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/length_spec.rb b/spec/ruby/library/thread/queue/length_spec.rb deleted file mode 100644 index 7b0a3ccccd04e2..00000000000000 --- a/spec/ruby/library/thread/queue/length_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/length' - -describe "Thread::Queue#length" do - it_behaves_like :queue_length, :length, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/num_waiting_spec.rb b/spec/ruby/library/thread/queue/num_waiting_spec.rb deleted file mode 100644 index dcb6d6fb72625e..00000000000000 --- a/spec/ruby/library/thread/queue/num_waiting_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/num_waiting' - -describe "Thread::Queue#num_waiting" do - it_behaves_like :queue_num_waiting, :num_waiting, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/pop_spec.rb b/spec/ruby/library/thread/queue/pop_spec.rb deleted file mode 100644 index ab4f44d6d46ba1..00000000000000 --- a/spec/ruby/library/thread/queue/pop_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/deque' - -describe "Thread::Queue#pop" do - it_behaves_like :queue_deq, :pop, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/push_spec.rb b/spec/ruby/library/thread/queue/push_spec.rb deleted file mode 100644 index 670a2095e3289b..00000000000000 --- a/spec/ruby/library/thread/queue/push_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/enque' - -describe "Thread::Queue#push" do - it_behaves_like :queue_enq, :push, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/shift_spec.rb b/spec/ruby/library/thread/queue/shift_spec.rb deleted file mode 100644 index fc3f6e9b0c5036..00000000000000 --- a/spec/ruby/library/thread/queue/shift_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/deque' - -describe "Thread::Queue#shift" do - it_behaves_like :queue_deq, :shift, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue/size_spec.rb b/spec/ruby/library/thread/queue/size_spec.rb deleted file mode 100644 index 00c3d19f322cab..00000000000000 --- a/spec/ruby/library/thread/queue/size_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/length' - -describe "Thread::Queue#size" do - it_behaves_like :queue_length, :size, -> { Queue.new } -end diff --git a/spec/ruby/library/thread/queue_spec.rb b/spec/ruby/library/thread/queue_spec.rb new file mode 100644 index 00000000000000..c7e2bb1b501f55 --- /dev/null +++ b/spec/ruby/library/thread/queue_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "Thread::Queue" do + it "is the same class as ::Queue" do + Thread.should have_constant(:Queue) + Thread::Queue.should equal ::Queue + end +end diff --git a/spec/ruby/library/thread/shared/queue/close.rb b/spec/ruby/library/thread/shared/queue/close.rb deleted file mode 100644 index 4457f3ae8b97a4..00000000000000 --- a/spec/ruby/library/thread/shared/queue/close.rb +++ /dev/null @@ -1,26 +0,0 @@ -describe :queue_close, shared: true do - it "closes the queue and returns nil for further #pop" do - q = @object.call - q << 1 - q.close - q.pop.should == 1 - q.pop.should == nil - q.pop.should == nil - end - - it "prevents further #push" do - q = @object.call - q.close - lambda { - q << 1 - }.should raise_error(ClosedQueueError) - end - - it "may be called multiple times" do - q = @object.call - q.close - q.closed?.should be_true - q.close # no effect - q.closed?.should be_true - end -end diff --git a/spec/ruby/library/thread/shared/queue/deque.rb b/spec/ruby/library/thread/shared/queue/deque.rb deleted file mode 100644 index 1b06dffa2cbc59..00000000000000 --- a/spec/ruby/library/thread/shared/queue/deque.rb +++ /dev/null @@ -1,37 +0,0 @@ -describe :queue_deq, shared: true do - it "removes an item from the Queue" do - q = @object.call - q << Object.new - q.size.should == 1 - q.send(@method) - q.size.should == 0 - end - - it "returns items in the order they were added" do - q = @object.call - q << 1 - q << 2 - q.send(@method).should == 1 - q.send(@method).should == 2 - end - - it "blocks the thread until there are items in the queue" do - q = @object.call - v = 0 - - th = Thread.new do - q.send(@method) - v = 1 - end - - v.should == 0 - q << Object.new - th.join - v.should == 1 - end - - it "raises a ThreadError if Queue is empty" do - q = @object.call - lambda { q.send(@method,true) }.should raise_error(ThreadError) - end -end diff --git a/spec/ruby/library/thread/shared/queue/enque.rb b/spec/ruby/library/thread/shared/queue/enque.rb deleted file mode 100644 index 36b98d3a0738ac..00000000000000 --- a/spec/ruby/library/thread/shared/queue/enque.rb +++ /dev/null @@ -1,10 +0,0 @@ -describe :queue_enq, shared: true do - it "adds an element to the Queue" do - q = @object.call - q.size.should == 0 - q.send(@method, Object.new) - q.size.should == 1 - q.send(@method, Object.new) - q.size.should == 2 - end -end diff --git a/spec/ruby/library/thread/sizedqueue/append_spec.rb b/spec/ruby/library/thread/sizedqueue/append_spec.rb deleted file mode 100644 index daf65178dee010..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/append_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/enque' -require_relative 'shared/enque' - -describe "Thread::SizedQueue#<<" do - it_behaves_like :queue_enq, :<<, -> { SizedQueue.new(10) } -end - -describe "Thread::SizedQueue#<<" do - it_behaves_like :sizedqueue_enq, :<< -end diff --git a/spec/ruby/library/thread/sizedqueue/clear_spec.rb b/spec/ruby/library/thread/sizedqueue/clear_spec.rb deleted file mode 100644 index 86e16f275e1b31..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/clear_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/clear' - -describe "Thread::SizedQueue#clear" do - it_behaves_like :queue_clear, :clear, -> { SizedQueue.new(10) } - - # TODO: test for atomicity of Queue#clear -end diff --git a/spec/ruby/library/thread/sizedqueue/deq_spec.rb b/spec/ruby/library/thread/sizedqueue/deq_spec.rb deleted file mode 100644 index 0ed42cba917080..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/deq_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/deque' - -describe "Thread::SizedQueue#deq" do - it_behaves_like :queue_deq, :deq, -> { SizedQueue.new(10) } -end diff --git a/spec/ruby/library/thread/sizedqueue/empty_spec.rb b/spec/ruby/library/thread/sizedqueue/empty_spec.rb deleted file mode 100644 index d44ea405d59836..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/empty_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/empty' - -describe "Thread::SizedQueue#empty?" do - it_behaves_like :queue_empty?, :empty?, -> { SizedQueue.new(10) } -end diff --git a/spec/ruby/library/thread/sizedqueue/enq_spec.rb b/spec/ruby/library/thread/sizedqueue/enq_spec.rb deleted file mode 100644 index b81fd2b22503cd..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/enq_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/enque' -require_relative 'shared/enque' - -describe "Thread::SizedQueue#enq" do - it_behaves_like :queue_enq, :enq, -> { SizedQueue.new(10) } -end - -describe "Thread::SizedQueue#enq" do - it_behaves_like :sizedqueue_enq, :enq -end diff --git a/spec/ruby/library/thread/sizedqueue/length_spec.rb b/spec/ruby/library/thread/sizedqueue/length_spec.rb deleted file mode 100644 index e15db85f82059c..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/length_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/length' - -describe "Thread::SizedQueue#length" do - it_behaves_like :queue_length, :length, -> { SizedQueue.new(10) } -end diff --git a/spec/ruby/library/thread/sizedqueue/max_spec.rb b/spec/ruby/library/thread/sizedqueue/max_spec.rb deleted file mode 100644 index 75b1957e8671b0..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/max_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' - -describe "Thread::SizedQueue#max" do - before :each do - @sized_queue = SizedQueue.new(5) - end - - it "returns the size of the queue" do - @sized_queue.max.should == 5 - end -end - -describe "Thread::SizedQueue#max=" do - before :each do - @sized_queue = SizedQueue.new(5) - end - - it "sets the size of the queue" do - @sized_queue.max.should == 5 - @sized_queue.max = 10 - @sized_queue.max.should == 10 - end - - it "does not remove items already in the queue beyond the maximum" do - @sized_queue.enq 1 - @sized_queue.enq 2 - @sized_queue.enq 3 - @sized_queue.max = 2 - (@sized_queue.size > @sized_queue.max).should be_true - @sized_queue.deq.should == 1 - @sized_queue.deq.should == 2 - @sized_queue.deq.should == 3 - end - - it "raises a TypeError when given a non-numeric value" do - lambda { @sized_queue.max = "foo" }.should raise_error(TypeError) - lambda { @sized_queue.max = Object.new }.should raise_error(TypeError) - end - - it "raises an argument error when set to zero" do - @sized_queue.max.should == 5 - lambda { @sized_queue.max = 0 }.should raise_error(ArgumentError) - @sized_queue.max.should == 5 - end - - it "raises an argument error when set to a negative number" do - @sized_queue.max.should == 5 - lambda { @sized_queue.max = -1 }.should raise_error(ArgumentError) - @sized_queue.max.should == 5 - end -end diff --git a/spec/ruby/library/thread/sizedqueue/new_spec.rb b/spec/ruby/library/thread/sizedqueue/new_spec.rb deleted file mode 100644 index 364e10f6948a09..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/new_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' - -describe "Thread::SizedQueue#new" do - it "returns a new SizedQueue" do - SizedQueue.new(1).should be_kind_of(SizedQueue) - end - - it "raises a TypeError when the given argument is not Numeric" do - lambda { SizedQueue.new("foo") }.should raise_error(TypeError) - lambda { SizedQueue.new(Object.new) }.should raise_error(TypeError) - end - - it "raises an argument error when no argument is given" do - lambda { SizedQueue.new }.should raise_error(ArgumentError) - end - - it "raises an argument error when the given argument is zero" do - lambda { SizedQueue.new(0) }.should raise_error(ArgumentError) - end - - it "raises an argument error when the given argument is negative" do - lambda { SizedQueue.new(-1) }.should raise_error(ArgumentError) - end -end diff --git a/spec/ruby/library/thread/sizedqueue/num_waiting_spec.rb b/spec/ruby/library/thread/sizedqueue/num_waiting_spec.rb deleted file mode 100644 index 7c5243250d8259..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/num_waiting_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/num_waiting' - -describe "Thread::SizedQueue#num_waiting" do - it_behaves_like :queue_num_waiting, :num_waiting, -> { SizedQueue.new(10) } - - it "reports the number of threads waiting to push" do - q = SizedQueue.new(1) - q.push(1) - t = Thread.new { q.push(2) } - sleep 0.05 until t.stop? - q.num_waiting.should == 1 - - q.pop - t.join - end -end diff --git a/spec/ruby/library/thread/sizedqueue/pop_spec.rb b/spec/ruby/library/thread/sizedqueue/pop_spec.rb deleted file mode 100644 index 21508bd26b1625..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/pop_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/deque' - -describe "Thread::SizedQueue#pop" do - it_behaves_like :queue_deq, :pop, -> { SizedQueue.new(10) } -end diff --git a/spec/ruby/library/thread/sizedqueue/push_spec.rb b/spec/ruby/library/thread/sizedqueue/push_spec.rb deleted file mode 100644 index aefe6eb28aae4f..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/push_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/enque' -require_relative 'shared/enque' - -describe "Thread::SizedQueue#push" do - it_behaves_like :queue_enq, :push, -> { SizedQueue.new(10) } -end - -describe "Thread::SizedQueue#push" do - it_behaves_like :sizedqueue_enq, :push -end diff --git a/spec/ruby/library/thread/sizedqueue/shift_spec.rb b/spec/ruby/library/thread/sizedqueue/shift_spec.rb deleted file mode 100644 index 6c2adfce9d5988..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/shift_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/deque' - -describe "Thread::SizedQueue#shift" do - it_behaves_like :queue_deq, :shift, -> { SizedQueue.new(10) } -end diff --git a/spec/ruby/library/thread/sizedqueue/size_spec.rb b/spec/ruby/library/thread/sizedqueue/size_spec.rb deleted file mode 100644 index 1bacef1eb15ceb..00000000000000 --- a/spec/ruby/library/thread/sizedqueue/size_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../spec_helper' -require 'thread' -require_relative '../shared/queue/length' - -describe "Thread::SizedQueue#size" do - it_behaves_like :queue_length, :size, -> { SizedQueue.new(10) } -end diff --git a/spec/ruby/library/thread/sizedqueue_spec.rb b/spec/ruby/library/thread/sizedqueue_spec.rb new file mode 100644 index 00000000000000..6151ff437c4853 --- /dev/null +++ b/spec/ruby/library/thread/sizedqueue_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "Thread::SizedQueue" do + it "is the same class as ::SizedQueue" do + Thread.should have_constant(:SizedQueue) + Thread::SizedQueue.should equal ::SizedQueue + end +end diff --git a/spec/ruby/library/win32ole/win32ole/new_spec.rb b/spec/ruby/library/win32ole/win32ole/new_spec.rb index cb45488288e7cd..d1c3e02593d375 100644 --- a/spec/ruby/library/win32ole/win32ole/new_spec.rb +++ b/spec/ruby/library/win32ole/win32ole/new_spec.rb @@ -17,7 +17,7 @@ end it "raises WIN32OLERuntimeError if invalid string is given" do - lambda { WIN32OLESpecs.new_ole('foo') }.should raise_error( WIN32OLERuntimeError ) + lambda { WIN32OLE.new('foo') }.should raise_error( WIN32OLERuntimeError ) end end diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb index 4128a636adc5bc..3cff55ac6e6ac8 100644 --- a/spec/ruby/shared/kernel/raise.rb +++ b/spec/ruby/shared/kernel/raise.rb @@ -44,11 +44,11 @@ it "re-raises the previously rescued exception if no exception is specified" do lambda do begin - raise Exception, "outer" + @object.raise Exception, "outer" ScratchPad.record :no_abort rescue begin - raise StandardError, "inner" + @object.raise StandardError, "inner" rescue end @@ -62,16 +62,17 @@ it "re-raises a previously rescued exception without overwriting the backtrace" do begin - raise 'raised' + initial_raise_line = __LINE__; @object.raise 'raised' rescue => raised begin - raise_again_line = __LINE__; raise raised + raise_again_line = __LINE__; @object.raise raised rescue => raised_again # This spec is written using #backtrace and matching the line number # from the string, as backtrace_locations is a more advanced # method that is not always supported by implementations. - raised_again.backtrace.first.should_not include(":#{raise_again_line}:") + raised_again.backtrace.first.should include("#{__FILE__}:#{initial_raise_line}:") + raised_again.backtrace.first.should_not include("#{__FILE__}:#{raise_again_line}:") end end end diff --git a/spec/ruby/library/thread/shared/queue/clear.rb b/spec/ruby/shared/queue/clear.rb similarity index 84% rename from spec/ruby/library/thread/shared/queue/clear.rb rename to spec/ruby/shared/queue/clear.rb index 59ea37d6153617..5db5a5b49735ff 100644 --- a/spec/ruby/library/thread/shared/queue/clear.rb +++ b/spec/ruby/shared/queue/clear.rb @@ -7,4 +7,6 @@ queue.clear queue.empty?.should be_true end + + # TODO: test for atomicity of Queue#clear end diff --git a/spec/ruby/shared/queue/close.rb b/spec/ruby/shared/queue/close.rb new file mode 100644 index 00000000000000..0e7c69acbaeaa6 --- /dev/null +++ b/spec/ruby/shared/queue/close.rb @@ -0,0 +1,14 @@ +describe :queue_close, shared: true do + it "may be called multiple times" do + q = @object.call + q.close + q.closed?.should be_true + q.close # no effect + q.closed?.should be_true + end + + it "returns self" do + q = @object.call + q.close.should == q + end +end diff --git a/spec/ruby/library/thread/shared/queue/closed.rb b/spec/ruby/shared/queue/closed.rb similarity index 100% rename from spec/ruby/library/thread/shared/queue/closed.rb rename to spec/ruby/shared/queue/closed.rb diff --git a/spec/ruby/shared/queue/deque.rb b/spec/ruby/shared/queue/deque.rb new file mode 100644 index 00000000000000..3590f367bac59d --- /dev/null +++ b/spec/ruby/shared/queue/deque.rb @@ -0,0 +1,85 @@ +describe :queue_deq, shared: true do + it "removes an item from the queue" do + q = @object.call + q << Object.new + q.size.should == 1 + q.send @method + q.size.should == 0 + end + + it "returns items in the order they were added" do + q = @object.call + q << 1 + q << 2 + q.send(@method).should == 1 + q.send(@method).should == 2 + end + + it "blocks the thread until there are items in the queue" do + q = @object.call + v = 0 + + th = Thread.new do + q.send(@method) + v = 1 + end + + v.should == 0 + q << Object.new + th.join + v.should == 1 + end + + it "removes an item from a closed queue" do + q = @object.call + q << 1 + q.close + q.send(@method).should == 1 + end + + it "returns nil for a closed empty queue" do + q = @object.call + q.close + q.send(@method).should == nil + end + + it "returns nil for an empty queue that becomes closed" do + q = @object.call + + t = Thread.new { + q.send(@method).should == nil + } + + Thread.pass until t.status == "sleep" && q.num_waiting == 1 + q.close + t.join + end + + describe "in non-blocking mode" do + it "removes an item from the queue" do + q = @object.call + q << Object.new + q.size.should == 1 + q.send(@method, true) + q.size.should == 0 + end + + it "raises a ThreadError if the queue is empty" do + q = @object.call + lambda { q.send(@method, true) }.should raise_error(ThreadError) + end + + it "removes an item from a closed queue" do + q = @object.call + q << 1 + q.close + q.send(@method, true).should == 1 + end + + it "raises a ThreadError for a closed empty queue" do + q = @object.call + q.close + lambda { q.send(@method, true) }.should raise_error(ThreadError) + end + end +end diff --git a/spec/ruby/library/thread/shared/queue/empty.rb b/spec/ruby/shared/queue/empty.rb similarity index 100% rename from spec/ruby/library/thread/shared/queue/empty.rb rename to spec/ruby/shared/queue/empty.rb diff --git a/spec/ruby/shared/queue/enque.rb b/spec/ruby/shared/queue/enque.rb new file mode 100644 index 00000000000000..ad413e1f469814 --- /dev/null +++ b/spec/ruby/shared/queue/enque.rb @@ -0,0 +1,18 @@ +describe :queue_enq, shared: true do + it "adds an element to the Queue" do + q = @object.call + q.size.should == 0 + q.send @method, Object.new + q.size.should == 1 + q.send @method, Object.new + q.size.should == 2 + end + + it "is an error for a closed queue" do + q = @object.call + q.close + lambda { + q.send @method, Object.new + }.should raise_error(ClosedQueueError) + end +end diff --git a/spec/ruby/library/thread/shared/queue/length.rb b/spec/ruby/shared/queue/length.rb similarity index 100% rename from spec/ruby/library/thread/shared/queue/length.rb rename to spec/ruby/shared/queue/length.rb diff --git a/spec/ruby/library/thread/shared/queue/num_waiting.rb b/spec/ruby/shared/queue/num_waiting.rb similarity index 100% rename from spec/ruby/library/thread/shared/queue/num_waiting.rb rename to spec/ruby/shared/queue/num_waiting.rb diff --git a/spec/ruby/library/thread/sizedqueue/shared/enque.rb b/spec/ruby/shared/sizedqueue/enque.rb similarity index 68% rename from spec/ruby/library/thread/sizedqueue/shared/enque.rb rename to spec/ruby/shared/sizedqueue/enque.rb index 627f8bd74525b6..b724101269ed26 100644 --- a/spec/ruby/library/thread/sizedqueue/shared/enque.rb +++ b/spec/ruby/shared/sizedqueue/enque.rb @@ -1,6 +1,6 @@ describe :sizedqueue_enq, shared: true do it "blocks if queued elements exceed size" do - q = SizedQueue.new(1) + q = @object.call(1) q.size.should == 0 q.send(@method, :first_element) @@ -19,7 +19,7 @@ end it "raises a ThreadError if queued elements exceed size when not blocking" do - q = SizedQueue.new(2) + q = @object.call(2) non_blocking = true add_to_queue = lambda { q.send(@method, Object.new, non_blocking) } @@ -31,4 +31,20 @@ q.size.should == 2 add_to_queue.should raise_error(ThreadError) end + + it "interrupts enqueuing threads with ClosedQueueError when the queue is closed" do + q = @object.call(1) + q << 1 + + t = Thread.new { + lambda { q.send(@method, 2) }.should raise_error(ClosedQueueError) + } + + Thread.pass until q.num_waiting == 1 + + q.close + + t.join + q.pop.should == 1 + end end diff --git a/spec/ruby/shared/sizedqueue/max.rb b/spec/ruby/shared/sizedqueue/max.rb new file mode 100644 index 00000000000000..cd4b47f1c1492c --- /dev/null +++ b/spec/ruby/shared/sizedqueue/max.rb @@ -0,0 +1,47 @@ +describe :sizedqueue_max, shared: true do + it "returns the size of the queue" do + q = @object.call(5) + q.max.should == 5 + end +end + +describe :sizedqueue_max=, shared: true do + it "sets the size of the queue" do + q = @object.call(5) + q.max.should == 5 + q.max = 10 + q.max.should == 10 + end + + it "does not remove items already in the queue beyond the maximum" do + q = @object.call(5) + q.enq 1 + q.enq 2 + q.enq 3 + q.max = 2 + (q.size > q.max).should be_true + q.deq.should == 1 + q.deq.should == 2 + q.deq.should == 3 + end + + it "raises a TypeError when given a non-numeric value" do + q = @object.call(5) + lambda { q.max = "foo" }.should raise_error(TypeError) + lambda { q.max = Object.new }.should raise_error(TypeError) + end + + it "raises an argument error when set to zero" do + q = @object.call(5) + q.max.should == 5 + lambda { q.max = 0 }.should raise_error(ArgumentError) + q.max.should == 5 + end + + it "raises an argument error when set to a negative number" do + q = @object.call(5) + q.max.should == 5 + lambda { q.max = -1 }.should raise_error(ArgumentError) + q.max.should == 5 + end +end diff --git a/spec/ruby/shared/sizedqueue/new.rb b/spec/ruby/shared/sizedqueue/new.rb new file mode 100644 index 00000000000000..4439f2a9c6692d --- /dev/null +++ b/spec/ruby/shared/sizedqueue/new.rb @@ -0,0 +1,18 @@ +describe :sizedqueue_new, shared: true do + it "raises a TypeError when the given argument is not Numeric" do + lambda { @object.call("foo") }.should raise_error(TypeError) + lambda { @object.call(Object.new) }.should raise_error(TypeError) + end + + it "raises an argument error when no argument is given" do + lambda { @object.call }.should raise_error(ArgumentError) + end + + it "raises an argument error when the given argument is zero" do + lambda { @object.call(0) }.should raise_error(ArgumentError) + end + + it "raises an argument error when the given argument is negative" do + lambda { @object.call(-1) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/shared/sizedqueue/num_waiting.rb b/spec/ruby/shared/sizedqueue/num_waiting.rb new file mode 100644 index 00000000000000..8c31e48ca57f8f --- /dev/null +++ b/spec/ruby/shared/sizedqueue/num_waiting.rb @@ -0,0 +1,12 @@ +describe :sizedqueue_num_waiting, shared: true do + it "reports the number of threads waiting to push" do + q = @object.call(1) + q.push(1) + t = Thread.new { q.push(2) } + sleep 0.05 until t.stop? + q.num_waiting.should == 1 + + q.pop + t.join + end +end diff --git a/st.c b/st.c index 1a47525707a239..5256f95b6ea852 100644 --- a/st.c +++ b/st.c @@ -1196,7 +1196,7 @@ st_insert(st_table *tab, st_data_t key, st_data_t value) /* Insert (KEY, VALUE, HASH) into table TAB. The table should not have entry with KEY before the insertion. */ -static inline void +void st_add_direct_with_hash(st_table *tab, st_data_t key, st_data_t value, st_hash_t hash) { @@ -1766,6 +1766,10 @@ st_values_check(st_table *tab, st_data_t *values, st_index_t size, #define C1 BIG_CONSTANT(0x87c37b91,0x114253d5); #define C2 BIG_CONSTANT(0x4cf5ad43,0x2745937f); #endif +NO_SANITIZE("unsigned-integer-overflow", static inline st_index_t murmur_step(st_index_t h, st_index_t k)); +NO_SANITIZE("unsigned-integer-overflow", static inline st_index_t murmur_finish(st_index_t h)); +NO_SANITIZE("unsigned-integer-overflow", extern st_index_t st_hash(const void *ptr, size_t len, st_index_t h)); + static inline st_index_t murmur_step(st_index_t h, st_index_t k) { @@ -1969,6 +1973,7 @@ st_hash_uint32(st_index_t h, uint32_t i) return murmur_step(h, i); } +NO_SANITIZE("unsigned-integer-overflow", extern st_index_t st_hash_uint(st_index_t h, st_index_t i)); st_index_t st_hash_uint(st_index_t h, st_index_t i) { @@ -2006,18 +2011,18 @@ strhash(st_data_t arg) int st_locale_insensitive_strcasecmp(const char *s1, const char *s2) { - unsigned int c1, c2; + char c1, c2; while (1) { - c1 = (unsigned char)*s1++; - c2 = (unsigned char)*s2++; + c1 = *s1++; + c2 = *s2++; if (c1 == '\0' || c2 == '\0') { if (c1 != '\0') return 1; if (c2 != '\0') return -1; return 0; } - if ((unsigned int)(c1 - 'A') <= ('Z' - 'A')) c1 += 'a' - 'A'; - if ((unsigned int)(c2 - 'A') <= ('Z' - 'A')) c2 += 'a' - 'A'; + if (('A' <= c1) && (c1 <= 'Z')) c1 += 'a' - 'A'; + if (('A' <= c2) && (c2 <= 'Z')) c2 += 'a' - 'A'; if (c1 != c2) { if (c1 > c2) return 1; @@ -2030,18 +2035,19 @@ st_locale_insensitive_strcasecmp(const char *s1, const char *s2) int st_locale_insensitive_strncasecmp(const char *s1, const char *s2, size_t n) { - unsigned int c1, c2; + char c1, c2; + size_t i; - while (n--) { - c1 = (unsigned char)*s1++; - c2 = (unsigned char)*s2++; + for (i = 0; i < n; i++) { + c1 = *s1++; + c2 = *s2++; if (c1 == '\0' || c2 == '\0') { if (c1 != '\0') return 1; if (c2 != '\0') return -1; return 0; } - if ((unsigned int)(c1 - 'A') <= ('Z' - 'A')) c1 += 'a' - 'A'; - if ((unsigned int)(c2 - 'A') <= ('Z' - 'A')) c2 += 'a' - 'A'; + if (('A' <= c1) && (c1 <= 'Z')) c1 += 'a' - 'A'; + if (('A' <= c2) && (c2 <= 'Z')) c2 += 'a' - 'A'; if (c1 != c2) { if (c1 > c2) return 1; @@ -2052,7 +2058,7 @@ st_locale_insensitive_strncasecmp(const char *s1, const char *s2, size_t n) return 0; } -PUREFUNC(static st_index_t strcasehash(st_data_t)); +NO_SANITIZE("unsigned-integer-overflow", PUREFUNC(static st_index_t strcasehash(st_data_t))); static st_index_t strcasehash(st_data_t arg) { @@ -2178,13 +2184,13 @@ st_rehash_indexed(st_table *tab) ind = hash_bin(p->hash, tab); for(;;) { st_index_t bin = get_bin(bins, size_ind, ind); - st_table_entry *q = &tab->entries[bin - ENTRY_BASE]; if (EMPTY_OR_DELETED_BIN_P(bin)) { /* ok, new room */ set_bin(bins, size_ind, ind, i + ENTRY_BASE); break; } else { + st_table_entry *q = &tab->entries[bin - ENTRY_BASE]; DO_PTR_EQUAL_CHECK(tab, q, p->hash, p->key, eq_p, rebuilt_p); if (EXPECT(rebuilt_p, 0)) return TRUE; @@ -2281,24 +2287,16 @@ st_insert_generic(st_table *tab, long argc, const VALUE *argv, VALUE hash) st_rehash(tab); } -/* Mimics ruby's { foo => bar } syntax. This function is placed here - because it touches table internals and write barriers at once. */ +/* Mimics ruby's { foo => bar } syntax. This function is subpart + of rb_hash_bulk_insert. */ void -rb_hash_bulk_insert(long argc, const VALUE *argv, VALUE hash) +rb_hash_bulk_insert_into_st_table(long argc, const VALUE *argv, VALUE hash) { - st_index_t n; - st_table *tab = RHASH(hash)->ntbl; - - st_assert(argc % 2 == 0); - if (! argc) - return; - if (! tab) { - VALUE tmp = rb_hash_new_with_size(argc / 2); - RBASIC_CLEAR_CLASS(tmp); - RHASH(hash)->ntbl = tab = RHASH(tmp)->ntbl; - RHASH(tmp)->ntbl = NULL; - } - n = tab->num_entries + argc / 2; + st_index_t n, size = argc / 2; + st_table *tab = RHASH_ST_TABLE(hash); + + tab = RHASH_TBL_RAW(hash); + n = tab->num_entries + size; st_expand_table(tab, n); if (UNLIKELY(tab->num_entries)) st_insert_generic(tab, argc, argv, hash); diff --git a/string.c b/string.c index e654a3023d186e..184d7df4504dfd 100644 --- a/string.c +++ b/string.c @@ -490,7 +490,7 @@ search_nonascii(const char *p, const char *e) } } #endif -#ifdef HAVE_BUILTIN___BUILTIN_ASSUME_ALIGNED +#if defined(HAVE_BUILTIN___BUILTIN_ASSUME_ALIGNED) &&! UNALIGNED_WORD_ACCESS #define aligned_ptr(value) \ __builtin_assume_aligned((value), sizeof(uintptr_t)) #else @@ -803,6 +803,11 @@ VALUE rb_str_new_cstr(const char *ptr) { must_not_null(ptr); + /* rb_str_new_cstr() can take pointer from non-malloc-generated + * memory regions, and that cannot be detected by the MSAN. Just + * trust the programmer that the argument passed here is a sane C + * string. */ + __msan_unpoison_string(ptr); return rb_str_new(ptr, strlen(ptr)); } @@ -2006,9 +2011,7 @@ rb_str_format_m(VALUE str, VALUE arg) VALUE tmp = rb_check_array_type(arg); if (!NIL_P(tmp)) { - VALUE rv = rb_str_format(RARRAY_LENINT(tmp), RARRAY_CONST_PTR(tmp), str); - RB_GC_GUARD(tmp); - return rv; + return rb_str_format(RARRAY_LENINT(tmp), RARRAY_CONST_PTR(tmp), str); } return rb_str_format(1, &arg, str); } @@ -5410,7 +5413,8 @@ rb_str_setbyte(VALUE str, VALUE index, VALUE value) long pos = NUM2LONG(index); int byte = NUM2INT(value); long len = RSTRING_LEN(str); - char *head, *ptr, *left = 0; + char *head, *left = 0; + unsigned char *ptr; rb_encoding *enc; int cr = ENC_CODERANGE_UNKNOWN, width, nlen; @@ -5418,17 +5422,21 @@ rb_str_setbyte(VALUE str, VALUE index, VALUE value) rb_raise(rb_eIndexError, "index %ld out of string", pos); if (pos < 0) pos += len; + if (byte < 0) + rb_raise(rb_eRangeError, "integer %d too small to convert into `unsigned char'", byte); + if (UCHAR_MAX < byte) + rb_raise(rb_eRangeError, "integer %d too big to convert into `unsigned char'", byte); if (!str_independent(str)) str_make_independent(str); enc = STR_ENC_GET(str); head = RSTRING_PTR(str); - ptr = &head[pos]; + ptr = (unsigned char *)&head[pos]; if (!STR_EMBED_P(str)) { cr = ENC_CODERANGE(str); switch (cr) { case ENC_CODERANGE_7BIT: - left = ptr; + left = (char *)ptr; *ptr = byte; if (ISASCII(byte)) goto end; nlen = rb_enc_precise_mbclen(left, head+len, enc); @@ -6182,7 +6190,7 @@ undump_after_backslash(VALUE undumped, const char **ss, const char *s_end, rb_en unsigned int c; int codelen; size_t hexlen; - char buf[6]; + unsigned char buf[6]; static rb_encoding *enc_utf8 = NULL; switch (*s) { @@ -6200,8 +6208,8 @@ undump_after_backslash(VALUE undumped, const char **ss, const char *s_end, rb_en case 'b': case 'a': case 'e': - *buf = (char)unescape_ascii(*s); - rb_str_cat(undumped, buf, 1); + *buf = unescape_ascii(*s); + rb_str_cat(undumped, (char *)buf, 1); s++; break; case 'u': @@ -6241,8 +6249,8 @@ undump_after_backslash(VALUE undumped, const char **ss, const char *s_end, rb_en if (0xd800 <= c && c <= 0xdfff) { rb_raise(rb_eRuntimeError, "invalid Unicode codepoint"); } - codelen = rb_enc_mbcput(c, buf, *penc); - rb_str_cat(undumped, buf, codelen); + codelen = rb_enc_mbcput(c, (char *)buf, *penc); + rb_str_cat(undumped, (char *)buf, codelen); s += hexlen; } } @@ -6254,8 +6262,8 @@ undump_after_backslash(VALUE undumped, const char **ss, const char *s_end, rb_en if (0xd800 <= c && c <= 0xdfff) { rb_raise(rb_eRuntimeError, "invalid Unicode codepoint"); } - codelen = rb_enc_mbcput(c, buf, *penc); - rb_str_cat(undumped, buf, codelen); + codelen = rb_enc_mbcput(c, (char *)buf, *penc); + rb_str_cat(undumped, (char *)buf, codelen); s += hexlen; } break; @@ -6271,7 +6279,7 @@ undump_after_backslash(VALUE undumped, const char **ss, const char *s_end, rb_en if (hexlen != 2) { rb_raise(rb_eRuntimeError, "invalid hex escape"); } - rb_str_cat(undumped, buf, 1); + rb_str_cat(undumped, (char *)buf, 1); s += hexlen; break; default: @@ -6907,7 +6915,7 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) int cflag = 0; unsigned int c, c0, last = 0; int modify = 0, i, l; - char *s, *send; + unsigned char *s, *send; VALUE hash = 0; int singlebyte = single_byte_optimizable(str); int termlen; @@ -6991,18 +6999,18 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) if (cr == ENC_CODERANGE_VALID && rb_enc_asciicompat(e1)) cr = ENC_CODERANGE_7BIT; str_modify_keep_cr(str); - s = RSTRING_PTR(str); send = RSTRING_END(str); + s = (unsigned char *)RSTRING_PTR(str); send = (unsigned char *)RSTRING_END(str); termlen = rb_enc_mbminlen(enc); if (sflag) { int clen, tlen; long offset, max = RSTRING_LEN(str); unsigned int save = -1; - char *buf = ALLOC_N(char, max + termlen), *t = buf; + unsigned char *buf = ALLOC_N(unsigned char, max + termlen), *t = buf; while (s < send) { int may_modify = 0; - c0 = c = rb_enc_codepoint_len(s, send, &clen, e1); + c0 = c = rb_enc_codepoint_len((char *)s, (char *)send, &clen, e1); tlen = enc == e1 ? clen : rb_enc_codelen(c, enc); s += clen; @@ -7038,7 +7046,7 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) if ((offset = t - buf) + tlen > max) { size_t MAYBE_UNUSED(old) = max + termlen; max = offset + tlen + (send - s); - SIZED_REALLOC_N(buf, char, max + termlen, old); + SIZED_REALLOC_N(buf, unsigned char, max + termlen, old); t = buf + offset; } rb_enc_mbcput(c, t, enc); @@ -7051,8 +7059,8 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) if (!STR_EMBED_P(str)) { ruby_sized_xfree(STR_HEAP_PTR(str), STR_HEAP_SIZE(str)); } - TERM_FILL(t, termlen); - RSTRING(str)->as.heap.ptr = buf; + TERM_FILL((char *)t, termlen); + RSTRING(str)->as.heap.ptr = (char *)buf; RSTRING(str)->as.heap.len = t - buf; STR_SET_NOEMBED(str); RSTRING(str)->as.heap.aux.capa = max; @@ -7078,11 +7086,11 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) else { int clen, tlen; long offset, max = (long)((send - s) * 1.2); - char *buf = ALLOC_N(char, max + termlen), *t = buf; + unsigned char *buf = ALLOC_N(unsigned char, max + termlen), *t = buf; while (s < send) { int may_modify = 0; - c0 = c = rb_enc_codepoint_len(s, send, &clen, e1); + c0 = c = rb_enc_codepoint_len((char *)s, (char *)send, &clen, e1); tlen = enc == e1 ? clen : rb_enc_codelen(c, enc); if (c < 256) { @@ -7111,7 +7119,7 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) if ((offset = t - buf) + tlen > max) { size_t MAYBE_UNUSED(old) = max + termlen; max = offset + tlen + (long)((send - s) * 1.2); - SIZED_REALLOC_N(buf, char, max + termlen, old); + SIZED_REALLOC_N(buf, unsigned char, max + termlen, old); t = buf + offset; } if (s != t) { @@ -7127,8 +7135,8 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) if (!STR_EMBED_P(str)) { ruby_sized_xfree(STR_HEAP_PTR(str), STR_HEAP_SIZE(str)); } - TERM_FILL(t, termlen); - RSTRING(str)->as.heap.ptr = buf; + TERM_FILL((char *)t, termlen); + RSTRING(str)->as.heap.ptr = (char *)buf; RSTRING(str)->as.heap.len = t - buf; STR_SET_NOEMBED(str); RSTRING(str)->as.heap.aux.capa = max; @@ -7397,7 +7405,7 @@ rb_str_squeeze_bang(int argc, VALUE *argv, VALUE str) char squeez[TR_TABLE_SIZE]; rb_encoding *enc = 0; VALUE del = 0, nodel = 0; - char *s, *send, *t; + unsigned char *s, *send, *t; int i, modify = 0; int ascompat, singlebyte = single_byte_optimizable(str); unsigned int save; @@ -7418,15 +7426,15 @@ rb_str_squeeze_bang(int argc, VALUE *argv, VALUE str) } str_modify_keep_cr(str); - s = t = RSTRING_PTR(str); + s = t = (unsigned char *)RSTRING_PTR(str); if (!s || RSTRING_LEN(str) == 0) return Qnil; - send = RSTRING_END(str); + send = (unsigned char *)RSTRING_END(str); save = -1; ascompat = rb_enc_asciicompat(enc); if (singlebyte) { while (s < send) { - unsigned int c = *(unsigned char*)s++; + unsigned int c = *s++; if (c != save || (argc > 0 && !squeez[c])) { *t++ = save = c; } @@ -7437,14 +7445,14 @@ rb_str_squeeze_bang(int argc, VALUE *argv, VALUE str) unsigned int c; int clen; - if (ascompat && (c = *(unsigned char*)s) < 0x80) { + if (ascompat && (c = *s) < 0x80) { if (c != save || (argc > 0 && !squeez[c])) { *t++ = save = c; } s++; } else { - c = rb_enc_codepoint_len(s, send, &clen, enc); + c = rb_enc_codepoint_len((char *)s, (char *)send, &clen, enc); if (c != save || (argc > 0 && !tr_find(c, squeez, del, nodel))) { if (t != s) rb_enc_mbcput(c, t, enc); @@ -7456,9 +7464,9 @@ rb_str_squeeze_bang(int argc, VALUE *argv, VALUE str) } } - TERM_FILL(t, TERM_LEN(str)); - if (t - RSTRING_PTR(str) != RSTRING_LEN(str)) { - STR_SET_LEN(str, t - RSTRING_PTR(str)); + TERM_FILL((char *)t, TERM_LEN(str)); + if ((char *)t - RSTRING_PTR(str) != RSTRING_LEN(str)) { + STR_SET_LEN(str, (char *)t - RSTRING_PTR(str)); modify = 1; } @@ -8451,7 +8459,7 @@ rb_str_each_grapheme_cluster_size(VALUE str, VALUE args, VALUE eobj) rb_encoding *enc = rb_enc_from_index(ENCODING_GET(str)); const char *ptr, *end; - if (!rb_enc_unicode_p(enc) || single_byte_optimizable(str)) { + if (!rb_enc_unicode_p(enc)) { return rb_str_length(str); } @@ -8477,15 +8485,15 @@ rb_str_enumerate_grapheme_clusters(VALUE str, VALUE ary) VALUE orig = str; regex_t *reg_grapheme_cluster = NULL; rb_encoding *enc = rb_enc_from_index(ENCODING_GET(str)); - const char *ptr, *end; + const char *ptr0, *ptr, *end; - if (!rb_enc_unicode_p(enc) || single_byte_optimizable(str)) { + if (!rb_enc_unicode_p(enc)) { return rb_str_enumerate_chars(str, ary); } if (!ary) str = rb_str_new_frozen(str); reg_grapheme_cluster = get_reg_grapheme_cluster(enc); - ptr = RSTRING_PTR(str); + ptr0 = ptr = RSTRING_PTR(str); end = RSTRING_END(str); while (ptr < end) { @@ -8493,7 +8501,7 @@ rb_str_enumerate_grapheme_clusters(VALUE str, VALUE ary) (const OnigUChar *)ptr, (const OnigUChar *)end, (const OnigUChar *)ptr, NULL, 0); if (len <= 0) break; - ENUM_ELEM(ary, rb_enc_str_new(ptr, len, enc)); + ENUM_ELEM(ary, rb_str_subseq(str, ptr-ptr0, len)); ptr += len; } RB_GC_GUARD(str); diff --git a/struct.c b/struct.c index bed3a68ad4a173..45c9a6df4e294b 100644 --- a/struct.c +++ b/struct.c @@ -12,6 +12,7 @@ #include "internal.h" #include "vm_core.h" #include "id.h" +#include "transient_heap.h" /* only for struct[:field] access */ enum { @@ -654,6 +655,43 @@ rb_struct_initialize(VALUE self, VALUE values) return rb_struct_initialize_m(RARRAY_LENINT(values), RARRAY_CONST_PTR(values), self); } +static VALUE * +struct_heap_alloc(VALUE st, size_t len) +{ + VALUE *ptr = rb_transient_heap_alloc((VALUE)st, sizeof(VALUE) * len); + + if (ptr) { + RSTRUCT_TRANSIENT_SET(st); + return ptr; + } + else { + RSTRUCT_TRANSIENT_UNSET(st); + return ALLOC_N(VALUE, len); + } +} + +#if USE_TRANSIENT_HEAP +void +rb_struct_transient_heap_evacuate(VALUE obj, int promote) +{ + if (RSTRUCT_TRANSIENT_P(obj)) { + const VALUE *old_ptr = rb_struct_const_heap_ptr(obj); + VALUE *new_ptr; + long len = RSTRUCT_LEN(obj); + + if (promote) { + new_ptr = ALLOC_N(VALUE, len); + FL_UNSET_RAW(obj, RSTRUCT_TRANSIENT_FLAG); + } + else { + new_ptr = struct_heap_alloc(obj, len); + } + MEMCPY(new_ptr, old_ptr, VALUE, len); + RSTRUCT(obj)->as.heap.ptr = new_ptr; + } +} +#endif + static VALUE struct_alloc(VALUE klass) { @@ -668,9 +706,9 @@ struct_alloc(VALUE klass) rb_mem_clear((VALUE *)st->as.ary, n); } else { - st->as.heap.ptr = ALLOC_N(VALUE, n); - rb_mem_clear((VALUE *)st->as.heap.ptr, n); - st->as.heap.len = n; + st->as.heap.ptr = struct_heap_alloc((VALUE)st, n); + rb_mem_clear((VALUE *)st->as.heap.ptr, n); + st->as.heap.len = n; } return (VALUE)st; @@ -1062,6 +1100,8 @@ rb_struct_values_at(int argc, VALUE *argv, VALUE s) * call-seq: * struct.select {|obj| block } -> array * struct.select -> enumerator + * struct.filter {|obj| block } -> array + * struct.filter -> enumerator * * Yields each member value from the struct to the block and returns an Array * containing the member values from the +struct+ for which the given block @@ -1070,6 +1110,8 @@ rb_struct_values_at(int argc, VALUE *argv, VALUE s) * Lots = Struct.new(:a, :b, :c, :d, :e, :f) * l = Lots.new(11, 22, 33, 44, 55, 66) * l.select {|v| v.even? } #=> [22, 44, 66] + * + * Struct#filter is an alias for Struct#select. */ static VALUE @@ -1093,15 +1135,12 @@ rb_struct_select(int argc, VALUE *argv, VALUE s) static VALUE recursive_equal(VALUE s, VALUE s2, int recur) { - const VALUE *ptr, *ptr2; long i, len; if (recur) return Qtrue; /* Subtle! */ - ptr = RSTRUCT_CONST_PTR(s); - ptr2 = RSTRUCT_CONST_PTR(s2); len = RSTRUCT_LEN(s); for (i=0; i-') + end + + assert_warning(/Invalid ERB trim mode/) do + @erb.new("", trim_mode: 3) + end + end + def test_run out = StringIO.new orig, $stdout = $stdout, out diff --git a/test/excludes/_appveyor/TestArray.rb b/test/excludes/_appveyor/TestArray.rb new file mode 100644 index 00000000000000..4917b54ba76c1f --- /dev/null +++ b/test/excludes/_appveyor/TestArray.rb @@ -0,0 +1,5 @@ +# https://ci.appveyor.com/project/ruby/ruby/builds/20339189/job/ltdpffep976xtj85 +# `test_push_over_ary_max': failed to allocate memory (NoMemoryError) +exclude(:test_push_over_ary_max, 'Sometimes AppVeyor has insufficient memory to run this test') +# https://ci.appveyor.com/project/ruby/ruby/builds/20427662/job/prq9i2lkfxv2j0uy +exclude(:test_splice_over_ary_max, 'Sometimes AppVeyor has insufficient memory to run this test') diff --git a/test/excludes/_travis/IMAPTest.rb b/test/excludes/_travis/osx/IMAPTest.rb similarity index 100% rename from test/excludes/_travis/IMAPTest.rb rename to test/excludes/_travis/osx/IMAPTest.rb diff --git a/test/excludes/_travis/TestGemRemoteFetcher.rb b/test/excludes/_travis/osx/TestGemRemoteFetcher.rb similarity index 100% rename from test/excludes/_travis/TestGemRemoteFetcher.rb rename to test/excludes/_travis/osx/TestGemRemoteFetcher.rb diff --git a/test/excludes/_travis/osx/TestWEBrickUtils.rb b/test/excludes/_travis/osx/TestWEBrickUtils.rb new file mode 100644 index 00000000000000..1b1dd877f55d05 --- /dev/null +++ b/test/excludes/_travis/osx/TestWEBrickUtils.rb @@ -0,0 +1,3 @@ +# https://travis-ci.org/ruby/ruby/jobs/454707326 +# maybe this test's timeout should be re-arranged for Travis osx +exclude(:test_nested_timeout_outer, 'This test randomly fails on Travis osx') diff --git a/test/excludes/_wercker/jit-wait/TestDelegateClass.rb b/test/excludes/_wercker/jit-wait/TestDelegateClass.rb new file mode 100644 index 00000000000000..384bb03b4c51d0 --- /dev/null +++ b/test/excludes/_wercker/jit-wait/TestDelegateClass.rb @@ -0,0 +1,2 @@ +# https://app.wercker.com/ruby/ruby/runs/mjit-test2/5bda979a191eda000655a8d2?step=5bda9fe4591ca80007653f64 +exclude(:test_frozen, 'somehow FrozenError is not raised with --jit-wait') diff --git a/test/excludes/_wercker/test-mjit-wait/TestParallel/TestParallel.rb b/test/excludes/_wercker/jit-wait/TestParallel/TestParallel.rb similarity index 100% rename from test/excludes/_wercker/test-mjit-wait/TestParallel/TestParallel.rb rename to test/excludes/_wercker/jit-wait/TestParallel/TestParallel.rb diff --git a/test/excludes/_wercker/test-mjit-wait/TestThreadQueue.rb b/test/excludes/_wercker/jit-wait/TestThreadQueue.rb similarity index 100% rename from test/excludes/_wercker/test-mjit-wait/TestThreadQueue.rb rename to test/excludes/_wercker/jit-wait/TestThreadQueue.rb diff --git a/test/excludes/_wercker/test-mjit/TestThreadQueue.rb b/test/excludes/_wercker/jit/TestThreadQueue.rb similarity index 100% rename from test/excludes/_wercker/test-mjit/TestThreadQueue.rb rename to test/excludes/_wercker/jit/TestThreadQueue.rb diff --git a/test/fiddle/test_function.rb b/test/fiddle/test_function.rb index 12a6425ff76ad1..c6f78284d534af 100644 --- a/test/fiddle/test_function.rb +++ b/test/fiddle/test_function.rb @@ -92,7 +92,7 @@ def test_nogvl_poll n1 = f.call(nil, 0, msec) n2 = th.value t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - assert_in_delta(msec, t1 - t0, 100, 'slept amount of time') + assert_in_delta(msec, t1 - t0, 150, 'slept amount of time') assert_equal(0, n1, perror("poll(2) in main-thread")) assert_equal(0, n2, perror("poll(2) in sub-thread")) end diff --git a/test/io/nonblock/test_flush.rb b/test/io/nonblock/test_flush.rb index 63e16db5a30b13..08d129de3f485f 100644 --- a/test/io/nonblock/test_flush.rb +++ b/test/io/nonblock/test_flush.rb @@ -53,6 +53,7 @@ def flush_test(r, w) def test_nonblock IO.pipe {|r, w| + w.nonblock = false assert_equal(false, w.nonblock?) w.nonblock do assert_equal(true, w.nonblock?) diff --git a/test/lib/jit_support.rb b/test/lib/jit_support.rb index fa1402e4b47c2a..82f53ec59d741d 100644 --- a/test/lib/jit_support.rb +++ b/test/lib/jit_support.rb @@ -30,6 +30,10 @@ def eval_with_jit_without_retry(env = nil, script, verbose: 0, min_calls: 5, sav args << '--jit-save-temps' if save_temps args << '-e' << script base_env = { 'MJIT_SEARCH_BUILD_DIR' => 'true' } # workaround to skip requiring `make install` for `make test-all` + if preloadenv = RbConfig::CONFIG['PRELOADENV'] and !preloadenv.empty? + so = "mjit_build_dir.#{RbConfig::CONFIG['SOEXT']}" + base_env[preloadenv] = File.realpath(so) rescue nil + end args.unshift(env ? base_env.merge!(env) : base_env) EnvUtil.invoke_ruby(args, '', true, true, timeout: timeout, diff --git a/test/matrix/test_matrix.rb b/test/matrix/test_matrix.rb index d40e0c04302198..3ecb9d2f8ba211 100644 --- a/test/matrix/test_matrix.rb +++ b/test/matrix/test_matrix.rb @@ -283,7 +283,18 @@ def test_column end def test_collect - assert_equal(Matrix[[1, 4, 9], [16, 25, 36]], @m1.collect {|x| x ** 2 }) + m1 = Matrix.zero(2,2) + m2 = Matrix.build(3,4){|row, col| 1} + + assert_equal(Matrix[[5, 5, 5, 5], [5, 5, 5, 5], [5, 5, 5, 5]], m2.collect{|e| e * 5}) + assert_equal(Matrix[[7, 0],[0, 7]], m1.collect(:diagonal){|e| e + 7}) + assert_equal(Matrix[[0, 5],[5, 0]], m1.collect(:off_diagonal){|e| e + 5}) + assert_equal(Matrix[[8, 1, 1, 1], [8, 8, 1, 1], [8, 8, 8, 1]], m2.collect(:lower){|e| e + 7}) + assert_equal(Matrix[[1, 1, 1, 1], [-11, 1, 1, 1], [-11, -11, 1, 1]], m2.collect(:strict_lower){|e| e - 12}) + assert_equal(Matrix[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], m2.collect(:strict_upper){|e| e ** 2}) + assert_equal(Matrix[[-1, -1, -1, -1], [1, -1, -1, -1], [1, 1, -1, -1]], m2.collect(:upper){|e| -e}) + assert_raise(ArgumentError) {m1.collect(:test){|e| e + 7}} + assert_not_equal(m2, m2.collect {|e| e * 2 }) end def test_minor @@ -623,6 +634,134 @@ def test_combine assert_equal Matrix.empty(0,3), Matrix.combine(Matrix.empty(0,3), Matrix.empty(0,3)) { raise } end + def test_set_element + src = Matrix[ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ] + rows = { + range: [1..2, 1...3, 1..-1, -2..2, 1.., 1..., -2.., -2...], + int: [2, -1], + invalid: [-4, 4, -4..2, 2..-4, 0...0, 2..0, -4..], + } + columns = { + range: [2..3, 2...4, 2..-1, -2..3, 2.., 2..., -2..., -2..], + int: [3, -1], + invalid: [-5, 5, -5..2, 2..-5, 0...0, -5..], + } + values = { + element: 42, + matrix: Matrix[[20, 21], [22, 23]], + vector: Vector[30, 31], + row: Matrix[[60, 61]], + column: Matrix[[50], [51]], + mismatched_matrix: Matrix.identity(3), + mismatched_vector: Vector[0, 1, 2, 3], + } + solutions = { + [:int, :int] => { + element: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 42]], + }, + [:range , :int] => { + element: Matrix[[1, 2, 3, 4], [5, 6, 7, 42], [9, 10, 11, 42]], + column: Matrix[[1, 2, 3, 4], [5, 6, 7, 50], [9, 10, 11, 51]], + vector: Matrix[[1, 2, 3, 4], [5, 6, 7, 30], [9, 10, 11, 31]], + }, + [:int, :range] => { + element: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 42, 42]], + row: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 60, 61]], + vector: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 30, 31]], + }, + [:range , :range] => { + element: Matrix[[1, 2, 3, 4], [5, 6, 42, 42], [9, 10, 42, 42]], + matrix: Matrix[[1, 2, 3, 4], [5, 6, 20, 21], [9, 10, 22, 23]], + }, + } + solutions.default = Hash.new(IndexError) + + rows.each do |row_style, row_arguments| + row_arguments.each do |row_argument| + columns.each do |column_style, column_arguments| + column_arguments.each do |column_argument| + values.each do |value_type, value| + expected = solutions[[row_style, column_style]][value_type] || Matrix::ErrDimensionMismatch + + result = src.clone + begin + result[row_argument, column_argument] = value + assert_equal expected, result, + "m[#{row_argument.inspect}][#{column_argument.inspect}] = #{value.inspect} failed" + rescue Exception => e + raise if e.class != expected + end + end + end + end + end + end + end + + def test_map! + m1 = Matrix.zero(2,2) + m2 = Matrix.build(3,4){|row, col| 1} + m3 = Matrix.zero(3,5).freeze + m4 = Matrix.empty.freeze + + assert_equal Matrix[[5, 5, 5, 5], [5, 5, 5, 5], [5, 5, 5, 5]], m2.map!{|e| e * 5} + assert_equal Matrix[[7, 0],[0, 7]], m1.map!(:diagonal){|e| e + 7} + assert_equal Matrix[[7, 5],[5, 7]], m1.map!(:off_diagonal){|e| e + 5} + assert_equal Matrix[[12, 5, 5, 5], [12, 12, 5, 5], [12, 12, 12, 5]], m2.map!(:lower){|e| e + 7} + assert_equal Matrix[[12, 5, 5, 5], [0, 12, 5, 5], [0, 0, 12, 5]], m2.map!(:strict_lower){|e| e - 12} + assert_equal Matrix[[12, 25, 25, 25], [0, 12, 25, 25], [0, 0, 12, 25]], m2.map!(:strict_upper){|e| e ** 2} + assert_equal Matrix[[-12, -25, -25, -25], [0, -12, -25, -25], [0, 0, -12, -25]], m2.map!(:upper){|e| -e} + assert_equal m1, m1.map!{|e| e ** 2 } + assert_equal m2, m2.map!(:lower){ |e| e - 3 } + assert_raise(ArgumentError) {m1.map!(:test){|e| e + 7}} + assert_raise(FrozenError) { m3.map!{|e| e * 2} } + assert_raise(FrozenError) { m4.map!{} } + end + + def test_freeze + m = Matrix[[1, 2, 3],[4, 5, 6]] + f = m.freeze + assert_equal true, f.frozen? + assert m.equal?(f) + assert m.equal?(f.freeze) + assert_raise(FrozenError){ m[0, 1] = 56 } + assert_equal m.dup, m + end + + def test_clone + a = Matrix[[4]] + def a.foo + 42 + end + + m = a.clone + m[0, 0] = 2 + assert_equal a, m * 2 + assert_equal 42, m.foo + + a.freeze + m = a.clone + assert m.frozen? + assert_equal 42, m.foo + end + + def test_dup + a = Matrix[[4]] + def a.foo + 42 + end + a.freeze + + m = a.dup + m[0, 0] = 2 + assert_equal a, m * 2 + assert !m.respond_to?(:foo) + end + def test_eigenvalues_and_eigenvectors_symmetric m = Matrix[ [8, 1], diff --git a/test/matrix/test_vector.rb b/test/matrix/test_vector.rb index 631b4d809a2dd8..8b771efee33094 100644 --- a/test/matrix/test_vector.rb +++ b/test/matrix/test_vector.rb @@ -27,6 +27,108 @@ def test_basis assert_raise(ArgumentError) { Vector.basis(index: 3) } end + def test_get_element + assert_equal(@v1[0..], [1, 2, 3]) + assert_equal(@v1[0..1], [1, 2]) + assert_equal(@v1[2], 3) + assert_equal(@v1[4], nil) + end + + def test_set_element + + assert_block do + v = Vector[5, 6, 7, 8, 9] + v[1..2] = Vector[1, 2] + v == Vector[5, 1, 2, 8, 9] + end + + assert_block do + v = Vector[6, 7, 8] + v[1..2] = Matrix[[1, 3]] + v == Vector[6, 1, 3] + end + + assert_block do + v = Vector[1, 2, 3, 4, 5, 6] + v[0..2] = 8 + v == Vector[8, 8, 8, 4, 5, 6] + end + + assert_block do + v = Vector[1, 3, 4, 5] + v[2] = 5 + v == Vector[1, 3, 5, 5] + end + + assert_block do + v = Vector[2, 3, 5] + v[-2] = 13 + v == Vector[2, 13, 5] + end + + assert_block do + v = Vector[4, 8, 9, 11, 30] + v[1..-2] = Vector[1, 2, 3] + v == Vector[4, 1, 2, 3, 30] + end + + assert_raise(IndexError) {Vector[1, 3, 4, 5][5..6] = 17} + assert_raise(IndexError) {Vector[1, 3, 4, 5][6] = 17} + assert_raise(Matrix::ErrDimensionMismatch) {Vector[1, 3, 4, 5][0..2] = Matrix[[1], [2], [3]]} + assert_raise(ArgumentError) {Vector[1, 2, 3, 4, 5, 6][0..2] = Vector[1, 2, 3, 4, 5, 6]} + assert_raise(FrozenError) { Vector[7, 8, 9].freeze[0..1] = 5} + end + + def test_map! + v1 = Vector[1, 2, 3] + v2 = Vector[1, 3, 5].freeze + v3 = Vector[].freeze + assert_equal Vector[1, 4, 9], v1.map!{|e| e ** 2} + assert_equal v1, v1.map!{|e| e - 8} + assert_raise(FrozenError) { v2.map!{|e| e + 2 }} + assert_raise(FrozenError){ v3.map!{} } + end + + def test_freeze + v = Vector[1,2,3] + f = v.freeze + assert_equal true, f.frozen? + assert v.equal?(f) + assert v.equal?(f.freeze) + assert_raise(FrozenError){ v[1] = 56 } + assert_equal v.dup, v + end + + def test_clone + a = Vector[4] + def a.foo + 42 + end + + v = a.clone + v[0] = 2 + assert_equal a, v * 2 + assert_equal 42, v.foo + + a.freeze + v = a.clone + assert v.frozen? + assert_equal 42, v.foo + end + + def test_dup + a = Vector[4] + def a.foo + 42 + end + a.freeze + + v = a.dup + v[0] = 2 + assert_equal a, v * 2 + assert !v.respond_to?(:foo) + end + def test_identity assert_same @v1, @v1 assert_not_same @v1, @v2 diff --git a/test/minitest/test_minitest_unit.rb b/test/minitest/test_minitest_unit.rb index 833d58249689d6..3d92b865c37779 100644 --- a/test/minitest/test_minitest_unit.rb +++ b/test/minitest/test_minitest_unit.rb @@ -1528,7 +1528,9 @@ def test_refute_match def test_refute_match_matcher_object @assertion_count = 2 - @tc.refute_match Object.new, 5 # default #=~ returns false + non_verbose do + @tc.refute_match Object.new, 5 # default #=~ returns false + end end def test_refute_match_object_triggered diff --git a/test/mkmf/base.rb b/test/mkmf/base.rb index 3ba3e033879c33..80dec1421a4e34 100644 --- a/test/mkmf/base.rb +++ b/test/mkmf/base.rb @@ -1,5 +1,13 @@ # frozen_string_literal: false $extmk = true +require 'rbconfig' +RbConfig.fire_update!("top_srcdir", File.expand_path("../..", __dir__)) +File.foreach(RbConfig::CONFIG["topdir"]+"/Makefile") do |line| + if /^CC_WRAPPER\s*=\s*/ =~ line + RbConfig.fire_update!('CC_WRAPPER', $'.strip) + break + end +end require 'test/unit' require 'mkmf' diff --git a/test/monitor/test_monitor.rb b/test/monitor/test_monitor.rb index a0360696479170..8854b59ccc8b63 100644 --- a/test/monitor/test_monitor.rb +++ b/test/monitor/test_monitor.rb @@ -269,4 +269,26 @@ def test_timedwait # end # cumber_thread.kill end + + def test_wait_interruption + queue = Queue.new + cond = @monitor.new_cond + @monitor.define_singleton_method(:mon_enter_for_cond) do |*args| + queue.deq + super(*args) + end + th = Thread.start { + @monitor.synchronize do + begin + cond.wait(0.1) + rescue Interrupt + @monitor.instance_variable_get(:@mon_owner) + end + end + } + sleep(0.1) + th.raise(Interrupt) + queue.enq(nil) + assert_equal th, th.value + end end diff --git a/test/net/ftp/test_ftp.rb b/test/net/ftp/test_ftp.rb index 98322fdd57edb6..a5219644bbc865 100644 --- a/test/net/ftp/test_ftp.rb +++ b/test/net/ftp/test_ftp.rb @@ -335,7 +335,7 @@ def test_read_timeout_exceeded sleep(0.1) sock.print("331 Please specify the password.\r\n") commands.push(sock.gets) - sleep(0.3) + sleep(2.0) # Net::ReadTimeout sock.print("230 Login successful.\r\n") commands.push(sock.gets) sleep(0.1) @@ -344,7 +344,7 @@ def test_read_timeout_exceeded begin begin ftp = Net::FTP.new - ftp.read_timeout = 0.2 + ftp.read_timeout = 0.4 ftp.connect(SERVER_ADDR, server.port) assert_raise(Net::ReadTimeout) do ftp.login @@ -377,7 +377,7 @@ def test_read_timeout_not_exceeded begin begin ftp = Net::FTP.new - ftp.read_timeout = 0.2 + ftp.read_timeout = 1.0 ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) @@ -386,7 +386,7 @@ def test_read_timeout_not_exceeded assert_equal(nil, commands.shift) ensure ftp.close - assert_equal(0.2, ftp.read_timeout) + assert_equal(1.0, ftp.read_timeout) end ensure server.close @@ -662,7 +662,7 @@ def test_retrbinary_read_timeout_not_exceeded sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") conn = TCPSocket.new(host, port) binary_data.scan(/.{1,1024}/nm) do |s| - sleep(0.1) + sleep(0.2) conn.print(s) end conn.shutdown(Socket::SHUT_WR) @@ -673,7 +673,7 @@ def test_retrbinary_read_timeout_not_exceeded begin begin ftp = Net::FTP.new - ftp.read_timeout = 0.2 + ftp.read_timeout = 1.0 ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) @@ -2171,7 +2171,7 @@ def test_getbinaryfile_command_injection begin ftp = Net::FTP.new ftp.resume = resume - ftp.read_timeout = RubyVM::MJIT.enabled? ? 1 : 0.2 # use large timeout for --jit-wait + ftp.read_timeout = RubyVM::MJIT.enabled? ? 5 : 0.2 # use large timeout for --jit-wait ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) diff --git a/test/pathname/test_pathname.rb b/test/pathname/test_pathname.rb index 42bab36bc5e3ce..3bea182ed4743a 100644 --- a/test/pathname/test_pathname.rb +++ b/test/pathname/test_pathname.rb @@ -291,9 +291,10 @@ def relative?(path) end def relative_path_from(dest_directory, base_directory) - Pathname.new(dest_directory).relative_path_from(Pathname.new(base_directory)).to_s + Pathname.new(dest_directory).relative_path_from(base_directory).to_s end + defassert(:relative_path_from, "../a", Pathname.new("a"), "b") defassert(:relative_path_from, "../a", "a", "b") defassert(:relative_path_from, "../a", "a", "b/") defassert(:relative_path_from, "../a", "a/", "b") diff --git a/test/psych/test_safe_load.rb b/test/psych/test_safe_load.rb index 6c7a8d0f5a545d..e3972712fc6d64 100644 --- a/test/psych/test_safe_load.rb +++ b/test/psych/test_safe_load.rb @@ -30,12 +30,12 @@ def test_no_recursion def test_explicit_recursion x = [] x << x - assert_equal(x, Psych.safe_load(Psych.dump(x), whitelist_classes: [], whitelist_symbols: [], aliases: true)) + assert_equal(x, Psych.safe_load(Psych.dump(x), permitted_classes: [], permitted_symbols: [], aliases: true)) # deprecated interface assert_equal(x, Psych.safe_load(Psych.dump(x), [], [], true)) end - def test_symbol_whitelist + def test_permitted_symbol yml = Psych.dump :foo assert_raises(Psych::DisallowedClass) do Psych.safe_load yml @@ -44,8 +44,8 @@ def test_symbol_whitelist :foo, Psych.safe_load( yml, - whitelist_classes: [Symbol], - whitelist_symbols: [:foo] + permitted_classes: [Symbol], + permitted_symbols: [:foo] ) ) @@ -58,7 +58,7 @@ def test_symbol assert_safe_cycle :foo end assert_raises(Psych::DisallowedClass) do - Psych.safe_load '--- !ruby/symbol foo', whitelist_classes: [] + Psych.safe_load '--- !ruby/symbol foo', permitted_classes: [] end # deprecated interface @@ -66,9 +66,9 @@ def test_symbol Psych.safe_load '--- !ruby/symbol foo', [] end - assert_safe_cycle :foo, whitelist_classes: [Symbol] - assert_safe_cycle :foo, whitelist_classes: %w{ Symbol } - assert_equal :foo, Psych.safe_load('--- !ruby/symbol foo', whitelist_classes: [Symbol]) + assert_safe_cycle :foo, permitted_classes: [Symbol] + assert_safe_cycle :foo, permitted_classes: %w{ Symbol } + assert_equal :foo, Psych.safe_load('--- !ruby/symbol foo', permitted_classes: [Symbol]) # deprecated interface assert_equal :foo, Psych.safe_load('--- !ruby/symbol foo', [Symbol]) @@ -76,7 +76,7 @@ def test_symbol def test_foo assert_raises(Psych::DisallowedClass) do - Psych.safe_load '--- !ruby/object:Foo {}', whitelist_classes: [Foo] + Psych.safe_load '--- !ruby/object:Foo {}', permitted_classes: [Foo] end # deprecated interface @@ -87,7 +87,7 @@ def test_foo assert_raises(Psych::DisallowedClass) do assert_safe_cycle Foo.new end - assert_kind_of(Foo, Psych.safe_load(Psych.dump(Foo.new), whitelist_classes: [Foo])) + assert_kind_of(Foo, Psych.safe_load(Psych.dump(Foo.new), permitted_classes: [Foo])) # deprecated interface assert_kind_of(Foo, Psych.safe_load(Psych.dump(Foo.new), [Foo])) @@ -95,27 +95,27 @@ def test_foo X = Struct.new(:x) def test_struct_depends_on_sym - assert_safe_cycle(X.new, whitelist_classes: [X, Symbol]) + assert_safe_cycle(X.new, permitted_classes: [X, Symbol]) assert_raises(Psych::DisallowedClass) do - cycle X.new, whitelist_classes: [X] + cycle X.new, permitted_classes: [X] end end def test_anon_struct - assert Psych.safe_load(<<-eoyml, whitelist_classes: [Struct, Symbol]) + assert Psych.safe_load(<<-eoyml, permitted_classes: [Struct, Symbol]) --- !ruby/struct foo: bar eoyml assert_raises(Psych::DisallowedClass) do - Psych.safe_load(<<-eoyml, whitelist_classes: [Struct]) + Psych.safe_load(<<-eoyml, permitted_classes: [Struct]) --- !ruby/struct foo: bar eoyml end assert_raises(Psych::DisallowedClass) do - Psych.safe_load(<<-eoyml, whitelist_classes: [Symbol]) + Psych.safe_load(<<-eoyml, permitted_classes: [Symbol]) --- !ruby/struct foo: bar eoyml @@ -157,14 +157,14 @@ def test_safe_load_raises_on_bad_input private - def cycle object, whitelist_classes: [] - Psych.safe_load(Psych.dump(object), whitelist_classes: whitelist_classes) + def cycle object, permitted_classes: [] + Psych.safe_load(Psych.dump(object), permitted_classes: permitted_classes) # deprecated interface test - Psych.safe_load(Psych.dump(object), whitelist_classes) + Psych.safe_load(Psych.dump(object), permitted_classes) end - def assert_safe_cycle object, whitelist_classes: [] - other = cycle object, whitelist_classes: whitelist_classes + def assert_safe_cycle object, permitted_classes: [] + other = cycle object, permitted_classes: permitted_classes assert_equal object, other end end diff --git a/test/rdoc/minitest_helper.rb b/test/rdoc/minitest_helper.rb index 1814669bf9a3a9..9fb0cd676b72e4 100644 --- a/test/rdoc/minitest_helper.rb +++ b/test/rdoc/minitest_helper.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +require 'bundler/errors' begin gem 'minitest', '~> 5.0' -rescue NoMethodError, Gem::LoadError +rescue NoMethodError, Gem::LoadError, Bundler::GemfileNotFound # for ruby tests end diff --git a/test/rdoc/test_rdoc_generator_json_index.rb b/test/rdoc/test_rdoc_generator_json_index.rb index ca7260c820ff15..b057832329a033 100644 --- a/test/rdoc/test_rdoc_generator_json_index.rb +++ b/test/rdoc/test_rdoc_generator_json_index.rb @@ -96,7 +96,7 @@ def test_generate assert_file 'js/navigation.js' assert_file 'js/search_index.js' - srcdir = File.expand_path("../../lib/rdoc", __FILE__) + srcdir = File.expand_path('lib/rdoc', @pwd) if !File.directory? srcdir # for Ruby core repository srcdir = File.expand_path("../../../lib/rdoc", __FILE__) @@ -109,7 +109,11 @@ def test_generate assert orig_file.mtime.inspect == generated_file.mtime.inspect, '.js files should be tha same timestamp of original' - assert generated_file.mtime < now, '.js files should be the same timestamp' + assert generated_file.mtime < now, + ".js files should be the same timestamp,\n" + + "path: #{generated_file.inspect},\n" + + "time: #{generated_file.mtime.inspect},\n" + + "now : #{now.inspect}" generated_search_index = Pathname(File.join @tmpdir, 'js/search_index.js') assert generated_search_index.mtime > (now - 1), 'search_index.js should be generated timestamp' diff --git a/test/rdoc/test_rdoc_rdoc.rb b/test/rdoc/test_rdoc_rdoc.rb index 4014ecfa548177..97e98cffb73854 100644 --- a/test/rdoc/test_rdoc_rdoc.rb +++ b/test/rdoc/test_rdoc_rdoc.rb @@ -69,6 +69,13 @@ def test_document_with_dry_run # functional test end def test_gather_files + # TODO: dummy finish + # RDoc::Options#@exclude is initialized as an empty array. + # Then, #finish converts it to a regexp or nil and reassign it to @exclude. + # RDoc#gather_files assumes that #finish has been already called. + # So, it forces to assign nil to @exclude. + @rdoc.options.exclude = nil + a = File.expand_path __FILE__ b = File.expand_path '../test_rdoc_text.rb', __FILE__ @@ -182,6 +189,13 @@ def test_normalized_file_list_non_file_directory end def test_normalized_file_list_with_dot_doc + # TODO: dummy finish + # RDoc::Options#@exclude is initialized as an empty array. + # Then, #finish converts it to a regexp or nil and reassign it to @exclude. + # RDoc#normalized_file_list assumes that #finish has been already called. + # So, it forces to assign nil to @exclude. + @rdoc.options.exclude = nil + expected_files = [] files = temp_dir do |dir| a = File.expand_path('a.rb') diff --git a/test/rdoc/test_rdoc_ri_driver.rb b/test/rdoc/test_rdoc_ri_driver.rb index f636c6346f7d7c..18dcb5b68aee2c 100644 --- a/test/rdoc/test_rdoc_ri_driver.rb +++ b/test/rdoc/test_rdoc_ri_driver.rb @@ -260,7 +260,7 @@ def test_add_method_that_is_alias_for_original para('alias comment'), blank_line, blank_line, - para('(this method is alias for Qux#original)'), + para('(This method is an alias for Qux#original.)'), blank_line, para('original comment'), blank_line, diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index d75a431bcce03a..669d86bd834455 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -159,7 +159,7 @@ def test_no_server u.bind("127.0.0.1", 0) _, port, _, host = u.addr u.close - # A rase condition here. + # A race condition here. # Another program may use the port. # But no way to prevent it. Timeout.timeout(5) do diff --git a/test/ruby/enc/test_emoji_breaks.rb b/test/ruby/enc/test_emoji_breaks.rb new file mode 100644 index 00000000000000..78aa5db88f26d0 --- /dev/null +++ b/test/ruby/enc/test_emoji_breaks.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true +# Copyright © 2018 Martin J. Dürst (duerst@it.aoyama.ac.jp) + +require "test/unit" + +class BreakTest + attr_reader :string, :comment, :filename, :line_number, :type, :shortname + + def initialize (filename, line_number, data, comment='') + @filename = filename + @line_number = line_number + @comment = comment.gsub(/\s+/, ' ').strip + if filename=='emoji-test' + codes, @type = data.split(/\s*;\s*/) + @shortname = '' + else + codes, @type, @shortname = data.split(/\s*;\s*/) + end + @type = @type.gsub(/\s+/, ' ').strip + @shortname = @shortname.gsub(/\s+/, ' ').strip + @string = codes.split(/\s+/) + .map do |ch| + c = ch.to_i(16) + # eliminate cases with surrogates + # raise ArgumentError if 0xD800 <= c and c <= 0xDFFF + c.chr('UTF-8') + end.join + raise ArgumentError if data.match? /genie/ or comment.match? /genie/ + raise ArgumentError if data.match? /zombie/ or comment.match? /zombie/ + raise ArgumentError if data.match? /wrestling/ or comment.match? /wrestling/ + end +end + +class TestEmojiBreaks < Test::Unit::TestCase + EMOJI_DATA_FILES = %w[emoji-sequences emoji-test emoji-variation-sequences emoji-zwj-sequences] + EMOJI_VERSION = '5.0' # hard-coded, should be replaced by + # RbConfig::CONFIG['UNICODE_EMOJI_VERSION'] or so, see feature #15341 + EMOJI_DATA_PATH = File.expand_path("../../../enc/unicode/data/emoji/#{EMOJI_VERSION}", __dir__) + + def self.expand_filename(basename) + File.expand_path("#{EMOJI_DATA_PATH}/#{basename}.txt", __dir__) + end + + def self.data_files_available? + EMOJI_DATA_FILES.all? do |f| + File.exist?(expand_filename(f)) + end + end + + def test_data_files_available + unless TestEmojiBreaks.data_files_available? + skip "Emoji data files not available in #{EMOJI_DATA_PATH}." + end + end +end + +TestEmojiBreaks.data_files_available? and class TestEmojiBreaks + def read_data + tests = [] + EMOJI_DATA_FILES.each do |filename| + version_mismatch = true + file_tests = [] + IO.foreach(TestEmojiBreaks.expand_filename(filename), encoding: Encoding::UTF_8) do |line| + line.chomp! + raise "File Name Mismatch" if $.==1 and not line=="# #{filename}.txt" + version_mismatch = false if line=="# Version: #{EMOJI_VERSION}" + next if /\A(#|\z)/.match? line + file_tests << BreakTest.new(filename, $., *line.split('#')) rescue 'whatever' + end + raise "File Version Mismatch" if version_mismatch + tests += file_tests + end + tests + end + + def all_tests + @@tests ||= read_data + rescue Errno::ENOENT + @@tests ||= [] + end + + def test_single_emoji + all_tests.each do |test| + expected = [test.string] + actual = test.string.each_grapheme_cluster.to_a + assert_equal expected, actual, + "file: #{test.filename}, line #{test.line_number}, " + + "type: #{test.type}, shortname: #{test.shortname}, comment: #{test.comment}" + end + end + + def test_embedded_emoji + all_tests.each do |test| + expected = ["A", test.string, "Z"] + actual = "A#{test.string}Z".each_grapheme_cluster.to_a + assert_equal expected, actual, + "file: #{test.filename}, line #{test.line_number}, " + + "type: #{test.type}, shortname: #{test.shortname}, comment: #{test.comment}" + end + end + + # test some pseodorandom combinations of emoji + def test_mixed_emoji + srand 0 + length = all_tests.length + step = 503 # use a prime number + all_tests.each do |test1| + start = rand step + start.step(by: step, to: length-1) do |t2| + test2 = all_tests[t2] + expected = [test1.string, test2.string] + actual = (test1.string+test2.string).each_grapheme_cluster.to_a + assert_equal expected, actual, + "file1: #{test1.filename}, line1 #{test1.line_number}, " + + "file2: #{test2.filename}, line2 #{test2.line_number},\n" + + "type1: #{test1.type}, shortname1: #{test1.shortname}, comment1: #{test1.comment},\n" + + "type2: #{test2.type}, shortname2: #{test2.shortname}, comment2: #{test2.comment}" + end + end + end +end diff --git a/test/ruby/enc/test_grapheme_breaks.rb b/test/ruby/enc/test_grapheme_breaks.rb new file mode 100644 index 00000000000000..7f6c77611375b7 --- /dev/null +++ b/test/ruby/enc/test_grapheme_breaks.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +# Copyright © 2018 Martin J. Dürst (duerst@it.aoyama.ac.jp) + +require "test/unit" + +class BreakTest + attr_reader :clusters, :string, :comment, :line_number + + def initialize (line_number, data, comment) + @line_number = line_number + @comment = comment + @clusters = data.sub(/\A\s*÷\s*/, '') + .sub(/\s*÷\s*\z/, '') + .split(/\s*÷\s*/) + .map do |cl| + cl.split(/\s*×\s*/) + .map do |ch| + c = ch.to_i(16) + # eliminate cases with surrogates + raise ArgumentError if 0xD800 <= c and c <= 0xDFFF + c.chr('UTF-8') + end.join + end + @string = @clusters.join + end +end + +class TestGraphemeBreaksFromFile < Test::Unit::TestCase + UNICODE_VERSION = RbConfig::CONFIG['UNICODE_VERSION'] + path = File.expand_path("../../../enc/unicode/data/#{UNICODE_VERSION}", __dir__) + UNICODE_DATA_PATH = File.directory?("#{path}/ucd/auxiliary") ? "#{path}/ucd/auxiliary" : path + GRAPHEME_BREAK_TEST_FILE = File.expand_path("#{UNICODE_DATA_PATH}/GraphemeBreakTest.txt", __dir__) + + def self.file_available? + File.exist? GRAPHEME_BREAK_TEST_FILE + end + + def test_data_files_available + unless TestGraphemeBreaksFromFile.file_available? + skip "Unicode data file GraphemeBreakTest not available in #{UNICODE_DATA_PATH}." + end + end +end + +TestGraphemeBreaksFromFile.file_available? and class TestGraphemeBreaksFromFile + def read_data + tests = [] + IO.foreach(GRAPHEME_BREAK_TEST_FILE, encoding: Encoding::UTF_8) do |line| + if $. == 1 and not line.start_with?("# GraphemeBreakTest-#{UNICODE_VERSION}.txt") + raise "File Version Mismatch" + end + next if /\A#/.match? line + tests << BreakTest.new($., *line.chomp.split('#')) rescue 'whatever' + end + tests + end + + def all_tests + @@tests ||= read_data + rescue Errno::ENOENT + @@tests ||= [] + end + + def test_each_grapheme_cluster + all_tests.each do |test| + expected = test.clusters + actual = test.string.each_grapheme_cluster.to_a + assert_equal expected, actual, + "line #{test.line_number}, expected '#{expected}', " + + "but got '#{actual}', comment: #{test.comment}" + end + end + + def test_backslash_X + all_tests.each do |test| + clusters = test.clusters.dup + string = test.string.dup + removals = 0 + while string.sub!(/\A\X/, '') + removals += 1 + clusters.shift + expected = clusters.join + assert_equal expected, string, + "line #{test.line_number}, removals: #{removals}, expected '#{expected}', " + + "but got '#{string}', comment: #{test.comment}" + end + assert_equal expected, string, + "line #{test.line_number}, after last removal, expected '#{expected}', " + + "but got '#{string}', comment: #{test.comment}" + end + end +end diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 1b102c46582e2f..aaf3eca58c5952 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -549,7 +549,9 @@ def test_collect assert_kind_of Enumerator, @cls[1, 2, 3].collect - assert_equal([[1, 2, 3]], [[1, 2, 3]].collect(&->(a, b, c) {[a, b, c]})) + assert_raise(ArgumentError) { + assert_equal([[1, 2, 3]], [[1, 2, 3]].collect(&->(a, b, c) {[a, b, c]})) + } end # also update map! diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index d053ce5a498706..31744e9215e88a 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -3,7 +3,7 @@ require 'tempfile' class RubyVM - module AST + module AbstractSyntaxTree class Node class CodePosition include Comparable @@ -64,14 +64,14 @@ def validate_not_cared def ast return @ast if defined?(@ast) - @ast = RubyVM::AST.parse_file(@path) + @ast = RubyVM::AbstractSyntaxTree.parse_file(@path) end private def validate_range0(node) beg_pos, end_pos = node.beg_pos, node.end_pos - children = node.children.grep(RubyVM::AST::Node) + children = node.children.grep(RubyVM::AbstractSyntaxTree::Node) return true if children.empty? # These NODE_D* has NODE_ARRAY as nd_next->nd_next whose last locations @@ -99,7 +99,7 @@ def validate_range0(node) def validate_not_cared0(node) beg_pos, end_pos = node.beg_pos, node.end_pos - children = node.children.grep(RubyVM::AST::Node) + children = node.children.grep(RubyVM::AbstractSyntaxTree::Node) @errors << { type: :first_lineno, node: node } if beg_pos.lineno == 0 @errors << { type: :first_column, node: node } if beg_pos.column == -1 @@ -131,32 +131,32 @@ def validate_not_cared0(node) end def test_allocate - assert_raise(TypeError) {RubyVM::AST::Node.allocate} + assert_raise(TypeError) {RubyVM::AbstractSyntaxTree::Node.allocate} end def test_column_with_long_heredoc_identifier term = "A"*257 - ast = RubyVM::AST.parse("<<-#{term}\n""ddddddd\n#{term}\n") + ast = RubyVM::AbstractSyntaxTree.parse("<<-#{term}\n""ddddddd\n#{term}\n") node = ast.children[2] assert_equal("NODE_STR", node.type) assert_equal(0, node.first_column) end def test_column_of_heredoc - node = RubyVM::AST.parse("<<-SRC\nddddddd\nSRC\n").children[2] + node = RubyVM::AbstractSyntaxTree.parse("<<-SRC\nddddddd\nSRC\n").children[2] assert_equal("NODE_STR", node.type) assert_equal(0, node.first_column) assert_equal(6, node.last_column) - node = RubyVM::AST.parse("< other + GC.start + i <=> other.i + end + end + assert_equal [1, 2, 3, 4, 5], (1..5).sort_by{|e| klass.new e} + end end diff --git a/test/ruby/test_enumerator.rb b/test/ruby/test_enumerator.rb index 0839c2c3dd71ac..afd356105df9e7 100644 --- a/test/ruby/test_enumerator.rb +++ b/test/ruby/test_enumerator.rb @@ -670,5 +670,119 @@ def test_uniq assert_equal([0, 1], u.force) assert_equal([0, 1], u.force) end -end + def test_enum_chain_and_plus + r = 1..5 + + e1 = r.chain() + assert_kind_of(Enumerator::Chain, e1) + assert_equal(5, e1.size) + ary = [] + e1.each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5], ary) + + e2 = r.chain([6, 7, 8]) + assert_kind_of(Enumerator::Chain, e2) + assert_equal(8, e2.size) + ary = [] + e2.each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5, 6, 7, 8], ary) + + e3 = r.chain([6, 7], 8.step) + assert_kind_of(Enumerator::Chain, e3) + assert_equal(Float::INFINITY, e3.size) + ary = [] + e3.take(10).each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ary) + + # `a + b + c` should not return `Enumerator::Chain.new(a, b, c)` + # because it is expected that `(a + b).each` be called. + e4 = e2.dup + class << e4 + attr_reader :each_is_called + def each + super + @each_is_called = true + end + end + e5 = e4 + 9.step + assert_kind_of(Enumerator::Chain, e5) + assert_equal(Float::INFINITY, e5.size) + ary = [] + e5.take(10).each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ary) + assert_equal(true, e4.each_is_called) + end + + def test_chained_enums + a = (1..5).each + + e0 = Enumerator::Chain.new() + assert_kind_of(Enumerator::Chain, e0) + assert_equal(0, e0.size) + ary = [] + e0.each { |x| ary << x } + assert_equal([], ary) + + e1 = Enumerator::Chain.new(a) + assert_kind_of(Enumerator::Chain, e1) + assert_equal(5, e1.size) + ary = [] + e1.each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5], ary) + + e2 = Enumerator::Chain.new(a, [6, 7, 8]) + assert_kind_of(Enumerator::Chain, e2) + assert_equal(8, e2.size) + ary = [] + e2.each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5, 6, 7, 8], ary) + + e3 = Enumerator::Chain.new(a, [6, 7], 8.step) + assert_kind_of(Enumerator::Chain, e3) + assert_equal(Float::INFINITY, e3.size) + ary = [] + e3.take(10).each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ary) + + e4 = Enumerator::Chain.new(a, Enumerator.new { |y| y << 6 << 7 << 8 }) + assert_kind_of(Enumerator::Chain, e4) + assert_equal(nil, e4.size) + ary = [] + e4.each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5, 6, 7, 8], ary) + + e5 = Enumerator::Chain.new(e1, e2) + assert_kind_of(Enumerator::Chain, e5) + assert_equal(13, e5.size) + ary = [] + e5.each { |x| ary << x } + assert_equal([1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6, 7, 8], ary) + + rewound = [] + e1.define_singleton_method(:rewind) { rewound << object_id } + e2.define_singleton_method(:rewind) { rewound << object_id } + e5.rewind + assert_equal(rewound, [e2.object_id, e1.object_id]) + + rewound = [] + a = [1] + e6 = Enumerator::Chain.new(a) + a.define_singleton_method(:rewind) { rewound << object_id } + e6.rewind + assert_equal(rewound, []) + + assert_equal( + '#' + + ']>, ' + + '#, ' + + '[6, 7, 8]' + + ']>' + + ']>', + e5.inspect + ) + end +end diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index 8784dd7dd7c12b..88b02293e86a10 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -699,6 +699,12 @@ def test_cause_at_raised assert_same(a, e.cause.cause) end + def test_cause_at_end + assert_in_out_err([], <<-'end;', [], [/-: unexpected return\n/, /.*undefined local variable or method `n'.*\n/]) + END{n}; END{return} + end; + end + def test_raise_with_cause msg = "[Feature #8257]" cause = ArgumentError.new("foobar") diff --git a/test/ruby/test_hash.rb b/test/ruby/test_hash.rb index 03ebd38f1f10a1..7db3c27f87a582 100644 --- a/test/ruby/test_hash.rb +++ b/test/ruby/test_hash.rb @@ -755,6 +755,14 @@ def test_replace_bug9230 assert_predicate(h, :compare_by_identity?) end + def test_replace_bug15358 + h1 = {} + h2 = {a:1,b:2,c:3,d:4,e:5} + h2.replace(h1) + GC.start + assert(true) + end + def test_shift h = @h.dup diff --git a/test/ruby/test_io.rb b/test/ruby/test_io.rb index 117e4c95d6a751..2fc28f67a99559 100644 --- a/test/ruby/test_io.rb +++ b/test/ruby/test_io.rb @@ -2146,6 +2146,10 @@ def test_autoclose end def test_autoclose_true_closed_by_finalizer + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1465760 + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1469765 + skip 'this randomly fails with MJIT' if RubyVM::MJIT.enabled? + feature2250 = '[ruby-core:26222]' pre = 'ft2250' t = Tempfile.new(pre) @@ -3251,17 +3255,12 @@ def test_readpartial_locktmp assert_equal 100, buf.bytesize - begin + msg = /can't modify string; temporarily locked/ + assert_raise_with_message(RuntimeError, msg) do buf.replace("") - rescue RuntimeError => e - assert_match(/can't modify string; temporarily locked/, e.message) - Thread.pass - end until buf.empty? - - assert_empty(buf, bug6099) + end assert_predicate(th, :alive?) w.write(data) - Thread.pass while th.alive? th.join end assert_equal(data, buf, bug6099) @@ -3775,14 +3774,6 @@ def test_recycled_fd_close th = Thread.new { r.read(1) } w.write(dot) - # XXX not sure why this is needed on Linux, otherwise - # the "good" reader thread doesn't always join properly - # because the reader never sees the first write - if RUBY_PLATFORM =~ /linux/ - # assert_equal can fail if this is another char... - w.write(dot) - end - assert_same th, th.join(15), '"good" reader timeout' assert_equal(dot, th.value) end @@ -3801,9 +3792,6 @@ def test_recycled_fd_close end Thread.pass until th.stop? - # XXX not sure why, this reduces Linux CI failures - assert_nil th.join(0.001) - r.close assert_same th, th.join(30), '"bad" reader timeout' assert_match(/stream closed/, th.value.message) diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb index d193f41c6e3dde..ccff553e6f57ca 100644 --- a/test/ruby/test_iseq.rb +++ b/test/ruby/test_iseq.rb @@ -256,7 +256,7 @@ def test_compile_file_error f.puts "end" f.close path = f.path - assert_in_out_err(%W[- #{path}], "#{<<-"begin;"}\n#{<<-"end;"}", /keyword_end/, [], success: true) + assert_in_out_err(%W[- #{path}], "#{<<-"begin;"}\n#{<<-"end;"}", /unexpected end/, [], success: true) begin; path = ARGV[0] begin @@ -286,8 +286,12 @@ def test_inspect end end + def strip_lineno(source) + source.gsub(/^.*?: /, "") + end + def sample_iseq - ISeq.compile <<-EOS.gsub(/^.*?: /, "") + ISeq.compile(strip_lineno(<<-EOS)) 1: class C 2: def foo 3: begin @@ -439,17 +443,74 @@ def q; end end; end - def test_to_binary_tracepoint - filename = "#{File.basename(__FILE__)}_#{__LINE__}" - iseq = RubyVM::InstructionSequence.compile("x = 1\n y = 2", filename) + def collect_from_binary_tracepoint_lines(tracepoint_type, filename) + iseq = RubyVM::InstructionSequence.compile(strip_lineno(<<-RUBY), filename) + class A + class B + 2.times { + def self.foo + a = 'good day' + raise + rescue + 'dear reader' + end + } + end + B.foo + end + RUBY + iseq_bin = iseq.to_binary - ary = [] - TracePoint.new(:line){|tp| + lines = [] + TracePoint.new(tracepoint_type){|tp| next unless tp.path == filename - ary << [tp.path, tp.lineno] + lines << tp.lineno }.enable{ ISeq.load_from_binary(iseq_bin).eval } - assert_equal [[filename, 1], [filename, 2]], ary, '[Bug #14702]' + + lines + end + + def test_to_binary_line_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:line, filename) + + assert_equal [1, 2, 3, 4, 4, 12, 5, 6, 8], lines, '[Bug #14702]' + end + + def test_to_binary_class_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:class, filename) + + assert_equal [1, 2], lines, '[Bug #14702]' + end + + def test_to_binary_end_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:end, filename) + + assert_equal [11, 13], lines, '[Bug #14702]' + end + + def test_to_binary_return_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:return, filename) + + assert_equal [9], lines, '[Bug #14702]' + end + + def test_to_binary_b_call_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:b_call, filename) + + assert_equal [3, 3], lines, '[Bug #14702]' + end + + def test_to_binary_b_return_tracepoint + filename = "#{File.basename(__FILE__)}_#{__LINE__}" + lines = collect_from_binary_tracepoint_lines(:b_return, filename) + + assert_equal [10, 10], lines, '[Bug #14702]' end end diff --git a/test/ruby/test_jit.rb b/test/ruby/test_jit.rb index 5c31b504c45981..fdcb0726ef014a 100644 --- a/test/ruby/test_jit.rb +++ b/test/ruby/test_jit.rb @@ -444,7 +444,7 @@ def test_compile_insn_checktype end def test_compile_insn_inlinecache - assert_compile_once('Struct', result_inspect: 'Struct', insns: %i[getinlinecache setinlinecache]) + assert_compile_once('Struct', result_inspect: 'Struct', insns: %i[opt_getinlinecache opt_setinlinecache]) end def test_compile_insn_once @@ -463,6 +463,7 @@ def test_compile_insn_checkmatch_opt_case_dispatch def test_compile_insn_opt_calc assert_compile_once('4 + 2 - ((2 * 3 / 2) % 2)', result_inspect: '5', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) + assert_compile_once('4.0 + 2.0 - ((2.0 * 3.0 / 2.0) % 2.0)', result_inspect: '5.0', insns: %i[opt_plus opt_minus opt_mult opt_div opt_mod]) assert_compile_once('4 + 2', result_inspect: '6') end @@ -566,7 +567,7 @@ def test_jit_output assert_match(/^Successful MJIT finish$/, err) end - def test_unload_units + def test_unload_units_and_compaction Dir.mktmpdir("jit_test_unload_units_") do |dir| # MIN_CACHE_SIZE is 10 out, err = eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~'end;'}", verbose: 1, min_calls: 1, max_cache: 10) @@ -581,6 +582,12 @@ def mjit#{i} EOS i += 1 end + + if defined?(fork) + # test the child does not try to delete files which are deleted by parent, + # and test possible deadlock on fork during MJIT unload and JIT compaction on child + Process.waitpid(Process.fork {}) + end end; debug_info = "stdout:\n```\n#{out}\n```\n\nstderr:\n```\n#{err}```\n" @@ -597,7 +604,7 @@ def mjit#{i} # On --jit-wait, when the number of JIT-ed code reaches --jit-max-cache, # it should trigger compaction. unless RUBY_PLATFORM.match?(/mswin|mingw/) # compaction is not supported on Windows yet - assert_equal(2, compactions.size, debug_info) + assert_equal(3, compactions.size, debug_info) end if appveyor_mswin? @@ -769,6 +776,22 @@ def test_clean_so end end + def test_clean_objects_on_exec + if /mswin|mingw/ =~ RUBY_PLATFORM + # TODO: check call stack and close handle of code which is not on stack, and remove objects on best-effort basis + skip 'Removing so file being used does not work on Windows' + end + Dir.mktmpdir("jit_test_clean_objects_on_exec_") do |dir| + eval_with_jit({"TMPDIR"=>dir}, "#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 1) + begin; + def a; end; a + exec "true" + end; + error_message = "Undeleted files:\n #{Dir.glob("#{dir}/*").join("\n ")}\n" + assert_send([Dir, :empty?, dir], error_message) + end + end + def test_lambda_longjmp assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: '5', success_count: 1) begin; @@ -790,7 +813,7 @@ def test_stack_pointer_with_assignment end; end - def test_program_pointer_with_regexpmatch + def test_program_counter_with_regexpmatch assert_eval_with_jit("#{<<~"begin;"}\n#{<<~"end;"}", stdout: "aa", success_count: 1) begin; 2.times do @@ -837,6 +860,36 @@ def a # 6 assert_equal("-e:8:in `a'\n", lines[1]) end + def test_fork_with_mjit_worker_thread + Dir.mktmpdir("jit_test_fork_with_mjit_worker_thread_") do |dir| + # min_calls: 2 to skip fork block + out, err = eval_with_jit({ "TMPDIR" => dir }, "#{<<~"begin;"}\n#{<<~"end;"}", min_calls: 2, verbose: 1) + begin; + def before_fork; end + def after_fork; end + + before_fork; before_fork # the child should not delete this .o file + pid = Process.fork do # this child should not delete shared .pch file + after_fork; after_fork # this child does not share JIT-ed after_fork with parent + end + after_fork; after_fork # this parent does not share JIT-ed after_fork with child + + Process.waitpid(pid) + end; + success_count = err.scan(/^#{JIT_SUCCESS_PREFIX}:/).size + assert_equal(3, success_count) + + lines = err.lines + debug_info = "stdout:\n```\n#{out}\n```\n\nstderr:\n```\n#{err}```\n" + + # assert no remove error + assert_equal("Successful MJIT finish\n" * 2, err.gsub(/^#{JIT_SUCCESS_PREFIX}:[^\n]+\n/, ''), debug_info) + + # ensure objects are deleted + assert_send([Dir, :empty?, dir], debug_info) + end + end if defined?(fork) + private def appveyor_mswin? diff --git a/test/ruby/test_literal.rb b/test/ruby/test_literal.rb index 3b6aa0c096bfec..178cc83fa3f60d 100644 --- a/test/ruby/test_literal.rb +++ b/test/ruby/test_literal.rb @@ -177,6 +177,12 @@ def test_frozen_string end end + def test_frozen_string_in_array_literal + list = eval("# frozen-string-literal: true\n""['foo', 'bar']") + assert_equal 2, list.length + list.each { |str| assert_predicate str, :frozen? } + end + if defined?(RubyVM::InstructionSequence.compile_option) and RubyVM::InstructionSequence.compile_option.key?(:debug_frozen_string_literal) def test_debug_frozen_string @@ -189,6 +195,17 @@ def test_debug_frozen_string str << "x" } end + + def test_debug_frozen_string_in_array_literal + src = '["foo"]'; f = "test.rb"; n = 1 + opt = {frozen_string_literal: true, debug_frozen_string_literal: true} + ary = RubyVM::InstructionSequence.compile(src, f, f, n, opt).eval + assert_equal("foo", ary.first) + assert_predicate(ary.first, :frozen?) + assert_raise_with_message(FrozenError, /created at #{Regexp.quote(f)}:#{n}/) { + ary.first << "x" + } + end end def test_regexp diff --git a/test/ruby/test_m17n.rb b/test/ruby/test_m17n.rb index 80750baf30beb2..0251315fcfd460 100644 --- a/test/ruby/test_m17n.rb +++ b/test/ruby/test_m17n.rb @@ -1524,6 +1524,17 @@ def test_setbyte } end + def test_setbyte_range + s = u("\xE3\x81\x82\xE3\x81\x84") + assert_raise(RangeError) { s.setbyte(0, -1) } + assert_nothing_raised { s.setbyte(0, 0x00) } + assert_nothing_raised { s.setbyte(0, 0x7F) } + assert_nothing_raised { s.setbyte(0, 0x80) } + assert_nothing_raised { s.setbyte(0, 0xff) } + assert_raise(RangeError) { s.setbyte(0, 0x100) } + assert_raise(RangeError) { s.setbyte(0, 0x4f7574206f6620636861722072616e6765) } + end + def test_compatible assert_nil Encoding.compatible?("",0) assert_equal(Encoding::UTF_8, Encoding.compatible?(u(""), ua("abc"))) diff --git a/test/ruby/test_method.rb b/test/ruby/test_method.rb index fe0171dd57d9f8..65d9e3d2e1c059 100644 --- a/test/ruby/test_method.rb +++ b/test/ruby/test_method.rb @@ -1040,4 +1040,55 @@ def test_eqq assert_operator(0.method(:<), :===, 5) assert_not_operator(0.method(:<), :===, -5) end + + def test_compose_with_method + c = Class.new { + def f(x) x * 2 end + def g(x) x + 1 end + } + f = c.new.method(:f) + g = c.new.method(:g) + + assert_equal(6, (f << g).call(2)) + assert_equal(6, (g >> f).call(2)) + end + + def test_compose_with_proc + c = Class.new { + def f(x) x * 2 end + } + f = c.new.method(:f) + g = proc {|x| x + 1} + + assert_equal(6, (f << g).call(2)) + assert_equal(6, (g >> f).call(2)) + end + + def test_compose_with_callable + c = Class.new { + def f(x) x * 2 end + } + c2 = Class.new { + def call(x) x + 1 end + } + f = c.new.method(:f) + g = c2.new + + assert_equal(6, (f << g).call(2)) + assert_equal(5, (f >> g).call(2)) + end + + def test_compose_with_noncallable + c = Class.new { + def f(x) x * 2 end + } + f = c.new.method(:f) + + assert_raise(NoMethodError) { + (f << 5).call(2) + } + assert_raise(NoMethodError) { + (f >> 5).call(2) + } + end end diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index 95f8e352268bf8..658208d9df8595 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -802,6 +802,13 @@ def test_invalid_warning assert_warning(/\A(.* in '\u{3042}'\n)+\z/) { [].pack("\u{3042}") } + + assert_warning(/\A.* in '.*U'\Z/) { + assert_equal "\000", [0].pack("\0U") + } + assert_warning(/\A.* in '.*U'\Z/) { + "\000".unpack("\0U") + } end def test_pack_resize diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index 1f713f3dbc39e6..8e8f7198921317 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -1416,4 +1416,69 @@ def method_for_test_proc_without_block_for_symbol def test_proc_without_block_for_symbol assert_equal('1', method_for_test_proc_without_block_for_symbol(&:to_s).call(1), '[Bug #14782]') end + + def test_compose + f = proc {|x| x * 2} + g = proc {|x| x + 1} + + assert_equal(6, (f << g).call(2)) + assert_equal(6, (g >> f).call(2)) + end + + def test_compose_with_multiple_args + f = proc {|x| x * 2} + g = proc {|x, y| x + y} + + assert_equal(6, (f << g).call(1, 2)) + assert_equal(6, (g >> f).call(1, 2)) + end + + def test_compose_with_block + f = proc {|x| x * 2} + g = proc {|&blk| blk.call(1) } + + assert_equal(8, (f << g).call { |x| x + 3 }) + assert_equal(8, (g >> f).call { |x| x + 3 }) + end + + def test_compose_with_lambda + f = lambda {|x| x * 2} + g = lambda {|x| x} + + assert_predicate((f << g), :lambda?) + assert_predicate((g >> f), :lambda?) + end + + def test_compose_with_method + f = proc {|x| x * 2} + c = Class.new { + def g(x) x + 1 end + } + g = c.new.method(:g) + + assert_equal(6, (f << g).call(2)) + assert_equal(5, (f >> g).call(2)) + end + + def test_compose_with_callable + f = proc {|x| x * 2} + c = Class.new { + def call(x) x + 1 end + } + g = c.new + + assert_equal(6, (f << g).call(2)) + assert_equal(5, (f >> g).call(2)) + end + + def test_compose_with_noncallable + f = proc {|x| x * 2} + + assert_raise(NoMethodError) { + (f << 5).call(2) + } + assert_raise(NoMethodError) { + (f >> 5).call(2) + } + end end diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb index 96660e5d16c166..4c212c13f2676a 100644 --- a/test/ruby/test_process.rb +++ b/test/ruby/test_process.rb @@ -263,7 +263,7 @@ def test_execopts_rlimit } end - MANDATORY_ENVS = %w[RUBYLIB] + MANDATORY_ENVS = %w[RUBYLIB MJIT_SEARCH_BUILD_DIR] case RbConfig::CONFIG['target_os'] when /linux/ MANDATORY_ENVS << 'LD_PRELOAD' @@ -275,6 +275,9 @@ def test_execopts_rlimit if e = RbConfig::CONFIG['LIBPATHENV'] MANDATORY_ENVS << e end + if e = RbConfig::CONFIG['PRELOADENV'] and !e.empty? + MANDATORY_ENVS << e + end PREENVARG = ['-e', "%w[#{MANDATORY_ENVS.join(' ')}].each{|e|ENV.delete(e)}"] ENVARG = ['-e', 'ENV.each {|k,v| puts "#{k}=#{v}" }'] ENVCOMMAND = [RUBY].concat(PREENVARG).concat(ENVARG) @@ -340,6 +343,11 @@ def test_execopts_env end def test_execopt_env_path + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1455223 + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1450027 + # http://ci.rvm.jp/results/trunk-mjit@silicon-docker/1469867 + skip 'this randomly fails with MJIT' if RubyVM::MJIT.enabled? + bug8004 = '[ruby-core:53103] [Bug #8004]' Dir.mktmpdir do |d| open("#{d}/tmp_script.cmd", "w") {|f| f.puts ": ;"; f.chmod(0755)} @@ -762,6 +770,15 @@ def test_execopts_redirect_pipe Process.wait pid end } + + # ensure standard FDs we redirect to are blocking for compatibility + with_pipes(3) do |pipes| + src = 'p [STDIN,STDOUT,STDERR].map(&:nonblock?)' + rdr = { 0 => pipes[0][0], 1 => pipes[1][1], 2 => pipes[2][1] } + pid = spawn(RUBY, '-rio/nonblock', '-e', src, rdr) + assert_equal("[false, false, false]\n", pipes[1][0].gets) + Process.wait pid + end end end diff --git a/test/ruby/test_rational.rb b/test/ruby/test_rational.rb index c44ff0393a1ebb..b289753347a7ec 100644 --- a/test/ruby/test_rational.rb +++ b/test/ruby/test_rational.rb @@ -120,6 +120,9 @@ def test_conv assert_raise_with_message(ArgumentError, /\u{221a 2668}/) { Rational("\u{221a 2668}") } + assert_warning('') { + assert_predicate(Rational('1e-99999999999999999999'), :zero?) + } assert_raise(TypeError){Rational(Object.new)} assert_raise(TypeError){Rational(Object.new, Object.new)} diff --git a/test/ruby/test_refinement.rb b/test/ruby/test_refinement.rb index 050842724a2c28..06150d998f29ce 100644 --- a/test/ruby/test_refinement.rb +++ b/test/ruby/test_refinement.rb @@ -19,6 +19,10 @@ def a return "Foo#a" end + def b + return "Foo#b" + end + def call_x return x end @@ -41,6 +45,10 @@ def z def a return "FooExt#a" end + + private def b + return "FooExt#b" + end end end @@ -94,6 +102,18 @@ def self.send_z_on(foo) return foo.send(:z) end + def self.send_b_on(foo) + return foo.send(:b) + end + + def self.public_send_z_on(foo) + return foo.public_send(:z) + end + + def self.public_send_b_on(foo) + return foo.public_send(:b) + end + def self.method_z(foo) return foo.method(:z) end @@ -179,11 +199,20 @@ def test_send_should_use_refinements foo = Foo.new assert_raise(NoMethodError) { foo.send(:z) } assert_equal("FooExt#z", FooExtClient.send_z_on(foo)) + assert_equal("FooExt#b", FooExtClient.send_b_on(foo)) assert_raise(NoMethodError) { foo.send(:z) } assert_equal(true, RespondTo::Sub.new.respond_to?(:foo)) end + def test_public_send_should_use_refinements + foo = Foo.new + assert_raise(NoMethodError) { foo.public_send(:z) } + assert_equal("FooExt#z", FooExtClient.public_send_z_on(foo)) + assert_equal("Foo#b", foo.public_send(:b)) + assert_raise(NoMethodError) { FooExtClient.public_send_b_on(foo) } + end + def test_method_should_not_use_refinements foo = Foo.new assert_raise(NameError) { foo.method(:z) } @@ -297,9 +326,9 @@ def foo; "foo"; end end end - def test_respond_to_should_not_use_refinements + def test_respond_to_should_use_refinements assert_equal(false, 1.respond_to?(:foo)) - assert_equal(false, eval_using(IntegerFooExt, "1.respond_to?(:foo)")) + assert_equal(true, eval_using(IntegerFooExt, "1.respond_to?(:foo)")) end module StringCmpExt @@ -907,6 +936,10 @@ def self.send_z_on(foo) return foo.send(:z) end + def self.public_send_z_on(foo) + return foo.public_send(:z) + end + def self.method_z(foo) return foo.method(:z) end diff --git a/test/ruby/test_settracefunc.rb b/test/ruby/test_settracefunc.rb index edb356d3dd7437..edbc1e99566617 100644 --- a/test/ruby/test_settracefunc.rb +++ b/test/ruby/test_settracefunc.rb @@ -1914,4 +1914,174 @@ def test_lineno_in_optimized_insn EOF assert_equal "7\n", actual, '[Bug #14809]' end + + def method_for_enable_target1 + a = 1 + b = 2 + 1.times{|i| + x = i + } + c = a + b + end + + def method_for_enable_target2 + a = 1 + b = 2 + 1.times{|i| + x = i + } + c = a + b + end + + def check_with_events *trace_events + all_events = [[:call, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:b_call, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:b_return, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:return, :method_for_enable_target1], + # repeat + [:call, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:b_call, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:b_return, :method_for_enable_target1], + [:line, :method_for_enable_target1], + [:return, :method_for_enable_target1], + ] + events = [] + TracePoint.new(*trace_events) do |tp| + next unless target_thread? + events << [tp.event, tp.method_id] + end.enable(target: method(:method_for_enable_target1)) do + method_for_enable_target1 + method_for_enable_target2 + method_for_enable_target1 + end + assert_equal all_events.find_all{|(ev, m)| trace_events.include? ev}, events + end + + def test_tracepoint_enable_target + check_with_events :line + check_with_events :call, :return + check_with_events :line, :call, :return + check_with_events :call, :return, :b_call, :b_return + check_with_events :line, :call, :return, :b_call, :b_return + end + + def test_tracepoint_nested_enabled_with_target + code1 = proc{ + a = 1 + } + code2 = proc{ + b = 2 + } + + ## error + + # targetted TP and targetted TP + ex = assert_raise(ArgumentError) do + tp = TracePoint.new(:line){} + tp.enable(target: code1){ + tp.enable(target: code2){} + } + end + assert_equal "can't nest-enable a targetting TracePoint", ex.message + + # global TP and targetted TP + ex = assert_raise(ArgumentError) do + tp = TracePoint.new(:line){} + tp.enable{ + tp.enable(target: code2){} + } + end + assert_equal "can't nest-enable a targetting TracePoint", ex.message + + # targetted TP and global TP + ex = assert_raise(ArgumentError) do + tp = TracePoint.new(:line){} + tp.enable(target: code1){ + tp.enable{} + } + end + assert_equal "can't nest-enable a targetting TracePoint", ex.message + + # targetted TP and disable + ex = assert_raise(ArgumentError) do + tp = TracePoint.new(:line){} + tp.enable(target: code1){ + tp.disable{} + } + end + assert_equal "can't disable a targetting TracePoint in a block", ex.message + + ## success with two nesting targetting tracepoints + events = [] + tp1 = TracePoint.new(:line){|tp| events << :tp1} + tp2 = TracePoint.new(:line){|tp| events << :tp2} + tp1.enable(target: code1) do + tp2.enable(target: code1) do + code1.call + events << :___ + end + end + assert_equal [:tp2, :tp1, :___], events + + # succss with two tracepoints (global/targetting) + events = [] + tp1 = TracePoint.new(:line){|tp| events << :tp1} + tp2 = TracePoint.new(:line){|tp| events << :tp2} + tp1.enable do + tp2.enable(target: code1) do + code1.call + events << :___ + end + end + assert_equal [:tp1, :tp1, :tp1, :tp1, :tp2, :tp1, :___], events + + # succss with two tracepoints (targetting/global) + events = [] + tp1 = TracePoint.new(:line){|tp| events << :tp1} + tp2 = TracePoint.new(:line){|tp| events << :tp2} + tp1.enable(target: code1) do + tp2.enable do + code1.call + events << :___ + end + end + assert_equal [:tp2, :tp2, :tp1, :tp2, :___], events + end + + def test_tracepoint_enable_with_target_line + events = [] + line_0 = __LINE__ + code1 = proc{ + events << 1 + events << 2 + events << 3 + } + tp = TracePoint.new(:line) do |tp| + events << :tp + end + tp.enable(target: code1, target_line: line_0 + 3) do + code1.call + end + assert_equal [1, :tp, 2, 3], events + + + e = assert_raise(ArgumentError) do + TracePoint.new(:line){}.enable(target_line: 10){} + end + assert_equal 'only target_line is specified', e.message + + e = assert_raise(ArgumentError) do + TracePoint.new(:call){}.enable(target: code1, target_line: 10){} + end + assert_equal 'target_line is specified, but line event is not specified', e.message + end end diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 1a6d87f11f7e06..ffae04bc219244 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -973,6 +973,7 @@ def test_chars def test_each_grapheme_cluster [ + "\u{0D 0A}", "\u{20 200d}", "\u{600 600}", "\u{600 20}", @@ -986,6 +987,7 @@ def test_each_grapheme_cluster ].each do |g| assert_equal [g], g.each_grapheme_cluster.to_a assert_equal 1, g.each_grapheme_cluster.size + assert_predicate g.dup.taint.each_grapheme_cluster.to_a[0], :tainted? end [ @@ -995,6 +997,9 @@ def test_each_grapheme_cluster ].each do |str, grapheme_clusters| assert_equal grapheme_clusters, str.each_grapheme_cluster.to_a assert_equal grapheme_clusters.size, str.each_grapheme_cluster.size + str.dup.taint.each_grapheme_cluster do |g| + assert_predicate g, :tainted? + end end s = ("x"+"\u{10ABCD}"*250000) @@ -1015,6 +1020,7 @@ def test_grapheme_clusters "\u{1f469 200d 2764 fe0f 200d 1f469}", ].each do |g| assert_equal [g], g.grapheme_clusters + assert_predicate g.dup.taint.grapheme_clusters[0], :tainted? end assert_equal ["\u000A", "\u0308"], "\u{a 308}".grapheme_clusters @@ -1028,12 +1034,14 @@ def test_grapheme_clusters else warning = /passing a block to String#grapheme_clusters is deprecated/ assert_warning(warning) { - s = "ABC".b + s = "ABC".b.taint res = [] assert_same s, s.grapheme_clusters {|x| res << x } + assert_equal(3, res.size) assert_equal("A", res[0]) assert_equal("B", res[1]) assert_equal("C", res[2]) + res.each {|g| assert_predicate(g, :tainted?)} } end end diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index 564fc21c47f48f..dee9187a9ca709 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -179,6 +179,11 @@ def test_keyword_empty_splat bug13756 = '[ruby-core:82113] [Bug #13756]' assert_valid_syntax("defined? foo(**{})", bug13756) end; + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + bug15271 = '[ruby-core:89648] [Bug #15271]' + assert_valid_syntax("a **{}", bug15271) + end; end def test_keyword_self_reference @@ -754,6 +759,10 @@ def test_dedented_heredoc_invalid_identifer assert_syntax_error('<<~ "#{}"', /unexpected < RbConfig::LIMITS['FIXNUM_MAX'], @@ -629,7 +629,8 @@ def test_select_wait end Thread.pass until t.stop? assert_predicate(t, :alive?) - t.kill + ensure + t&.kill end def test_mutex_deadlock @@ -1111,7 +1112,7 @@ def test_mutex_owned2 Thread.pass until mutex.locked? assert_equal(mutex.owned?, false) ensure - th.kill if th + th&.kill end end diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index 50ac569c4eee83..7c5ee5466d2d5d 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -1056,6 +1056,29 @@ def test_2038 assert_equal(min, t.min) assert_equal(sec, t.sec) } + assert_equal(Time.local(2038,3,1), Time.local(2038,2,29)) + assert_equal(Time.local(2038,3,2), Time.local(2038,2,30)) + assert_equal(Time.local(2038,3,3), Time.local(2038,2,31)) + assert_equal(Time.local(2040,2,29), Time.local(2040,2,29)) + assert_equal(Time.local(2040,3,1), Time.local(2040,2,30)) + assert_equal(Time.local(2040,3,2), Time.local(2040,2,31)) + n = 2 ** 64 + n += 400 - n % 400 # n is over 2^64 and multiple of 400 + assert_equal(Time.local(n,2,29),Time.local(n,2,29)) + assert_equal(Time.local(n,3,1), Time.local(n,2,30)) + assert_equal(Time.local(n,3,2), Time.local(n,2,31)) + n += 100 + assert_equal(Time.local(n,3,1), Time.local(n,2,29)) + assert_equal(Time.local(n,3,2), Time.local(n,2,30)) + assert_equal(Time.local(n,3,3), Time.local(n,2,31)) + n += 4 + assert_equal(Time.local(n,2,29),Time.local(n,2,29)) + assert_equal(Time.local(n,3,1), Time.local(n,2,30)) + assert_equal(Time.local(n,3,2), Time.local(n,2,31)) + n += 1 + assert_equal(Time.local(n,3,1), Time.local(n,2,29)) + assert_equal(Time.local(n,3,2), Time.local(n,2,30)) + assert_equal(Time.local(n,3,3), Time.local(n,2,31)) end def test_future @@ -1138,6 +1161,7 @@ def test_memsize case size when 20 then expect = 50 when 40 then expect = 86 + when 48 then expect = 94 else flunk "Unsupported RVALUE_SIZE=#{size}, update test_memsize" end diff --git a/test/ruby/test_time_tz.rb b/test/ruby/test_time_tz.rb index 97ec3d08a60856..8e60cf44c59ad4 100644 --- a/test/ruby/test_time_tz.rb +++ b/test/ruby/test_time_tz.rb @@ -503,45 +503,86 @@ def utc_to_local(t) def abbr(t) @abbr end + + def ==(other) + @name == other.name and @abbr == other.abbr(0) and @offset == other.offset + end + + def inspect + "#" + end end end module TestTimeTZ::WithTZ - def subtest_new(time_class, tz, tzname, abbr, utc_offset) - t = time_class.new(2018, 9, 1, 12, 0, 0, tz) + def subtest_new(time_class, tz, tzarg, tzname, abbr, utc_offset) + t = time_class.new(2018, 9, 1, 12, 0, 0, tzarg) assert_equal([2018, 9, 1, 12, 0, 0, tz], [t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.zone]) h, m = (-utc_offset / 60).divmod(60) assert_equal(time_class.utc(2018, 9, 1, 12+h, m, 0).to_i, t.to_i) end - def subtest_getlocal(time_class, tz, tzname, abbr, utc_offset) - t = time_class.utc(2018, 9, 1, 12, 0, 0).getlocal(tz) + def subtest_getlocal(time_class, tz, tzarg, tzname, abbr, utc_offset) + t = time_class.utc(2018, 9, 1, 12, 0, 0).getlocal(tzarg) h, m = (utc_offset / 60).divmod(60) assert_equal([2018, 9, 1, 12+h, m, 0, tz], [t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.zone]) assert_equal(time_class.utc(2018, 9, 1, 12, 0, 0), t) end - def subtest_strftime(time_class, tz, tzname, abbr, utc_offset) - t = time_class.new(2018, 9, 1, 12, 0, 0, tz) + def subtest_strftime(time_class, tz, tzarg, tzname, abbr, utc_offset) + t = time_class.new(2018, 9, 1, 12, 0, 0, tzarg) h, m = (utc_offset.abs / 60).divmod(60) h = -h if utc_offset < 0 assert_equal("%+.2d%.2d %s" % [h, m, abbr], t.strftime("%z %Z")) end - def subtest_plus(time_class, tz, tzname, abbr, utc_offset) - t = time_class.new(2018, 9, 1, 12, 0, 0, tz) + 4000 + def subtest_plus(time_class, tz, tzarg, tzname, abbr, utc_offset) + t = time_class.new(2018, 9, 1, 12, 0, 0, tzarg) + 4000 assert_equal([2018, 9, 1, 13, 6, 40, tz], [t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.zone]) m, s = (4000-utc_offset).divmod(60) h, m = m.divmod(60) assert_equal(time_class.utc(2018, 9, 1, 12+h, m, s), t) end - def subtest_marshal(time_class, tz, tzname, abbr, utc_offset) - t = time_class.new(2018, 9, 1, 12, 0, 0, tz) + def subtest_at(time_class, tz, tzarg, tzname, abbr, utc_offset) + h, m = (utc_offset / 60).divmod(60) + utc = time_class.utc(2018, 9, 1, 12, 0, 0) + t = time_class.at(utc, in: tzarg) + assert_equal([2018, 9, 1, 12+h, m, 0, tz], [t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.zone]) + assert_equal(utc.to_i, t.to_i) + utc = utc.to_i + t = time_class.at(utc, in: tzarg) + assert_equal([2018, 9, 1, 12+h, m, 0, tz], [t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.zone]) + assert_equal(utc, t.to_i) + end + + def subtest_marshal(time_class, tz, tzarg, tzname, abbr, utc_offset) + t = time_class.new(2018, 9, 1, 12, 0, 0, tzarg) t2 = Marshal.load(Marshal.dump(t)) assert_equal(t, t2) assert_equal(t.utc_offset, t2.utc_offset) assert_equal(t.utc_offset, (t2+1).utc_offset) + assert_instance_of(t.zone.class, t2.zone) + end + + def test_invalid_zone + make_timezone("INVALID", "INV", 0) + rescue => e + assert_kind_of(StandardError, e) + else + assert false, "ArgumentError expected but nothing was raised." + end + + def nametest_marshal_compatibility(time_class, tzname, abbr, utc_offset) + data = [ + "\x04\x08Iu:".b, Marshal.dump(time_class)[3..-1], + "\x0d""\xEF\xA7\x1D\x80\x00\x00\x00\x00".b, + Marshal.dump({offset: utc_offset, zone: abbr})[3..-1], + ].join('') + t = Marshal.load(data) + assert_equal(utc_offset, t.utc_offset) + assert_equal(utc_offset, (t+1).utc_offset) + # t.zone may be a mere String or timezone object. end ZONES = { @@ -560,7 +601,18 @@ def make_timezone(tzname, abbr, utc_offset) define_method("#{test}@#{tzname}") do tz = make_timezone(tzname, abbr, utc_offset) time_class = self.class::TIME_CLASS - __send__(subtest, time_class, tz, tzname, abbr, utc_offset) + __send__(subtest, time_class, tz, tz, tzname, abbr, utc_offset) + __send__(subtest, time_class, tz, tzname, tzname, abbr, utc_offset) + end + end + end + + instance_methods(false).grep(/\Aname(?=test_)/) do |subtest| + test = $' + ZONES.each_pair do |tzname, (abbr, utc_offset)| + define_method("#{test}@#{tzname}") do + time_class = self.class::TIME_CLASS + __send__(subtest, time_class, tzname, abbr, utc_offset) end end end @@ -581,3 +633,41 @@ def self.make_timezone(tzname, abbr, utc_offset) TestTimeTZ::TZ.new(tzname, abbr, utc_offset) end end + +begin + require "tzinfo" +rescue LoadError +else + class TestTimeTZ::GemTZInfo < Test::Unit::TestCase + include TestTimeTZ::WithTZ + + class TIME_CLASS < ::Time + def self.find_timezone(tzname) + TZInfo::Timezone.get(tzname) + end + end + + def tz + @tz ||= TZInfo::Timezone.get(tzname) + end + end +end + +begin + require "timezone" +rescue LoadError +else + class TestTimeTZ::GemTimezone < Test::Unit::TestCase + include TestTimeTZ::WithTZ + + class TIME_CLASS < ::Time + def self.find_timezone(name) + Timezone.fetch(name) + end + end + + def tz + @tz ||= Timezone[tzname] + end + end +end diff --git a/test/ruby/test_undef.rb b/test/ruby/test_undef.rb index 6d513a238fa0ba..e0add7c3abae7c 100644 --- a/test/ruby/test_undef.rb +++ b/test/ruby/test_undef.rb @@ -25,7 +25,7 @@ def test_undef y = Undef1.new assert_equal "bar", y.bar z = Undef2.new - assert_raise(NoMethodError) { z.foo } + assert_raise(NoMethodError) { z.bar } end def test_special_const_undef diff --git a/test/rubygems/rubygems_plugin.rb b/test/rubygems/rubygems_plugin.rb index 30a67789c61e2d..7fac2ebec645a4 100644 --- a/test/rubygems/rubygems_plugin.rb +++ b/test/rubygems/rubygems_plugin.rb @@ -23,4 +23,3 @@ def execute end Gem::CommandManager.instance.register_command :interrupt - diff --git a/test/rubygems/test_config.rb b/test/rubygems/test_config.rb index f8aadb4a232aed..70fc4e23f0858d 100644 --- a/test/rubygems/test_config.rb +++ b/test/rubygems/test_config.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rubygems/test_case' require 'rubygems' +require 'shellwords' class TestConfig < Gem::TestCase @@ -13,12 +14,16 @@ def test_datadir def test_good_rake_path_is_escaped path = Gem::TestCase.class_eval('@@good_rake') - assert_match(/#{Gem.ruby} "[^"]*good_rake.rb"/, path) + ruby, rake = path.shellsplit + assert_equal(Gem.ruby, ruby) + assert_match(/\/good_rake.rb\z/, rake) end def test_bad_rake_path_is_escaped path = Gem::TestCase.class_eval('@@bad_rake') - assert_match(/#{Gem.ruby} "[^"]*bad_rake.rb"/, path) + ruby, rake = path.shellsplit + assert_equal(Gem.ruby, ruby) + assert_match(/\/bad_rake.rb\z/, rake) end end diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index ae271d8c99ffc6..682fa84612ef81 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -278,6 +278,41 @@ def test_activate_bin_path_resolves_eagerly assert_equal %w(a-1 b-2 c-1), loaded_spec_names end + def test_activate_bin_path_gives_proper_error_for_bundler + bundler = util_spec 'bundler', '2' do |s| + s.executables = ['bundle'] + end + + install_specs bundler + + File.open("Gemfile.lock", "w") do |f| + f.write <<-L.gsub(/ {8}/, "") + GEM + remote: https://rubygems.org/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + 9999 + L + end + + File.open("Gemfile", "w") { |f| f.puts('source "https://rubygems.org"') } + + e = assert_raises Gem::GemNotFoundException do + load Gem.activate_bin_path("bundler", "bundle", ">= 0.a") + end + + assert_includes e.message, "Could not find 'bundler' (9999) required by your #{File.expand_path("Gemfile.lock")}." + assert_includes e.message, "To update to the latest version installed on your system, run `bundle update --bundler`." + assert_includes e.message, "To install the missing version, run `gem install bundler:9999`" + refute_includes e.message, "can't find gem bundler (>= 0.a) with executable bundle" + end + def test_self_bin_path_no_exec_name e = assert_raises ArgumentError do Gem.bin_path 'a' @@ -531,7 +566,7 @@ def test_self_ensure_gem_directories_missing_parents assert File.directory?(util_cache_dir) end - unless win_platform? || Process.uid.zero? then # only for FS that support write protection + unless win_platform? || Process.uid.zero? # only for FS that support write protection def test_self_ensure_gem_directories_write_protected gemdir = File.join @tempdir, "egd" FileUtils.rm_r gemdir rescue nil @@ -958,7 +993,7 @@ def test_self_env_requirement assert_equal Gem::Requirement.default, Gem.env_requirement('qux') end - def test_self_ruby_version_1_8_5 + def test_self_ruby_version_with_patchlevel_less_ancient_rubies util_set_RUBY_VERSION '1.8.5' assert_equal Gem::Version.new('1.8.5'), Gem.ruby_version @@ -966,7 +1001,7 @@ def test_self_ruby_version_1_8_5 util_restore_RUBY_VERSION end - def test_self_ruby_version_1_8_6p287 + def test_self_ruby_version_with_release util_set_RUBY_VERSION '1.8.6', 287 assert_equal Gem::Version.new('1.8.6.287'), Gem.ruby_version @@ -974,10 +1009,34 @@ def test_self_ruby_version_1_8_6p287 util_restore_RUBY_VERSION end - def test_self_ruby_version_1_9_2dev_r23493 - util_set_RUBY_VERSION '1.9.2', -1, 23493 + def test_self_ruby_version_with_non_mri_implementations + util_set_RUBY_VERSION '2.5.0', 0, 60928, 'jruby 9.2.0.0 (2.5.0) 2018-05-24 81156a8 OpenJDK 64-Bit Server VM 25.171-b11 on 1.8.0_171-8u171-b11-0ubuntu0.16.04.1-b11 [linux-x86_64]' + + assert_equal Gem::Version.new('2.5.0'), Gem.ruby_version + ensure + util_restore_RUBY_VERSION + end + + def test_self_ruby_version_with_prerelease + util_set_RUBY_VERSION '2.6.0', -1, 63539, 'ruby 2.6.0preview2 (2018-05-31 trunk 63539) [x86_64-linux]' + + assert_equal Gem::Version.new('2.6.0.preview2'), Gem.ruby_version + ensure + util_restore_RUBY_VERSION + end + + def test_self_ruby_version_with_non_mri_implementations_with_mri_prerelase_compatibility + util_set_RUBY_VERSION '2.6.0', -1, 63539, 'weirdjruby 9.2.0.0 (2.6.0preview2) 2018-05-24 81156a8 OpenJDK 64-Bit Server VM 25.171-b11 on 1.8.0_171-8u171-b11-0ubuntu0.16.04.1-b11 [linux-x86_64]', 'weirdjruby', '9.2.0.0' + + assert_equal Gem::Version.new('2.6.0.preview2'), Gem.ruby_version + ensure + util_restore_RUBY_VERSION + end + + def test_self_ruby_version_with_trunk + util_set_RUBY_VERSION '1.9.2', -1, 23493, 'ruby 1.9.2dev (2009-05-20 trunk 23493) [x86_64-linux]' - assert_equal Gem::Version.new('1.9.2.dev.23493'), Gem.ruby_version + assert_equal Gem::Version.new('1.9.2.dev'), Gem.ruby_version ensure util_restore_RUBY_VERSION end @@ -1225,7 +1284,7 @@ def test_self_user_dir end def test_self_user_home - if ENV['HOME'] then + if ENV['HOME'] assert_equal ENV['HOME'], Gem.user_home else assert true, 'count this test' @@ -1610,7 +1669,7 @@ def test_register_default_spec def test_default_gems_use_full_paths begin - if defined?(RUBY_ENGINE) then + if defined?(RUBY_ENGINE) engine = RUBY_ENGINE Object.send :remove_const, :RUBY_ENGINE end @@ -1623,7 +1682,7 @@ def test_default_gems_use_full_paths end begin - if defined?(RUBY_ENGINE) then + if defined?(RUBY_ENGINE) engine = RUBY_ENGINE Object.send :remove_const, :RUBY_ENGINE end @@ -1813,13 +1872,13 @@ def test_platform_defaults assert platform_defaults.is_a? Hash end - def ruby_install_name name + def ruby_install_name(name) orig_RUBY_INSTALL_NAME = RbConfig::CONFIG['ruby_install_name'] RbConfig::CONFIG['ruby_install_name'] = name yield ensure - if orig_RUBY_INSTALL_NAME then + if orig_RUBY_INSTALL_NAME RbConfig::CONFIG['ruby_install_name'] = orig_RUBY_INSTALL_NAME else RbConfig::CONFIG.delete 'ruby_install_name' diff --git a/test/rubygems/test_gem_command.rb b/test/rubygems/test_gem_command.rb index 4442c6108ea831..8caa9c6e2eb4a5 100644 --- a/test/rubygems/test_gem_command.rb +++ b/test/rubygems/test_gem_command.rb @@ -251,4 +251,3 @@ def test_show_lookup_failure_suggestions_remote end end - diff --git a/test/rubygems/test_gem_command_manager.rb b/test/rubygems/test_gem_command_manager.rb index bb379427dee163..6ada96f1c1c7cb 100644 --- a/test/rubygems/test_gem_command_manager.rb +++ b/test/rubygems/test_gem_command_manager.rb @@ -267,4 +267,3 @@ def test_process_args_update end end - diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index f7c92ebd7e042b..68b5724c613561 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -286,7 +286,7 @@ def test_build_signed_gem_with_cert_expiration_length_days gem_path = File.join Gem.user_home, ".gem" Dir.mkdir gem_path - trust_dir = Gem::Security.trust_dir + Gem::Security.trust_dir tmp_expired_cert_file = File.join gem_path, "gem-public_cert.pem" File.write(tmp_expired_cert_file, File.read(EXPIRED_CERT_FILE)) diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb index 97c081e110a540..d7381f14d0f4a3 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -2,7 +2,7 @@ require 'rubygems/test_case' require 'rubygems/commands/cert_command' -unless defined?(OpenSSL::SSL) then +unless defined?(OpenSSL::SSL) warn 'Skipping `gem cert` tests. openssl not found.' end @@ -192,7 +192,6 @@ def test_execute_build_expiration_days test = (cert.not_after - cert.not_before).to_i / (24 * 60 * 60) assert_equal(test, 26) - end def test_execute_build_bad_passphrase_confirmation @@ -795,4 +794,3 @@ def test_handle_options_sign_nonexistent end end if defined?(OpenSSL::SSL) - diff --git a/test/rubygems/test_gem_commands_check_command.rb b/test/rubygems/test_gem_commands_check_command.rb index b220b4d36c4b1a..6a6033d35d2a8b 100644 --- a/test/rubygems/test_gem_commands_check_command.rb +++ b/test/rubygems/test_gem_commands_check_command.rb @@ -10,7 +10,7 @@ def setup @cmd = Gem::Commands::CheckCommand.new end - def gem name + def gem(name) spec = quick_gem name do |gem| gem.files = %W[lib/#{name}.rb Rakefile] end diff --git a/test/rubygems/test_gem_commands_cleanup_command.rb b/test/rubygems/test_gem_commands_cleanup_command.rb index 7024e59fb939df..fdcd71ed8ab84f 100644 --- a/test/rubygems/test_gem_commands_cleanup_command.rb +++ b/test/rubygems/test_gem_commands_cleanup_command.rb @@ -264,4 +264,3 @@ def test_execute_user_install assert_path_exists d_2.gem_dir end end - diff --git a/test/rubygems/test_gem_commands_contents_command.rb b/test/rubygems/test_gem_commands_contents_command.rb index 65644f476b9f4f..a8d6efe7940aa7 100644 --- a/test/rubygems/test_gem_commands_contents_command.rb +++ b/test/rubygems/test_gem_commands_contents_command.rb @@ -10,7 +10,7 @@ def setup @cmd = Gem::Commands::ContentsCommand.new end - def gem name, version = 2 + def gem(name, version = 2) spec = quick_gem name, version do |gem| gem.files = %W[lib/#{name}.rb Rakefile] end @@ -237,4 +237,3 @@ def test_handle_options end end - diff --git a/test/rubygems/test_gem_commands_dependency_command.rb b/test/rubygems/test_gem_commands_dependency_command.rb index 7433c33d6391f3..25b759dd85a0c9 100644 --- a/test/rubygems/test_gem_commands_dependency_command.rb +++ b/test/rubygems/test_gem_commands_dependency_command.rb @@ -227,4 +227,3 @@ def test_execute_prerelease end end - diff --git a/test/rubygems/test_gem_commands_fetch_command.rb b/test/rubygems/test_gem_commands_fetch_command.rb index 6858327ed3c094..9989f57bd7c21a 100644 --- a/test/rubygems/test_gem_commands_fetch_command.rb +++ b/test/rubygems/test_gem_commands_fetch_command.rb @@ -124,4 +124,3 @@ def test_execute_version end end - diff --git a/test/rubygems/test_gem_commands_generate_index_command.rb b/test/rubygems/test_gem_commands_generate_index_command.rb index 7e30a6dccfb4c7..b4276702f47885 100644 --- a/test/rubygems/test_gem_commands_generate_index_command.rb +++ b/test/rubygems/test_gem_commands_generate_index_command.rb @@ -48,4 +48,3 @@ def test_handle_options_update end end if ''.respond_to? :to_xs - diff --git a/test/rubygems/test_gem_commands_help_command.rb b/test/rubygems/test_gem_commands_help_command.rb index 6542cab5993e22..55bc797b2890f4 100644 --- a/test/rubygems/test_gem_commands_help_command.rb +++ b/test/rubygems/test_gem_commands_help_command.rb @@ -48,7 +48,7 @@ def test_gem_help_commands assert_match(/\s+#{cmd}\s+\S+/, out) end - if defined?(OpenSSL::SSL) then + if defined?(OpenSSL::SSL) assert_empty err refute_match 'No command found for ', out @@ -64,7 +64,7 @@ def test_gem_no_args_shows_help end end - def util_gem *args + def util_gem(*args) @cmd.options[:args] = args use_ui @ui do diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index b024c77c70b661..0976a31b502433 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -393,6 +393,23 @@ def test_execute_prerelease_wins_over_previous_ver assert_equal %w[a-2.a], @cmd.installed_specs.map { |spec| spec.full_name } end + def test_execute_with_version_specified_by_colon + spec_fetcher do |fetcher| + fetcher.download 'a', 1 + fetcher.download 'a', 2 + end + + @cmd.options[:args] = %w[a:1] + + use_ui @ui do + assert_raises Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-1], @cmd.installed_specs.map { |spec| spec.full_name } + end + def test_execute_prerelease_skipped_when_non_pre_available spec_fetcher do |fetcher| fetcher.gem 'a', '2.pre' @@ -649,12 +666,31 @@ def test_execute_two_version assert_empty @cmd.installed_specs msg = "ERROR: Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirments using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" assert_empty @ui.output assert_equal msg, @ui.error.chomp end + def test_execute_two_version_specified_by_colon + spec_fetcher do |fetcher| + fetcher.gem 'a', 1 + fetcher.gem 'a', 2 + fetcher.gem 'b', 1 + fetcher.gem 'b', 2 + end + + @cmd.options[:args] = %w[a:1 b:1] + + use_ui @ui do + assert_raises Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-1 b-1], @cmd.installed_specs.map { |spec| spec.full_name } + end + def test_execute_conservative spec_fetcher do |fetcher| fetcher.download 'b', 2 diff --git a/test/rubygems/test_gem_commands_lock_command.rb b/test/rubygems/test_gem_commands_lock_command.rb index af1fce999c7a22..a35ed081cbdeaf 100644 --- a/test/rubygems/test_gem_commands_lock_command.rb +++ b/test/rubygems/test_gem_commands_lock_command.rb @@ -66,4 +66,3 @@ def test_execute_strict end end - diff --git a/test/rubygems/test_gem_commands_outdated_command.rb b/test/rubygems/test_gem_commands_outdated_command.rb index 626b29057de8ac..3b0220e84b57c7 100644 --- a/test/rubygems/test_gem_commands_outdated_command.rb +++ b/test/rubygems/test_gem_commands_outdated_command.rb @@ -30,4 +30,3 @@ def test_execute assert_equal "", @ui.error end end - diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index 3185074d9894cb..1f9a2efbcad1ab 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -66,7 +66,6 @@ def test_show_owners_dont_load_objects @cmd.show_owners("freewill") end end - end diff --git a/test/rubygems/test_gem_commands_pristine_command.rb b/test/rubygems/test_gem_commands_pristine_command.rb index 5d7a267974ef79..e4aa8d86560286 100644 --- a/test/rubygems/test_gem_commands_pristine_command.rb +++ b/test/rubygems/test_gem_commands_pristine_command.rb @@ -505,30 +505,6 @@ def test_execute_default_gem assert_empty(@ui.error) end - def test_execute_bundled_gem_on_old_rubies - util_set_RUBY_VERSION '1.9.3', 551 - - spec = util_spec 'bigdecimal', '1.1.0' do |s| - s.summary = "This bigdecimal is bundled with Ruby" - end - install_specs spec - - @cmd.options[:args] = %w[bigdecimal] - - use_ui @ui do - @cmd.execute - end - - assert_equal([ - "Restoring gems to pristine condition...", - "Skipped bigdecimal-1.1.0, it is bundled with old Ruby" - ], @ui.output.split("\n")) - - assert_empty @ui.error - ensure - util_restore_RUBY_VERSION - end - def test_handle_options @cmd.handle_options %w[] diff --git a/test/rubygems/test_gem_commands_search_command.rb b/test/rubygems/test_gem_commands_search_command.rb index 61caff1fc9b088..9187050c309f3e 100644 --- a/test/rubygems/test_gem_commands_search_command.rb +++ b/test/rubygems/test_gem_commands_search_command.rb @@ -15,4 +15,3 @@ def test_initialize end end - diff --git a/test/rubygems/test_gem_commands_server_command.rb b/test/rubygems/test_gem_commands_server_command.rb index b61fc30e9badcf..d511ce0b7b80d0 100644 --- a/test/rubygems/test_gem_commands_server_command.rb +++ b/test/rubygems/test_gem_commands_server_command.rb @@ -57,4 +57,3 @@ def test_handle_options_port end end - diff --git a/test/rubygems/test_gem_commands_setup_command.rb b/test/rubygems/test_gem_commands_setup_command.rb index eabd023d966226..d788c19ad56db8 100644 --- a/test/rubygems/test_gem_commands_setup_command.rb +++ b/test/rubygems/test_gem_commands_setup_command.rb @@ -66,7 +66,7 @@ def setup FileUtils.mkdir_p 'default/gems/bundler-audit-1.0.0' end - def gem_install name + def gem_install(name) gem = util_spec name do |s| s.executables = [name] s.files = %W[bin/#{name}] diff --git a/test/rubygems/test_gem_commands_signin_command.rb b/test/rubygems/test_gem_commands_signin_command.rb index 2cf86edd0980d9..afcb8d6d99c09d 100644 --- a/test/rubygems/test_gem_commands_signin_command.rb +++ b/test/rubygems/test_gem_commands_signin_command.rb @@ -74,7 +74,7 @@ def test_execute_with_valid_creds_set_for_default_host # Utility method to capture IO/UI within the block passed - def util_capture ui_stub = nil, host = nil, api_key = nil + def util_capture(ui_stub = nil, host = nil, api_key = nil) api_key ||= 'a5fdbb6ba150cbb83aad2bb2fede64cf040453903' response = [api_key, 200, 'OK'] email = 'you@example.com' diff --git a/test/rubygems/test_gem_commands_sources_command.rb b/test/rubygems/test_gem_commands_sources_command.rb index 54c024615032d8..ad92c26073b6b3 100644 --- a/test/rubygems/test_gem_commands_sources_command.rb +++ b/test/rubygems/test_gem_commands_sources_command.rb @@ -298,4 +298,3 @@ def test_execute_update end end - diff --git a/test/rubygems/test_gem_commands_specification_command.rb b/test/rubygems/test_gem_commands_specification_command.rb index 72e008b2cbe121..f56aa9777a9828 100644 --- a/test/rubygems/test_gem_commands_specification_command.rb +++ b/test/rubygems/test_gem_commands_specification_command.rb @@ -248,4 +248,3 @@ def test_execute_ruby end end - diff --git a/test/rubygems/test_gem_commands_uninstall_command.rb b/test/rubygems/test_gem_commands_uninstall_command.rb index f23f4c2461d4fb..407d2451a677f4 100644 --- a/test/rubygems/test_gem_commands_uninstall_command.rb +++ b/test/rubygems/test_gem_commands_uninstall_command.rb @@ -83,7 +83,7 @@ def test_execute_removes_executable end end - if win_platform? then + if win_platform? assert File.exist?(@executable) else assert File.symlink?(@executable) @@ -151,12 +151,14 @@ def test_execute_prerelease assert_match(/Successfully uninstalled/, output) end - def test_execute_with_force_leaves_executable + def test_execute_with_version_leaves_non_matching_versions ui = Gem::MockGemUi.new util_make_gems util_setup_gem ui + assert_equal 3, Gem::Specification.find_all_by_name('a').length + @cmd.options[:version] = '1' @cmd.options[:force] = true @cmd.options[:args] = ['a'] @@ -165,17 +167,43 @@ def test_execute_with_force_leaves_executable @cmd.execute end - assert !Gem::Specification.all_names.include?('a') + assert_equal 2, Gem::Specification.find_all_by_name('a').length + assert File.exist? File.join(@gemhome, 'bin', 'executable') end - def test_execute_with_force_uninstalls_all_versions + def test_execute_with_version_specified_as_colon ui = Gem::MockGemUi.new "y\n" util_make_gems util_setup_gem ui - assert Gem::Specification.find_all_by_name('a').length > 1 + assert_equal 3, Gem::Specification.find_all_by_name('a').length + + @cmd.options[:force] = true + @cmd.options[:args] = ['a:1'] + + use_ui ui do + @cmd.execute + end + + assert_equal 2, Gem::Specification.find_all_by_name('a').length + + assert File.exist? File.join(@gemhome, 'bin', 'executable') + end + + def test_execute_with_force_and_without_version_uninstalls_everything + ui = Gem::MockGemUi.new "y\n" + + a_1, = util_gem 'a', 1 + install_gem a_1 + + a_3a, = util_gem 'a', '3.a' + install_gem a_3a + + util_setup_gem ui + + assert_equal 3, Gem::Specification.find_all_by_name('a').length @cmd.options[:force] = true @cmd.options[:args] = ['a'] @@ -184,7 +212,9 @@ def test_execute_with_force_uninstalls_all_versions @cmd.execute end - refute_includes Gem::Specification.all_names, 'a' + assert_empty Gem::Specification.find_all_by_name('a') + assert_match "Removing executable", ui.output + refute File.exist? @executable end def test_execute_with_force_ignores_dependencies @@ -261,6 +291,25 @@ def test_handle_options_vendor assert_match expected, @ui.error end + def test_execute_two_version + @cmd.options[:args] = %w[a b] + @cmd.options[:version] = Gem::Requirement.new("> 1") + + use_ui @ui do + e = assert_raises Gem::MockGemUi::TermError do + @cmd.execute + end + + assert_equal 1, e.exit_code + end + + msg = "ERROR: Can't use --version with multiple gems. You can specify multiple gems with" \ + " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + + assert_empty @ui.output + assert_equal msg, @ui.error.lines.last.chomp + end + def test_handle_options_vendor_missing orig_vendordir = RbConfig::CONFIG['vendordir'] RbConfig::CONFIG.delete 'vendordir' @@ -318,4 +367,3 @@ def test_execute_with_gem_uninstall_error assert_match %r!Error: unable to successfully uninstall '#{@spec.name}'!, @ui.error end end - diff --git a/test/rubygems/test_gem_commands_unpack_command.rb b/test/rubygems/test_gem_commands_unpack_command.rb index 4fde83e5245ea3..7d96caaf573c95 100644 --- a/test/rubygems/test_gem_commands_unpack_command.rb +++ b/test/rubygems/test_gem_commands_unpack_command.rb @@ -223,4 +223,3 @@ def test_handle_options_metadata end end - diff --git a/test/rubygems/test_gem_commands_which_command.rb b/test/rubygems/test_gem_commands_which_command.rb index 0c2b177273c949..0d63bb9b374f6e 100644 --- a/test/rubygems/test_gem_commands_which_command.rb +++ b/test/rubygems/test_gem_commands_which_command.rb @@ -84,4 +84,3 @@ def util_foo_bar end end - diff --git a/test/rubygems/test_gem_commands_yank_command.rb b/test/rubygems/test_gem_commands_yank_command.rb index 70aa2263a6575b..d30c386aa6e5d2 100644 --- a/test/rubygems/test_gem_commands_yank_command.rb +++ b/test/rubygems/test_gem_commands_yank_command.rb @@ -97,4 +97,3 @@ def test_execute_host end end - diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index 5749905b5a6c2f..192699aaba9230 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -344,7 +344,7 @@ def test_rubygems_api_key_equals assert_equal expected, YAML.load_file(@cfg.credentials_path) - unless win_platform? then + unless win_platform? stat = File.stat @cfg.credentials_path assert_equal 0600, stat.mode & 0600 @@ -490,4 +490,3 @@ def test_disable_default_gem_server assert_equal(true, @cfg.disable_default_gem_server) end end - diff --git a/test/rubygems/test_gem_dependency.rb b/test/rubygems/test_gem_dependency.rb index d7eec3c090423f..daeb8f625ccd84 100644 --- a/test/rubygems/test_gem_dependency.rb +++ b/test/rubygems/test_gem_dependency.rb @@ -358,7 +358,7 @@ def test_to_specs_respects_bundler_version dep.to_specs end - assert_match "Could not find 'bundler' (3.5) required by reason.\nTo update to the lastest version installed on your system, run `bundle update --bundler`.\nTo install the missing version, run `gem install bundler:3.5`\n", e.message + assert_match "Could not find 'bundler' (3.5) required by reason.\nTo update to the latest version installed on your system, run `bundle update --bundler`.\nTo install the missing version, run `gem install bundler:3.5`\n", e.message end Gem::BundlerVersionFinder.stub(:bundler_version_with_reason, ["2.0.0.pre.1", "reason"]) do @@ -387,4 +387,3 @@ def test_to_specs_indicates_total_gem_set_size end - diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb index 503bedf7199d21..74b7a6a83e81c8 100644 --- a/test/rubygems/test_gem_dependency_installer.rb +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -897,7 +897,7 @@ def test_install_platform_is_ignored_when_a_file_is_specified assert_equal %w[a-1-cpu-other_platform-1], inst.installed_gems.map { |s| s.full_name } end - if defined? OpenSSL then + if defined? OpenSSL def test_install_security_policy util_setup_gems @@ -922,7 +922,7 @@ def test_install_security_policy end # Wrappers don't work on mswin - unless win_platform? then + unless win_platform? def test_install_no_wrappers util_setup_gems @@ -1073,7 +1073,6 @@ def test_find_gems_with_sources_local assert_equal 'a-1', remote.spec.full_name, 'remote spec' assert_equal Gem::Source.new(@gem_repo), remote.source, 'remote path' - end def test_find_gems_with_sources_prerelease diff --git a/test/rubygems/test_gem_dependency_list.rb b/test/rubygems/test_gem_dependency_list.rb index 7ba91be95fac7d..9bd6ceb6f3ce9d 100644 --- a/test/rubygems/test_gem_dependency_list.rb +++ b/test/rubygems/test_gem_dependency_list.rb @@ -257,4 +257,3 @@ def util_diamond end end - diff --git a/test/rubygems/test_gem_dependency_resolution_error.rb b/test/rubygems/test_gem_dependency_resolution_error.rb index b9625a3c02c0b7..cf4663650e0b59 100644 --- a/test/rubygems/test_gem_dependency_resolution_error.rb +++ b/test/rubygems/test_gem_dependency_resolution_error.rb @@ -26,4 +26,3 @@ def test_message end end - diff --git a/test/rubygems/test_gem_doctor.rb b/test/rubygems/test_gem_doctor.rb index 8db65d70cefc32..a0e3a18d7f9019 100644 --- a/test/rubygems/test_gem_doctor.rb +++ b/test/rubygems/test_gem_doctor.rb @@ -4,7 +4,7 @@ class TestGemDoctor < Gem::TestCase - def gem name + def gem(name) spec = quick_gem name do |gem| gem.files = %W[lib/#{name}.rb Rakefile] end @@ -166,4 +166,3 @@ def test_gem_repository_eh end end - diff --git a/test/rubygems/test_gem_ext_cmake_builder.rb b/test/rubygems/test_gem_ext_cmake_builder.rb index 2d449fc2fde64d..6e629080906773 100644 --- a/test/rubygems/test_gem_ext_cmake_builder.rb +++ b/test/rubygems/test_gem_ext_cmake_builder.rb @@ -10,7 +10,7 @@ def setup # Details: https://github.com/rubygems/rubygems/issues/1270#issuecomment-177368340 skip "CmakeBuilder doesn't work on Windows." if Gem.win_platform? - `cmake #{Gem::Ext::Builder.redirector}` + system('cmake', out: IO::NULL, err: [:child, :out]) skip 'cmake not present' unless $?.success? diff --git a/test/rubygems/test_gem_ext_ext_conf_builder.rb b/test/rubygems/test_gem_ext_ext_conf_builder.rb index 8d5135e3096aff..6decb29a9958df 100644 --- a/test/rubygems/test_gem_ext_ext_conf_builder.rb +++ b/test/rubygems/test_gem_ext_ext_conf_builder.rb @@ -216,14 +216,14 @@ def test_class_make_no_Makefile assert_equal 'Makefile not found', error.message end - def configure_args args = nil + def configure_args(args = nil) configure_args = RbConfig::CONFIG['configure_args'] RbConfig::CONFIG['configure_args'] = args if args yield ensure - if configure_args then + if configure_args RbConfig::CONFIG['configure_args'] = configure_args else RbConfig::CONFIG.delete 'configure_args' diff --git a/test/rubygems/test_gem_gem_runner.rb b/test/rubygems/test_gem_gem_runner.rb index d68ac4da81e390..7c771de9e599f6 100644 --- a/test/rubygems/test_gem_gem_runner.rb +++ b/test/rubygems/test_gem_gem_runner.rb @@ -66,4 +66,3 @@ def test_extract_build_args end end - diff --git a/test/rubygems/test_gem_gemcutter_utilities.rb b/test/rubygems/test_gem_gemcutter_utilities.rb index 90f9142171fb8b..f0d2428391e2d2 100644 --- a/test/rubygems/test_gem_gemcutter_utilities.rb +++ b/test/rubygems/test_gem_gemcutter_utilities.rb @@ -187,7 +187,7 @@ def test_sign_in_with_bad_credentials assert_match %r{Access Denied.}, @sign_in_ui.output end - def util_sign_in response, host = nil, args = [] + def util_sign_in(response, host = nil, args = []) email = 'you@example.com' password = 'secret' @@ -204,7 +204,7 @@ def util_sign_in response, host = nil, args = [] @sign_in_ui = Gem::MockGemUi.new "#{email}\n#{password}\n" use_ui @sign_in_ui do - if args.length > 0 then + if args.length > 0 @cmd.sign_in(*args) else @cmd.sign_in diff --git a/test/rubygems/test_gem_impossible_dependencies_error.rb b/test/rubygems/test_gem_impossible_dependencies_error.rb index 027c99a9e58187..8a0f8d61963949 100644 --- a/test/rubygems/test_gem_impossible_dependencies_error.rb +++ b/test/rubygems/test_gem_impossible_dependencies_error.rb @@ -59,4 +59,3 @@ def test_message_conflict end end - diff --git a/test/rubygems/test_gem_indexer.rb b/test/rubygems/test_gem_indexer.rb index 5a9075e676a3f6..9f27744d48d876 100644 --- a/test/rubygems/test_gem_indexer.rb +++ b/test/rubygems/test_gem_indexer.rb @@ -2,7 +2,7 @@ require 'rubygems/test_case' require 'rubygems/indexer' -unless defined?(Builder::XChar) then +unless defined?(Builder::XChar) warn "Gem::Indexer tests are being skipped. Install builder gem." if $VERBOSE end @@ -363,4 +363,3 @@ def refute_indexed(dir, name) end end if defined?(Builder::XChar) - diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 0ff4954d820514..23c153d69beba4 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -53,11 +53,11 @@ def test_app_script_text version = \">= 0.a\" -if ARGV.first - str = ARGV.first - str = str.dup.force_encoding("BINARY") - if str =~ /\\A_(.*)_\\z/ and Gem::Version.correct?($1) then - version = $1 +str = ARGV.first +if str + str = str.b[/\\A_(.*)_\\z/, 1] + if str and Gem::Version.correct?(str) + version = str ARGV.shift end end @@ -121,7 +121,7 @@ def test_check_executable_overwrite_default_bin_dir ensure Object.const_set :RUBY_FRAMEWORK_VERSION, orig_RUBY_FRAMEWORK_VERSION if orig_RUBY_FRAMEWORK_VERSION - if orig_bindir then + if orig_bindir RbConfig::CONFIG['bindir'] = orig_bindir else RbConfig::CONFIG.delete 'bindir' @@ -250,7 +250,7 @@ def test_check_that_user_bin_dir_is_in_path_not_in_path expected = @installer.bin_dir - if Gem.win_platform? then + if Gem.win_platform? expected = expected.downcase.gsub(File::SEPARATOR, File::ALT_SEPARATOR) end @@ -488,7 +488,7 @@ def test_generate_bin_script_wrappers real_exec = File.join @spec.gem_dir, 'bin', 'executable' # fake --no-wrappers for previous install - unless Gem.win_platform? then + unless Gem.win_platform? FileUtils.mkdir_p File.dirname(installed_exec) FileUtils.ln_s real_exec, installed_exec end @@ -965,7 +965,7 @@ def test_install_with_no_prior_files def test_install_force use_ui @ui do - installer = Gem::Installer.at old_ruby_required, :force => true + installer = Gem::Installer.at old_ruby_required('= 1.4.6'), :force => true installer.install end @@ -1380,16 +1380,32 @@ def test_pre_install_checks_dependencies_install_dir def test_pre_install_checks_ruby_version use_ui @ui do - installer = Gem::Installer.at old_ruby_required + installer = Gem::Installer.at old_ruby_required('= 1.4.6') e = assert_raises Gem::RuntimeRequirementNotMetError do installer.pre_install_checks end - rv = Gem.ruby_api_version + rv = Gem.ruby_version assert_equal "old_ruby_required requires Ruby version = 1.4.6. The current ruby version is #{rv}.", e.message end end + def test_pre_install_checks_ruby_version_with_prereleases + util_set_RUBY_VERSION '2.6.0', -1, '63539', 'ruby 2.6.0preview2 (2018-05-31 trunk 63539) [x86_64-linux]' + + installer = Gem::Installer.at old_ruby_required('>= 2.6.0.preview2') + assert installer.pre_install_checks + + installer = Gem::Installer.at old_ruby_required('> 2.6.0.preview2') + e = assert_raises Gem::RuntimeRequirementNotMetError do + assert installer.pre_install_checks + end + assert_equal "old_ruby_required requires Ruby version > 2.6.0.preview2. The current ruby version is 2.6.0.preview2.", + e.message + ensure + util_restore_RUBY_VERSION + end + def test_pre_install_checks_wrong_rubygems_version spec = util_spec 'old_rubygems_required', '1' do |s| s.required_rubygems_version = '< 0' @@ -1415,7 +1431,7 @@ def test_pre_install_checks_malicious_name def spec.full_name # so the spec is buildable "malicious-1" end - def spec.validate packaging, strict; end + def spec.validate(packaging, strict); end util_build_gem spec @@ -1720,9 +1736,9 @@ def test_default_gem assert_equal ['bin/executable'], default_spec.files end - def old_ruby_required + def old_ruby_required(requirement) spec = util_spec 'old_ruby_required', '1' do |s| - s.required_ruby_version = '= 1.4.6' + s.required_ruby_version = requirement end util_build_gem spec @@ -1737,7 +1753,7 @@ def util_execless @installer = util_installer @spec, @gemhome end - def util_conflict_executable wrappers + def util_conflict_executable(wrappers) conflict = quick_gem 'conflict' do |spec| util_make_exec spec end diff --git a/test/rubygems/test_gem_local_remote_options.rb b/test/rubygems/test_gem_local_remote_options.rb index a51182b11c5778..6c21300c2ad11a 100644 --- a/test/rubygems/test_gem_local_remote_options.rb +++ b/test/rubygems/test_gem_local_remote_options.rb @@ -132,4 +132,3 @@ def test_source_option_bad end end - diff --git a/test/rubygems/test_gem_name_tuple.rb b/test/rubygems/test_gem_name_tuple.rb index 9bc2924cf619b3..5331e1cdbb481a 100644 --- a/test/rubygems/test_gem_name_tuple.rb +++ b/test/rubygems/test_gem_name_tuple.rb @@ -42,4 +42,3 @@ def test_spaceship end end - diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index a53cda12740734..b868fe237c8324 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -94,7 +94,7 @@ def test_add_checksums } } - if defined?(OpenSSL::Digest) then + if defined?(OpenSSL::Digest) expected['SHA256'] = { 'metadata.gz' => metadata_sha256, 'data.tar.gz' => Digest::SHA256.hexdigest(tar), @@ -150,8 +150,6 @@ def test_add_files end def test_add_files_symlink - skip 'symlink not supported' if Gem.win_platform? && RUBY_VERSION < '2.3' - spec = Gem::Specification.new spec.files = %w[lib/code.rb lib/code_sym.rb] @@ -472,8 +470,6 @@ def test_extract_tar_gz_absolute end def test_extract_tar_gz_symlink_relative_path - skip 'symlink not supported' if Gem.win_platform? && RUBY_VERSION < '2.3' - package = Gem::Package.new @gem tgz_io = util_tar_gz do |tar| @@ -501,8 +497,6 @@ def test_extract_tar_gz_symlink_relative_path end def test_extract_symlink_parent - skip 'symlink not supported' if Gem.win_platform? && RUBY_VERSION < '2.3' - package = Gem::Package.new @gem tgz_io = util_tar_gz do |tar| diff --git a/test/rubygems/test_gem_package_old.rb b/test/rubygems/test_gem_package_old.rb index 604981b3c1fc9f..ab7934dde5da76 100644 --- a/test/rubygems/test_gem_package_old.rb +++ b/test/rubygems/test_gem_package_old.rb @@ -87,4 +87,3 @@ def test_verify end end - diff --git a/test/rubygems/test_gem_package_tar_header.rb b/test/rubygems/test_gem_package_tar_header.rb index 2dbfffa14cbdaf..5e804bb4188cf7 100644 --- a/test/rubygems/test_gem_package_tar_header.rb +++ b/test/rubygems/test_gem_package_tar_header.rb @@ -165,4 +165,3 @@ def test_from_bad_octal end end - diff --git a/test/rubygems/test_gem_package_tar_reader.rb b/test/rubygems/test_gem_package_tar_reader.rb index e8a902c3112de4..489685d09d8510 100644 --- a/test/rubygems/test_gem_package_tar_reader.rb +++ b/test/rubygems/test_gem_package_tar_reader.rb @@ -87,4 +87,3 @@ def test_seek_missing end end - diff --git a/test/rubygems/test_gem_package_tar_writer.rb b/test/rubygems/test_gem_package_tar_writer.rb index 2808d87718369e..408db07de7ed3e 100644 --- a/test/rubygems/test_gem_package_tar_writer.rb +++ b/test/rubygems/test_gem_package_tar_writer.rb @@ -136,7 +136,6 @@ def test_add_file_signer assert_equal 2048, @io.pos end - end def test_add_file_signer_empty diff --git a/test/rubygems/test_gem_package_task.rb b/test/rubygems/test_gem_package_task.rb index f69ee36bd34444..2794ab81fb555b 100644 --- a/test/rubygems/test_gem_package_task.rb +++ b/test/rubygems/test_gem_package_task.rb @@ -81,4 +81,3 @@ def test_package_dir_path end end if defined?(Rake::PackageTask) - diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb index c9abd08868bdf8..fc8b7030fb837d 100644 --- a/test/rubygems/test_gem_platform.rb +++ b/test/rubygems/test_gem_platform.rb @@ -117,7 +117,7 @@ def test_initialize_mswin32_vc6 assert_equal expected, platform.to_a, 'i386-mswin32 VC6' ensure - if orig_RUBY_SO_NAME then + if orig_RUBY_SO_NAME RbConfig::CONFIG['RUBY_SO_NAME'] = orig_RUBY_SO_NAME else RbConfig::CONFIG.delete 'RUBY_SO_NAME' @@ -145,7 +145,7 @@ def test_initialize_test end def test_to_s - if win_platform? then + if win_platform? assert_equal 'x86-mswin32-60', Gem::Platform.local.to_s else assert_equal 'x86-darwin-8', Gem::Platform.local.to_s @@ -297,12 +297,11 @@ def test_equals_tilde assert_local_match 'sparc-solaris2.8-mq5.3' end - def assert_local_match name + def assert_local_match(name) assert_match Gem::Platform.local, name end - def refute_local_match name + def refute_local_match(name) refute_match Gem::Platform.local, name end end - diff --git a/test/rubygems/test_gem_rdoc.rb b/test/rubygems/test_gem_rdoc.rb index 0355883cb3c41f..073578527d5e04 100644 --- a/test/rubygems/test_gem_rdoc.rb +++ b/test/rubygems/test_gem_rdoc.rb @@ -49,7 +49,7 @@ def rdoc_3_8_or_better? end def test_initialize - if rdoc_4? then + if rdoc_4? refute @hook.generate_rdoc else assert @hook.generate_rdoc @@ -269,4 +269,3 @@ def test_setup_unwritable end end - diff --git a/test/rubygems/test_gem_remote_fetcher.rb b/test/rubygems/test_gem_remote_fetcher.rb index 0c4367d4170b4e..6561ba6bdaeed1 100644 --- a/test/rubygems/test_gem_remote_fetcher.rb +++ b/test/rubygems/test_gem_remote_fetcher.rb @@ -9,7 +9,7 @@ e.message =~ / -- openssl$/ end -unless defined?(OpenSSL::SSL) then +unless defined?(OpenSSL::SSL) warn 'Skipping Gem::Request tests. openssl not found.' end @@ -211,20 +211,20 @@ def test_cache_update_path_no_update refute_path_exists path end - def util_fuck_with_fetcher data, blow = false + def util_fuck_with_fetcher(data, blow = false) fetcher = Gem::RemoteFetcher.fetcher fetcher.instance_variable_set :@test_data, data - unless blow then - def fetcher.fetch_path arg, *rest + unless blow + def fetcher.fetch_path(arg, *rest) @test_arg = arg @test_data end else - def fetcher.fetch_path arg, *rest + def fetcher.fetch_path(arg, *rest) # OMG I'm such an ass class << self; remove_method :fetch_path; end - def self.fetch_path arg, *rest + def self.fetch_path(arg, *rest) @test_arg = arg @test_data end @@ -424,7 +424,7 @@ def test_download_to_cache util_setup_spec_fetcher @a1, @a2 @fetcher.instance_variable_set :@a1, @a1 @fetcher.instance_variable_set :@a2, @a2 - def @fetcher.fetch_path uri, mtime = nil, head = false + def @fetcher.fetch_path(uri, mtime = nil, head = false) case uri.request_uri when /#{@a1.spec_name}/ then Gem.deflate Marshal.dump @a1 @@ -570,7 +570,7 @@ def test_fetch_http def fetcher.request(uri, request_class, last_modified = nil) url = 'http://gems.example.com/redirect' - unless defined? @requested then + unless defined? @requested @requested = true res = Net::HTTPMovedPermanently.new nil, 301, nil res.add_field 'Location', url diff --git a/test/rubygems/test_gem_request.rb b/test/rubygems/test_gem_request.rb index d816e91f62131f..8b475fae42a1d3 100644 --- a/test/rubygems/test_gem_request.rb +++ b/test/rubygems/test_gem_request.rb @@ -4,7 +4,7 @@ require 'ostruct' require 'base64' -unless defined?(OpenSSL::SSL) then +unless defined?(OpenSSL::SSL) warn 'Skipping Gem::Request tests. openssl not found.' end @@ -17,7 +17,7 @@ class TestGemRequest < Gem::TestCase PUBLIC_CERT_FILE = cert_path 'public' SSL_CERT = load_cert 'ssl' - def make_request uri, request_class, last_modified, proxy + def make_request(uri, request_class, last_modified, proxy) Gem::Request.create_with_proxy uri, request_class, last_modified, proxy end @@ -442,7 +442,7 @@ def test_verify_certificate_message_UNABLE_TO_VERIFY_LEAF_SIGNATURE message = Gem::Request.verify_certificate_message error_number, EXPIRED_CERT - assert_equal "You must add #{EXPIRED_CERT.issuer} to your local trusted store", + assert_equal "Cannot verify certificate issued by #{EXPIRED_CERT.issuer}", message end @@ -465,7 +465,7 @@ def util_save_version @orig_RUBY_REVISION = RUBY_REVISION if defined? RUBY_REVISION end - def util_stub_net_http hash + def util_stub_net_http(hash) old_client = Gem::Request::ConnectionPools.client conn = Conn.new OpenStruct.new(hash) Gem::Request::ConnectionPools.client = conn @@ -477,7 +477,7 @@ def util_stub_net_http hash class Conn attr_accessor :payload - def new *args; self; end + def new(*args); self; end def use_ssl=(bool); end def verify_callback=(setting); end def verify_mode=(setting); end diff --git a/test/rubygems/test_gem_request_connection_pools.rb b/test/rubygems/test_gem_request_connection_pools.rb index ecd1e9861f6bd9..eb48ba45d212a6 100644 --- a/test/rubygems/test_gem_request_connection_pools.rb +++ b/test/rubygems/test_gem_request_connection_pools.rb @@ -5,7 +5,7 @@ class TestGemRequestConnectionPool < Gem::TestCase class FakeHttp - def initialize *args + def initialize(*args) end def start diff --git a/test/rubygems/test_gem_request_set_gem_dependency_api.rb b/test/rubygems/test_gem_request_set_gem_dependency_api.rb index 71a095b07ff423..320fdcb6043240 100644 --- a/test/rubygems/test_gem_request_set_gem_dependency_api.rb +++ b/test/rubygems/test_gem_request_set_gem_dependency_api.rb @@ -19,7 +19,7 @@ def setup @gda.instance_variable_set :@vendor_set, @vendor_set end - def with_engine_version name, version + def with_engine_version(name, version) engine = RUBY_ENGINE if Object.const_defined? :RUBY_ENGINE engine_version_const = "#{Gem.ruby_engine.upcase}_VERSION" engine_version = Object.const_get engine_version_const @@ -824,4 +824,3 @@ def test_with_engine_version end end - diff --git a/test/rubygems/test_gem_request_set_lockfile.rb b/test/rubygems/test_gem_request_set_lockfile.rb index 7460b7efad21d7..9ac691ebdd1261 100644 --- a/test/rubygems/test_gem_request_set_lockfile.rb +++ b/test/rubygems/test_gem_request_set_lockfile.rb @@ -21,14 +21,13 @@ def setup @set.instance_variable_set :@vendor_set, @vendor_set @gem_deps_file = 'gem.deps.rb' - end def lockfile Gem::RequestSet::Lockfile.build @set, @gem_deps_file end - def write_lockfile lockfile + def write_lockfile(lockfile) @lock_file = File.expand_path "#{@gem_deps_file}.lock" File.open @lock_file, 'w' do |io| diff --git a/test/rubygems/test_gem_request_set_lockfile_parser.rb b/test/rubygems/test_gem_request_set_lockfile_parser.rb index 296cf5f4a03e23..27ee519ea77bc7 100644 --- a/test/rubygems/test_gem_request_set_lockfile_parser.rb +++ b/test/rubygems/test_gem_request_set_lockfile_parser.rb @@ -530,13 +530,13 @@ def test_parse_missing refute lockfile_set end - def write_lockfile lockfile + def write_lockfile(lockfile) File.open @lock_file, 'w' do |io| io.write lockfile end end - def parse_lockfile set, platforms + def parse_lockfile(set, platforms) tokenizer = Gem::RequestSet::Lockfile::Tokenizer.from_file @lock_file parser = tokenizer.make_parser set, platforms parser.parse diff --git a/test/rubygems/test_gem_request_set_lockfile_tokenizer.rb b/test/rubygems/test_gem_request_set_lockfile_tokenizer.rb index f4aba6d94ac178..48e66cf56a67b9 100644 --- a/test/rubygems/test_gem_request_set_lockfile_tokenizer.rb +++ b/test/rubygems/test_gem_request_set_lockfile_tokenizer.rb @@ -294,7 +294,7 @@ def test_unget assert_equal :token, parser.get end - def write_lockfile lockfile + def write_lockfile(lockfile) File.open @lock_file, 'w' do |io| io.write lockfile end diff --git a/test/rubygems/test_gem_requirement.rb b/test/rubygems/test_gem_requirement.rb index 7bca00e58b32a7..1564ffb0eda922 100644 --- a/test/rubygems/test_gem_requirement.rb +++ b/test/rubygems/test_gem_requirement.rb @@ -387,26 +387,26 @@ def test_hash_with_multiple_versions # Assert that two requirements are equal. Handles Gem::Requirements, # strings, arrays, numbers, and versions. - def assert_requirement_equal expected, actual + def assert_requirement_equal(expected, actual) assert_equal req(expected), req(actual) end # Assert that +version+ satisfies +requirement+. - def assert_satisfied_by version, requirement + def assert_satisfied_by(version, requirement) assert req(requirement).satisfied_by?(v(version)), "#{requirement} is satisfied by #{version}" end # Refute the assumption that two requirements are equal. - def refute_requirement_equal unexpected, actual + def refute_requirement_equal(unexpected, actual) refute_equal req(unexpected), req(actual) end # Refute the assumption that +version+ satisfies +requirement+. - def refute_satisfied_by version, requirement + def refute_satisfied_by(version, requirement) refute req(requirement).satisfied_by?(v(version)), "#{requirement} is not satisfied by #{version}" end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 99cb77afaf30a7..fa1c595afdf3ac 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -23,7 +23,7 @@ def set(*specs) StaticSet.new(specs) end - def assert_resolves_to expected, resolver + def assert_resolves_to(expected, resolver) actual = resolver.resolve exp = expected.sort_by { |s| s.full_name } diff --git a/test/rubygems/test_gem_resolver_activation_request.rb b/test/rubygems/test_gem_resolver_activation_request.rb index ee1b38887aa5e7..5d5de1a2d51715 100644 --- a/test/rubygems/test_gem_resolver_activation_request.rb +++ b/test/rubygems/test_gem_resolver_activation_request.rb @@ -71,4 +71,3 @@ def test_others_possible_eh end end - diff --git a/test/rubygems/test_gem_resolver_api_set.rb b/test/rubygems/test_gem_resolver_api_set.rb index 5746614039d29a..f6b3e49c74a130 100644 --- a/test/rubygems/test_gem_resolver_api_set.rb +++ b/test/rubygems/test_gem_resolver_api_set.rb @@ -206,4 +206,3 @@ def test_prefetch_local end end - diff --git a/test/rubygems/test_gem_resolver_api_specification.rb b/test/rubygems/test_gem_resolver_api_specification.rb index 8c17d42386dbad..87ff9013208d7b 100644 --- a/test/rubygems/test_gem_resolver_api_specification.rb +++ b/test/rubygems/test_gem_resolver_api_specification.rb @@ -166,4 +166,3 @@ def test_spec_jruby_platform end end - diff --git a/test/rubygems/test_gem_resolver_best_set.rb b/test/rubygems/test_gem_resolver_best_set.rb index 556f0e8349f66f..dc6c9b4c440789 100644 --- a/test/rubygems/test_gem_resolver_best_set.rb +++ b/test/rubygems/test_gem_resolver_best_set.rb @@ -135,4 +135,3 @@ def test_replace_failed_api_set_no_api_set end end - diff --git a/test/rubygems/test_gem_resolver_composed_set.rb b/test/rubygems/test_gem_resolver_composed_set.rb index f1c2da04547ae7..0e745433a9801f 100644 --- a/test/rubygems/test_gem_resolver_composed_set.rb +++ b/test/rubygems/test_gem_resolver_composed_set.rb @@ -43,4 +43,3 @@ def test_remote_equals end end - diff --git a/test/rubygems/test_gem_resolver_conflict.rb b/test/rubygems/test_gem_resolver_conflict.rb index d4b345557085e1..a078136510f068 100644 --- a/test/rubygems/test_gem_resolver_conflict.rb +++ b/test/rubygems/test_gem_resolver_conflict.rb @@ -85,4 +85,3 @@ def test_request_path end end - diff --git a/test/rubygems/test_gem_resolver_dependency_request.rb b/test/rubygems/test_gem_resolver_dependency_request.rb index ac8f48a526146a..51f0be9dcd1646 100644 --- a/test/rubygems/test_gem_resolver_dependency_request.rb +++ b/test/rubygems/test_gem_resolver_dependency_request.rb @@ -82,4 +82,3 @@ def test_requirement end end - diff --git a/test/rubygems/test_gem_resolver_git_set.rb b/test/rubygems/test_gem_resolver_git_set.rb index b87a80d44e7bf1..f38859c8b496ff 100644 --- a/test/rubygems/test_gem_resolver_git_set.rb +++ b/test/rubygems/test_gem_resolver_git_set.rb @@ -187,4 +187,3 @@ def test_prefetch_root_dir end end - diff --git a/test/rubygems/test_gem_resolver_git_specification.rb b/test/rubygems/test_gem_resolver_git_specification.rb index 211757eb204aaf..3a9bb6a802dff4 100644 --- a/test/rubygems/test_gem_resolver_git_specification.rb +++ b/test/rubygems/test_gem_resolver_git_specification.rb @@ -111,4 +111,3 @@ def test_install_installed end end - diff --git a/test/rubygems/test_gem_resolver_index_set.rb b/test/rubygems/test_gem_resolver_index_set.rb index 0f18572904c03f..d9a30d0f5865cb 100644 --- a/test/rubygems/test_gem_resolver_index_set.rb +++ b/test/rubygems/test_gem_resolver_index_set.rb @@ -87,4 +87,3 @@ def test_find_all_prerelease end end - diff --git a/test/rubygems/test_gem_resolver_index_specification.rb b/test/rubygems/test_gem_resolver_index_specification.rb index 656589275087d5..3768dd9bae97fc 100644 --- a/test/rubygems/test_gem_resolver_index_specification.rb +++ b/test/rubygems/test_gem_resolver_index_specification.rb @@ -87,4 +87,3 @@ def test_spec_local end end - diff --git a/test/rubygems/test_gem_resolver_installed_specification.rb b/test/rubygems/test_gem_resolver_installed_specification.rb index 1dfbb054f33cbd..2b54fbc6290031 100644 --- a/test/rubygems/test_gem_resolver_installed_specification.rb +++ b/test/rubygems/test_gem_resolver_installed_specification.rb @@ -47,4 +47,3 @@ def test_installable_platform_eh end - diff --git a/test/rubygems/test_gem_resolver_installer_set.rb b/test/rubygems/test_gem_resolver_installer_set.rb index 9644e0a7a99fad..50350e2d3d03d4 100644 --- a/test/rubygems/test_gem_resolver_installer_set.rb +++ b/test/rubygems/test_gem_resolver_installer_set.rb @@ -255,4 +255,3 @@ def test_remote_equals_remote end end - diff --git a/test/rubygems/test_gem_resolver_local_specification.rb b/test/rubygems/test_gem_resolver_local_specification.rb index d04d1ec8b360a7..82598f51886189 100644 --- a/test/rubygems/test_gem_resolver_local_specification.rb +++ b/test/rubygems/test_gem_resolver_local_specification.rb @@ -43,4 +43,3 @@ def test_installable_platform_eh end end - diff --git a/test/rubygems/test_gem_resolver_lock_set.rb b/test/rubygems/test_gem_resolver_lock_set.rb index 969f13fa8b2bb6..d5258477d7c1b3 100644 --- a/test/rubygems/test_gem_resolver_lock_set.rb +++ b/test/rubygems/test_gem_resolver_lock_set.rb @@ -61,4 +61,3 @@ def test_prefetch end end - diff --git a/test/rubygems/test_gem_resolver_lock_specification.rb b/test/rubygems/test_gem_resolver_lock_specification.rb index 3105ad65b33a5e..7b9b0ac8f7f1d0 100644 --- a/test/rubygems/test_gem_resolver_lock_specification.rb +++ b/test/rubygems/test_gem_resolver_lock_specification.rb @@ -97,4 +97,3 @@ def test_spec_loaded end end - diff --git a/test/rubygems/test_gem_resolver_requirement_list.rb b/test/rubygems/test_gem_resolver_requirement_list.rb index 6361dc32e6bcfc..4cbb9391998b96 100644 --- a/test/rubygems/test_gem_resolver_requirement_list.rb +++ b/test/rubygems/test_gem_resolver_requirement_list.rb @@ -18,4 +18,3 @@ def test_each end end - diff --git a/test/rubygems/test_gem_resolver_specification.rb b/test/rubygems/test_gem_resolver_specification.rb index 50ac7ec3a18b0c..c184cea35278a5 100644 --- a/test/rubygems/test_gem_resolver_specification.rb +++ b/test/rubygems/test_gem_resolver_specification.rb @@ -7,7 +7,7 @@ class TestSpec < Gem::Resolver::Specification attr_writer :source attr_reader :spec - def initialize spec + def initialize(spec) super() @spec = spec @@ -62,4 +62,3 @@ def test_source end end - diff --git a/test/rubygems/test_gem_resolver_vendor_set.rb b/test/rubygems/test_gem_resolver_vendor_set.rb index c5b9ad019f806a..3fc79fec16e742 100644 --- a/test/rubygems/test_gem_resolver_vendor_set.rb +++ b/test/rubygems/test_gem_resolver_vendor_set.rb @@ -81,4 +81,3 @@ def test_load_spec end end - diff --git a/test/rubygems/test_gem_resolver_vendor_specification.rb b/test/rubygems/test_gem_resolver_vendor_specification.rb index 1f9337c3fa50c9..315ce05539b98e 100644 --- a/test/rubygems/test_gem_resolver_vendor_specification.rb +++ b/test/rubygems/test_gem_resolver_vendor_specification.rb @@ -81,4 +81,3 @@ def test_version end end - diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index 962f858b5bf194..b5a887abb80923 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -2,7 +2,7 @@ require 'rubygems/test_case' require 'rubygems/security' -unless defined?(OpenSSL::SSL) then +unless defined?(OpenSSL::SSL) warn 'Skipping Gem::Security tests. openssl not found.' end @@ -308,4 +308,3 @@ def test_class_write_encrypted_cipher end end if defined?(OpenSSL::SSL) - diff --git a/test/rubygems/test_gem_security_policy.rb b/test/rubygems/test_gem_security_policy.rb index 6cd032baf511e8..0012d188cfbefc 100644 --- a/test/rubygems/test_gem_security_policy.rb +++ b/test/rubygems/test_gem_security_policy.rb @@ -3,7 +3,7 @@ require 'rubygems/test_case' -unless defined?(OpenSSL::SSL) then +unless defined?(OpenSSL::SSL) warn 'Skipping Gem::Security::Policy tests. openssl not found.' end @@ -518,17 +518,17 @@ def s.full_name() 'metadata.gz' end end end - def digest data + def digest(data) digester = @digest.new digester << data digester end - def sign data, key = PRIVATE_KEY + def sign(data, key = PRIVATE_KEY) key.sign @digest.new, data.digest end - def dummy_signatures key = PRIVATE_KEY + def dummy_signatures(key = PRIVATE_KEY) data = digest 'hello' digests = { Gem::Security::DIGEST_NAME => { 0 => data } } @@ -538,4 +538,3 @@ def dummy_signatures key = PRIVATE_KEY end end if defined?(OpenSSL::SSL) - diff --git a/test/rubygems/test_gem_security_signer.rb b/test/rubygems/test_gem_security_signer.rb index 8a0b7cfde9066a..631afa87c4d5ea 100644 --- a/test/rubygems/test_gem_security_signer.rb +++ b/test/rubygems/test_gem_security_signer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rubygems/test_case' -unless defined?(OpenSSL::SSL) then +unless defined?(OpenSSL::SSL) warn 'Skipping Gem::Security::Signer tests. openssl not found.' end @@ -216,4 +216,3 @@ def test_sign_no_certs end end if defined?(OpenSSL::SSL) - diff --git a/test/rubygems/test_gem_security_trust_dir.rb b/test/rubygems/test_gem_security_trust_dir.rb index ab02ceb772e8f8..f2b7b006dd57cc 100644 --- a/test/rubygems/test_gem_security_trust_dir.rb +++ b/test/rubygems/test_gem_security_trust_dir.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rubygems/test_case' -unless defined?(OpenSSL::SSL) then +unless defined?(OpenSSL::SSL) warn 'Skipping Gem::Security::TrustDir tests. openssl not found.' end @@ -98,4 +98,3 @@ def test_verify_wrong_permissions end end if defined?(OpenSSL::SSL) - diff --git a/test/rubygems/test_gem_source.rb b/test/rubygems/test_gem_source.rb index a969aae9edd733..f8cd031c7272a4 100644 --- a/test/rubygems/test_gem_source.rb +++ b/test/rubygems/test_gem_source.rb @@ -236,4 +236,3 @@ def test_update_cache_eh_home_nonexistent end end - diff --git a/test/rubygems/test_gem_source_fetch_problem.rb b/test/rubygems/test_gem_source_fetch_problem.rb index 4a245f25df791b..f3b57e838442ab 100644 --- a/test/rubygems/test_gem_source_fetch_problem.rb +++ b/test/rubygems/test_gem_source_fetch_problem.rb @@ -25,4 +25,3 @@ def test_password_redacted refute_match sf.wordy, 'secret' end end - diff --git a/test/rubygems/test_gem_source_git.rb b/test/rubygems/test_gem_source_git.rb index 8f5d3ee745747f..586e71bae6a7ac 100644 --- a/test/rubygems/test_gem_source_git.rb +++ b/test/rubygems/test_gem_source_git.rb @@ -306,4 +306,3 @@ def test_uri_hash end end - diff --git a/test/rubygems/test_gem_source_installed.rb b/test/rubygems/test_gem_source_installed.rb index 6c7974831fd779..b469f842fc5ae0 100644 --- a/test/rubygems/test_gem_source_installed.rb +++ b/test/rubygems/test_gem_source_installed.rb @@ -34,4 +34,3 @@ def test_spaceship end end - diff --git a/test/rubygems/test_gem_source_lock.rb b/test/rubygems/test_gem_source_lock.rb index 6bd4002b6c9510..6edfb00e1a3f11 100644 --- a/test/rubygems/test_gem_source_lock.rb +++ b/test/rubygems/test_gem_source_lock.rb @@ -112,4 +112,3 @@ def test_uri end end - diff --git a/test/rubygems/test_gem_source_vendor.rb b/test/rubygems/test_gem_source_vendor.rb index 01b20bdd51f39b..3082bb7e68588f 100644 --- a/test/rubygems/test_gem_source_vendor.rb +++ b/test/rubygems/test_gem_source_vendor.rb @@ -29,4 +29,3 @@ def test_spaceship end end - diff --git a/test/rubygems/test_gem_spec_fetcher.rb b/test/rubygems/test_gem_spec_fetcher.rb index 558869fe993e50..99862ce87193de 100644 --- a/test/rubygems/test_gem_spec_fetcher.rb +++ b/test/rubygems/test_gem_spec_fetcher.rb @@ -328,4 +328,3 @@ def test_available_specs_with_bad_source end end - diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index 0a7531af53f58c..4fbb1e73e0bb54 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -1561,7 +1561,7 @@ def test_build_extensions_extensions_dir_unwritable @ext.build_extensions refute_path_exists @ext.extension_dir ensure - unless ($DEBUG or win_platform? or Process.uid.zero?) then + unless ($DEBUG or win_platform? or Process.uid.zero?) FileUtils.chmod 0755, File.join(@ext.base_dir, 'extensions') FileUtils.chmod 0755, @ext.base_dir end @@ -2131,7 +2131,7 @@ class << Gem remove_method :default_ext_dir_for end - def Gem.default_ext_dir_for base_dir + def Gem.default_ext_dir_for(base_dir) '/foo' end @@ -2613,7 +2613,7 @@ def test_validate end end - def x s; s.gsub(/xxx/, ''); end + def x(s); s.gsub(/xxx/, ''); end def w; x "WARxxxNING"; end def t; x "TOxxxDO"; end def f; x "FxxxIXME"; end @@ -2875,7 +2875,7 @@ def test_validate_executables end def test_validate_empty_require_paths - if win_platform? then + if win_platform? skip 'test_validate_empty_require_paths skipped on MS Windows (symlink)' else util_setup_validate @@ -2914,6 +2914,8 @@ def test_validate_files def test_unresolved_specs specification = Gem::Specification.clone + set_orig specification + specification.define_singleton_method(:unresolved_deps) do { b: Gem::Dependency.new("x","1") } end @@ -2937,6 +2939,8 @@ def test_unresolved_specs def test_unresolved_specs_with_versions specification = Gem::Specification.clone + set_orig specification + specification.define_singleton_method(:unresolved_deps) do { b: Gem::Dependency.new("x","1") } end @@ -2963,6 +2967,12 @@ def test_unresolved_specs_with_versions end end + def set_orig(cls) + s_cls = cls.singleton_class + s_cls.send :alias_method, :orig_unresolved_deps , :unresolved_deps + s_cls.send :alias_method, :orig_find_all_by_name, :find_all_by_name + end + def test_validate_files_recursive util_setup_validate FileUtils.touch @a1.file_name @@ -3714,18 +3724,6 @@ def test_load_default_gem assert_equal ["default-2.0.0.0"], Gem::Specification.map(&:full_name) end - def test_detect_bundled_gem_in_old_ruby - util_set_RUBY_VERSION '1.9.3', 551 - - spec = util_spec 'bigdecimal', '1.1.0' do |s| - s.summary = "This bigdecimal is bundled with Ruby" - end - - assert spec.bundled_gem_in_old_ruby? - ensure - util_restore_RUBY_VERSION - end - def util_setup_deps @gem = util_spec "awesome", "1.0" do |awesome| awesome.add_runtime_dependency "bonobo", [] diff --git a/test/rubygems/test_gem_stub_specification.rb b/test/rubygems/test_gem_stub_specification.rb index f9a3a236c0878e..398820694456a8 100644 --- a/test/rubygems/test_gem_stub_specification.rb +++ b/test/rubygems/test_gem_stub_specification.rb @@ -240,7 +240,6 @@ def stub_without_version return stub end - end def stub_with_extension @@ -293,4 +292,3 @@ def stub_without_extension end end - diff --git a/test/rubygems/test_gem_unsatisfiable_dependency_error.rb b/test/rubygems/test_gem_unsatisfiable_dependency_error.rb index 8b9c5604c925e5..e68185ce255161 100644 --- a/test/rubygems/test_gem_unsatisfiable_dependency_error.rb +++ b/test/rubygems/test_gem_unsatisfiable_dependency_error.rb @@ -30,4 +30,3 @@ def test_version end end - diff --git a/test/rubygems/test_gem_uri_formatter.rb b/test/rubygems/test_gem_uri_formatter.rb index c16ce98d23679f..b19bae9939f869 100644 --- a/test/rubygems/test_gem_uri_formatter.rb +++ b/test/rubygems/test_gem_uri_formatter.rb @@ -26,4 +26,3 @@ def test_unescape end end - diff --git a/test/rubygems/test_gem_util.rb b/test/rubygems/test_gem_util.rb index 93171f0813c610..0fd8441d26ef5c 100644 --- a/test/rubygems/test_gem_util.rb +++ b/test/rubygems/test_gem_util.rb @@ -59,4 +59,3 @@ def test_linked_list_find end end - diff --git a/test/rubygems/test_gem_validator.rb b/test/rubygems/test_gem_validator.rb index 5365a1dabb44c7..b9f597b7d3b444 100644 --- a/test/rubygems/test_gem_validator.rb +++ b/test/rubygems/test_gem_validator.rb @@ -43,4 +43,3 @@ def test_alien_default end end - diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index a2572fb6114ef5..939360c7a2e325 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -212,19 +212,19 @@ def test_canonical_segments # Asserts that +version+ is a prerelease. - def assert_prerelease version + def assert_prerelease(version) assert v(version).prerelease?, "#{version} is a prerelease" end # Assert that +expected+ is the "approximate" recommendation for +version+. - def assert_approximate_equal expected, version + def assert_approximate_equal(expected, version) assert_equal expected, v(version).approximate_recommendation end # Assert that the "approximate" recommendation for +version+ satifies +version+. - def assert_approximate_satisfies_itself version + def assert_approximate_satisfies_itself(version) gem_version = v(version) assert Gem::Requirement.new(gem_version.approximate_recommendation).satisfied_by?(gem_version) @@ -232,33 +232,33 @@ def assert_approximate_satisfies_itself version # Assert that bumping the +unbumped+ version yields the +expected+. - def assert_bumped_version_equal expected, unbumped + def assert_bumped_version_equal(expected, unbumped) assert_version_equal expected, v(unbumped).bump end # Assert that +release+ is the correct non-prerelease +version+. - def assert_release_equal release, version + def assert_release_equal(release, version) assert_version_equal release, v(version).release end # Assert that two versions are equal. Handles strings or # Gem::Version instances. - def assert_version_equal expected, actual + def assert_version_equal(expected, actual) assert_equal v(expected), v(actual) assert_equal v(expected).hash, v(actual).hash, "since #{actual} == #{expected}, they must have the same hash" end # Assert that two versions are eql?. Checks both directions. - def assert_version_eql first, second + def assert_version_eql(first, second) first, second = v(first), v(second) assert first.eql?(second), "#{first} is eql? #{second}" assert second.eql?(first), "#{second} is eql? #{first}" end - def assert_less_than left, right + def assert_less_than(left, right) l = v(left) r = v(right) assert l < r, "#{left} not less than #{right}" @@ -266,14 +266,14 @@ def assert_less_than left, right # Refute the assumption that +version+ is a prerelease. - def refute_prerelease version + def refute_prerelease(version) refute v(version).prerelease?, "#{version} is NOT a prerelease" end # Refute the assumption that two versions are eql?. Checks both # directions. - def refute_version_eql first, second + def refute_version_eql(first, second) first, second = v(first), v(second) refute first.eql?(second), "#{first} is NOT eql? #{second}" refute second.eql?(first), "#{second} is NOT eql? #{first}" @@ -281,7 +281,7 @@ def refute_version_eql first, second # Refute the assumption that the two versions are equal?. - def refute_version_equal unexpected, actual + def refute_version_equal(unexpected, actual) refute_equal v(unexpected), v(actual) end end diff --git a/test/rubygems/test_gem_version_option.rb b/test/rubygems/test_gem_version_option.rb index c06c716616f9cb..a680c5154e65f1 100644 --- a/test/rubygems/test_gem_version_option.rb +++ b/test/rubygems/test_gem_version_option.rb @@ -164,4 +164,3 @@ def test_version_option_twice end end - diff --git a/test/rubygems/test_remote_fetch_error.rb b/test/rubygems/test_remote_fetch_error.rb index 6b0f5477d61815..432c48b878d63f 100644 --- a/test/rubygems/test_remote_fetch_error.rb +++ b/test/rubygems/test_remote_fetch_error.rb @@ -18,4 +18,3 @@ def test_to_s assert_equal error.to_s, 'There was an error fetching (https://gemsource.org)' end end - diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index 6af896507a95e9..e3bdc218f7f3f1 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -4,7 +4,7 @@ class TestGemRequire < Gem::TestCase class Latch - def initialize count = 1 + def initialize(count = 1) @count = count @lock = Monitor.new @cv = @lock.new_cond diff --git a/test/socket/test_basicsocket.rb b/test/socket/test_basicsocket.rb index d388b4f0dd6495..c8e9b23f839326 100644 --- a/test/socket/test_basicsocket.rb +++ b/test/socket/test_basicsocket.rb @@ -161,6 +161,7 @@ def test_read_write_nonblock if ssock.respond_to?(:nonblock?) assert_not_predicate(ssock, :nonblock?) assert_not_predicate(csock, :nonblock?) + csock.nonblock = ssock.nonblock = false # Linux may use MSG_DONTWAIT to avoid setting O_NONBLOCK if RUBY_PLATFORM.match?(/linux/) && Socket.const_defined?(:MSG_DONTWAIT) diff --git a/test/testunit/test_parallel.rb b/test/testunit/test_parallel.rb index b11330b46d7606..2e5ad86733dfeb 100644 --- a/test/testunit/test_parallel.rb +++ b/test/testunit/test_parallel.rb @@ -5,7 +5,8 @@ module TestParallel PARALLEL_RB = "#{File.dirname(__FILE__)}/../lib/test/unit/parallel.rb" TESTS = "#{File.dirname(__FILE__)}/tests_for_parallel" - TIMEOUT = RubyVM::MJIT.enabled? ? 100 : 10 # use large timeout for --jit-wait + # use large timeout for --jit-wait + TIMEOUT = EnvUtil.apply_timeout_scale(RubyVM::MJIT.enabled? ? 100 : 10) class TestParallelWorker < Test::Unit::TestCase def setup diff --git a/test/win32ole/test_win32ole_event.rb b/test/win32ole/test_win32ole_event.rb index e23097e2e1871e..a1546da4a28422 100644 --- a/test/win32ole/test_win32ole_event.rb +++ b/test/win32ole/test_win32ole_event.rb @@ -117,7 +117,10 @@ def test_s_new_loop message_loop GC.start end - assert_match(/OnObjectReady/, @event) + + # @event randomly becomes "OnCompleted" here. Try to wait until it matches. + # https://ci.appveyor.com/project/ruby/ruby/builds/19963142/job/8gaxepksa0i3b998 + assert_match_with_retries(/OnObjectReady/, :@event) end def test_on_event @@ -147,6 +150,19 @@ def exec_notification_query_async end raise end + + def assert_match_with_retries(regexp, ivarname) + ivar = instance_variable_get(ivarname) + + tries = 0 + while tries < 5 && !ivar.match(regexp) + $stderr.puts "test_win32ole_event.rb: retrying until #{ivarname} matches #{regexp} (tries: #{tries})..." + sleep(2 ** tries) # sleep at most 31s in total + ivar = instance_variable_get(ivarname) + end + + assert_match(regexp, ivar) + end end end diff --git a/thread.c b/thread.c index be6667fe1b21c6..d76f05aa416f9e 100644 --- a/thread.c +++ b/thread.c @@ -321,9 +321,9 @@ rb_thread_s_debug_set(VALUE self, VALUE val) #endif #ifndef fill_thread_id_str -# define fill_thread_id_string(thid, buf) (void *)(thid) +# define fill_thread_id_string(thid, buf) ((void *)(uintptr_t)(thid)) # define fill_thread_id_str(th) (void)0 -# define thread_id_str(th) ((void *)(th)->thread_id) +# define thread_id_str(th) ((void *)(uintptr_t)(th)->thread_id) # define PRI_THREAD_ID "p" #endif @@ -419,6 +419,7 @@ rb_vm_gvl_destroy(rb_vm_t *vm) if (0) { /* may be held by running threads */ rb_native_mutex_destroy(&vm->waitpid_lock); + rb_native_mutex_destroy(&vm->workqueue_lock); } } @@ -654,23 +655,42 @@ rb_vm_proc_local_ep(VALUE proc) } static void -thread_do_start(rb_thread_t *th, VALUE args) +thread_do_start(rb_thread_t *th) { native_set_thread_name(th); - if (!th->first_func) { + + if (th->invoke_type == thread_invoke_type_proc) { + VALUE args = th->invoke_arg.proc.args; + long args_len = RARRAY_LEN(args); + const VALUE *args_ptr; + VALUE procval = th->invoke_arg.proc.proc; rb_proc_t *proc; - GetProcPtr(th->first_proc, proc); - th->ec->errinfo = Qnil; - th->ec->root_lep = rb_vm_proc_local_ep(th->first_proc); + GetProcPtr(procval, proc); + + th->ec->errinfo = Qnil; + th->ec->root_lep = rb_vm_proc_local_ep(procval); th->ec->root_svar = Qfalse; - EXEC_EVENT_HOOK(th->ec, RUBY_EVENT_THREAD_BEGIN, th->self, 0, 0, 0, Qundef); - th->value = rb_vm_invoke_proc(th->ec, proc, - (int)RARRAY_LEN(args), RARRAY_CONST_PTR(args), - VM_BLOCK_HANDLER_NONE); - EXEC_EVENT_HOOK(th->ec, RUBY_EVENT_THREAD_END, th->self, 0, 0, 0, Qundef); + + EXEC_EVENT_HOOK(th->ec, RUBY_EVENT_THREAD_BEGIN, th->self, 0, 0, 0, Qundef); + + if (args_len < 8) { + /* free proc.args if the length is enough small */ + args_ptr = ALLOCA_N(VALUE, args_len); + MEMCPY((VALUE *)args_ptr, RARRAY_CONST_PTR_TRANSIENT(args), VALUE, args_len); + th->invoke_arg.proc.args = Qnil; + } + else { + args_ptr = RARRAY_CONST_PTR(args); + } + + th->value = rb_vm_invoke_proc(th->ec, proc, + (int)args_len, args_ptr, + VM_BLOCK_HANDLER_NONE); + + EXEC_EVENT_HOOK(th->ec, RUBY_EVENT_THREAD_END, th->self, 0, 0, 0, Qundef); } else { - th->value = (*th->first_func)((void *)args); + th->value = (*th->invoke_arg.func.func)(th->invoke_arg.func.arg); } } @@ -680,7 +700,6 @@ static int thread_start_func_2(rb_thread_t *th, VALUE *stack_start, VALUE *register_stack_start) { enum ruby_tag_type state; - VALUE args = th->first_args; rb_thread_list_t *join_list; rb_thread_t *main_th; VALUE errinfo = Qnil; @@ -703,7 +722,7 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start, VALUE *register_stack_s EC_PUSH_TAG(th->ec); if ((state = EC_EXEC_TAG()) == TAG_NONE) { - SAVE_ROOT_JMPBUF(th, thread_do_start(th, args)); + SAVE_ROOT_JMPBUF(th, thread_do_start(th)); } else { errinfo = th->ec->errinfo; @@ -793,10 +812,16 @@ thread_create_core(VALUE thval, VALUE args, VALUE (*fn)(ANYARGS)) "can't start a new thread (frozen ThreadGroup)"); } - /* setup thread environment */ - th->first_func = fn; - th->first_proc = fn ? Qfalse : rb_block_proc(); - th->first_args = args; /* GC: shouldn't put before above line */ + if (fn) { + th->invoke_type = thread_invoke_type_func; + th->invoke_arg.func.func = fn; + th->invoke_arg.func.arg = (void *)args; + } + else { + th->invoke_type = thread_invoke_type_proc; + th->invoke_arg.proc.proc = rb_block_proc(); + th->invoke_arg.proc.args = args; + } th->priority = current_th->priority; th->thgroup = current_th->thgroup; @@ -818,7 +843,7 @@ thread_create_core(VALUE thval, VALUE args, VALUE (*fn)(ANYARGS)) return thval; } -#define threadptr_initialized(th) ((th)->first_args != 0) +#define threadptr_initialized(th) ((th)->invoke_type != thread_invoke_type_none) /* * call-seq: @@ -874,6 +899,17 @@ thread_start(VALUE klass, VALUE args) return thread_create_core(rb_thread_alloc(klass), args, 0); } +static VALUE +threadptr_invoke_proc_location(rb_thread_t *th) +{ + if (th->invoke_type == thread_invoke_type_proc) { + return rb_proc_location(th->invoke_arg.proc.proc); + } + else { + return Qnil; + } +} + /* :nodoc: */ static VALUE thread_initialize(VALUE thread, VALUE args) @@ -881,19 +917,21 @@ thread_initialize(VALUE thread, VALUE args) rb_thread_t *th = rb_thread_ptr(thread); if (!rb_block_given_p()) { - rb_raise(rb_eThreadError, "must be called with a block"); - } - else if (th->first_args) { - VALUE proc = th->first_proc, loc; - if (!proc || !RTEST(loc = rb_proc_location(proc))) { - rb_raise(rb_eThreadError, "already initialized thread"); - } - rb_raise(rb_eThreadError, - "already initialized thread - %"PRIsVALUE":%"PRIsVALUE, - RARRAY_AREF(loc, 0), RARRAY_AREF(loc, 1)); + rb_raise(rb_eThreadError, "must be called with a block"); + } + else if (th->invoke_type != thread_invoke_type_none) { + VALUE loc = threadptr_invoke_proc_location(th); + if (!NIL_P(loc)) { + rb_raise(rb_eThreadError, + "already initialized thread - %"PRIsVALUE":%"PRIsVALUE, + RARRAY_AREF(loc, 0), RARRAY_AREF(loc, 1)); + } + else { + rb_raise(rb_eThreadError, "already initialized thread"); + } } else { - return thread_create_core(thread, args, 0); + return thread_create_core(thread, args, NULL); } } @@ -3081,20 +3119,17 @@ rb_thread_to_s(VALUE thread) VALUE cname = rb_class_path(rb_obj_class(thread)); rb_thread_t *target_th = rb_thread_ptr(thread); const char *status; - VALUE str; + VALUE str, loc; status = thread_status_name(target_th, TRUE); str = rb_sprintf("#<%"PRIsVALUE":%p", cname, (void *)thread); if (!NIL_P(target_th->name)) { - rb_str_catf(str, "@%"PRIsVALUE, target_th->name); - } - if (!target_th->first_func && target_th->first_proc) { - VALUE loc = rb_proc_location(target_th->first_proc); - if (!NIL_P(loc)) { - rb_str_catf(str, "@%"PRIsVALUE":%"PRIsVALUE, - RARRAY_AREF(loc, 0), RARRAY_AREF(loc, 1)); - rb_gc_force_recycle(loc); - } + rb_str_catf(str, "@%"PRIsVALUE, target_th->name); + } + if ((loc = threadptr_invoke_proc_location(target_th)) != Qnil) { + rb_str_catf(str, "@%"PRIsVALUE":%"PRIsVALUE, + RARRAY_AREF(loc, 0), RARRAY_AREF(loc, 1)); + rb_gc_force_recycle(loc); } rb_str_catf(str, " %s>", status); OBJ_INFECT(str, thread); @@ -3492,11 +3527,11 @@ rb_thread_variable_p(VALUE thread, VALUE key) locals = rb_ivar_get(thread, id_locals); - if (!RHASH(locals)->ntbl) + if (rb_hash_lookup(locals, ID2SYM(id)) != Qnil) { + return Qtrue; + } + else { return Qfalse; - - if (st_lookup(RHASH(locals)->ntbl, ID2SYM(id), 0)) { - return Qtrue; } return Qfalse; @@ -3970,12 +4005,21 @@ rb_thread_fd_select(int max, rb_fdset_t * read, rb_fdset_t * write, rb_fdset_t * set.th = GET_THREAD(); RUBY_VM_CHECK_INTS_BLOCKING(set.th->ec); set.max = max; - set.sigwait_fd = rb_sigwait_fd_get(set.th); set.rset = read; set.wset = write; set.eset = except; set.timeout = timeout; + if (!set.rset && !set.wset && !set.eset) { + if (!timeout) { + rb_thread_sleep_forever(); + return 0; + } + rb_thread_wait_for(*timeout); + return 0; + } + + set.sigwait_fd = rb_sigwait_fd_get(set.th); if (set.sigwait_fd >= 0) { if (set.rset) rb_fd_set(set.sigwait_fd, set.rset); @@ -3985,15 +4029,6 @@ rb_thread_fd_select(int max, rb_fdset_t * read, rb_fdset_t * write, rb_fdset_t * set.max = set.sigwait_fd + 1; } } - if (!set.rset && !set.wset && !set.eset) { - if (!timeout) { - rb_thread_sleep_forever(); - return 0; - } - rb_thread_wait_for(*timeout); - return 0; - } - #define fd_init_copy(f) do { \ if (set.f) { \ rb_fd_resize(set.max - 1, set.f); \ @@ -4034,52 +4069,63 @@ rb_wait_for_single_fd(int fd, int events, struct timeval *timeout) int result = 0, lerrno; rb_hrtime_t *to, rel, end = 0; int drained; - rb_thread_t *th = GET_THREAD(); nfds_t nfds; rb_unblock_function_t *ubf; + struct waiting_fd wfd; + int state; - RUBY_VM_CHECK_INTS_BLOCKING(th->ec); - timeout_prepare(&to, &rel, &end, timeout); - fds[0].fd = fd; - fds[0].events = (short)events; - do { + wfd.th = GET_THREAD(); + wfd.fd = fd; + list_add(&wfd.th->vm->waiting_fds, &wfd.wfd_node); + EC_PUSH_TAG(wfd.th->ec); + if ((state = EC_EXEC_TAG()) == TAG_NONE) { + RUBY_VM_CHECK_INTS_BLOCKING(wfd.th->ec); + timeout_prepare(&to, &rel, &end, timeout); + fds[0].fd = fd; + fds[0].events = (short)events; fds[0].revents = 0; - fds[1].fd = rb_sigwait_fd_get(th); - - if (fds[1].fd >= 0) { - fds[1].events = POLLIN; - fds[1].revents = 0; - nfds = 2; - ubf = ubf_sigwait; - } - else { - nfds = 1; - ubf = ubf_select; - } + do { + fds[1].fd = rb_sigwait_fd_get(wfd.th); - lerrno = 0; - BLOCKING_REGION(th, { - const rb_hrtime_t *sto; - struct timespec ts; - - sto = sigwait_timeout(th, fds[1].fd, to, &drained); - if (!RUBY_VM_INTERRUPTED(th->ec)) { - result = ppoll(fds, nfds, rb_hrtime2timespec(&ts, sto), NULL); - if (result < 0) lerrno = errno; + if (fds[1].fd >= 0) { + fds[1].events = POLLIN; + fds[1].revents = 0; + nfds = 2; + ubf = ubf_sigwait; + } + else { + nfds = 1; + ubf = ubf_select; } - }, ubf, th, TRUE); - if (fds[1].fd >= 0) { - if (result > 0 && fds[1].revents) { - result--; - fds[1].revents = 0; + lerrno = 0; + BLOCKING_REGION(wfd.th, { + const rb_hrtime_t *sto; + struct timespec ts; + + sto = sigwait_timeout(wfd.th, fds[1].fd, to, &drained); + if (!RUBY_VM_INTERRUPTED(wfd.th->ec)) { + result = ppoll(fds, nfds, rb_hrtime2timespec(&ts, sto), 0); + if (result < 0) lerrno = errno; + } + }, ubf, wfd.th, TRUE); + + if (fds[1].fd >= 0) { + if (result > 0 && fds[1].revents) { + result--; + } + (void)check_signals_nogvl(wfd.th, fds[1].fd); + rb_sigwait_fd_put(wfd.th, fds[1].fd); + rb_sigwait_fd_migrate(wfd.th->vm); } - (void)check_signals_nogvl(th, fds[1].fd); - rb_sigwait_fd_put(th, fds[1].fd); - rb_sigwait_fd_migrate(th->vm); - } - RUBY_VM_CHECK_INTS_BLOCKING(th->ec); - } while (wait_retryable(&result, lerrno, to, end)); + RUBY_VM_CHECK_INTS_BLOCKING(wfd.th->ec); + } while (wait_retryable(&result, lerrno, to, end)); + } + EC_POP_TAG(); + list_del(&wfd.wfd_node); + if (state) { + EC_JUMP_TAG(wfd.th->ec, state); + } if (result < 0) { errno = lerrno; @@ -4118,6 +4164,7 @@ struct select_args { rb_fdset_t *read; rb_fdset_t *write; rb_fdset_t *except; + struct waiting_fd wfd; struct timeval *tv; }; @@ -4148,6 +4195,7 @@ select_single_cleanup(VALUE ptr) { struct select_args *args = (struct select_args *)ptr; + list_del(&args->wfd.wfd_node); if (args->read) rb_fd_term(args->read); if (args->write) rb_fd_term(args->write); if (args->except) rb_fd_term(args->except); @@ -4168,7 +4216,10 @@ rb_wait_for_single_fd(int fd, int events, struct timeval *tv) args.write = (events & RB_WAITFD_OUT) ? init_set_fd(fd, &wfds) : NULL; args.except = (events & RB_WAITFD_PRI) ? init_set_fd(fd, &efds) : NULL; args.tv = tv; + args.wfd.fd = fd; + args.wfd.th = GET_THREAD(); + list_add(&args.wfd.th->vm->waiting_fds, &args.wfd.wfd_node); r = (int)rb_ensure(select_single, ptr, select_single_cleanup, ptr); if (r == -1) errno = args.as.error; @@ -4349,7 +4400,7 @@ rb_clear_coverages(void) { VALUE coverages = rb_get_coverages(); if (RTEST(coverages)) { - st_foreach(rb_hash_tbl_raw(coverages), clear_coverage_i, 0); + rb_hash_foreach(coverages, clear_coverage_i, 0); } } @@ -4372,6 +4423,7 @@ rb_thread_atfork_internal(rb_thread_t *th, void (*atfork)(rb_thread_t *, const r /* may be held by MJIT threads in parent */ rb_native_mutex_initialize(&vm->waitpid_lock); + rb_native_mutex_initialize(&vm->workqueue_lock); /* may be held by any thread in parent */ rb_native_mutex_initialize(&th->interrupt_lock); @@ -4392,7 +4444,6 @@ terminate_atfork_i(rb_thread_t *th, const rb_thread_t *current_th) } } -/* mjit.c */ void rb_fiber_atfork(rb_thread_t *); void rb_thread_atfork(void) @@ -4405,6 +4456,8 @@ rb_thread_atfork(void) /* We don't want reproduce CVE-2003-0900. */ rb_reset_random_seed(); + + /* For child, starting MJIT worker thread in this place which is safer than immediately after `after_fork_ruby`. */ mjit_child_after_fork(); } @@ -5132,6 +5185,7 @@ Init_Thread(void) gvl_init(th->vm); gvl_acquire(th->vm, th); rb_native_mutex_initialize(&th->vm->waitpid_lock); + rb_native_mutex_initialize(&th->vm->workqueue_lock); rb_native_mutex_initialize(&th->interrupt_lock); th->pending_interrupt_queue = rb_ary_tmp_new(0); @@ -5289,7 +5343,7 @@ rb_resolve_me_location(const rb_method_entry_t *me, VALUE resolved_location[5]) break; } case VM_METHOD_TYPE_BMETHOD: { - const rb_iseq_t *iseq = rb_proc_get_iseq(me->def->body.proc, 0); + const rb_iseq_t *iseq = rb_proc_get_iseq(me->def->body.bmethod.proc, 0); if (iseq) { rb_iseq_location_t *loc; rb_iseq_check(iseq); diff --git a/thread_pthread.c b/thread_pthread.c index a8bb4c717868fc..c87e952750de7d 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -1342,11 +1342,17 @@ ubf_select(void *ptr) * in unblock_function_clear. */ if (cur != vm->gvl.timer && cur != sigwait_th) { - rb_native_mutex_lock(&vm->gvl.lock); - if (!vm->gvl.timer) { - rb_thread_wakeup_timer_thread(-1); + /* + * Double-checked locking above was to prevent nested locking + * by the SAME thread. We use trylock here to prevent deadlocks + * between DIFFERENT threads + */ + if (native_mutex_trylock(&vm->gvl.lock) == 0) { + if (!vm->gvl.timer) { + rb_thread_wakeup_timer_thread(-1); + } + rb_native_mutex_unlock(&vm->gvl.lock); } - rb_native_mutex_unlock(&vm->gvl.lock); } ubf_wakeup_thread(th); @@ -1389,11 +1395,13 @@ static int ubf_threads_empty(void) { return 1; } static struct { /* pipes are closed in forked children when owner_process does not match */ int normal[2]; /* [0] == sigwait_fd */ + int ub_main[2]; /* unblock main thread from native_ppoll_sleep */ /* volatile for signal handler use: */ volatile rb_pid_t owner_process; } signal_self_pipe = { {-1, -1}, + {-1, -1}, }; /* only use signal-safe system calls here */ @@ -1605,36 +1613,36 @@ setup_communication_pipe_internal(int pipes[2]) # define SET_CURRENT_THREAD_NAME(name) prctl(PR_SET_NAME, name) #endif +static VALUE threadptr_invoke_proc_location(rb_thread_t *th); + static void native_set_thread_name(rb_thread_t *th) { #ifdef SET_CURRENT_THREAD_NAME - if (!th->first_func && th->first_proc) { - VALUE loc; - if (!NIL_P(loc = th->name)) { - SET_CURRENT_THREAD_NAME(RSTRING_PTR(loc)); - } - else if (!NIL_P(loc = rb_proc_location(th->first_proc))) { - char *name, *p; - char buf[16]; - size_t len; - int n; - - name = RSTRING_PTR(RARRAY_AREF(loc, 0)); - p = strrchr(name, '/'); /* show only the basename of the path. */ - if (p && p[1]) - name = p + 1; - - n = snprintf(buf, sizeof(buf), "%s:%d", name, NUM2INT(RARRAY_AREF(loc, 1))); - rb_gc_force_recycle(loc); /* acts as a GC guard, too */ - - len = (size_t)n; - if (len >= sizeof(buf)) { - buf[sizeof(buf)-2] = '*'; - buf[sizeof(buf)-1] = '\0'; - } - SET_CURRENT_THREAD_NAME(buf); - } + VALUE loc; + if (!NIL_P(loc = th->name)) { + SET_CURRENT_THREAD_NAME(RSTRING_PTR(loc)); + } + else if ((loc = threadptr_invoke_proc_location(th)) != Qnil) { + char *name, *p; + char buf[16]; + size_t len; + int n; + + name = RSTRING_PTR(RARRAY_AREF(loc, 0)); + p = strrchr(name, '/'); /* show only the basename of the path. */ + if (p && p[1]) + name = p + 1; + + n = snprintf(buf, sizeof(buf), "%s:%d", name, NUM2INT(RARRAY_AREF(loc, 1))); + rb_gc_force_recycle(loc); /* acts as a GC guard, too */ + + len = (size_t)n; + if (len >= sizeof(buf)) { + buf[sizeof(buf)-2] = '*'; + buf[sizeof(buf)-1] = '\0'; + } + SET_CURRENT_THREAD_NAME(buf); } #endif } @@ -1711,10 +1719,12 @@ rb_thread_create_timer_thread(void) if (owner && owner != current) { CLOSE_INVALIDATE_PAIR(signal_self_pipe.normal); + CLOSE_INVALIDATE_PAIR(signal_self_pipe.ub_main); ubf_timer_invalidate(); } if (setup_communication_pipe_internal(signal_self_pipe.normal) < 0) return; + if (setup_communication_pipe_internal(signal_self_pipe.ub_main) < 0) return; if (owner != current) { /* validate pipe on this process */ @@ -1832,13 +1842,18 @@ ruby_stack_overflowed_p(const rb_thread_t *th, const void *addr) int rb_reserved_fd_p(int fd) { + /* no false-positive if out-of-FD at startup */ + if (fd < 0) + return 0; + #if UBF_TIMER == UBF_TIMER_PTHREAD if (fd == timer_pthread.low[0] || fd == timer_pthread.low[1]) goto check_pid; #endif if (fd == signal_self_pipe.normal[0] || fd == signal_self_pipe.normal[1]) goto check_pid; - + if (fd == signal_self_pipe.ub_main[0] || fd == signal_self_pipe.ub_main[1]) + goto check_pid; return 0; check_pid: if (signal_self_pipe.owner_process == getpid()) /* async-signal-safe */ @@ -1970,6 +1985,9 @@ rb_sigwait_sleep(rb_thread_t *th, int sigwait_fd, const rb_hrtime_t *rel) * tricky: this needs to return on spurious wakeup (no auto-retry). * But we also need to distinguish between periodic quantum * wakeups, so we care about the result of consume_communication_pipe + * + * We want to avoid spurious wakeup for Mutex#sleep compatibility + * [ruby-core:88102] */ for (;;) { const rb_hrtime_t *sto = sigwait_timeout(th, sigwait_fd, &to, &n); @@ -1986,6 +2004,17 @@ rb_sigwait_sleep(rb_thread_t *th, int sigwait_fd, const rb_hrtime_t *rel) } } +/* + * we need to guarantee wakeups from native_ppoll_sleep because + * ubf_select may not be going through ubf_list if other threads + * are all sleeping. + */ +static void +ubf_ppoll_sleep(void *ignore) +{ + rb_thread_wakeup_timer_thread_fd(signal_self_pipe.ub_main[1]); +} + /* * This function does not exclusively acquire sigwait_fd, so it * cannot safely read from it. However, it can be woken up in @@ -2000,22 +2029,26 @@ static void native_ppoll_sleep(rb_thread_t *th, rb_hrtime_t *rel) { rb_native_mutex_lock(&th->interrupt_lock); - th->unblock.func = ubf_select; - th->unblock.arg = th; + th->unblock.func = ubf_ppoll_sleep; rb_native_mutex_unlock(&th->interrupt_lock); GVL_UNLOCK_BEGIN(th); if (!RUBY_VM_INTERRUPTED(th->ec)) { - struct pollfd pfd; + struct pollfd pfd[2]; struct timespec ts; - pfd.fd = signal_self_pipe.normal[0]; /* sigwait_fd */ - pfd.events = POLLIN; - (void)ppoll(&pfd, 1, rb_hrtime2timespec(&ts, rel), 0); - + pfd[0].fd = signal_self_pipe.normal[0]; /* sigwait_fd */ + pfd[1].fd = signal_self_pipe.ub_main[0]; + pfd[0].events = pfd[1].events = POLLIN; + if (ppoll(pfd, 2, rb_hrtime2timespec(&ts, rel), 0) > 0) { + if (pfd[1].revents & POLLIN) { + (void)consume_communication_pipe(pfd[1].fd); + } + } /* - * do not read the fd, here, let uplevel callers or other threads - * that, otherwise we may steal and starve other threads + * do not read the sigwait_fd, here, let uplevel callers + * or other threads that, otherwise we may steal and starve + * other threads */ } unblock_function_clear(th); diff --git a/time.c b/time.c index 28a50057fbcb3f..21b9fecf8c700e 100644 --- a/time.c +++ b/time.c @@ -38,7 +38,7 @@ static ID id_divmod, id_submicro, id_nano_num, id_nano_den, id_offset, id_zone; static ID id_quo, id_div; static ID id_nanosecond, id_microsecond, id_millisecond, id_nsec, id_usec; -static ID id_local_to_utc, id_utc_to_local; +static ID id_local_to_utc, id_utc_to_local, id_find_timezone; static ID id_year, id_mon, id_mday, id_hour, id_min, id_sec, id_isdst, id_name; #ifndef TM_IS_TIME @@ -642,6 +642,7 @@ static uint32_t obj2subsecx(VALUE obj, VALUE *subsecx); static VALUE time_gmtime(VALUE); static VALUE time_localtime(VALUE); static VALUE time_fixoff(VALUE); +static VALUE time_zonelocal(VALUE time, VALUE off); static time_t timegm_noleapsecond(struct tm *tm); static int tmcmp(struct tm *a, struct tm *b); @@ -2040,6 +2041,13 @@ maybe_tzobj_p(VALUE obj) return TRUE; } +NORETURN(static void invalid_utc_offset(void)); +static void +invalid_utc_offset(void) +{ + rb_raise(rb_eArgError, "\"+HH:MM\" or \"-HH:MM\" expected for utc_offset"); +} + static VALUE utc_offset_arg(VALUE arg) { @@ -2049,7 +2057,7 @@ utc_offset_arg(VALUE arg) char *s = RSTRING_PTR(tmp); if (!rb_enc_str_asciicompat_p(tmp)) { invalid_utc_offset: - rb_raise(rb_eArgError, "\"+HH:MM\" or \"-HH:MM\" expected for utc_offset"); + return Qnil; } switch (RSTRING_LEN(tmp)) { case 9: @@ -2210,6 +2218,14 @@ zone_localtime(VALUE zone, VALUE time) return 1; } +static VALUE +find_timezone(VALUE time, VALUE zone) +{ + VALUE klass = CLASS_OF(time); + + return rb_check_funcall_default(klass, id_find_timezone, 1, &zone, Qnil); +} + static VALUE time_init_1(int argc, VALUE *argv, VALUE time) { @@ -2255,8 +2271,9 @@ time_init_1(int argc, VALUE *argv, VALUE time) vtm.isdst = 0; else if (maybe_tzobj_p(arg)) zone = arg; - else - vtm.utc_offset = utc_offset_arg(arg); + else if (NIL_P(vtm.utc_offset = utc_offset_arg(arg))) + if (NIL_P(zone = find_timezone(time, arg))) + invalid_utc_offset(); } validate_vtm(&vtm); @@ -2272,8 +2289,9 @@ time_init_1(int argc, VALUE *argv, VALUE time) if (zone_timelocal(zone, time)) { return time; } - else { - vtm.utc_offset = utc_offset_arg(zone); + else if (NIL_P(vtm.utc_offset = utc_offset_arg(zone))) { + if (NIL_P(zone = find_timezone(time, zone)) || !zone_timelocal(zone, time)) + invalid_utc_offset(); } } @@ -2472,11 +2490,18 @@ rb_time_num_new(VALUE timev, VALUE off) VALUE time = time_new_timew(rb_cTime, rb_time_magnify(v2w(timev))); if (!NIL_P(off)) { - if (maybe_tzobj_p(off)) { + VALUE zone = off; + + if (maybe_tzobj_p(zone)) { time_gmtime(time); - if (zone_timelocal(off, time)) return time; + if (zone_timelocal(zone, time)) return time; + } + if (NIL_P(off = utc_offset_arg(off))) { + if (NIL_P(zone = find_timezone(time, zone))) invalid_utc_offset(); + time_gmtime(time); + if (!zone_timelocal(zone, time)) invalid_utc_offset(); + return time; } - off = utc_offset_arg(off); validate_utc_offset(off); time_set_utc_offset(time, off); return time; @@ -2645,6 +2670,14 @@ get_scale(VALUE unit) * Time.at(seconds, microseconds, :microsecond) -> time * Time.at(seconds, nanoseconds, :nsec) -> time * Time.at(seconds, nanoseconds, :nanosecond) -> time + * Time.at(time, in: tz) -> time + * Time.at(seconds_with_frac, in: tz) -> time + * Time.at(seconds, microseconds_with_frac, in: tz) -> time + * Time.at(seconds, milliseconds, :millisecond, in: tz) -> time + * Time.at(seconds, microseconds, :usec, in: tz) -> time + * Time.at(seconds, microseconds, :microsecond, in: tz) -> time + * Time.at(seconds, nanoseconds, :nsec, in: tz) -> time + * Time.at(seconds, nanoseconds, :nanosecond, in: tz) -> time * * Creates a new Time object with the value given by +time+, * the given number of +seconds_with_frac+, or @@ -2653,7 +2686,8 @@ get_scale(VALUE unit) * can be an Integer, Float, Rational, or other Numeric. * non-portable feature allows the offset to be negative on some systems. * - * If a numeric argument is given, the result is in local time. + * If +in+ argument is given, the result is in that timezone or UTC offset, or + * if a numeric argument is given, the result is in local time. * * Time.at(0) #=> 1969-12-31 18:00:00 -0600 * Time.at(Time.at(0)) #=> 1969-12-31 18:00:00 -0600 @@ -2667,10 +2701,19 @@ get_scale(VALUE unit) static VALUE time_s_at(int argc, VALUE *argv, VALUE klass) { - VALUE time, t, unit = Qundef; + VALUE time, t, unit = Qundef, zone = Qundef, opts; wideval_t timew; - if (rb_scan_args(argc, argv, "12", &time, &t, &unit) >= 2) { + argc = rb_scan_args(argc, argv, "12:", &time, &t, &unit, &opts); + if (!NIL_P(opts)) { + ID ids[1]; + VALUE vals[numberof(ids)]; + + CONST_ID(ids[0], "in"); + rb_get_kwargs(opts, ids, 0, 1, vals); + zone = vals[0]; + } + if (argc >= 2) { int scale = argc == 3 ? get_scale(unit) : 1000000; time = num_exact(time); t = num_exact(t); @@ -2688,6 +2731,9 @@ time_s_at(int argc, VALUE *argv, VALUE klass) timew = rb_time_magnify(v2w(num_exact(time))); t = time_new_timew(klass, timew); } + if (zone != Qundef) { + time_zonelocal(t, zone); + } return t; } @@ -2876,6 +2922,29 @@ time_arg(int argc, const VALUE *argv, struct vtm *vtm) vtm->mday = obj2ubits(v[2], 5); } + /* normalize month-mday */ + switch (vtm->mon) { + case 2: + { + /* this drops higher bits but it's not a problem to calc leap year */ + unsigned int mday2 = leap_year_v_p(vtm->year) ? 29 : 28; + if (vtm->mday > mday2) { + vtm->mday -= mday2; + vtm->mon++; + } + } + break; + case 4: + case 6: + case 9: + case 11: + if (vtm->mday == 31) { + vtm->mon++; + vtm->mday = 1; + } + break; + } + vtm->hour = NIL_P(v[3])?0:obj2ubits(v[3], 5); vtm->min = NIL_P(v[4])?0:obj2ubits(v[4], 6); @@ -3651,6 +3720,23 @@ time_localtime(VALUE time) return time; } +static VALUE +time_zonelocal(VALUE time, VALUE off) +{ + VALUE zone = off; + if (zone_localtime(zone, time)) return time; + + if (NIL_P(off = utc_offset_arg(off))) { + if (NIL_P(zone = find_timezone(time, zone))) invalid_utc_offset(); + if (!zone_localtime(zone, time)) invalid_utc_offset(); + return time; + } + validate_utc_offset(off); + + time_set_utc_offset(time, off); + return time_fixoff(time); +} + /* * call-seq: * time.localtime -> time @@ -3681,13 +3767,7 @@ time_localtime_m(int argc, VALUE *argv, VALUE time) rb_scan_args(argc, argv, "01", &off); if (!NIL_P(off)) { - if (zone_localtime(off, time)) return time; - - off = utc_offset_arg(off); - validate_utc_offset(off); - - time_set_utc_offset(time, off); - return time_fixoff(time); + return time_zonelocal(time, off); } return time_localtime(time); @@ -3804,12 +3884,18 @@ time_getlocaltime(int argc, VALUE *argv, VALUE time) rb_scan_args(argc, argv, "01", &off); if (!NIL_P(off)) { - if (maybe_tzobj_p(off)) { + VALUE zone = off; + if (maybe_tzobj_p(zone)) { VALUE t = time_dup(time); if (zone_localtime(off, t)) return t; } - off = utc_offset_arg(off); + if (NIL_P(off = utc_offset_arg(off))) { + if (NIL_P(zone = find_timezone(time, zone))) invalid_utc_offset(); + time = time_dup(time); + if (!zone_localtime(zone, time)) invalid_utc_offset(); + return time; + } validate_utc_offset(off); time = time_dup(time); @@ -4879,6 +4965,26 @@ time_dump(int argc, VALUE *argv, VALUE time) return str; } +static VALUE +mload_findzone(VALUE arg) +{ + VALUE *argp = (VALUE *)arg; + VALUE time = argp[0], zone = argp[1]; + return find_timezone(time, zone); +} + +static VALUE +mload_zone(VALUE time, VALUE zone) +{ + VALUE z, args[2]; + args[0] = time; + args[1] = zone; + z = rb_rescue(mload_findzone, (VALUE)args, (VALUE (*)(ANYARGS))NULL, Qnil); + if (NIL_P(z)) return rb_fstring(zone); + if (RB_TYPE_P(z, T_STRING)) return rb_fstring(z); + return z; +} + /* :nodoc: */ static VALUE time_mload(VALUE time, VALUE str) @@ -4993,7 +5099,7 @@ end_submicro: ; time_fixoff(time); } if (!NIL_P(zone)) { - zone = rb_fstring(zone); + zone = mload_zone(time, zone); tobj->vtm.zone = zone; } @@ -5010,7 +5116,8 @@ time_load(VALUE klass, VALUE str) return time; } -/* Document-class: Time::TM +/* :nodoc:*/ +/* Document-class: Time::tm * * A container class for timezone conversion. */ @@ -5018,9 +5125,9 @@ time_load(VALUE klass, VALUE str) /* * call-seq: * - * Time::TM.from_time(t) -> tm + * Time::tm.from_time(t) -> tm * - * Creates new Time::TM object from a Time object. + * Creates new Time::tm object from a Time object. */ static VALUE @@ -5071,9 +5178,9 @@ tm_from_time(VALUE klass, VALUE time) /* * call-seq: * - * Time::TM.new(year, month=nil, day=nil, hour=nil, min=nil, sec=nil, tz=nil) -> tm + * Time::tm.new(year, month=nil, day=nil, hour=nil, min=nil, sec=nil, tz=nil) -> tm * - * Creates new Time::TM object. + * Creates new Time::tm object. */ static VALUE @@ -5173,6 +5280,65 @@ tm_minus(VALUE tm, VALUE offset) } #endif +static VALUE +Init_tm(VALUE outer, const char *name) +{ + /* :stopdoc:*/ + VALUE tm; +#if TM_IS_TIME + tm = rb_define_class_under(outer, name, rb_cObject); + rb_define_alloc_func(tm, time_s_alloc); + rb_define_method(tm, "sec", time_sec, 0); + rb_define_method(tm, "min", time_min, 0); + rb_define_method(tm, "hour", time_hour, 0); + rb_define_method(tm, "mday", time_mday, 0); + rb_define_method(tm, "day", time_mday, 0); + rb_define_method(tm, "mon", time_mon, 0); + rb_define_method(tm, "month", time_mon, 0); + rb_define_method(tm, "year", time_year, 0); + rb_define_method(tm, "isdst", time_isdst, 0); + rb_define_method(tm, "dst?", time_isdst, 0); + rb_define_method(tm, "zone", time_zone, 0); + rb_define_method(tm, "gmtoff", rb_time_utc_offset, 0); + rb_define_method(tm, "gmt_offset", rb_time_utc_offset, 0); + rb_define_method(tm, "utc_offset", rb_time_utc_offset, 0); + rb_define_method(tm, "utc?", time_utc_p, 0); + rb_define_method(tm, "gmt?", time_utc_p, 0); + rb_define_method(tm, "to_s", time_to_s, 0); + rb_define_method(tm, "inspect", time_to_s, 0); + rb_define_method(tm, "to_a", time_to_a, 0); + rb_define_method(tm, "tv_sec", time_to_i, 0); + rb_define_method(tm, "tv_usec", time_usec, 0); + rb_define_method(tm, "usec", time_usec, 0); + rb_define_method(tm, "tv_nsec", time_nsec, 0); + rb_define_method(tm, "nsec", time_nsec, 0); + rb_define_method(tm, "subsec", time_subsec, 0); + rb_define_method(tm, "to_i", time_to_i, 0); + rb_define_method(tm, "to_f", time_to_f, 0); + rb_define_method(tm, "to_r", time_to_r, 0); + rb_define_method(tm, "+", tm_plus, 1); + rb_define_method(tm, "-", tm_minus, 1); +#else + tm = rb_struct_define_under(outer, "tm", + "sec", "min", "hour", + "mday", "mon", "year", + "to_i", NULL); + rb_define_method(tm, "subsec", tm_subsec, 0); + rb_define_method(tm, "utc_offset", tm_utc_offset, 0); + rb_define_method(tm, "to_s", tm_to_s, 0); + rb_define_method(tm, "inspect", tm_to_s, 0); + rb_define_method(tm, "isdst", tm_isdst, 0); + rb_define_method(tm, "dst?", tm_isdst, 0); +#endif + rb_define_method(tm, "initialize", tm_initialize, -1); + rb_define_method(tm, "utc", tm_to_time, 0); + rb_alias(tm, rb_intern("to_time"), rb_intern("utc")); + rb_define_singleton_method(tm, "from_time", tm_from_time, 1); + /* :startdoc:*/ + + return tm; +} + VALUE rb_time_zone_abbreviation(VALUE zone, VALUE time) { @@ -5320,6 +5486,14 @@ rb_time_zone_abbreviation(VALUE zone, VALUE time) * object are not able to dump by Marshal. * * The +abbr+ method is used by '%Z' in #strftime. + * + * === Auto conversion to Timezone + * + * At loading marshaled data, a timezone name will be converted to a timezone + * object by +find_timezone+ class method, if the method is defined. + * + * Similary, that class method will be called when a timezone argument does + * not have the necessary methods mentioned above. */ void @@ -5351,60 +5525,11 @@ Init_Time(void) id_sec = rb_intern("sec"); id_isdst = rb_intern("isdst"); id_name = rb_intern("name"); + id_find_timezone = rb_intern("find_timezone"); rb_cTime = rb_define_class("Time", rb_cObject); rb_include_module(rb_cTime, rb_mComparable); -#if TM_IS_TIME - rb_cTimeTM = rb_define_class_under(rb_cTime, "TM", rb_cObject); - rb_define_alloc_func(rb_cTimeTM, time_s_alloc); - rb_define_method(rb_cTimeTM, "sec", time_sec, 0); - rb_define_method(rb_cTimeTM, "min", time_min, 0); - rb_define_method(rb_cTimeTM, "hour", time_hour, 0); - rb_define_method(rb_cTimeTM, "mday", time_mday, 0); - rb_define_method(rb_cTimeTM, "day", time_mday, 0); - rb_define_method(rb_cTimeTM, "mon", time_mon, 0); - rb_define_method(rb_cTimeTM, "month", time_mon, 0); - rb_define_method(rb_cTimeTM, "year", time_year, 0); - rb_define_method(rb_cTimeTM, "isdst", time_isdst, 0); - rb_define_method(rb_cTimeTM, "dst?", time_isdst, 0); - rb_define_method(rb_cTimeTM, "zone", time_zone, 0); - rb_define_method(rb_cTimeTM, "gmtoff", rb_time_utc_offset, 0); - rb_define_method(rb_cTimeTM, "gmt_offset", rb_time_utc_offset, 0); - rb_define_method(rb_cTimeTM, "utc_offset", rb_time_utc_offset, 0); - rb_define_method(rb_cTimeTM, "utc?", time_utc_p, 0); - rb_define_method(rb_cTimeTM, "gmt?", time_utc_p, 0); - rb_define_method(rb_cTimeTM, "to_s", time_to_s, 0); - rb_define_method(rb_cTimeTM, "inspect", time_to_s, 0); - rb_define_method(rb_cTimeTM, "to_a", time_to_a, 0); - rb_define_method(rb_cTimeTM, "tv_sec", time_to_i, 0); - rb_define_method(rb_cTimeTM, "tv_usec", time_usec, 0); - rb_define_method(rb_cTimeTM, "usec", time_usec, 0); - rb_define_method(rb_cTimeTM, "tv_nsec", time_nsec, 0); - rb_define_method(rb_cTimeTM, "nsec", time_nsec, 0); - rb_define_method(rb_cTimeTM, "subsec", time_subsec, 0); - rb_define_method(rb_cTimeTM, "to_i", time_to_i, 0); - rb_define_method(rb_cTimeTM, "to_f", time_to_f, 0); - rb_define_method(rb_cTimeTM, "to_r", time_to_r, 0); - rb_define_method(rb_cTimeTM, "+", tm_plus, 1); - rb_define_method(rb_cTimeTM, "-", tm_minus, 1); -#else - rb_cTimeTM = rb_struct_define_under(rb_cTime, "TM", - "sec", "min", "hour", - "mday", "mon", "year", - "to_i", NULL); - rb_define_method(rb_cTimeTM, "subsec", tm_subsec, 0); - rb_define_method(rb_cTimeTM, "utc_offset", tm_utc_offset, 0); - rb_define_method(rb_cTimeTM, "to_s", tm_to_s, 0); - rb_define_method(rb_cTimeTM, "inspect", tm_to_s, 0); - rb_define_method(rb_cTimeTM, "isdst", tm_isdst, 0); - rb_define_method(rb_cTimeTM, "dst?", tm_isdst, 0); -#endif - rb_define_method(rb_cTimeTM, "initialize", tm_initialize, -1); - rb_define_method(rb_cTimeTM, "utc", tm_to_time, 0); - rb_alias(rb_cTimeTM, rb_intern("to_time"), rb_intern("utc")); - rb_define_singleton_method(rb_cTimeTM, "from_time", tm_from_time, 1); - rb_define_alloc_func(rb_cTime, time_s_alloc); rb_define_singleton_method(rb_cTime, "now", time_s_now, 0); rb_define_singleton_method(rb_cTime, "at", time_s_at, -1); @@ -5490,4 +5615,6 @@ Init_Time(void) #ifdef DEBUG_FIND_TIME_NUMGUESS rb_define_virtual_variable("$find_time_numguess", find_time_numguess_getter, NULL); #endif + + rb_cTimeTM = Init_tm(rb_cTime, "tm"); } diff --git a/tool/enc-unicode.rb b/tool/enc-unicode.rb index c5c593f617f1a6..f64cecb8817f20 100755 --- a/tool/enc-unicode.rb +++ b/tool/enc-unicode.rb @@ -18,7 +18,11 @@ abort "Usage: #{$0} data_directory emoji_data_directory" end -$unicode_version = File.basename(ARGV[0])[/\A[.\d]+\z/] +pat = /(?:\A|\/)([.\d]+)\z/ +$versions = { + :Unicode => ARGV[0][pat, 1], + :Emoji => ARGV[1][pat, 1], +} POSIX_NAMES = %w[NEWLINE Alpha Blank Cntrl Digit Graph Lower Print XPosixPunct Space Upper XDigit Word Alnum ASCII Punct] @@ -307,18 +311,30 @@ def get_file(name) def data_foreach(name, &block) fn = get_file(name) warn "Reading #{name}" - pat = /^# #{File.basename(name).sub(/\./, '-([\\d.]+)\\.')}/ + if /^emoji-/ =~ name + sep = "" + pat = /^# #{Regexp.quote(File.basename(name))}.*^# Version: ([\d.]+)/m + type = :Emoji + else + sep = "\n" + pat = /^# #{File.basename(name).sub(/\./, '-([\\d.]+)\\.')}/ + type = :Unicode + end File.open(fn, 'rb') do |f| - line = f.gets - unless /^emoji-/ =~ name - unless pat =~ line - raise ArgumentError, "#{name}: no Unicode version" - end - if !$unicode_version - $unicode_version = $1 - elsif $unicode_version != $1 - raise ArgumentError, "#{name}: Unicode version mismatch: #$1" - end + line = f.gets(sep) + unless version = line[pat, 1] + raise ArgumentError, <<-ERROR +#{name}: no #{type} version +#{line.gsub(/^/, '> ')} + ERROR + end + if !(v = $versions[type]) + $versions[type] = version + elsif v != version + raise ArgumentError, <<-ERROR +#{name}: #{type} version mismatch: #{version} to #{v} +#{line.gsub(/^/, '> ')} + ERROR end f.each(&block) end @@ -448,9 +464,11 @@ def write(str) }; #define uniname2ctype_offset(str) offsetof(struct uniname2ctype_pool_t, uniname2ctype_pool_##str) -#if !(/*ANSI*/+0) -static const struct uniname2ctype_struct *uniname2ctype_p(const char *, unsigned int); +static const struct uniname2ctype_struct *uniname2ctype_p( +#if !(/*ANSI*/+0) /* if ANSI, old style not to conflict with generated prototype */ + const char *, unsigned int #endif +); %} struct uniname2ctype_struct; %% @@ -508,17 +526,20 @@ def write(str) return -1; } __HEREDOC -versions = $unicode_version.scan(/\d+/) -print("#if defined ONIG_UNICODE_VERSION_STRING && !( \\\n") -%w[MAJOR MINOR TEENY].zip(versions) do |n, v| - print(" ONIG_UNICODE_VERSION_#{n} == #{v} && \\\n") -end -print(" 1)\n") -print("# error ONIG_UNICODE_VERSION_STRING mismatch\n") -print("#endif\n") -print("#define ONIG_UNICODE_VERSION_STRING #{$unicode_version.dump}\n") -%w[MAJOR MINOR TEENY].zip(versions) do |n, v| - print("#define ONIG_UNICODE_VERSION_#{n} #{v}\n") +$versions.each do |type, ver| + name = type == :Unicode ? "ONIG_UNICODE_VERSION" : "ONIG_UNICODE_EMOJI_VERSION" + versions = ver.scan(/\d+/) + print("#if defined #{name}_STRING && !( \\\n") + versions.zip(%w[MAJOR MINOR TEENY]) do |v, n| + print(" #{name}_#{n} == #{v} && \\\n") + end + print(" 1)\n") + print("# error #{name}_STRING mismatch\n") + print("#endif\n") + print("#define #{name}_STRING #{ver.dump}\n") + versions.zip(%w[MAJOR MINOR TEENY]) do |v, n| + print("#define #{name}_#{n} #{v}\n") + end end output.restore diff --git a/tool/fake.rb b/tool/fake.rb index dc8cf83e69976c..42174052e21914 100644 --- a/tool/fake.rb +++ b/tool/fake.rb @@ -12,20 +12,9 @@ class File static = !!(defined?($static) && $static) $:.unshift(builddir) posthook = proc do - config = RbConfig::CONFIG - mkconfig = RbConfig::MAKEFILE_CONFIG - [ - ["top_srcdir", $top_srcdir], - ["topdir", $topdir], - ].each do |var, val| - next unless val - mkconfig[var] = config[var] = val - t = /\A#{Regexp.quote(val)}(?=\/)/ - $hdrdir.sub!(t) {"$(#{var})"} - mkconfig.keys.grep(/dir\z/) do |k| - mkconfig[k] = "$(#{var})#$'" if t =~ mkconfig[k] - end - end + RbConfig.fire_update!("top_srcdir", $top_srcdir) + RbConfig.fire_update!("topdir", $topdir) + $hdrdir.sub!(/\A#{Regexp.quote($top_srcdir)}(?=\/)/, "$(top_srcdir)") if $extmk $ruby = "$(topdir)/miniruby -I'$(topdir)' -I'$(top_srcdir)/lib' -I'$(extout)/$(arch)' -I'$(extout)/common'" else @@ -54,16 +43,14 @@ class File $extout_prefix = '$(extout)$(target_prefix)/' config = RbConfig::CONFIG mkconfig = RbConfig::MAKEFILE_CONFIG - mkconfig["builddir"] = config["builddir"] = builddir - mkconfig["buildlibdir"] = config["buildlibdir"] = builddir - mkconfig["libdir"] = config["libdir"] = builddir - mkconfig["top_srcdir"] = $top_srcdir if $top_srcdir - mkconfig["extout"] ||= $extout - config["top_srcdir"] = File.expand_path($top_srcdir ||= top_srcdir) - config["rubyhdrdir"] = join[$top_srcdir, "include"] - config["rubyarchhdrdir"] = join[builddir, config["EXTOUT"], "include", config["arch"]] - config["extout"] ||= join[$topdir, ".ext"] - mkconfig["libdirname"] = "buildlibdir" + RbConfig.fire_update!("builddir", builddir) + RbConfig.fire_update!("buildlibdir", builddir) + RbConfig.fire_update!("libdir", builddir) + RbConfig.fire_update!("top_srcdir", $top_srcdir ||= top_srcdir) + RbConfig.fire_update!("extout", $extout) + RbConfig.fire_update!("rubyhdrdir", "$(top_srcdir)/include") + RbConfig.fire_update!("rubyarchhdrdir", "$(extout)/include/$(arch)") + RbConfig.fire_update!("libdirname", "buildlibdir") trace_var(:$ruby, posthook) untrace_var(:$extmk, prehook) end diff --git a/tool/m4/_colorize_result_prepare.m4 b/tool/m4/_colorize_result_prepare.m4 index 292155f1130fff..cc2bbaa7039de3 100644 --- a/tool/m4/_colorize_result_prepare.m4 +++ b/tool/m4/_colorize_result_prepare.m4 @@ -1,7 +1,13 @@ # -*- Autoconf -*- AC_DEFUN([_COLORIZE_RESULT_PREPARE], [ msg_checking= msg_result_yes= msg_result_no= msg_result_other= msg_reset= - AS_IF([test "x${CONFIGURE_TTY}" = xyes -o -t 1], [ + AS_CASE(["x${CONFIGURE_TTY}"], + [xyes|xalways],[configure_tty=1], + [xno|xnever], [configure_tty=0], + [AS_IF([test -t 1], + [configure_tty=1], + [configure_tty=0])]) + AS_IF([test $configure_tty -eq 1], [ msg_begin="`tput smso 2>/dev/null`" AS_CASE(["$msg_begin"], ['@<:@'*m], [msg_begin="`echo "$msg_begin" | sed ['s/[0-9]*m$//']`" diff --git a/tool/m4/ruby_decl_attribute.m4 b/tool/m4/ruby_decl_attribute.m4 index f15947c0abb518..3187b9be606b17 100644 --- a/tool/m4/ruby_decl_attribute.m4 +++ b/tool/m4/ruby_decl_attribute.m4 @@ -29,6 +29,7 @@ ${rbcv_cond+[@%:@define ]attrib[](attrib_params)[ x]} ${rbcv_cond+[@%:@endif]}) $6 @%:@define mesg ("") +@%:@define san "address" attrib[](attrib_params)[;], [], [rbcv="$mac"; break]) done diff --git a/tool/mjit_archflag.sh b/tool/mjit_archflag.sh new file mode 100644 index 00000000000000..082fb4bcd03145 --- /dev/null +++ b/tool/mjit_archflag.sh @@ -0,0 +1,40 @@ +# -*- sh -*- + +quote() { + printf "#${indent}define $1" + shift + ${1+printf} ${1+' "%s"'$sep} ${1+"$@"} + echo +} + +archs="" +arch_flag="" + +parse_arch_flags() { + for arch in $1; do + archs="${archs:+$archs }${arch%=*}" + done + + while shift && [ "$#" -gt 0 ]; do + case "$1" in + -arch) + shift + archs="${archs:+$archs }$1" + ;; + *) + arch_flag="${arch_flag:+${arch_flag} }$1" + ;; + esac + done +} + +define_arch_flags() { + ${archs:+echo} ${archs:+'#if 0'} + for arch in $archs; do + echo "#elif defined __${arch}__" + quote "MJIT_ARCHFLAG " -arch "${arch}" + done + ${archs:+echo} ${archs:+'#else'} + quote "MJIT_ARCHFLAG /* ${arch_flag:-no flag} */" ${arch_flag} + ${archs:+echo} ${archs:+'#endif'} +} diff --git a/tool/mkconfig.rb b/tool/mkconfig.rb index c3d3230d34617c..ec39e5f8bd5158 100755 --- a/tool/mkconfig.rb +++ b/tool/mkconfig.rb @@ -310,6 +310,38 @@ def RbConfig::expand(val, config = CONFIG) RbConfig::expand(val) end + # :nodoc: + # call-seq: + # + # RbConfig.fire_update!(key, val) -> string + # RbConfig.fire_update!(key, val, mkconf, conf) -> string + # + # updates +key+ in +mkconf+ with +val+, and all values depending on + # the +key+ in +mkconf+. + # + # RbConfig::MAKEFILE_CONFIG.values_at("CC", "LDSHARED") # => ["gcc", "$(CC) -shared"] + # RbConfig::CONFIG.values_at("CC", "LDSHARED") # => ["gcc", "gcc -shared"] + # RbConfig.fire_update!("CC", "gcc-8") # => ["CC", "LDSHARED"] + # RbConfig::MAKEFILE_CONFIG.values_at("CC", "LDSHARED") # => ["gcc-8", "$(CC) -shared"] + # RbConfig::CONFIG.values_at("CC", "LDSHARED") # => ["gcc-8", "gcc-8 -shared"] + # + # returns updated keys list, or +nil+ if nothing changed. + def RbConfig.fire_update!(key, val, mkconf = MAKEFILE_CONFIG, conf = CONFIG) + return if (old = mkconf[key]) == val + mkconf[key] = val + keys = [key] + deps = [] + begin + re = Regexp.new("\\\\$\\\\((?:%1$s)\\\\)|\\\\$\\\\{(?:%1$s)\\\\}" % keys.join('|')) + deps |= keys + keys.clear + mkconf.each {|k,v| keys << k if re =~ v} + end until keys.empty? + deps.each {|k| conf[k] = mkconf[k].dup} + deps.each {|k| expand(conf[k])} + deps + end + # call-seq: # # RbConfig.ruby -> path diff --git a/tool/rbinstall.rb b/tool/rbinstall.rb index 6c7fc600027648..13a897cdc5c052 100755 --- a/tool/rbinstall.rb +++ b/tool/rbinstall.rb @@ -342,6 +342,8 @@ def CONFIG.[](name, mandatory = false) arc = CONFIG["LIBRUBY_A", true] load_relative = CONFIG["LIBRUBY_RELATIVE"] == 'yes' +rdoc_noinst = %w[created.rid] + install?(:local, :arch, :bin, :'bin-arch') do prepare "binary commands", bindir @@ -424,13 +426,13 @@ def CONFIG.[](name, mandatory = false) if $rdocdir ridatadir = File.join(CONFIG['ridir'], CONFIG['ruby_version'], "system") prepare "rdoc", ridatadir - install_recursive($rdocdir, ridatadir, :mode => $data_mode) + install_recursive($rdocdir, ridatadir, :no_install => rdoc_noinst, :mode => $data_mode) end end install?(:doc, :html) do if $htmldir prepare "html-docs", docdir - install_recursive($htmldir, docdir+"/html", :mode => $data_mode) + install_recursive($htmldir, docdir+"/html", :no_install => rdoc_noinst, :mode => $data_mode) end end install?(:doc, :capi) do @@ -779,7 +781,7 @@ def install_default_gem(dir, srcdir) makedirs(bin_dir) gemspec.executables.map {|exec| - $script_installer.install(File.join(srcdir, 'bin', exec), + $script_installer.install(File.join(srcdir, 'libexec', exec), File.join(bin_dir, exec)) } end diff --git a/tool/ruby_vm/views/_leaf_helpers.erb b/tool/ruby_vm/views/_leaf_helpers.erb index 00c2e3882a2179..618fbd78dbad03 100644 --- a/tool/ruby_vm/views/_leaf_helpers.erb +++ b/tool/ruby_vm/views/_leaf_helpers.erb @@ -18,7 +18,7 @@ leafness_of_getglobal(VALUE gentry) else { /* We cannot write this function using a switch() because a * case label cannot be a function pointer. */ - static rb_gvar_getter_t *const whitelist[] = { + static rb_gvar_getter_t *const allowlist[] = { rb_gvar_val_getter, rb_gvar_var_getter, rb_gvar_undef_getter, @@ -26,8 +26,8 @@ leafness_of_getglobal(VALUE gentry) rb_gvar_getter_t *f = rb_gvar_getter_function_of(e); int i; - for (i = 0; i < numberof(whitelist); i++) { - if (f == whitelist[i]) { + for (i = 0; i < numberof(allowlist); i++) { + if (f == allowlist[i]) { return true; } } @@ -46,7 +46,7 @@ leafness_of_setglobal(VALUE gentry) else { /* We cannot write this function using a switch() because a * case label cannot be a function pointer. */ - static rb_gvar_setter_t *const whitelist[] = { + static rb_gvar_setter_t *const allowlist[] = { rb_gvar_val_setter, rb_gvar_readonly_setter, rb_gvar_var_setter, @@ -55,8 +55,8 @@ leafness_of_setglobal(VALUE gentry) rb_gvar_setter_t *f = rb_gvar_setter_function_of(e); int i; - for (i = 0; i < numberof(whitelist); i++) { - if (f == whitelist[i]) { + for (i = 0; i < numberof(allowlist); i++) { + if (f == allowlist[i]) { return true; } } diff --git a/tool/ruby_vm/views/_mjit_compile_insn.erb b/tool/ruby_vm/views/_mjit_compile_insn.erb index 203c60476c0ef2..a52c928abf2d45 100644 --- a/tool/ruby_vm/views/_mjit_compile_insn.erb +++ b/tool/ruby_vm/views/_mjit_compile_insn.erb @@ -61,7 +61,7 @@ % % # JIT: We should evaluate ISeq modified for TracePoint if it's enabled. Note: This is slow. % unless insn.always_leaf? - fprintf(f, " if (UNLIKELY(ruby_vm_event_enabled_flags & ISEQ_TRACE_EVENTS)) {\n"); + fprintf(f, " if (UNLIKELY(ruby_vm_event_enabled_global_flags & ISEQ_TRACE_EVENTS)) {\n"); fprintf(f, " reg_cfp->sp = (VALUE *)reg_cfp->bp + %d;\n", b->stack_size + (int)<%= insn.call_attribute('sp_inc') %> + 1); if (!pc_moved_p) { fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", next_pos); diff --git a/tool/ruby_vm/views/_mjit_compile_send.erb b/tool/ruby_vm/views/_mjit_compile_send.erb index 4adbb1f153f01a..40d803af7a2bd4 100644 --- a/tool/ruby_vm/views/_mjit_compile_send.erb +++ b/tool/ruby_vm/views/_mjit_compile_send.erb @@ -76,7 +76,7 @@ fprintf(f, " }\n"); % # JIT: We should evaluate ISeq modified for TracePoint if it's enabled. Note: This is slow. - fprintf(f, " if (UNLIKELY(ruby_vm_event_enabled_flags & ISEQ_TRACE_EVENTS)) {\n"); + fprintf(f, " if (UNLIKELY(ruby_vm_event_enabled_global_flags & ISEQ_TRACE_EVENTS)) {\n"); fprintf(f, " reg_cfp->sp = (VALUE *)reg_cfp->bp + %d;\n", b->stack_size + (int)<%= insn.call_attribute('sp_inc') %> + 1); if (!pc_moved_p) { fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", next_pos); diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index 72cb3b16f9b2cc..bb4fcdd2cc125b 100644 --- a/tool/sync_default_gems.rb +++ b/tool/sync_default_gems.rb @@ -1,6 +1,7 @@ # sync following repositories to ruby repository # # * https://github.com/rubygems/rubygems +# * https://github.com/bundler/bundler # * https://github.com/ruby/rdoc # * https://github.com/flori/json # * https://github.com/ruby/psych @@ -39,6 +40,7 @@ $repositories = { rubygems: 'rubygems/rubygems', + bundler: 'bundler/bundler', rdoc: 'ruby/rdoc', json: 'flori/json', psych: 'ruby/psych', @@ -83,11 +85,21 @@ def sync_default_gems(gem) `rm -rf lib/rubygems* test/rubygems` `cp -r ../../rubygems/rubygems/lib/rubygems* ./lib` `cp -r ../../rubygems/rubygems/test/rubygems ./test` + when "bundler" + `rm -rf lib/bundler* libexec/bundler libexec/bundle libexec/bundle_ruby spec/bundler man/bundle* man/gemfile*` + `cp -r ../../bundler/bundler/lib/bundler* ./lib` + `cp -r ../../bundler/bundler/exe/bundle* ./libexec` + `cp ../../bundler/bundler/bundler.gemspec ./lib` + `cp -r ../../bundler/bundler/spec spec/bundler` + `cp -r ../../bundler/bundler/man/*.{1,5,1\.txt,5\.txt,ronn} ./man` + `rm -rf spec/bundler/support/artifice/vcr_cassettes` when "rdoc" - `rm -rf lib/rdoc* test/rdoc` + `rm -rf lib/rdoc* test/rdoc libexec/rdoc libexec/ri` `cp -rf ../rdoc/lib/rdoc* ./lib` `cp -rf ../rdoc/test test/rdoc` `cp ../rdoc/rdoc.gemspec ./lib/rdoc` + `cp -rf ../rdoc/exe/rdoc ./libexec` + `cp -rf ../rdoc/exe/ri ./libexec` `rm -f lib/rdoc/markdown.kpeg lib/rdoc/markdown/literals.kpeg lib/rdoc/rd/block_parser.ry lib/rdoc/rd/inline_parser.ry` `git checkout lib/rdoc/.document` when "json" diff --git a/tool/transform_mjit_header.rb b/tool/transform_mjit_header.rb index 4784a47299e0d6..5f89446c818391 100644 --- a/tool/transform_mjit_header.rb +++ b/tool/transform_mjit_header.rb @@ -148,8 +148,10 @@ def self.separate_macro_and_code(code) end def self.write(code, out) - FileUtils.mkdir_p(File.dirname(out)) - File.binwrite("#{out}.new", code) + # create with strict permission, then will install proper + # permission + FileUtils.mkdir_p(File.dirname(out), mode: 0700) + File.binwrite("#{out}.new", code, perm: 0600) FileUtils.mv("#{out}.new", out) end diff --git a/tool/update-deps b/tool/update-deps index 7c4d5d33194683..f26cff26d51e61 100755 --- a/tool/update-deps +++ b/tool/update-deps @@ -239,7 +239,7 @@ DEPENDENCIES_SECTION_END_MARK = "\# AUTOGENERATED DEPENDENCIES END\n" def init_global ENV['LC_ALL'] = 'C' - if mkflag = ENV['GNUMAKEFLAGS'] and mkflag.sub!(/(\A|\s+)-j\d*(?=\s+|\z)/, '') + if mkflag0 = ENV['GNUMAKEFLAGS'] and (mkflag = mkflag0.sub(/(\A|\s+)-j\d*(?=\s+|\z)/, '')) != mkflag0 mkflag.strip! ENV['GNUMAKEFLAGS'] = mkflag end diff --git a/tool/vcs.rb b/tool/vcs.rb index 128dfe592e65b1..ca9c8964b543b4 100644 --- a/tool/vcs.rb +++ b/tool/vcs.rb @@ -480,10 +480,13 @@ def export_changelog(url, from, to, path) s.sub!(/^git-svn-id: .*@(\d+) .*\n+\z/, '') rev = $1 s.gsub!(/^ {8}/, '') if /^(?! {8}|$)/ !~ s + s.sub!(/\n\n\z/, "\n") if /\A(\d+)-(\d+)-(\d+)/ =~ time date = Time.new($1.to_i, $2.to_i, $3.to_i).strftime("%a, %d %b %Y") end - w.puts "r#{rev} | #{author} | #{time} (#{date}) | #{s.count("\n")} lines\n\n" + lines = s.count("\n") + lines = "#{lines} line#{lines == 1 ? '' : 's'}" + w.puts "r#{rev} | #{author} | #{time} (#{date}) | #{lines}\n\n" w.puts s, sep end end diff --git a/tool/ytab.sed b/tool/ytab.sed index 4b968fbed14e69..f7438077dcc19b 100755 --- a/tool/ytab.sed +++ b/tool/ytab.sed @@ -68,6 +68,7 @@ a\ s/fprintf *(stderr,/YYFPRINTF (p,/g } s/\( YYFPRINTF *(\)yyoutput,/\1p,/ +s/\( YYFPRINTF *(\)yyo,/\1p,/ s/\( YYFPRINTF *(\)stderr,/\1p,/ s/\( YYDPRINTF *((\)stderr,/\1p,/ s/^\([ ]*\)\(yyerror[ ]*([ ]*parser,\)/\1parser_\2/ diff --git a/transcode.c b/transcode.c index 6d31700711e698..94624331309141 100644 --- a/transcode.c +++ b/transcode.c @@ -4084,7 +4084,7 @@ econv_insert_output(VALUE self, VALUE string) } /* - * call-seq + * call-seq: * ec.putback -> string * ec.putback(max_numbytes) -> string * diff --git a/transient_heap.c b/transient_heap.c new file mode 100644 index 00000000000000..aca8875982ab3f --- /dev/null +++ b/transient_heap.c @@ -0,0 +1,872 @@ +/********************************************************************** + + transient_heap.c - implement transient_heap. + + Copyright (C) 2018 Koichi Sasada + +**********************************************************************/ + +#include "ruby/ruby.h" +#include "ruby/debug.h" +#include "vm_debug.h" +#include "gc.h" +#include "internal.h" +#include "ruby_assert.h" +#include "transient_heap.h" +#include "debug_counter.h" + +#if USE_TRANSIENT_HEAP /* USE_TRANSIENT_HEAP */ +/* + * 1: enable assertions + * 2: enable verify all transient heaps + */ +#ifndef TRANSIENT_HEAP_CHECK_MODE +#define TRANSIENT_HEAP_CHECK_MODE 0 +#endif +#define TH_ASSERT(expr) RUBY_ASSERT_MESG_WHEN(TRANSIENT_HEAP_CHECK_MODE > 0, expr, #expr) + +/* + * 1: show events + * 2: show dump at events + * 3: show all operations + */ +#define TRANSIENT_HEAP_DEBUG 0 + +/* For Debug: Provide blocks infinitely. + * This mode generates blocks unlimitedly + * and prohibit access free'ed blocks to check invalid access. + */ +#define TRANSIENT_HEAP_DEBUG_INFINITE_BLOCK 0 + +#if TRANSIENT_HEAP_DEBUG_INFINITE_BLOCK +#include +#include +#endif + +/* For Debug: Prohibit promoting to malloc space. + */ +#define TRANSIENT_HEAP_DEBUG_DONT_PROMOTE 0 + +/* size configuration */ +#define TRANSIENT_HEAP_PROMOTED_DEFAULT_SIZE 1024 + + /* K M */ +#define TRANSIENT_HEAP_BLOCK_SIZE (1024 * 32 ) /* 32KB int16_t */ +#define TRANSIENT_HEAP_TOTAL_SIZE (1024 * 1024 * 32) /* 32 MB */ +#define TRANSIENT_HEAP_ALLOC_MAX (1024 * 2 ) /* 2 KB */ +#define TRANSIENT_HEAP_BLOCK_NUM (TRANSIENT_HEAP_TOTAL_SIZE / TRANSIENT_HEAP_BLOCK_SIZE) + +#define TRANSIENT_HEAP_ALLOC_MAGIC 0xfeab +#define TRANSIENT_HEAP_ALLOC_ALIGN RUBY_ALIGNOF(void *) + +#define TRANSIENT_HEAP_ALLOC_MARKING_LAST -1 +#define TRANSIENT_HEAP_ALLOC_MARKING_FREE -2 + +enum transient_heap_status { + transient_heap_none, + transient_heap_marking, + transient_heap_escaping +}; + +struct transient_heap_block { + struct transient_heap_block_header { + int16_t size; /* sizeof(block) = TRANSIENT_HEAP_BLOCK_SIZE - sizeof(struct transient_heap_block_header) */ + int16_t index; + int16_t last_marked_index; + int16_t objects; + struct transient_heap_block *next_block; + } info; + char buff[TRANSIENT_HEAP_BLOCK_SIZE - sizeof(struct transient_heap_block_header)]; +}; + +struct transient_heap { + struct transient_heap_block *using_blocks; + struct transient_heap_block *marked_blocks; + struct transient_heap_block *free_blocks; + int total_objects; + int total_marked_objects; + int total_blocks; + enum transient_heap_status status; + + VALUE *promoted_objects; + int promoted_objects_size; + int promoted_objects_index; + + struct transient_heap_block *arena; + int arena_index; /* increment only */ +}; + +struct transient_alloc_header { + uint16_t magic; + uint16_t size; + int16_t next_marked_index; + int16_t dummy; + VALUE obj; +}; + +static struct transient_heap global_transient_heap; + +static void transient_heap_promote_add(struct transient_heap* theap, VALUE obj); +static const void *transient_heap_ptr(VALUE obj, int error); +static int transient_header_managed_ptr_p(struct transient_heap* theap, const void *ptr); + +#define ROUND_UP(v, a) (((size_t)(v) + (a) - 1) & ~((a) - 1)) + +static void +transient_heap_block_dump(struct transient_heap* theap, struct transient_heap_block *block) +{ + int i=0, n=0; + + while (iinfo.index) { + void *ptr = &block->buff[i]; + struct transient_alloc_header *header = ptr; + fprintf(stderr, "%4d %8d %p size:%4d next:%4d %s\n", n, i, ptr, header->size, header->next_marked_index, rb_obj_info(header->obj)); + i += header->size; + n++; + } +} + +static void +transient_heap_blocks_dump(struct transient_heap* theap, struct transient_heap_block *block, const char *type_str) +{ + while (block) { + fprintf(stderr, "- transient_heap_dump: %s:%p index:%d objects:%d last_marked_index:%d next:%p\n", + type_str, (void *)block, block->info.index, block->info.objects, block->info.last_marked_index, (void *)block->info.next_block); + + transient_heap_block_dump(theap, block); + block = block->info.next_block; + } +} + +static void +transient_heap_dump(struct transient_heap* theap) +{ + fprintf(stderr, "transient_heap_dump objects:%d marked_objects:%d blocks:%d\n", theap->total_objects, theap->total_marked_objects, theap->total_blocks); + transient_heap_blocks_dump(theap, theap->using_blocks, "using_blocks"); + transient_heap_blocks_dump(theap, theap->marked_blocks, "marked_blocks"); + transient_heap_blocks_dump(theap, theap->free_blocks, "free_blocks"); +} + +/* Debug: dump all tarnsient_heap blocks */ +void +rb_transient_heap_dump(void) +{ + transient_heap_dump(&global_transient_heap); +} + +#if TRANSIENT_HEAP_CHECK_MODE >= 2 +static void +transient_heap_ptr_check(struct transient_heap *theap, VALUE obj) +{ + if (obj != Qundef) { + const void *ptr = transient_heap_ptr(obj, FALSE); + TH_ASSERT(ptr == NULL || transient_header_managed_ptr_p(theap, ptr)); + } +} + +static int +transient_heap_block_verify(struct transient_heap *theap, struct transient_heap_block *block) +{ + int i=0, n=0; + struct transient_alloc_header *header; + + while (iinfo.index) { + header = (void *)&block->buff[i]; + TH_ASSERT(header->magic == TRANSIENT_HEAP_ALLOC_MAGIC); + transient_heap_ptr_check(theap, header->obj); + n ++; + i += header->size; + } + TH_ASSERT(block->info.objects == n); + + return n; +} + +static int +transient_heap_blocks_verify(struct transient_heap *theap, struct transient_heap_block *blocks, int *block_num_ptr) +{ + int n = 0; + struct transient_heap_block *block = blocks; + while (block) { + n += transient_heap_block_verify(theap, block); + *block_num_ptr += 1; + block = block->info.next_block; + } + + return n; +} +#endif + +static void +transient_heap_verify(struct transient_heap *theap) +{ +#if TRANSIENT_HEAP_CHECK_MODE >= 2 + int n=0, block_num=0; + + n += transient_heap_blocks_verify(theap, theap->using_blocks, &block_num); + n += transient_heap_blocks_verify(theap, theap->marked_blocks, &block_num); + + TH_ASSERT(n == theap->total_objects); + TH_ASSERT(n >= theap->total_marked_objects); + TH_ASSERT(block_num == theap->total_blocks); +#endif +} + +/* Debug: check assertions for all transient_heap blocks */ +void +rb_transient_heap_verify(void) +{ + transient_heap_verify(&global_transient_heap); +} + +static struct transient_heap* +transient_heap_get(void) +{ + struct transient_heap* theap = &global_transient_heap; + transient_heap_verify(theap); + return theap; +} + +static void +reset_block(struct transient_heap_block *block) +{ + __msan_allocated_memory(block, sizeof block); + block->info.size = TRANSIENT_HEAP_BLOCK_SIZE - sizeof(struct transient_heap_block_header); + block->info.index = 0; + block->info.objects = 0; + block->info.last_marked_index = TRANSIENT_HEAP_ALLOC_MARKING_LAST; + block->info.next_block = NULL; + __asan_poison_memory_region(&block->buff, sizeof block->buff); +} + +static void +connect_to_free_blocks(struct transient_heap *theap, struct transient_heap_block *block) +{ + block->info.next_block = theap->free_blocks; + theap->free_blocks = block; +} + +static void +connect_to_using_blocks(struct transient_heap *theap, struct transient_heap_block *block) +{ + block->info.next_block = theap->using_blocks; + theap->using_blocks = block; +} + +#if 0 +static void +connect_to_marked_blocks(struct transient_heap *theap, struct transient_heap_block *block) +{ + block->info.next_block = theap->marked_blocks; + theap->marked_blocks = block; +} +#endif + +static void +append_to_marked_blocks(struct transient_heap *theap, struct transient_heap_block *append_blocks) +{ + if (theap->marked_blocks) { + struct transient_heap_block *block = theap->marked_blocks, *last_block = NULL; + while (block) { + last_block = block; + block = block->info.next_block; + } + + TH_ASSERT(last_block->info.next_block == NULL); + last_block->info.next_block = append_blocks; + } + else { + theap->marked_blocks = append_blocks; + } +} + +static struct transient_heap_block * +transient_heap_block_alloc(struct transient_heap* theap) +{ + struct transient_heap_block *block; +#if TRANSIENT_HEAP_DEBUG_INFINITE_BLOCK + block = mmap(NULL, TRANSIENT_HEAP_BLOCK_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + if (block == MAP_FAILED) rb_bug("transient_heap_block_alloc: err:%d\n", errno); +#else + if (theap->arena == NULL) { + theap->arena = rb_aligned_malloc(TRANSIENT_HEAP_BLOCK_SIZE, TRANSIENT_HEAP_TOTAL_SIZE); + } + + TH_ASSERT(theap->arena_index < TRANSIENT_HEAP_BLOCK_NUM); + block = &theap->arena[theap->arena_index++]; + TH_ASSERT(((intptr_t)block & (TRANSIENT_HEAP_BLOCK_SIZE - 1)) == 0); +#endif + reset_block(block); + + TH_ASSERT(((intptr_t)block->buff & (TRANSIENT_HEAP_ALLOC_ALIGN-1)) == 0); + if (0) fprintf(stderr, "transient_heap_block_alloc: %4d %p\n", theap->total_blocks, (void *)block); + return block; +} + + +static struct transient_heap_block * +transient_heap_allocatable_block(struct transient_heap* theap) +{ + struct transient_heap_block *block; + +#if TRANSIENT_HEAP_DEBUG_INFINITE_BLOCK + block = transient_heap_block_alloc(theap); + theap->total_blocks++; +#else + /* get one block from free_blocks */ + block = theap->free_blocks; + if (block) { + theap->free_blocks = block->info.next_block; + block->info.next_block = NULL; + theap->total_blocks++; + } +#endif + + return block; +} + +static struct transient_alloc_header * +transient_heap_allocatable_header(struct transient_heap* theap, size_t size) +{ + struct transient_heap_block *block = theap->using_blocks; + + while (block) { + TH_ASSERT(block->info.size >= block->info.index); + + if (block->info.size - block->info.index >= (int32_t)size) { + struct transient_alloc_header *header = (void *)&block->buff[block->info.index]; + block->info.index += size; + block->info.objects++; + return header; + } + else { + block = transient_heap_allocatable_block(theap); + if (block) connect_to_using_blocks(theap, block); + } + } + + return NULL; +} + +void * +rb_transient_heap_alloc(VALUE obj, size_t req_size) +{ + struct transient_heap* theap = transient_heap_get(); + size_t size = ROUND_UP(req_size + sizeof(struct transient_alloc_header), TRANSIENT_HEAP_ALLOC_ALIGN); + + TH_ASSERT(RB_TYPE_P(obj, T_ARRAY) || + RB_TYPE_P(obj, T_OBJECT) || + RB_TYPE_P(obj, T_STRUCT) || + RB_TYPE_P(obj, T_HASH)); /* supported types */ + + if (size > TRANSIENT_HEAP_ALLOC_MAX) { + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "rb_transient_heap_alloc: [too big: %ld] %s\n", (long)size, rb_obj_info(obj)); + return NULL; + } +#if TRANSIENT_HEAP_DEBUG_DONT_PROMOTE == 0 + else if (RB_OBJ_PROMOTED_RAW(obj)) { + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "rb_transient_heap_alloc: [promoted object] %s\n", rb_obj_info(obj)); + return NULL; + } +#else + else if (RBASIC_CLASS(obj) == 0) { + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "rb_transient_heap_alloc: [hidden object] %s\n", rb_obj_info(obj)); + return NULL; + } +#endif + else { + struct transient_alloc_header *header = transient_heap_allocatable_header(theap, size); + if (header) { + void *ptr; + + /* header is poisoned to prevent buffer overflow, should + * unpoison first... */ + unpoison_memory_region(header, sizeof *header, true); + + header->size = size; + header->magic = TRANSIENT_HEAP_ALLOC_MAGIC; + header->next_marked_index = TRANSIENT_HEAP_ALLOC_MARKING_FREE; + header->obj = obj; /* TODO: can we eliminate it? */ + + /* header is fixed; shall poison again */ + poison_memory_region(header, sizeof *header); + ptr = header + 1; + + theap->total_objects++; /* statistics */ + +#if TRANSIENT_HEAP_DEBUG_DONT_PROMOTE + if (RB_OBJ_PROMOTED_RAW(obj)) { + transient_heap_promote_add(theap, obj); + } +#endif + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "rb_transient_heap_alloc: header:%p ptr:%p size:%d obj:%s\n", (void *)header, ptr, (int)size, rb_obj_info(obj)); + + RB_DEBUG_COUNTER_INC(theap_alloc); + + /* ptr is set up; OK to unpoison. */ + unpoison_memory_region(ptr, size, true); + return ptr; + } + else { + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "rb_transient_heap_alloc: [no enough space: %ld] %s\n", (long)size, rb_obj_info(obj)); + RB_DEBUG_COUNTER_INC(theap_alloc_fail); + return NULL; + } + } +} + +void +Init_TransientHeap(void) +{ + int i, block_num; + struct transient_heap* theap = transient_heap_get(); + +#if TRANSIENT_HEAP_DEBUG_INFINITE_BLOCK + block_num = 0; +#else + TH_ASSERT(TRANSIENT_HEAP_BLOCK_SIZE * TRANSIENT_HEAP_BLOCK_NUM == TRANSIENT_HEAP_TOTAL_SIZE); + block_num = TRANSIENT_HEAP_BLOCK_NUM; +#endif + for (i=0; iusing_blocks = transient_heap_allocatable_block(theap); + + theap->promoted_objects_size = TRANSIENT_HEAP_PROMOTED_DEFAULT_SIZE; + theap->promoted_objects_index = 0; + /* should not use ALLOC_N to be free from GC */ + theap->promoted_objects = malloc(sizeof(VALUE) * theap->promoted_objects_size); + if (theap->promoted_objects == NULL) rb_bug("Init_TransientHeap: malloc failed."); +} + +static struct transient_heap_block * +blocks_alloc_header_to_block(struct transient_heap *theap, struct transient_heap_block *blocks, struct transient_alloc_header *header) +{ + struct transient_heap_block *block = blocks; + + while (block) { + if (block->buff <= (char *)header && (char *)header < block->buff + block->info.size) { + return block; + } + block = block->info.next_block; + } + + return NULL; +} + +static struct transient_heap_block * +alloc_header_to_block_verbose(struct transient_heap *theap, struct transient_alloc_header *header) +{ + struct transient_heap_block *block; + + if ((block = blocks_alloc_header_to_block(theap, theap->marked_blocks, header)) != NULL) { + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "alloc_header_to_block: found in marked_blocks\n"); + return block; + } + else if ((block = blocks_alloc_header_to_block(theap, theap->using_blocks, header)) != NULL) { + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "alloc_header_to_block: found in using_blocks\n"); + return block; + } + else { + return NULL; + } +} + +static struct transient_alloc_header * +ptr_to_alloc_header(const void *ptr) +{ + struct transient_alloc_header *header = (void *)ptr; + header -= 1; + return header; +} + +static int +transient_header_managed_ptr_p(struct transient_heap* theap, const void *ptr) +{ + if (alloc_header_to_block_verbose(theap, ptr_to_alloc_header(ptr))) { + return TRUE; + } + else { + return FALSE; + } +} + + +int +rb_transient_heap_managed_ptr_p(const void *ptr) +{ + return transient_header_managed_ptr_p(transient_heap_get(), ptr); +} + +static struct transient_heap_block * +alloc_header_to_block(struct transient_heap *theap, struct transient_alloc_header *header) +{ + struct transient_heap_block *block; +#if TRANSIENT_HEAP_DEBUG_INFINITE_BLOCK + block = alloc_header_to_block_verbose(theap, header); + if (block == NULL) { + transient_heap_dump(theap); + rb_bug("alloc_header_to_block: not found in mark_blocks (%p)\n", header); + } +#else + block = (void *)((intptr_t)header & ~(TRANSIENT_HEAP_BLOCK_SIZE-1)); + TH_ASSERT(block == alloc_header_to_block_verbose(theap, header)); +#endif + return block; +} + +void +rb_transient_heap_mark(VALUE obj, const void *ptr) +{ + struct transient_alloc_header *header = ptr_to_alloc_header(ptr); + unpoison_memory_region(header, sizeof *header, false); + if (header->magic != TRANSIENT_HEAP_ALLOC_MAGIC) rb_bug("rb_transient_heap_mark: wrong header, %s (%p)", rb_obj_info(obj), ptr); + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "rb_transient_heap_mark: %s (%p)\n", rb_obj_info(obj), ptr); + +#if TRANSIENT_HEAP_CHECK_MODE > 0 + { + struct transient_heap* theap = transient_heap_get(); + TH_ASSERT(theap->status == transient_heap_marking); + TH_ASSERT(transient_header_managed_ptr_p(theap, ptr)); + + if (header->magic != TRANSIENT_HEAP_ALLOC_MAGIC) { + transient_heap_dump(theap); + rb_bug("rb_transient_heap_mark: magic is broken"); + } + else if (header->obj != obj) { + // transient_heap_dump(theap); + rb_bug("rb_transient_heap_mark: unmatch (%s is stored, but %s is given)\n", + rb_obj_info(header->obj), rb_obj_info(obj)); + } + } +#endif + + if (header->next_marked_index != TRANSIENT_HEAP_ALLOC_MARKING_FREE) { + /* already marked */ + return; + } + else { + struct transient_heap* theap = transient_heap_get(); + struct transient_heap_block *block = alloc_header_to_block(theap, header); + __asan_unpoison_memory_region(&block->info, sizeof block->info); + header->next_marked_index = block->info.last_marked_index; + block->info.last_marked_index = (int)((char *)header - block->buff); + theap->total_marked_objects++; + + transient_heap_verify(theap); + } +} + +static const void * +transient_heap_ptr(VALUE obj, int error) +{ + const void *ptr = NULL; + + switch (BUILTIN_TYPE(obj)) { + case T_ARRAY: + if (RARRAY_TRANSIENT_P(obj)) { + TH_ASSERT(!FL_TEST_RAW(obj, RARRAY_EMBED_FLAG)); + ptr = RARRAY(obj)->as.heap.ptr; + } + break; + case T_OBJECT: + if (ROBJ_TRANSIENT_P(obj)) { + ptr = ROBJECT_IVPTR(obj); + } + break; + case T_STRUCT: + if (RSTRUCT_TRANSIENT_P(obj)) { + ptr = rb_struct_const_heap_ptr(obj); + } + break; + case T_HASH: + if (RHASH_TRANSIENT_P(obj)) { + TH_ASSERT(RHASH_ARRAY_P(obj)); + ptr = (VALUE *)(RHASH(obj)->as.li); + } + else { + ptr = NULL; + } + break; + default: + if (error) { + rb_bug("transient_heap_ptr: unknown obj %s\n", rb_obj_info(obj)); + } + } + + return ptr; +} + +static void +transient_heap_promote_add(struct transient_heap* theap, VALUE obj) +{ + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, "rb_transient_heap_promote: %s\n", rb_obj_info(obj)); + + if (TRANSIENT_HEAP_DEBUG_DONT_PROMOTE) { + /* duplicate check */ + int i; + for (i=0; ipromoted_objects_index; i++) { + if (theap->promoted_objects[i] == obj) return; + } + } + + if (theap->promoted_objects_size <= theap->promoted_objects_index) { + theap->promoted_objects_size *= 2; + if (TRANSIENT_HEAP_DEBUG >= 1) fprintf(stderr, "rb_transient_heap_promote: expand table to %d\n", theap->promoted_objects_size); + theap->promoted_objects = realloc(theap->promoted_objects, theap->promoted_objects_size * sizeof(VALUE)); + if (theap->promoted_objects == NULL) rb_bug("rb_transient_heap_promote: realloc failed"); + } + theap->promoted_objects[theap->promoted_objects_index++] = obj; +} + +void +rb_transient_heap_promote(VALUE obj) +{ + if (transient_heap_ptr(obj, FALSE)) { + struct transient_heap* theap = transient_heap_get(); + transient_heap_promote_add(theap, obj); + } + else { + /* ignore */ + } +} + +static struct transient_alloc_header * +alloc_header(struct transient_heap_block* block, int index) +{ + return (void *)&block->buff[index]; +} + +static void +transient_heap_reset(void) +{ + struct transient_heap* theap = transient_heap_get(); + struct transient_heap_block* block; + + if (TRANSIENT_HEAP_DEBUG >= 1) fprintf(stderr, "!! transient_heap_reset\n"); + + block = theap->marked_blocks; + while (block) { + struct transient_heap_block *next_block = block->info.next_block; + theap->total_objects -= block->info.objects; +#if TRANSIENT_HEAP_DEBUG_INFINITE_BLOCK + if (madvise(block, TRANSIENT_HEAP_BLOCK_SIZE, MADV_DONTNEED) != 0) { + rb_bug("madvise err:%d", errno); + } + if (mprotect(block, TRANSIENT_HEAP_BLOCK_SIZE, PROT_NONE) != 0) { + rb_bug("mprotect err:%d", errno); + } +#else + reset_block(block); + connect_to_free_blocks(theap, block); +#endif + theap->total_blocks--; + block = next_block; + } + + if (TRANSIENT_HEAP_DEBUG >= 1) fprintf(stderr, "!! transient_heap_reset block_num:%d\n", theap->total_blocks); + + theap->marked_blocks = NULL; + theap->total_marked_objects = 0; +} + +static void +transient_heap_block_evacuate(struct transient_heap* theap, struct transient_heap_block* block) +{ + int marked_index = block->info.last_marked_index; + block->info.last_marked_index = TRANSIENT_HEAP_ALLOC_MARKING_LAST; + + while (marked_index >= 0) { + struct transient_alloc_header *header = alloc_header(block, marked_index); + VALUE obj = header->obj; + TH_ASSERT(header->magic == TRANSIENT_HEAP_ALLOC_MAGIC); + if (header->magic != TRANSIENT_HEAP_ALLOC_MAGIC) rb_bug("rb_transient_heap_mark: wrong header %s\n", rb_obj_info(obj)); + + if (TRANSIENT_HEAP_DEBUG >= 3) fprintf(stderr, " * transient_heap_block_evacuate %p %s\n", (void *)header, rb_obj_info(obj)); + + if (obj != Qnil) { + RB_DEBUG_COUNTER_INC(theap_evacuate); + + switch (BUILTIN_TYPE(obj)) { + case T_ARRAY: + rb_ary_transient_heap_evacuate(obj, !TRANSIENT_HEAP_DEBUG_DONT_PROMOTE); + break; + case T_OBJECT: + rb_obj_transient_heap_evacuate(obj, !TRANSIENT_HEAP_DEBUG_DONT_PROMOTE); + break; + case T_STRUCT: + rb_struct_transient_heap_evacuate(obj, !TRANSIENT_HEAP_DEBUG_DONT_PROMOTE); + break; + case T_HASH: + rb_hash_transient_heap_evacuate(obj, !TRANSIENT_HEAP_DEBUG_DONT_PROMOTE); + break; + default: + rb_bug("unsupporeted: %s\n", rb_obj_info(obj)); + } + header->obj = Qundef; /* for debug */ + } + marked_index = header->next_marked_index; + } +} + +static void +transient_heap_update_status(struct transient_heap* theap, enum transient_heap_status status) +{ + TH_ASSERT(theap->status != status); + theap->status = status; +} + +static void +transient_heap_evacuate(void *dmy) +{ + struct transient_heap* theap = transient_heap_get(); + + if (theap->status == transient_heap_marking) { + if (TRANSIENT_HEAP_DEBUG >= 1) fprintf(stderr, "!! transient_heap_evacuate: skip while transient_heap_marking\n"); + } + else { + VALUE gc_disabled = rb_gc_disable(); + struct transient_heap_block* block; + + if (TRANSIENT_HEAP_DEBUG >= 1) { + int i; + fprintf(stderr, "!! transient_heap_evacuate start total_blocks:%d\n", theap->total_blocks); + if (TRANSIENT_HEAP_DEBUG >= 4) { + for (i=0; ipromoted_objects_index; i++) fprintf(stderr, "%4d %s\n", i, rb_obj_info(theap->promoted_objects[i])); + } + } + if (TRANSIENT_HEAP_DEBUG >= 2) transient_heap_dump(theap); + + TH_ASSERT(theap->status == transient_heap_none); + transient_heap_update_status(theap, transient_heap_escaping); + + /* evacuate from marked blocks */ + block = theap->marked_blocks; + while (block) { + transient_heap_block_evacuate(theap, block); + block = block->info.next_block; + } + + /* evacuate from using blocks + only affect incremental marking */ + block = theap->using_blocks; + while (block) { + transient_heap_block_evacuate(theap, block); + block = block->info.next_block; + } + + /* all objects in marked_objects are escaped. */ + transient_heap_reset(); + + if (TRANSIENT_HEAP_DEBUG > 0) { + fprintf(stderr, "!! transient_heap_evacuate end total_blocks:%d\n", theap->total_blocks); + } + + transient_heap_verify(theap); + transient_heap_update_status(theap, transient_heap_none); + if (gc_disabled != Qtrue) rb_gc_enable(); + } +} + +static void +clear_marked_index(struct transient_heap_block* block) +{ + int marked_index = block->info.last_marked_index; + + while (marked_index != TRANSIENT_HEAP_ALLOC_MARKING_LAST) { + struct transient_alloc_header *header = alloc_header(block, marked_index); + TH_ASSERT(marked_index != TRANSIENT_HEAP_ALLOC_MARKING_FREE); + if (0) fprintf(stderr, "clear_marked_index - block:%p mark_index:%d\n", (void *)block, marked_index); + + marked_index = header->next_marked_index; + header->next_marked_index = TRANSIENT_HEAP_ALLOC_MARKING_FREE; + } + + block->info.last_marked_index = TRANSIENT_HEAP_ALLOC_MARKING_LAST; +} + +static void +blocks_clear_marked_index(struct transient_heap_block* block) +{ + while (block) { + clear_marked_index(block); + block = block->info.next_block; + } +} + +void +rb_transient_heap_start_marking(int full_marking) +{ + struct transient_heap* theap = transient_heap_get(); + + if (TRANSIENT_HEAP_DEBUG >= 1) fprintf(stderr, "!! rb_transient_heap_start_marking objects:%d blocks:%d promtoed:%d full_marking:%d\n", + theap->total_objects, theap->total_blocks, theap->promoted_objects_index, full_marking); + if (TRANSIENT_HEAP_DEBUG >= 2) transient_heap_dump(theap); + + blocks_clear_marked_index(theap->marked_blocks); + blocks_clear_marked_index(theap->using_blocks); + + if (theap->using_blocks) { + if (theap->using_blocks->info.objects > 0) { + append_to_marked_blocks(theap, theap->using_blocks); + theap->using_blocks = NULL; + } + else { + append_to_marked_blocks(theap, theap->using_blocks->info.next_block); + theap->using_blocks->info.next_block = NULL; + } + } + + if (theap->using_blocks == NULL) { + theap->using_blocks = transient_heap_allocatable_block(theap); + } + + TH_ASSERT(theap->status == transient_heap_none); + transient_heap_update_status(theap, transient_heap_marking); + theap->total_marked_objects = 0; + + if (full_marking) { + theap->promoted_objects_index = 0; + } + else { /* mark promoted objects */ + int i; + for (i=0; ipromoted_objects_index; i++) { + VALUE obj = theap->promoted_objects[i]; + const void *ptr = transient_heap_ptr(obj, TRUE); + if (ptr) { + rb_transient_heap_mark(obj, ptr); + } + } + } + + transient_heap_verify(theap); +} + +void +rb_transient_heap_finish_marking(void) +{ + struct transient_heap* theap = transient_heap_get(); + + if (TRANSIENT_HEAP_DEBUG >= 1) fprintf(stderr, "!! rb_transient_heap_finish_marking objects:%d, marked:%d\n", + theap->total_objects, + theap->total_marked_objects); + if (TRANSIENT_HEAP_DEBUG >= 2) transient_heap_dump(theap); + + TH_ASSERT(theap->total_objects >= theap->total_marked_objects); + + TH_ASSERT(theap->status == transient_heap_marking); + transient_heap_update_status(theap, transient_heap_none); + + if (theap->total_marked_objects > 0) { + if (TRANSIENT_HEAP_DEBUG >= 1) fprintf(stderr, "-> rb_transient_heap_finish_marking register escape func.\n"); + rb_postponed_job_register_one(0, transient_heap_evacuate, NULL); + } + else { + transient_heap_reset(); + } + + transient_heap_verify(theap); +} +#endif /* USE_TRANSIENT_HEAP */ diff --git a/transient_heap.h b/transient_heap.h new file mode 100644 index 00000000000000..a34aa1e4f8623a --- /dev/null +++ b/transient_heap.h @@ -0,0 +1,60 @@ +/********************************************************************** + + transient_heap.h - declarations of transient_heap related APIs. + + Copyright (C) 2018 Koichi Sasada + +**********************************************************************/ + +#ifndef RUBY_TRANSIENT_HEAP_H +#define RUBY_TRANSIENT_HEAP_H + +#include "internal.h" + +#if USE_TRANSIENT_HEAP + +/* public API */ + +/* Allocate req_size bytes from transient_heap. + Allocated memories are free-ed when next GC + if this memory is not marked by `rb_transient_heap_mark()`. + */ +void *rb_transient_heap_alloc(VALUE obj, size_t req_size); + +/* If `obj` uses a memory pointed by `ptr` from transient_heap, + you need to call `rb_transient_heap_mark(obj, ptr)` + to assert liveness of `obj` (and ptr). */ +void rb_transient_heap_mark(VALUE obj, const void *ptr); + +/* used by gc.c */ +void rb_transient_heap_promote(VALUE obj); +void rb_transient_heap_start_marking(int full_marking); +void rb_transient_heap_finish_marking(void); + +/* for debug API */ +void rb_transient_heap_dump(void); +void rb_transient_heap_verify(void); +int rb_transient_heap_managed_ptr_p(const void *ptr); + +/* evacuate functions for each type */ +void rb_ary_transient_heap_evacuate(VALUE ary, int promote); +void rb_obj_transient_heap_evacuate(VALUE obj, int promote); +void rb_hash_transient_heap_evacuate(VALUE hash, int promote); +void rb_struct_transient_heap_evacuate(VALUE st, int promote); + +#else /* USE_TRANSIENT_HEAP */ + +#define rb_transient_heap_alloc(o, s) NULL +#define rb_transient_heap_verify() ((void)0) +#define rb_transient_heap_promote(obj) ((void)0) +#define rb_transient_heap_start_marking(full_marking) ((void)0) +#define rb_transient_heap_finish_marking() ((void)0) +#define rb_transient_heap_mark(obj, ptr) ((void)0) + +#define rb_ary_transient_heap_evacuate(x, y) ((void)0) +#define rb_obj_transient_heap_evacuate(x, y) ((void)0) +#define rb_hash_transient_heap_evacuate(x, y) ((void)0) +#define rb_struct_transient_heap_evacuate(x, y) ((void)0) + +#endif /* USE_TRANSIENT_HEAP */ +#endif diff --git a/util.c b/util.c index 0e49e4439db2b5..7c83b50b8b5a7b 100644 --- a/util.c +++ b/util.c @@ -35,8 +35,12 @@ ruby_scan_oct(const char *start, size_t len, size_t *retlen) { register const char *s = start; register unsigned long retval = 0; + size_t i; - while (len-- && *s >= '0' && *s <= '7') { + for (i = 0; i < len; i++) { + if ((s[0] < '0') || ('7' < s[0])) { + break; + } retval <<= 3; retval |= *s++ - '0'; } @@ -50,8 +54,16 @@ ruby_scan_hex(const char *start, size_t len, size_t *retlen) register const char *s = start; register unsigned long retval = 0; const char *tmp; + size_t i = 0; - while (len-- && *s && (tmp = strchr(hexdigit, *s))) { + for (i = 0; i < len; i++) { + if (! s[0]) { + break; + } + tmp = strchr(hexdigit, *s); + if (! tmp) { + break; + } retval <<= 4; retval |= (tmp - hexdigit) & 15; s++; @@ -80,6 +92,7 @@ const signed char ruby_digit36_to_number_table[] = { /*f*/ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, }; +NO_SANITIZE("unsigned-integer-overflow", extern unsigned long ruby_scan_digits(const char *str, ssize_t len, int base, size_t *retlen, int *overflow)); unsigned long ruby_scan_digits(const char *str, ssize_t len, int base, size_t *retlen, int *overflow) { @@ -1528,6 +1541,7 @@ cmp(Bigint *a, Bigint *b) return 0; } +NO_SANITIZE("unsigned-integer-overflow", static Bigint * diff(Bigint *a, Bigint *b)); static Bigint * diff(Bigint *a, Bigint *b) { @@ -2007,6 +2021,7 @@ hexnan(double *rvp, const char **sp) #endif /*No_Hex_NaN*/ #endif /* INFNAN_CHECK */ +NO_SANITIZE("unsigned-integer-overflow", double ruby_strtod(const char *s00, char **se)); double ruby_strtod(const char *s00, char **se) { @@ -2966,6 +2981,7 @@ ruby_strtod(const char *s00, char **se) return sign ? -dval(rv) : dval(rv); } +NO_SANITIZE("unsigned-integer-overflow", static int quorem(Bigint *b, Bigint *S)); static int quorem(Bigint *b, Bigint *S) { diff --git a/variable.c b/variable.c index 1c23173d6c10f1..968d404c05b06b 100644 --- a/variable.c +++ b/variable.c @@ -22,6 +22,7 @@ #include "id_table.h" #include "debug_counter.h" #include "vm_core.h" +#include "transient_heap.h" static struct rb_id_table *rb_global_tbl; static ID autoload, classpath, tmp_classpath, classid; @@ -482,13 +483,25 @@ struct rb_global_variable { struct trace_var *trace; }; -MJIT_FUNC_EXPORTED struct rb_global_entry* -rb_global_entry(ID id) +static struct rb_global_entry* +rb_find_global_entry(ID id) { struct rb_global_entry *entry; VALUE data; if (!rb_id_table_lookup(rb_global_tbl, id, &data)) { + return NULL; + } + entry = (struct rb_global_entry *)data; + ASSUME(entry != NULL); + return entry; +} + +MJIT_FUNC_EXPORTED struct rb_global_entry* +rb_global_entry(ID id) +{ + struct rb_global_entry *entry = rb_find_global_entry(id); + if (!entry) { struct rb_global_variable *var; entry = ALLOC(struct rb_global_entry); var = ALLOC(struct rb_global_variable); @@ -504,9 +517,6 @@ rb_global_entry(ID id) var->trace = 0; rb_id_table_insert(rb_global_tbl, id, (VALUE)entry); } - else { - entry = (struct rb_global_entry *)data; - } return entry; } @@ -609,11 +619,34 @@ global_id(const char *name) if (name[0] == '$') id = rb_intern(name); else { size_t len = strlen(name); - char *buf = ALLOCA_N(char, len+1); + VALUE vbuf = 0; + char *buf = ALLOCV_N(char, vbuf, len+1); buf[0] = '$'; memcpy(buf+1, name, len); id = rb_intern2(buf, len+1); + ALLOCV_END(vbuf); + } + return id; +} + +static ID +find_global_id(const char *name) +{ + ID id; + size_t len = strlen(name); + + if (name[0] == '$') { + id = rb_check_id_cstr(name, len, NULL); + } + else { + VALUE vbuf = 0; + char *buf = ALLOCV_N(char, vbuf, len+1); + buf[0] = '$'; + memcpy(buf+1, name, len); + id = rb_check_id_cstr(buf, len+1, NULL); + ALLOCV_END(vbuf); } + return id; } @@ -855,8 +888,14 @@ VALUE rb_gv_get(const char *name) { struct rb_global_entry *entry; + ID id = find_global_id(name); - entry = rb_global_entry(global_id(name)); + if (!id) { + rb_warning("global variable `%s' not initialized", name); + return Qnil; + } + + entry = rb_global_entry(id); return rb_gvar_get(entry); } @@ -1333,61 +1372,133 @@ generic_ivar_set(VALUE obj, ID id, VALUE val) RB_OBJ_WRITTEN(obj, Qundef, val); } -VALUE -rb_ivar_set(VALUE obj, ID id, VALUE val) +static VALUE * +obj_ivar_heap_alloc(VALUE obj, size_t newsize) +{ + VALUE *newptr = rb_transient_heap_alloc(obj, sizeof(VALUE) * newsize); + + if (newptr != NULL) { + ROBJ_TRANSIENT_SET(obj); + } + else { + ROBJ_TRANSIENT_UNSET(obj); + newptr = ALLOC_N(VALUE, newsize); + } + return newptr; +} + +static VALUE * +obj_ivar_heap_realloc(VALUE obj, int32_t len, size_t newsize) +{ + VALUE *newptr; + int i; + + if (ROBJ_TRANSIENT_P(obj)) { + const VALUE *orig_ptr = ROBJECT(obj)->as.heap.ivptr; + if ((newptr = obj_ivar_heap_alloc(obj, newsize)) != NULL) { + /* ok */ + } + else { + newptr = ALLOC_N(VALUE, newsize); + ROBJ_TRANSIENT_UNSET(obj); + } + ROBJECT(obj)->as.heap.ivptr = newptr; + for (i=0; i<(int)len; i++) { + newptr[i] = orig_ptr[i]; + } + } + else { + REALLOC_N(ROBJECT(obj)->as.heap.ivptr, VALUE, newsize); + newptr = ROBJECT(obj)->as.heap.ivptr; + } + + return newptr; +} + +#if USE_TRANSIENT_HEAP +void +rb_obj_transient_heap_evacuate(VALUE obj, int promote) +{ + if (ROBJ_TRANSIENT_P(obj)) { + uint32_t len = ROBJECT_NUMIV(obj); + const VALUE *old_ptr = ROBJECT_IVPTR(obj); + VALUE *new_ptr; + + if (promote) { + new_ptr = ALLOC_N(VALUE, len); + ROBJ_TRANSIENT_UNSET(obj); + } + else { + new_ptr = obj_ivar_heap_alloc(obj, len); + } + MEMCPY(new_ptr, old_ptr, VALUE, len); + ROBJECT(obj)->as.heap.ivptr = new_ptr; + } +} +#endif + +static VALUE +obj_ivar_set(VALUE obj, ID id, VALUE val) { struct ivar_update ivup; uint32_t i, len; + ivup.iv_extended = 0; + ivup.u.iv_index_tbl = iv_index_tbl_make(obj); + iv_index_tbl_extend(&ivup, id); + len = ROBJECT_NUMIV(obj); + if (len <= ivup.index) { + VALUE *ptr = ROBJECT_IVPTR(obj); + if (ivup.index < ROBJECT_EMBED_LEN_MAX) { + RBASIC(obj)->flags |= ROBJECT_EMBED; + ptr = ROBJECT(obj)->as.ary; + for (i = 0; i < ROBJECT_EMBED_LEN_MAX; i++) { + ptr[i] = Qundef; + } + } + else { + VALUE *newptr; + uint32_t newsize = iv_index_tbl_newsize(&ivup); + + if (RBASIC(obj)->flags & ROBJECT_EMBED) { + newptr = obj_ivar_heap_alloc(obj, newsize); + MEMCPY(newptr, ptr, VALUE, len); + RBASIC(obj)->flags &= ~ROBJECT_EMBED; + ROBJECT(obj)->as.heap.ivptr = newptr; + } + else { + newptr = obj_ivar_heap_realloc(obj, len, newsize); + } + for (; len < newsize; len++) { + newptr[len] = Qundef; + } + ROBJECT(obj)->as.heap.numiv = newsize; + ROBJECT(obj)->as.heap.iv_index_tbl = ivup.u.iv_index_tbl; + } + } + RB_OBJ_WRITE(obj, &ROBJECT_IVPTR(obj)[ivup.index], val); + + return val; +} + +VALUE +rb_ivar_set(VALUE obj, ID id, VALUE val) +{ RB_DEBUG_COUNTER_INC(ivar_set_base); rb_check_frozen(obj); switch (BUILTIN_TYPE(obj)) { case T_OBJECT: - ivup.iv_extended = 0; - ivup.u.iv_index_tbl = iv_index_tbl_make(obj); - iv_index_tbl_extend(&ivup, id); - len = ROBJECT_NUMIV(obj); - if (len <= ivup.index) { - VALUE *ptr = ROBJECT_IVPTR(obj); - if (ivup.index < ROBJECT_EMBED_LEN_MAX) { - RBASIC(obj)->flags |= ROBJECT_EMBED; - ptr = ROBJECT(obj)->as.ary; - for (i = 0; i < ROBJECT_EMBED_LEN_MAX; i++) { - ptr[i] = Qundef; - } - } - else { - VALUE *newptr; - uint32_t newsize = iv_index_tbl_newsize(&ivup); - - if (RBASIC(obj)->flags & ROBJECT_EMBED) { - newptr = ALLOC_N(VALUE, newsize); - MEMCPY(newptr, ptr, VALUE, len); - RBASIC(obj)->flags &= ~ROBJECT_EMBED; - ROBJECT(obj)->as.heap.ivptr = newptr; - } - else { - REALLOC_N(ROBJECT(obj)->as.heap.ivptr, VALUE, newsize); - newptr = ROBJECT(obj)->as.heap.ivptr; - } - for (; len < newsize; len++) - newptr[len] = Qundef; - ROBJECT(obj)->as.heap.numiv = newsize; - ROBJECT(obj)->as.heap.iv_index_tbl = ivup.u.iv_index_tbl; - } - } - RB_OBJ_WRITE(obj, &ROBJECT_IVPTR(obj)[ivup.index], val); - break; + return obj_ivar_set(obj, id, val); case T_CLASS: case T_MODULE: - if (!RCLASS_IV_TBL(obj)) RCLASS_IV_TBL(obj) = st_init_numtable(); - rb_class_ivar_set(obj, id, val); + if (!RCLASS_IV_TBL(obj)) RCLASS_IV_TBL(obj) = st_init_numtable(); + rb_class_ivar_set(obj, id, val); break; default: - generic_ivar_set(obj, id, val); - break; + generic_ivar_set(obj, id, val); + break; } return val; } diff --git a/version.h b/version.h index 402b3a0e07ab00..f9d3c1e72130d4 100644 --- a/version.h +++ b/version.h @@ -3,8 +3,8 @@ #define RUBY_PATCHLEVEL -1 #define RUBY_RELEASE_YEAR 2018 -#define RUBY_RELEASE_MONTH 10 -#define RUBY_RELEASE_DAY 31 +#define RUBY_RELEASE_MONTH 12 +#define RUBY_RELEASE_DAY 1 #include "ruby/version.h" diff --git a/vm.c b/vm.c index f7d4b65f67241e..e86f4650f08531 100644 --- a/vm.c +++ b/vm.c @@ -299,7 +299,9 @@ static void vm_collect_usage_register(int reg, int isset); #endif static VALUE vm_make_env_object(const rb_execution_context_t *ec, rb_control_frame_t *cfp); -extern VALUE rb_vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, VALUE block_handler); +extern VALUE rb_vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, + int argc, const VALUE *argv, VALUE block_handler, + const rb_callable_method_entry_t *me); static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, VALUE block_handler); static VALUE rb_block_param_proxy; @@ -336,8 +338,11 @@ VALUE rb_mRubyVMFrozenCore; VALUE ruby_vm_const_missing_count = 0; rb_vm_t *ruby_current_vm_ptr = NULL; rb_execution_context_t *ruby_current_execution_context_ptr = NULL; + rb_event_flag_t ruby_vm_event_flags; -rb_event_flag_t ruby_vm_event_enabled_flags; +rb_event_flag_t ruby_vm_event_enabled_global_flags; +unsigned int ruby_vm_event_local_num; + rb_serial_t ruby_vm_global_method_state = 1; rb_serial_t ruby_vm_global_constant_state = 1; rb_serial_t ruby_vm_class_serial = 1; @@ -1013,6 +1018,9 @@ invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, co /* bmethod */ int arg_size = iseq->body->param.size; VALUE ret; + rb_hook_list_t *hooks; + + VM_ASSERT(me->def->type == VM_METHOD_TYPE_BMETHOD); vm_push_frame(ec, iseq, type | VM_FRAME_FLAG_BMETHOD, self, VM_GUARDED_PREV_EP(captured->ep), @@ -1024,25 +1032,41 @@ invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, co RUBY_DTRACE_METHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); EXEC_EVENT_HOOK(ec, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, Qnil); + + if (UNLIKELY((hooks = me->def->body.bmethod.hooks) != NULL) && + hooks->events & RUBY_EVENT_CALL) { + rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_CALL, self, + me->def->original_id, me->called_id, me->owner, Qnil, FALSE); + } VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_FINISH); ret = vm_exec(ec, TRUE); + EXEC_EVENT_HOOK(ec, RUBY_EVENT_RETURN, self, me->def->original_id, me->called_id, me->owner, ret); + if ((hooks = me->def->body.bmethod.hooks) != NULL && + hooks->events & RUBY_EVENT_RETURN) { + rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_RETURN, self, + me->def->original_id, me->called_id, me->owner, ret, FALSE); + } RUBY_DTRACE_METHOD_RETURN_HOOK(ec, me->owner, me->def->original_id); return ret; } +ALWAYS_INLINE(static VALUE + invoke_iseq_block_from_c(rb_execution_context_t *ec, const struct rb_captured_block *captured, + VALUE self, int argc, const VALUE *argv, VALUE passed_block_handler, + const rb_cref_t *cref, int is_lambda, const rb_callable_method_entry_t *me)); + static inline VALUE invoke_iseq_block_from_c(rb_execution_context_t *ec, const struct rb_captured_block *captured, VALUE self, int argc, const VALUE *argv, VALUE passed_block_handler, - const rb_cref_t *cref, int is_lambda) + const rb_cref_t *cref, int is_lambda, const rb_callable_method_entry_t *me) { const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq); int i, opt_pc; VALUE type = VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0); rb_control_frame_t *cfp = ec->cfp; VALUE *sp = cfp->sp; - const rb_callable_method_entry_t *me = ec->passed_bmethod_me; - ec->passed_bmethod_me = NULL; + stack_check(ec); CHECK_VM_STACK_OVERFLOW(cfp, argc); @@ -1076,12 +1100,12 @@ invoke_block_from_c_bh(rb_execution_context_t *ec, VALUE block_handler, const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler); return invoke_iseq_block_from_c(ec, captured, captured->self, argc, argv, passed_block_handler, - cref, is_lambda); + cref, is_lambda, NULL); } case block_handler_type_ifunc: return vm_yield_with_cfunc(ec, VM_BH_TO_IFUNC_BLOCK(block_handler), VM_BH_TO_IFUNC_BLOCK(block_handler)->self, - argc, argv, passed_block_handler); + argc, argv, passed_block_handler, NULL); case block_handler_type_symbol: return vm_yield_with_symbol(ec, VM_BH_TO_SYMBOL(block_handler), argc, argv, passed_block_handler); @@ -1139,19 +1163,26 @@ vm_yield_force_blockarg(rb_execution_context_t *ec, VALUE args) VM_BLOCK_HANDLER_NONE, NULL, FALSE, TRUE); } +ALWAYS_INLINE(static VALUE + invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, + VALUE self, int argc, const VALUE *argv, + VALUE passed_block_handler, int is_lambda, + const rb_callable_method_entry_t *me)); + static inline VALUE invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, - VALUE passed_block_handler, int is_lambda) + VALUE passed_block_handler, int is_lambda, + const rb_callable_method_entry_t *me) { const struct rb_block *block = &proc->block; again: switch (vm_block_type(block)) { case block_type_iseq: - return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, passed_block_handler, NULL, is_lambda); + return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, passed_block_handler, NULL, is_lambda, me); case block_type_ifunc: - return vm_yield_with_cfunc(ec, &block->as.captured, self, argc, argv, passed_block_handler); + return vm_yield_with_cfunc(ec, &block->as.captured, self, argc, argv, passed_block_handler, me); case block_type_symbol: return vm_yield_with_symbol(ec, block->as.symbol, argc, argv, passed_block_handler); case block_type_proc: @@ -1167,14 +1198,14 @@ static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, VALUE passed_block_handler) { - return invoke_block_from_c_proc(ec, proc, self, argc, argv, passed_block_handler, proc->is_lambda); + return invoke_block_from_c_proc(ec, proc, self, argc, argv, passed_block_handler, proc->is_lambda, NULL); } MJIT_FUNC_EXPORTED VALUE rb_vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, - int argc, const VALUE *argv, VALUE block_handler) + int argc, const VALUE *argv, VALUE block_handler, const rb_callable_method_entry_t *me) { - return invoke_block_from_c_proc(ec, proc, self, argc, argv, block_handler, TRUE); + return invoke_block_from_c_proc(ec, proc, self, argc, argv, block_handler, TRUE, me); } MJIT_FUNC_EXPORTED VALUE @@ -1185,7 +1216,7 @@ rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, vm_block_handler_verify(passed_block_handler); if (proc->is_from_method) { - return rb_vm_invoke_bmethod(ec, proc, self, argc, argv, passed_block_handler); + return rb_vm_invoke_bmethod(ec, proc, self, argc, argv, passed_block_handler, NULL); } else { return vm_invoke_proc(ec, proc, self, argc, argv, passed_block_handler); @@ -1674,39 +1705,72 @@ frame_name(const rb_control_frame_t *cfp) #endif static void -hook_before_rewind(rb_execution_context_t *ec, const rb_control_frame_t *cfp, int will_finish_vm_exec, int state, struct vm_throw_data *err) +hook_before_rewind(rb_execution_context_t *ec, const rb_control_frame_t *cfp, + int will_finish_vm_exec, int state, struct vm_throw_data *err) { if (state == TAG_RAISE && RBASIC_CLASS(err) == rb_eSysStackError) { return; } - switch (VM_FRAME_TYPE(ec->cfp)) { - case VM_FRAME_MAGIC_METHOD: - RUBY_DTRACE_METHOD_RETURN_HOOK(ec, 0, 0); - EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err)); - THROW_DATA_CONSUMED_SET(err); - break; - case VM_FRAME_MAGIC_BLOCK: - if (VM_FRAME_BMETHOD_P(ec->cfp)) { - EXEC_EVENT_HOOK(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err)); - - if (!will_finish_vm_exec) { - /* kick RUBY_EVENT_RETURN at invoke_block_from_c() for bmethod */ - EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self, - rb_vm_frame_method_entry(ec->cfp)->def->original_id, - rb_vm_frame_method_entry(ec->cfp)->called_id, - rb_vm_frame_method_entry(ec->cfp)->owner, - frame_return_value(err)); - } - THROW_DATA_CONSUMED_SET(err); - } - else { - EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err)); - THROW_DATA_CONSUMED_SET(err); - } - break; - case VM_FRAME_MAGIC_CLASS: - EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_END, ec->cfp->self, 0, 0, 0, Qnil); - break; + else { + const rb_iseq_t *iseq = cfp->iseq; + rb_hook_list_t *local_hooks = iseq->local_hooks; + + switch (VM_FRAME_TYPE(ec->cfp)) { + case VM_FRAME_MAGIC_METHOD: + RUBY_DTRACE_METHOD_RETURN_HOOK(ec, 0, 0); + EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err)); + + if (UNLIKELY(local_hooks && local_hooks->events & RUBY_EVENT_RETURN)) { + rb_exec_event_hook_orig(ec, local_hooks, RUBY_EVENT_RETURN, + ec->cfp->self, 0, 0, 0, frame_return_value(err), TRUE); + } + + THROW_DATA_CONSUMED_SET(err); + break; + case VM_FRAME_MAGIC_BLOCK: + if (VM_FRAME_BMETHOD_P(ec->cfp)) { + EXEC_EVENT_HOOK(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err)); + if (UNLIKELY(local_hooks && local_hooks->events & RUBY_EVENT_B_RETURN)) { + rb_exec_event_hook_orig(ec, local_hooks, RUBY_EVENT_B_RETURN, + ec->cfp->self, 0, 0, 0, frame_return_value(err), FALSE); + } + + if (!will_finish_vm_exec) { + const rb_callable_method_entry_t *me = rb_vm_frame_method_entry(ec->cfp); + + /* kick RUBY_EVENT_RETURN at invoke_block_from_c() for bmethod */ + EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self, + rb_vm_frame_method_entry(ec->cfp)->def->original_id, + rb_vm_frame_method_entry(ec->cfp)->called_id, + rb_vm_frame_method_entry(ec->cfp)->owner, + frame_return_value(err)); + + VM_ASSERT(me->def->type == VM_METHOD_TYPE_BMETHOD); + local_hooks = me->def->body.bmethod.hooks; + + if (UNLIKELY(local_hooks && local_hooks->events & RUBY_EVENT_RETURN)) { + rb_exec_event_hook_orig(ec, local_hooks, RUBY_EVENT_RETURN, ec->cfp->self, + rb_vm_frame_method_entry(ec->cfp)->def->original_id, + rb_vm_frame_method_entry(ec->cfp)->called_id, + rb_vm_frame_method_entry(ec->cfp)->owner, + frame_return_value(err), TRUE); + } + } + THROW_DATA_CONSUMED_SET(err); + } + else { + EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err)); + if (UNLIKELY(local_hooks && local_hooks->events & RUBY_EVENT_B_RETURN)) { + rb_exec_event_hook_orig(ec, local_hooks, RUBY_EVENT_B_RETURN, + ec->cfp->self, 0, 0, 0, frame_return_value(err), TRUE); + } + THROW_DATA_CONSUMED_SET(err); + } + break; + case VM_FRAME_MAGIC_CLASS: + EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_END, ec->cfp->self, 0, 0, 0, Qnil); + break; + } } } @@ -2122,8 +2186,6 @@ rb_vm_call_cfunc(VALUE recv, VALUE (*func)(VALUE), VALUE arg, /* vm */ -void rb_vm_trace_mark_event_hooks(rb_hook_list_t *hooks); - void rb_vm_mark(void *ptr) { @@ -2152,7 +2214,7 @@ rb_vm_mark(void *ptr) rb_mark_tbl(vm->loading_table); } - rb_vm_trace_mark_event_hooks(&vm->event_hooks); + rb_hook_list_mark(&vm->global_hooks); rb_gc_mark_values(RUBY_NSIG, vm->trap_list.cmd); @@ -2201,7 +2263,7 @@ ruby_vm_destruct(rb_vm_t *vm) struct rb_objspace *objspace = vm->objspace; vm->main_thread = 0; if (th) { - rb_fiber_reset_root_local_storage(th->self); + rb_fiber_reset_root_local_storage(th); thread_free(th); } rb_vm_living_threads_init(vm); @@ -2435,8 +2497,17 @@ thread_mark(void *ptr) rb_fiber_mark_self(th->ec->fiber_ptr); /* mark ruby objects */ - RUBY_MARK_UNLESS_NULL(th->first_proc); - if (th->first_proc) RUBY_MARK_UNLESS_NULL(th->first_args); + switch (th->invoke_type) { + case thread_invoke_type_proc: + RUBY_MARK_UNLESS_NULL(th->invoke_arg.proc.proc); + RUBY_MARK_UNLESS_NULL(th->invoke_arg.proc.args); + break; + case thread_invoke_type_func: + rb_gc_mark_maybe((VALUE)th->invoke_arg.func.arg); + break; + default: + break; + } RUBY_MARK_UNLESS_NULL(th->thgroup); RUBY_MARK_UNLESS_NULL(th->value); @@ -2535,7 +2606,7 @@ th_init(rb_thread_t *th, VALUE self) /* vm_stack_size is word number. * th->vm->default_params.thread_vm_stack_size is byte size. */ size_t size = th->vm->default_params.thread_vm_stack_size / sizeof(VALUE); - ec_set_vm_stack(th->ec, rb_thread_recycle_stack(size), size); + rb_ec_set_vm_stack(th->ec, rb_thread_recycle_stack(size), size); } th->ec->cfp = (void *)(th->ec->vm_stack + th->ec->vm_stack_size); diff --git a/vm_args.c b/vm_args.c index 4e989574ea9501..16284c0f337d91 100644 --- a/vm_args.c +++ b/vm_args.c @@ -164,7 +164,7 @@ args_copy(struct args_info *args) static inline const VALUE * args_rest_argv(struct args_info *args) { - return RARRAY_CONST_PTR(args->rest) + args->rest_index; + return RARRAY_CONST_PTR_TRANSIENT(args->rest) + args->rest_index; } static inline VALUE @@ -314,7 +314,7 @@ args_setup_post_parameters(struct args_info *args, int argc, VALUE *locals) { long len; len = RARRAY_LEN(args->rest); - MEMCPY(locals, RARRAY_CONST_PTR(args->rest) + len - argc, VALUE, argc); + MEMCPY(locals, RARRAY_CONST_PTR_TRANSIENT(args->rest) + len - argc, VALUE, argc); rb_ary_resize(args->rest, len - argc); } @@ -334,13 +334,13 @@ args_setup_opt_parameters(struct args_info *args, int opt_max, VALUE *locals) args->argc = 0; if (args->rest) { - int len = RARRAY_LENINT(args->rest); - const VALUE *argv = RARRAY_CONST_PTR(args->rest); + int len = RARRAY_LENINT(args->rest); + const VALUE *argv = RARRAY_CONST_PTR_TRANSIENT(args->rest); - for (; irest_index < len; i++, args->rest_index++) { - locals[i] = argv[args->rest_index]; - } - } + for (; irest_index < len; i++, args->rest_index++) { + locals[i] = argv[args->rest_index]; + } + } /* initialize by nil */ for (j=i; jsp--; if (!NIL_P(ary)) { - const VALUE *ptr = RARRAY_CONST_PTR(ary); - long len = RARRAY_LEN(ary), i; + const VALUE *ptr = RARRAY_CONST_PTR_TRANSIENT(ary); + long len = RARRAY_LEN(ary), i; - CHECK_VM_STACK_OVERFLOW(cfp, len); + CHECK_VM_STACK_OVERFLOW(cfp, len); - for (i = 0; i < len; i++) { - *cfp->sp++ = ptr[i]; - } - calling->argc += i - 1; + for (i = 0; i < len; i++) { + *cfp->sp++ = ptr[i]; + } + calling->argc += i - 1; } } diff --git a/vm_backtrace.c b/vm_backtrace.c index 0493ed8c894733..8264018da0d55d 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -33,8 +33,18 @@ inline static int calc_lineno(const rb_iseq_t *iseq, const VALUE *pc) { size_t pos = (size_t)(pc - iseq->body->iseq_encoded); - /* use pos-1 because PC points next instruction at the beginning of instruction */ - return rb_iseq_line_no(iseq, pos - 1); + if (LIKELY(pos)) { + /* use pos-1 because PC points next instruction at the beginning of instruction */ + pos--; + } +#if VMDEBUG && defined(HAVE_BUILTIN___BUILTIN_TRAP) + else { + /* SDR() is not possible; that causes infinite loop. */ + rb_print_backtrace(); + __builtin_trap(); + } +#endif + return rb_iseq_line_no(iseq, pos); } int @@ -683,7 +693,7 @@ rb_ec_backtrace_str_ary(const rb_execution_context_t *ec, long lev, long n) return backtrace_to_str_ary(rb_ec_backtrace_object(ec), lev, n); } -VALUE +static VALUE ec_backtrace_location_ary(const rb_execution_context_t *ec, long lev, long n) { return backtrace_to_location_ary(rb_ec_backtrace_object(ec), lev, n); diff --git a/vm_core.h b/vm_core.h index fbcb013e46f0ab..fd47aed8041ce0 100644 --- a/vm_core.h +++ b/vm_core.h @@ -298,6 +298,7 @@ typedef struct rb_iseq_location_struct { VALUE base_label; /* String */ VALUE label; /* String */ VALUE first_lineno; /* TODO: may be unsigned short */ + int node_id; rb_code_location_t code_location; } rb_iseq_location_t; @@ -473,7 +474,7 @@ struct rb_iseq_constant_body { /* typedef rb_iseq_t is in method.h */ struct rb_iseq_struct { VALUE flags; - VALUE reserved1; + struct rb_hook_list_struct *local_hooks; struct rb_iseq_constant_body *body; union { /* 4, 5 words */ @@ -484,7 +485,7 @@ struct rb_iseq_struct { int index; } loader; - rb_event_flag_t trace_events; + rb_event_flag_t global_trace_events; } aux; }; @@ -576,7 +577,8 @@ void rb_objspace_free(struct rb_objspace *); typedef struct rb_hook_list_struct { struct rb_event_hook_struct *hooks; rb_event_flag_t events; - int need_clean; + unsigned int need_clean; + unsigned int running; } rb_hook_list_t; typedef struct rb_vm_struct { @@ -607,8 +609,6 @@ typedef struct rb_vm_struct { unsigned int thread_report_on_exception: 1; unsigned int safe_level_: 1; - - int trace_running; int sleeper; /* object management */ @@ -633,17 +633,21 @@ typedef struct rb_vm_struct { } trap_list; /* hook */ - rb_hook_list_t event_hooks; + rb_hook_list_t global_hooks; /* relation table of ensure - rollback for callcc */ struct st_table *ensure_rollback_table; - /* postponed_job */ + /* postponed_job (async-signal-safe, NOT thread-safe) */ struct rb_postponed_job_struct *postponed_job_buffer; int postponed_job_index; int src_encoding_index; + /* workqueue (thread-safe, NOT async-signal-safe) */ + struct list_head workqueue; /* <=> rb_workqueue_job.jnode */ + rb_nativethread_lock_t workqueue_lock; + VALUE verbose, debug, orig_progname, progname; VALUE coverages; int coverage_mode; @@ -857,7 +861,6 @@ typedef struct rb_execution_context_struct { /* temporary places */ VALUE errinfo; VALUE passed_block_handler; /* for rb_iterate */ - const rb_callable_method_entry_t *passed_bmethod_me; /* for bmethod */ uint8_t raised_flag; /* only 3 bits needed */ @@ -880,7 +883,7 @@ typedef struct rb_execution_context_struct { } machine; } rb_execution_context_t; -void ec_set_vm_stack(rb_execution_context_t *ec, VALUE *stack, size_t size); +void rb_ec_set_vm_stack(rb_execution_context_t *ec, VALUE *stack, size_t size); typedef struct rb_thread_struct { struct list_node vmlt_node; @@ -935,9 +938,22 @@ typedef struct rb_thread_struct { rb_thread_list_t *join_list; - VALUE first_proc; - VALUE first_args; - VALUE (*first_func)(ANYARGS); + union { + struct { + VALUE proc; + VALUE args; + } proc; + struct { + VALUE (*func)(ANYARGS); + void *arg; + } func; + } invoke_arg; + + enum { + thread_invoke_type_none = 0, + thread_invoke_type_proc, + thread_invoke_type_func + } invoke_type; /* statistics data for profiler */ VALUE stat_insn_usage; @@ -1616,6 +1632,7 @@ rb_vm_living_threads_init(rb_vm_t *vm) { list_head_init(&vm->waiting_fds); list_head_init(&vm->waiting_pids); + list_head_init(&vm->workqueue); list_head_init(&vm->waiting_grps); list_head_init(&vm->living_threads); vm->living_thread_num = 0; @@ -1681,7 +1698,8 @@ RUBY_SYMBOL_EXPORT_BEGIN RUBY_EXTERN rb_vm_t *ruby_current_vm_ptr; RUBY_EXTERN rb_execution_context_t *ruby_current_execution_context_ptr; RUBY_EXTERN rb_event_flag_t ruby_vm_event_flags; -RUBY_EXTERN rb_event_flag_t ruby_vm_event_enabled_flags; +RUBY_EXTERN rb_event_flag_t ruby_vm_event_enabled_global_flags; +RUBY_EXTERN unsigned int ruby_vm_event_local_num; RUBY_SYMBOL_EXPORT_END @@ -1730,14 +1748,21 @@ rb_current_vm(void) return ruby_current_vm_ptr; } -#define rb_thread_set_current_raw(th) (void)(ruby_current_execution_context_ptr = (th)->ec) -#define rb_thread_set_current(th) do { \ - if ((th)->vm->running_thread != (th)) { \ - (th)->running_time_us = 0; \ - } \ - rb_thread_set_current_raw(th); \ - (th)->vm->running_thread = (th); \ -} while (0) +static inline void +rb_thread_set_current_raw(const rb_thread_t *th) +{ + ruby_current_execution_context_ptr = th->ec; +} + +static inline void +rb_thread_set_current(rb_thread_t *th) +{ + if (th->vm->running_thread != th) { + th->running_time_us = 0; + } + rb_thread_set_current_raw(th); + th->vm->running_thread = th; +} #else #error "unsupported thread model" @@ -1785,6 +1810,7 @@ rb_vm_check_ints(rb_execution_context_t *ec) } /* tracer */ + struct rb_trace_arg_struct { rb_event_flag_t event; rb_execution_context_t *ec; @@ -1802,24 +1828,29 @@ struct rb_trace_arg_struct { VALUE path; }; -void rb_exec_event_hooks(struct rb_trace_arg_struct *trace_arg, int pop_p); +void rb_hook_list_mark(rb_hook_list_t *hooks); +void rb_hook_list_free(rb_hook_list_t *hooks); +void rb_hook_list_connect_tracepoint(VALUE target, rb_hook_list_t *list, VALUE tpval, unsigned int target_line); +void rb_hook_list_remove_tracepoint(rb_hook_list_t *list, VALUE tpval); + +void rb_exec_event_hooks(struct rb_trace_arg_struct *trace_arg, rb_hook_list_t *hooks, int pop_p); -#define EXEC_EVENT_HOOK_ORIG(ec_, flag_, vm_flags_, self_, id_, called_id_, klass_, data_, pop_p_) do { \ +#define EXEC_EVENT_HOOK_ORIG(ec_, hooks_, flag_, self_, id_, called_id_, klass_, data_, pop_p_) do { \ const rb_event_flag_t flag_arg_ = (flag_); \ - if (UNLIKELY(vm_flags_ & (flag_arg_))) { \ - /* defer evaluating the other arguments */ \ - rb_exec_event_hook_orig(ec_, flag_arg_, self_, id_, called_id_, klass_, data_, pop_p_); \ + rb_hook_list_t *hooks_arg_ = (hooks_); \ + if (UNLIKELY((hooks_arg_)->events & (flag_arg_))) { \ + /* defer evaluating the other arguments */ \ + rb_exec_event_hook_orig(ec_, hooks_arg_, flag_arg_, self_, id_, called_id_, klass_, data_, pop_p_); \ } \ } while (0) static inline void -rb_exec_event_hook_orig(rb_execution_context_t *ec, const rb_event_flag_t flag, - VALUE self, ID id, ID called_id, VALUE klass, VALUE data, int pop_p) +rb_exec_event_hook_orig(rb_execution_context_t *ec, rb_hook_list_t *hooks, rb_event_flag_t flag, + VALUE self, ID id, ID called_id, VALUE klass, VALUE data, int pop_p) { struct rb_trace_arg_struct trace_arg; - VM_ASSERT(rb_ec_vm_ptr(ec)->event_hooks.events == ruby_vm_event_flags); - VM_ASSERT(rb_ec_vm_ptr(ec)->event_hooks.events & flag); + VM_ASSERT((hooks->events & flag) != 0); trace_arg.event = flag; trace_arg.ec = ec; @@ -1831,14 +1862,21 @@ rb_exec_event_hook_orig(rb_execution_context_t *ec, const rb_event_flag_t flag, trace_arg.data = data; trace_arg.path = Qundef; trace_arg.klass_solved = 0; - rb_exec_event_hooks(&trace_arg, pop_p); + + rb_exec_event_hooks(&trace_arg, hooks, pop_p); +} + +static inline rb_hook_list_t * +rb_vm_global_hooks(const rb_execution_context_t *ec) +{ + return &rb_ec_vm_ptr(ec)->global_hooks; } #define EXEC_EVENT_HOOK(ec_, flag_, self_, id_, called_id_, klass_, data_) \ - EXEC_EVENT_HOOK_ORIG(ec_, flag_, ruby_vm_event_flags, self_, id_, called_id_, klass_, data_, 0) + EXEC_EVENT_HOOK_ORIG(ec_, rb_vm_global_hooks(ec_), flag_, self_, id_, called_id_, klass_, data_, 0) #define EXEC_EVENT_HOOK_AND_POP_FRAME(ec_, flag_, self_, id_, called_id_, klass_, data_) \ - EXEC_EVENT_HOOK_ORIG(ec_, flag_, ruby_vm_event_flags, self_, id_, called_id_, klass_, data_, 1) + EXEC_EVENT_HOOK_ORIG(ec_, rb_vm_global_hooks(ec_), flag_, self_, id_, called_id_, klass_, data_, 1) RUBY_SYMBOL_EXPORT_BEGIN diff --git a/vm_dump.c b/vm_dump.c index 15b16a95030c70..1d36eb0690fbb3 100644 --- a/vm_dump.c +++ b/vm_dump.c @@ -1095,7 +1095,7 @@ rb_vmdebug_stack_dump_all_threads(void) ruby_fill_thread_id_string(th->thread_id, buf); fprintf(stderr, "th: %p, native_id: %s\n", th, buf); #else - fprintf(stderr, "th: %p, native_id: %p\n", (void *)th, (void *)th->thread_id); + fprintf(stderr, "th: %p, native_id: %p\n", (void *)th, (void *)(uintptr_t)th->thread_id); #endif rb_vmdebug_stack_dump_raw(th->ec, th->ec->cfp); } diff --git a/vm_eval.c b/vm_eval.c index 1176c494e104eb..47191a0aab9865 100644 --- a/vm_eval.c +++ b/vm_eval.c @@ -290,8 +290,16 @@ rb_call0(rb_execution_context_t *ec, VALUE recv, ID mid, int argc, const VALUE *argv, call_type scope, VALUE self) { - const rb_callable_method_entry_t *me = rb_search_method_entry(recv, mid); - enum method_missing_reason call_status = rb_method_call_status(ec, me, scope, self); + const rb_callable_method_entry_t *me; + enum method_missing_reason call_status; + + if (scope == CALL_PUBLIC) { + me = rb_callable_method_entry_with_refinements(CLASS_OF(recv), mid, NULL); + } + else { + me = rb_search_method_entry(recv, mid); + } + call_status = rb_method_call_status(ec, me, scope, self); if (call_status != MISSING_NONE) { return method_missing(recv, mid, argc, argv, call_status); @@ -764,7 +772,7 @@ rb_apply(VALUE recv, ID mid, VALUE args) return ret; } argv = ALLOCA_N(VALUE, argc); - MEMCPY(argv, RARRAY_CONST_PTR(args), VALUE, argc); + MEMCPY(argv, RARRAY_CONST_PTR_TRANSIENT(args), VALUE, argc); return rb_call(recv, mid, argc, argv, CALL_FCALL); } @@ -2035,10 +2043,9 @@ static void local_var_list_add(const struct local_var_list *vars, ID lid) { if (lid && rb_is_local_id(lid)) { - /* should skip temporary variable */ - st_table *tbl = RHASH_TBL_RAW(vars->tbl); - st_data_t idx = 0; /* tbl->num_entries */ - st_update(tbl, ID2SYM(lid), local_var_list_update, idx); + /* should skip temporary variable */ + st_data_t idx = 0; /* tbl->num_entries */ + rb_hash_stlike_update(vars->tbl, ID2SYM(lid), local_var_list_update, idx); } } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index e162e9d32f23ac..32a105a4ac35e0 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1265,11 +1265,14 @@ vm_expandarray(VALUE *sp, VALUE ary, rb_num_t num, int flag) len = 1; } else { - ptr = RARRAY_CONST_PTR(ary); + ptr = RARRAY_CONST_PTR_TRANSIENT(ary); len = (rb_num_t)RARRAY_LEN(ary); } - if (flag & 0x02) { + if (space_size == 0) { + /* no space left on stack */ + } + else if (flag & 0x02) { /* post: ..., nil ,ary[-1], ..., ary[0..-num] # top */ rb_num_t i = 0, j; @@ -1943,9 +1946,8 @@ vm_call_bmethod_body(rb_execution_context_t *ec, struct rb_calling_info *calling VALUE val; /* control block frame */ - ec->passed_bmethod_me = cc->me; - GetProcPtr(cc->me->def->body.proc, proc); - val = rb_vm_invoke_bmethod(ec, proc, calling->recv, calling->argc, argv, calling->block_handler); + GetProcPtr(cc->me->def->body.bmethod.proc, proc); + val = rb_vm_invoke_bmethod(ec, proc, calling->recv, calling->argc, argv, calling->block_handler, cc->me); return val; } @@ -2512,13 +2514,12 @@ block_proc_is_lambda(const VALUE procval) static VALUE vm_yield_with_cfunc(rb_execution_context_t *ec, const struct rb_captured_block *captured, - VALUE self, int argc, const VALUE *argv, VALUE block_handler) + VALUE self, int argc, const VALUE *argv, VALUE block_handler, + const rb_callable_method_entry_t *me) { int is_lambda = FALSE; /* TODO */ VALUE val, arg, blockarg; const struct vm_ifunc *ifunc = captured->code.ifunc; - const rb_callable_method_entry_t *me = ec->passed_bmethod_me; - ec->passed_bmethod_me = NULL; if (is_lambda) { arg = rb_ary_new4(argc, argv); @@ -2533,10 +2534,11 @@ vm_yield_with_cfunc(rb_execution_context_t *ec, blockarg = rb_vm_bh_to_procval(ec, block_handler); vm_push_frame(ec, (const rb_iseq_t *)captured->code.ifunc, - VM_FRAME_MAGIC_IFUNC | VM_FRAME_FLAG_CFRAME, + VM_FRAME_MAGIC_IFUNC | VM_FRAME_FLAG_CFRAME | + (me ? VM_FRAME_FLAG_BMETHOD : 0), self, VM_GUARDED_PREV_EP(captured->ep), - (VALUE)me, + (VALUE)me, 0, ec->cfp->sp, 0, 0); val = (*ifunc->func)(arg, ifunc->data, argc, argv, blockarg); rb_vm_pop_frame(ec); @@ -2683,7 +2685,7 @@ vm_invoke_ifunc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, int argc; CALLER_SETUP_ARG(ec->cfp, calling, ci); argc = calling->argc; - val = vm_yield_with_cfunc(ec, captured, captured->self, argc, STACK_ADDR_FROM_TOP(argc), calling->block_handler); + val = vm_yield_with_cfunc(ec, captured, captured->self, argc, STACK_ADDR_FROM_TOP(argc), calling->block_handler, NULL); POPN(argc); /* TODO: should put before C/yield? */ return val; } @@ -3209,7 +3211,7 @@ vm_opt_newarray_max(rb_num_t num, const VALUE *ptr) else { struct cmp_opt_data cmp_opt = { 0, 0 }; VALUE result = *ptr; - rb_num_t i = num - 1; + rb_snum_t i = num - 1; while (i-- > 0) { const VALUE v = *++ptr; if (OPTIMIZED_CMP(v, result, cmp_opt) > 0) { @@ -3235,7 +3237,7 @@ vm_opt_newarray_min(rb_num_t num, const VALUE *ptr) else { struct cmp_opt_data cmp_opt = { 0, 0 }; VALUE result = *ptr; - rb_num_t i = num - 1; + rb_snum_t i = num - 1; while (i-- > 0) { const VALUE v = *++ptr; if (OPTIMIZED_CMP(v, result, cmp_opt) < 0) { @@ -3327,7 +3329,7 @@ vm_case_dispatch(CDHASH hash, OFFSET else_offset, VALUE key) key = FIXABLE(kval) ? LONG2FIX((long)kval) : rb_dbl2big(kval); } } - if (st_lookup(RHASH_TBL_RAW(hash), key, &val)) { + if (rb_hash_stlike_lookup(hash, key, &val)) { return FIX2LONG((VALUE)val); } else { @@ -3451,7 +3453,7 @@ vm_opt_div(VALUE recv, VALUE obj) } else if (FLONUM_2_P(recv, obj) && BASIC_OP_UNREDEFINED_P(BOP_DIV, FLOAT_REDEFINED_OP_FLAG)) { - return DBL2NUM(RFLOAT_VALUE(recv) / RFLOAT_VALUE(obj)); + return rb_flo_div_flo(recv, obj); } else if (SPECIAL_CONST_P(recv) || SPECIAL_CONST_P(obj)) { return Qundef; @@ -3459,7 +3461,7 @@ vm_opt_div(VALUE recv, VALUE obj) else if (RBASIC_CLASS(recv) == rb_cFloat && RBASIC_CLASS(obj) == rb_cFloat && BASIC_OP_UNREDEFINED_P(BOP_DIV, FLOAT_REDEFINED_OP_FLAG)) { - return DBL2NUM(RFLOAT_VALUE(recv) / RFLOAT_VALUE(obj)); + return rb_flo_div_flo(recv, obj); } else { return Qundef; @@ -3760,18 +3762,40 @@ vm_opt_empty_p(VALUE recv) } } +static VALUE +fix_succ(VALUE x) +{ + switch (x) { + case ~0UL: + /* 0xFFFF_FFFF == INT2FIX(-1) + * `-1.succ` is of course 0. */ + return INT2FIX(0); + case RSHIFT(~0UL, 1): + /* 0x7FFF_FFFF == LONG2FIX(0x3FFF_FFFF) + * 0x3FFF_FFFF + 1 == 0x4000_0000, which is a Bignum. */ + return rb_uint2big(1UL << (SIZEOF_LONG * CHAR_BIT - 2)); + default: + /* LONG2FIX(FIX2LONG(x)+FIX2LONG(y)) + * == ((lx*2+1)/2 + (ly*2+1)/2)*2+1 + * == lx*2 + ly*2 + 1 + * == (lx*2+1) + (ly*2+1) - 1 + * == x + y - 1 + * + * Here, if we put y := INT2FIX(1): + * + * == x + INT2FIX(1) - 1 + * == x + 2 . + */ + return x + 2; + } +} + static VALUE vm_opt_succ(VALUE recv) { if (FIXNUM_P(recv) && BASIC_OP_UNREDEFINED_P(BOP_SUCC, INTEGER_REDEFINED_OP_FLAG)) { - /* fixnum + INT2FIX(1) */ - if (recv == LONG2FIX(FIXNUM_MAX)) { - return LONG2NUM(FIXNUM_MAX + 1); - } - else { - return recv - 1 + INT2FIX(1); - } + return fix_succ(recv); } else if (SPECIAL_CONST_P(recv)) { return Qundef; @@ -3823,23 +3847,62 @@ rb_event_flag_t rb_iseq_event_flags(const rb_iseq_t *iseq, size_t pos); NOINLINE(static void vm_trace(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE *pc)); +static inline void +vm_trace_hook(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE *pc, + rb_event_flag_t pc_events, rb_event_flag_t target_event, + rb_hook_list_t *global_hooks, rb_hook_list_t *local_hooks, VALUE val) +{ + rb_event_flag_t event = pc_events & target_event; + VALUE self = GET_SELF(); + + VM_ASSERT(rb_popcount64((uint64_t)event) == 1); + + if (event & global_hooks->events) { + /* increment PC because source line is calculated with PC-1 */ + reg_cfp->pc++; + vm_dtrace(event, ec); + rb_exec_event_hook_orig(ec, global_hooks, event, self, 0, 0, 0 , val, 0); + reg_cfp->pc--; + } + + if (local_hooks != NULL) { + if (event & local_hooks->events) { + /* increment PC because source line is calculated with PC-1 */ + reg_cfp->pc++; + rb_exec_event_hook_orig(ec, local_hooks, event, self, 0, 0, 0 , val, 0); + reg_cfp->pc--; + } + } +} + +#define VM_TRACE_HOOK(target_event, val) do { \ + if ((pc_events & (target_event)) & enabled_flags) { \ + vm_trace_hook(ec, reg_cfp, pc, pc_events, (target_event), global_hooks, local_hooks, (val)); \ + } \ +} while (0) + static void vm_trace(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE *pc) { - rb_event_flag_t vm_event_flags = ruby_vm_event_flags; + rb_event_flag_t enabled_flags = ruby_vm_event_flags & ISEQ_TRACE_EVENTS; - if (vm_event_flags == 0) { - return; + if (enabled_flags == 0 && ruby_vm_event_local_num == 0) { + return; } else { const rb_iseq_t *iseq = reg_cfp->iseq; size_t pos = pc - iseq->body->iseq_encoded; - rb_event_flag_t events = rb_iseq_event_flags(iseq, pos); - rb_event_flag_t event; + rb_event_flag_t pc_events = rb_iseq_event_flags(iseq, pos); + rb_hook_list_t *local_hooks = iseq->local_hooks; + rb_event_flag_t local_hook_events = local_hooks != NULL ? local_hooks->events : 0; + enabled_flags |= local_hook_events; + + VM_ASSERT((local_hook_events & ~ISEQ_TRACE_EVENTS) == 0); - if ((events & vm_event_flags) == 0) { + if ((pc_events & enabled_flags) == 0) { #if 0 /* disable trace */ + /* TODO: incomplete */ rb_iseq_trace_set(iseq, vm_event_flags & ISEQ_TRACE_EVENTS); #else /* do not disable trace because of performance problem @@ -3847,60 +3910,33 @@ vm_trace(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE *p */ #endif return; - } - - if (ec->trace_arg != NULL) return; - - if (0) { - fprintf(stderr, "vm_trace>>%4d (%4x) - %s:%d %s\n", - (int)pos, - (int)events, - RSTRING_PTR(rb_iseq_path(iseq)), - (int)rb_iseq_line_no(iseq, pos), - RSTRING_PTR(rb_iseq_label(iseq))); - } - - VM_ASSERT(reg_cfp->pc == pc); - VM_ASSERT(events != 0); - VM_ASSERT(vm_event_flags & events); - - /* increment PC because source line is calculated with PC-1 */ - if ((event = (events & (RUBY_EVENT_CLASS | RUBY_EVENT_CALL | RUBY_EVENT_B_CALL))) != 0) { - VM_ASSERT(event == RUBY_EVENT_CLASS || - event == RUBY_EVENT_CALL || - event == RUBY_EVENT_B_CALL); - reg_cfp->pc++; - vm_dtrace(event, ec); - EXEC_EVENT_HOOK(ec, event, GET_SELF(), 0, 0, 0, Qundef); - reg_cfp->pc--; - } - if (events & RUBY_EVENT_LINE) { - reg_cfp->pc++; - vm_dtrace(RUBY_EVENT_LINE, ec); - EXEC_EVENT_HOOK(ec, RUBY_EVENT_LINE, GET_SELF(), 0, 0, 0, Qundef); - reg_cfp->pc--; - } - if (events & RUBY_EVENT_COVERAGE_LINE) { - reg_cfp->pc++; - vm_dtrace(RUBY_EVENT_COVERAGE_LINE, ec); - EXEC_EVENT_HOOK(ec, RUBY_EVENT_COVERAGE_LINE, GET_SELF(), 0, 0, 0, Qundef); - reg_cfp->pc--; - } - if (events & RUBY_EVENT_COVERAGE_BRANCH) { - reg_cfp->pc++; - vm_dtrace(RUBY_EVENT_COVERAGE_BRANCH, ec); - EXEC_EVENT_HOOK(ec, RUBY_EVENT_COVERAGE_BRANCH, GET_SELF(), 0, 0, 0, Qundef); - reg_cfp->pc--; } - if ((event = (events & (RUBY_EVENT_END | RUBY_EVENT_RETURN | RUBY_EVENT_B_RETURN))) != 0) { - VM_ASSERT(event == RUBY_EVENT_END || - event == RUBY_EVENT_RETURN || - event == RUBY_EVENT_B_RETURN); - reg_cfp->pc++; - vm_dtrace(event, ec); - EXEC_EVENT_HOOK(ec, event, GET_SELF(), 0, 0, 0, TOPN(0)); - reg_cfp->pc--; - } + else if (ec->trace_arg != NULL) { + /* already tracing */ + return; + } + else { + rb_hook_list_t *global_hooks = rb_vm_global_hooks(ec); + + if (0) { + fprintf(stderr, "vm_trace>>%4d (%4x) - %s:%d %s\n", + (int)pos, + (int)pc_events, + RSTRING_PTR(rb_iseq_path(iseq)), + (int)rb_iseq_line_no(iseq, pos), + RSTRING_PTR(rb_iseq_label(iseq))); + } + VM_ASSERT(reg_cfp->pc == pc); + VM_ASSERT(pc_events != 0); + VM_ASSERT(enabled_flags & pc_events); + + /* check traces */ + VM_TRACE_HOOK(RUBY_EVENT_CLASS | RUBY_EVENT_CALL | RUBY_EVENT_B_CALL, Qundef); + VM_TRACE_HOOK(RUBY_EVENT_LINE, Qundef); + VM_TRACE_HOOK(RUBY_EVENT_COVERAGE_LINE, Qundef); + VM_TRACE_HOOK(RUBY_EVENT_COVERAGE_BRANCH, Qundef); + VM_TRACE_HOOK(RUBY_EVENT_END | RUBY_EVENT_RETURN | RUBY_EVENT_B_RETURN, TOPN(0)); + } } } diff --git a/vm_method.c b/vm_method.c index c4f41bc1189e18..697621258c84a8 100644 --- a/vm_method.c +++ b/vm_method.c @@ -152,7 +152,9 @@ rb_method_definition_release(rb_method_definition_t *def, int complemented) VM_ASSERT(complemented_count >= 0); if (alias_count + complemented_count == 0) { - if (METHOD_DEBUG) fprintf(stderr, "-%p-%s:%d,%d (remove)\n", (void *)def, rb_id2name(def->original_id), alias_count, complemented_count); + if (METHOD_DEBUG) fprintf(stderr, "-%p-%s:%d,%d (remove)\n", (void *)def, + rb_id2name(def->original_id), alias_count, complemented_count); + VM_ASSERT(def->type == VM_METHOD_TYPE_BMETHOD ? def->body.bmethod.hooks == NULL : TRUE); xfree(def); } else { @@ -277,7 +279,7 @@ rb_method_definition_set(const rb_method_entry_t *me, rb_method_definition_t *de return; } case VM_METHOD_TYPE_BMETHOD: - RB_OBJ_WRITE(me, &def->body.proc, (VALUE)opts); + RB_OBJ_WRITE(me, &def->body.bmethod.proc, (VALUE)opts); return; case VM_METHOD_TYPE_NOTIMPLEMENTED: setup_method_cfunc_struct(UNALIGNED_MEMBER_PTR(def, body.cfunc), rb_f_notimplement, -1); @@ -318,7 +320,9 @@ method_definition_reset(const rb_method_entry_t *me) RB_OBJ_WRITTEN(me, Qundef, def->body.attr.location); break; case VM_METHOD_TYPE_BMETHOD: - RB_OBJ_WRITTEN(me, Qundef, def->body.proc); + RB_OBJ_WRITTEN(me, Qundef, def->body.bmethod.proc); + /* give up to check all in a list */ + if (def->body.bmethod.hooks) rb_gc_writebarrier_remember((VALUE)me); break; case VM_METHOD_TYPE_REFINED: RB_OBJ_WRITTEN(me, Qundef, def->body.refined.orig_me); @@ -579,7 +583,7 @@ rb_method_entry_make(VALUE klass, ID mid, VALUE defined_class, rb_method_visibil iseq = def_iseq_ptr(old_def); break; case VM_METHOD_TYPE_BMETHOD: - iseq = rb_proc_get_iseq(old_def->body.proc, 0); + iseq = rb_proc_get_iseq(old_def->body.bmethod.proc, 0); break; default: break; @@ -1089,7 +1093,14 @@ rb_export_method(VALUE klass, ID name, rb_method_visibility_t visi) int rb_method_boundp(VALUE klass, ID id, int ex) { - const rb_method_entry_t *me = rb_method_entry_without_refinements(klass, id, NULL); + const rb_method_entry_t *me; + + if (ex & BOUND_RESPONDS) { + me = method_entry_resolve_refinement(klass, id, TRUE, NULL); + } + else { + me = rb_method_entry_without_refinements(klass, id, NULL); + } if (me != 0) { if ((ex & ~BOUND_RESPONDS) && @@ -1514,7 +1525,7 @@ rb_method_definition_eq(const rb_method_definition_t *d1, const rb_method_defini case VM_METHOD_TYPE_IVAR: return d1->body.attr.id == d2->body.attr.id; case VM_METHOD_TYPE_BMETHOD: - return RTEST(rb_equal(d1->body.proc, d2->body.proc)); + return RTEST(rb_equal(d1->body.bmethod.proc, d2->body.bmethod.proc)); case VM_METHOD_TYPE_MISSING: return d1->original_id == d2->original_id; case VM_METHOD_TYPE_ZSUPER: @@ -1548,7 +1559,7 @@ rb_hash_method_definition(st_index_t hash, const rb_method_definition_t *def) case VM_METHOD_TYPE_IVAR: return rb_hash_uint(hash, def->body.attr.id); case VM_METHOD_TYPE_BMETHOD: - return rb_hash_proc(hash, def->body.proc); + return rb_hash_proc(hash, def->body.bmethod.proc); case VM_METHOD_TYPE_MISSING: return rb_hash_uint(hash, def->original_id); case VM_METHOD_TYPE_ZSUPER: diff --git a/vm_trace.c b/vm_trace.c index 54aa34f8d284ee..23bba86d29a2fc 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -40,6 +40,7 @@ typedef struct rb_event_hook_struct { struct { rb_thread_t *th; + unsigned int target_line; } filter; } rb_event_hook_t; @@ -47,10 +48,8 @@ typedef void (*rb_event_hook_raw_arg_func_t)(VALUE data, const rb_trace_arg_t *a #define MAX_EVENT_NUM 32 -/* called from vm.c */ - void -rb_vm_trace_mark_event_hooks(rb_hook_list_t *hooks) +rb_hook_list_mark(rb_hook_list_t *hooks) { rb_event_hook_t *hook = hooks->hooks; @@ -60,13 +59,21 @@ rb_vm_trace_mark_event_hooks(rb_hook_list_t *hooks) } } +static void clean_hooks(const rb_execution_context_t *ec, rb_hook_list_t *list); + +void +rb_hook_list_free(rb_hook_list_t *hooks) +{ + clean_hooks(GET_EC(), hooks); +} + /* ruby_vm_event_flags management */ static void update_global_event_hook(rb_event_flag_t vm_events) { rb_event_flag_t new_iseq_events = vm_events & ISEQ_TRACE_EVENTS; - rb_event_flag_t enabled_iseq_events = ruby_vm_event_enabled_flags & ISEQ_TRACE_EVENTS; + rb_event_flag_t enabled_iseq_events = ruby_vm_event_enabled_global_flags & ISEQ_TRACE_EVENTS; if (new_iseq_events & ~enabled_iseq_events) { /* Stop calling all JIT-ed code. Compiling trace insns is not supported for now. */ @@ -79,7 +86,7 @@ update_global_event_hook(rb_event_flag_t vm_events) } ruby_vm_event_flags = vm_events; - ruby_vm_event_enabled_flags |= vm_events; + ruby_vm_event_enabled_global_flags |= vm_events; rb_objspace_set_event_hook(vm_events); } @@ -101,20 +108,33 @@ alloc_event_hook(rb_event_hook_func_t func, rb_event_flag_t events, VALUE data, hook->data = data; /* no filters */ - hook->filter.th = 0; + hook->filter.th = NULL; + hook->filter.target_line = 0; return hook; } static void -connect_event_hook(const rb_execution_context_t *ec, rb_event_hook_t *hook) +hook_list_connect(VALUE list_owner, rb_hook_list_t *list, rb_event_hook_t *hook, int global_p) { - rb_hook_list_t *list = &rb_ec_vm_ptr(ec)->event_hooks; - hook->next = list->hooks; list->hooks = hook; list->events |= hook->events; - update_global_event_hook(list->events); + + if (global_p) { + /* global hooks are root objects at GC mark. */ + update_global_event_hook(list->events); + } + else { + RB_OBJ_WRITTEN(list_owner, Qundef, hook->data); + } +} + +static void +connect_event_hook(const rb_execution_context_t *ec, rb_event_hook_t *hook) +{ + rb_hook_list_t *list = rb_vm_global_hooks(ec); + hook_list_connect(Qundef, list, hook, TRUE); } static void @@ -153,7 +173,7 @@ rb_add_event_hook2(rb_event_hook_func_t func, rb_event_flag_t events, VALUE data } static void -clean_hooks(rb_hook_list_t *list) +clean_hooks(const rb_execution_context_t *ec, rb_hook_list_t *list) { rb_event_hook_t *hook, **nextp = &list->hooks; VM_ASSERT(list->need_clean == TRUE); @@ -172,16 +192,25 @@ clean_hooks(rb_hook_list_t *list) } } - update_global_event_hook(list->events); + if (list == rb_vm_global_hooks(ec)) { + /* global events */ + update_global_event_hook(list->events); + } + else { + /* local events */ + if (list->events == 0) { + ruby_xfree(list); + } + } } static void -clean_hooks_check(rb_vm_t *vm, rb_hook_list_t *list) +clean_hooks_check(const rb_execution_context_t *ec, rb_hook_list_t *list) { if (UNLIKELY(list->need_clean != FALSE)) { - if (vm->trace_running == 0) { - clean_hooks(list); - } + if (list->running == 0) { + clean_hooks(ec, list); + } } } @@ -192,7 +221,7 @@ static int remove_event_hook(const rb_execution_context_t *ec, const rb_thread_t *filter_th, rb_event_hook_func_t func, VALUE data) { rb_vm_t *vm = rb_ec_vm_ptr(ec); - rb_hook_list_t *list = &vm->event_hooks; + rb_hook_list_t *list = &vm->global_hooks; int ret = 0; rb_event_hook_t *hook = list->hooks; @@ -209,7 +238,7 @@ remove_event_hook(const rb_execution_context_t *ec, const rb_thread_t *filter_th hook = hook->next; } - clean_hooks_check(vm, list); + clean_hooks_check(ec, list); return ret; } @@ -266,7 +295,8 @@ exec_hooks_body(const rb_execution_context_t *ec, rb_hook_list_t *list, const rb for (hook = list->hooks; hook; hook = hook->next) { if (!(hook->hook_flags & RUBY_EVENT_HOOK_FLAG_DELETED) && (trace_arg->event & hook->events) && - (hook->filter.th == 0 || hook->filter.th == rb_ec_thread_ptr(ec))) { + (LIKELY(hook->filter.th == 0) || hook->filter.th == rb_ec_thread_ptr(ec)) && + (LIKELY(hook->filter.target_line == 0) || (hook->filter.target_line == (unsigned int)rb_vm_get_sourceline(ec->cfp)))) { if (!(hook->hook_flags & RUBY_EVENT_HOOK_FLAG_RAW_ARG)) { (*hook->func)(trace_arg->event, hook->data, trace_arg->self, trace_arg->id, trace_arg->klass); } @@ -278,10 +308,10 @@ exec_hooks_body(const rb_execution_context_t *ec, rb_hook_list_t *list, const rb } static int -exec_hooks_precheck(const rb_execution_context_t *ec, rb_vm_t *vm, rb_hook_list_t *list, const rb_trace_arg_t *trace_arg) +exec_hooks_precheck(const rb_execution_context_t *ec, rb_hook_list_t *list, const rb_trace_arg_t *trace_arg) { if (list->events & trace_arg->event) { - vm->trace_running++; + list->running++; return TRUE; } else { @@ -290,27 +320,27 @@ exec_hooks_precheck(const rb_execution_context_t *ec, rb_vm_t *vm, rb_hook_list_ } static void -exec_hooks_postcheck(const rb_execution_context_t *ec, rb_vm_t *vm, rb_hook_list_t *list) +exec_hooks_postcheck(const rb_execution_context_t *ec, rb_hook_list_t *list) { - vm->trace_running--; - clean_hooks_check(vm, list); + list->running--; + clean_hooks_check(ec, list); } static void -exec_hooks_unprotected(const rb_execution_context_t *ec, rb_vm_t *vm, rb_hook_list_t *list, const rb_trace_arg_t *trace_arg) +exec_hooks_unprotected(const rb_execution_context_t *ec, rb_hook_list_t *list, const rb_trace_arg_t *trace_arg) { - if (exec_hooks_precheck(ec, vm, list, trace_arg) == 0) return; + if (exec_hooks_precheck(ec, list, trace_arg) == 0) return; exec_hooks_body(ec, list, trace_arg); - exec_hooks_postcheck(ec, vm, list); + exec_hooks_postcheck(ec, list); } static int -exec_hooks_protected(rb_execution_context_t *ec, rb_vm_t *vm, rb_hook_list_t *list, const rb_trace_arg_t *trace_arg) +exec_hooks_protected(rb_execution_context_t *ec, rb_hook_list_t *list, const rb_trace_arg_t *trace_arg) { enum ruby_tag_type state; volatile int raised; - if (exec_hooks_precheck(ec, vm, list, trace_arg) == 0) return 0; + if (exec_hooks_precheck(ec, list, trace_arg) == 0) return 0; raised = rb_ec_reset_raised(ec); @@ -322,7 +352,7 @@ exec_hooks_protected(rb_execution_context_t *ec, rb_vm_t *vm, rb_hook_list_t *li } EC_POP_TAG(); - exec_hooks_postcheck(ec, vm, list); + exec_hooks_postcheck(ec, list); if (raised) { rb_ec_set_raised(ec); @@ -332,20 +362,21 @@ exec_hooks_protected(rb_execution_context_t *ec, rb_vm_t *vm, rb_hook_list_t *li } MJIT_FUNC_EXPORTED void -rb_exec_event_hooks(rb_trace_arg_t *trace_arg, int pop_p) +rb_exec_event_hooks(rb_trace_arg_t *trace_arg, rb_hook_list_t *hooks, int pop_p) { rb_execution_context_t *ec = trace_arg->ec; - rb_vm_t *vm = rb_ec_vm_ptr(ec); - if (trace_arg->event & RUBY_INTERNAL_EVENT_MASK) { - if (ec->trace_arg && (ec->trace_arg->event & RUBY_INTERNAL_EVENT_MASK)) { - /* skip hooks because this thread doing INTERNAL_EVENT */ + if (UNLIKELY(trace_arg->event & RUBY_INTERNAL_EVENT_MASK)) { + if (ec->trace_arg && (ec->trace_arg->event & RUBY_INTERNAL_EVENT_MASK)) { + /* skip hooks because this thread doing INTERNAL_EVENT */ } else { rb_trace_arg_t *prev_trace_arg = ec->trace_arg; - ec->trace_arg = trace_arg; - exec_hooks_unprotected(ec, vm, &vm->event_hooks, trace_arg); - ec->trace_arg = prev_trace_arg; + + ec->trace_arg = trace_arg; + /* only global hooks */ + exec_hooks_unprotected(ec, rb_vm_global_hooks(ec), trace_arg); + ec->trace_arg = prev_trace_arg; } } else { @@ -355,15 +386,18 @@ rb_exec_event_hooks(rb_trace_arg_t *trace_arg, int pop_p) const VALUE old_recursive = ec->local_storage_recursive_hash; int state = 0; + /* setup */ ec->local_storage_recursive_hash = ec->local_storage_recursive_hash_for_trace; ec->errinfo = Qnil; ec->trace_arg = trace_arg; - state = exec_hooks_protected(ec, vm, &vm->event_hooks, trace_arg); - if (!state) { - ec->errinfo = errinfo; - } - ec->trace_arg = NULL; + /* kick hooks */ + if ((state = exec_hooks_protected(ec, hooks, trace_arg)) == TAG_NONE) { + ec->errinfo = errinfo; + } + + /* cleanup */ + ec->trace_arg = NULL; ec->local_storage_recursive_hash_for_trace = ec->local_storage_recursive_hash; ec->local_storage_recursive_hash = old_recursive; @@ -392,7 +426,6 @@ rb_suppress_tracing(VALUE (*func)(VALUE), VALUE arg) dummy_trace_arg.event = 0; if (!ec->trace_arg) { - vm->trace_running++; ec->trace_arg = &dummy_trace_arg; } @@ -413,7 +446,6 @@ rb_suppress_tracing(VALUE (*func)(VALUE), VALUE arg) if (ec->trace_arg == &dummy_trace_arg) { ec->trace_arg = NULL; - vm->trace_running--; } if (state) { @@ -669,6 +701,10 @@ typedef struct rb_tp_struct { rb_event_flag_t events; int tracing; /* bool */ rb_thread_t *target_th; + VALUE local_target_set; /* Hash: target -> + * Qtrue (if target is iseq) or + * Qfalse (if target is bmethod) + */ void (*func)(VALUE tpval, void *data); void *data; VALUE proc; @@ -680,6 +716,7 @@ tp_mark(void *ptr) { rb_tp_t *tp = ptr; rb_gc_mark(tp->proc); + rb_gc_mark(tp->local_target_set); if (tp->target_th) rb_gc_mark(tp->target_th->self); } @@ -1087,9 +1124,12 @@ VALUE rb_tracepoint_enable(VALUE tpval) { rb_tp_t *tp; - tp = tpptr(tpval); + if (tp->local_target_set != Qfalse) { + rb_raise(rb_eArgError, "can't nest-enable a targetting TracePoint"); + } + if (tp->target_th) { rb_thread_add_event_hook2(tp->target_th->self, (rb_event_hook_func_t)tp_call_trace, tp->events, tpval, RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG); @@ -1102,6 +1142,92 @@ rb_tracepoint_enable(VALUE tpval) return Qundef; } +static const rb_iseq_t * +iseq_of(VALUE target) +{ + VALUE iseqv = rb_funcall(rb_cISeq, rb_intern("of"), 1, target); + if (NIL_P(iseqv)) { + rb_raise(rb_eArgError, "specified target is not supported"); + } + else { + return rb_iseqw_to_iseq(iseqv); + } +} + +const rb_method_definition_t *rb_method_def(VALUE method); /* proc.c */ + +static VALUE +rb_tracepoint_enable_for_target(VALUE tpval, VALUE target, VALUE target_line) +{ + rb_tp_t *tp = tpptr(tpval); + const rb_iseq_t *iseq = iseq_of(target); + int n; + unsigned int line = 0; + + if (tp->tracing > 0) { + rb_raise(rb_eArgError, "can't nest-enable a targetting TracePoint"); + } + + if (!NIL_P(target_line)) { + if ((tp->events & RUBY_EVENT_LINE) == 0) { + rb_raise(rb_eArgError, "target_line is specified, but line event is not specified"); + } + else { + line = NUM2UINT(target_line); + } + } + + VM_ASSERT(tp->local_target_set == Qfalse); + tp->local_target_set = rb_obj_hide(rb_ident_hash_new()); + + /* iseq */ + n = rb_iseq_add_local_tracepoint_recursively(iseq, tp->events, tpval, line); + rb_hash_aset(tp->local_target_set, (VALUE)iseq, Qtrue); + + /* bmethod */ + if (rb_obj_is_method(target)) { + rb_method_definition_t *def = (rb_method_definition_t *)rb_method_def(target); + if (def->type == VM_METHOD_TYPE_BMETHOD && + (tp->events & (RUBY_EVENT_CALL | RUBY_EVENT_RETURN))) { + def->body.bmethod.hooks = ZALLOC(rb_hook_list_t); + rb_hook_list_connect_tracepoint(target, def->body.bmethod.hooks, tpval, 0); + rb_hash_aset(tp->local_target_set, target, Qfalse); + + n++; + } + } + + if (n == 0) { + rb_raise(rb_eArgError, "can not enable any hooks"); + } + + ruby_vm_event_local_num++; + + tp->tracing = 1; + + return Qnil; +} + +static int +disable_local_event_iseq_i(VALUE target, VALUE iseq_p, VALUE tpval) +{ + if (iseq_p) { + rb_iseq_remove_local_tracepoint_recursively((rb_iseq_t *)target, tpval); + } + else { + /* bmethod */ + rb_method_definition_t *def = (rb_method_definition_t *)rb_method_def(target); + rb_hook_list_t *hooks = def->body.bmethod.hooks; + VM_ASSERT(hooks != NULL); + rb_hook_list_remove_tracepoint(hooks, tpval); + if (hooks->running == 0) { + rb_hook_list_free(def->body.bmethod.hooks); + } + def->body.bmethod.hooks = NULL; + } + return ST_CONTINUE; +} + VALUE rb_tracepoint_disable(VALUE tpval) { @@ -1109,16 +1235,53 @@ rb_tracepoint_disable(VALUE tpval) tp = tpptr(tpval); - if (tp->target_th) { - rb_thread_remove_event_hook_with_data(tp->target_th->self, (rb_event_hook_func_t)tp_call_trace, tpval); + if (tp->local_target_set) { + rb_hash_foreach(tp->local_target_set, disable_local_event_iseq_i, tpval); + tp->local_target_set = Qfalse; + ruby_vm_event_local_num--; } else { - rb_remove_event_hook_with_data((rb_event_hook_func_t)tp_call_trace, tpval); + if (tp->target_th) { + rb_thread_remove_event_hook_with_data(tp->target_th->self, (rb_event_hook_func_t)tp_call_trace, tpval); + } + else { + rb_remove_event_hook_with_data((rb_event_hook_func_t)tp_call_trace, tpval); + } } tp->tracing = 0; return Qundef; } +void +rb_hook_list_connect_tracepoint(VALUE target, rb_hook_list_t *list, VALUE tpval, unsigned int target_line) +{ + rb_tp_t *tp = tpptr(tpval); + rb_event_hook_t *hook = alloc_event_hook((rb_event_hook_func_t)tp_call_trace, tp->events, tpval, + RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG); + hook->filter.target_line = target_line; + hook_list_connect(target, list, hook, FALSE); +} + +void +rb_hook_list_remove_tracepoint(rb_hook_list_t *list, VALUE tpval) +{ + rb_event_hook_t *hook = list->hooks; + rb_event_flag_t events = 0; + + while (hook) { + if (hook->data == tpval) { + hook->hook_flags |= RUBY_EVENT_HOOK_FLAG_DELETED; + list->need_clean = TRUE; + } + else { + events |= hook->events; + } + hook = hook->next; + } + + list->events = events; +} + /* * call-seq: * trace.enable -> true or false @@ -1157,11 +1320,20 @@ rb_tracepoint_disable(VALUE tpval) * */ static VALUE -tracepoint_enable_m(VALUE tpval) +tracepoint_enable_m(VALUE tpval, VALUE target, VALUE target_line) { rb_tp_t *tp = tpptr(tpval); int previous_tracing = tp->tracing; - rb_tracepoint_enable(tpval); + + if (NIL_P(target)) { + if (!NIL_P(target_line)) { + rb_raise(rb_eArgError, "only target_line is specified"); + } + rb_tracepoint_enable(tpval); + } + else { + rb_tracepoint_enable_for_target(tpval, target, target_line); + } if (rb_block_given_p()) { return rb_ensure(rb_yield, Qundef, @@ -1207,19 +1379,25 @@ tracepoint_enable_m(VALUE tpval) * trace.disable { p tp.lineno } * #=> RuntimeError: access from outside */ + static VALUE tracepoint_disable_m(VALUE tpval) { rb_tp_t *tp = tpptr(tpval); int previous_tracing = tp->tracing; - rb_tracepoint_disable(tpval); if (rb_block_given_p()) { - return rb_ensure(rb_yield, Qundef, + if (tp->local_target_set != Qfalse) { + rb_raise(rb_eArgError, "can't disable a targetting TracePoint in a block"); + } + + rb_tracepoint_disable(tpval); + return rb_ensure(rb_yield, Qundef, previous_tracing ? rb_tracepoint_enable : rb_tracepoint_disable, tpval); } else { + rb_tracepoint_disable(tpval); return previous_tracing ? Qtrue : Qfalse; } } @@ -1464,14 +1642,12 @@ tracepoint_stat_s(VALUE self) rb_vm_t *vm = GET_VM(); VALUE stat = rb_hash_new(); - tracepoint_stat_event_hooks(stat, vm->self, vm->event_hooks.hooks); + tracepoint_stat_event_hooks(stat, vm->self, vm->global_hooks.hooks); /* TODO: thread local hooks */ return stat; } -static void Init_postponed_job(void); - /* This function is called from inits.c */ void Init_vm_trace(void) @@ -1547,7 +1723,7 @@ Init_vm_trace(void) */ rb_define_singleton_method(rb_cTracePoint, "trace", tracepoint_trace_s, -1); - rb_define_method(rb_cTracePoint, "enable", tracepoint_enable_m, 0); + rb_define_method(rb_cTracePoint, "__enable", tracepoint_enable_m, 2); rb_define_method(rb_cTracePoint, "disable", tracepoint_disable_m, 0); rb_define_method(rb_cTracePoint, "enabled?", rb_tracepoint_enabled_p, 0); @@ -1566,10 +1742,6 @@ Init_vm_trace(void) rb_define_method(rb_cTracePoint, "raised_exception", tracepoint_attr_raised_exception, 0); rb_define_singleton_method(rb_cTracePoint, "stat", tracepoint_stat_s, 0); - - /* initialized for postponed job */ - - Init_postponed_job(); } typedef struct rb_postponed_job_struct { @@ -1580,12 +1752,18 @@ typedef struct rb_postponed_job_struct { #define MAX_POSTPONED_JOB 1000 #define MAX_POSTPONED_JOB_SPECIAL_ADDITION 24 -static void -Init_postponed_job(void) +struct rb_workqueue_job { + struct list_node jnode; /* <=> vm->workqueue */ + rb_postponed_job_t job; +}; + +void +Init_vm_postponed_job(void) { rb_vm_t *vm = GET_VM(); vm->postponed_job_buffer = ALLOC_N(rb_postponed_job_t, MAX_POSTPONED_JOB); vm->postponed_job_index = 0; + /* workqueue is initialized when VM locks are initialized */ } enum postponed_job_register_result { @@ -1667,13 +1845,43 @@ rb_postponed_job_register_one(unsigned int flags, rb_postponed_job_func_t func, } } +/* + * thread-safe and called from non-Ruby thread + * returns FALSE on failure (ENOMEM), TRUE otherwise + */ +int +rb_workqueue_register(unsigned flags, rb_postponed_job_func_t func, void *data) +{ + struct rb_workqueue_job *wq_job = malloc(sizeof(*wq_job)); + rb_vm_t *vm = GET_VM(); + + if (!wq_job) return FALSE; + wq_job->job.func = func; + wq_job->job.data = data; + + rb_nativethread_lock_lock(&vm->workqueue_lock); + list_add_tail(&vm->workqueue, &wq_job->jnode); + rb_nativethread_lock_unlock(&vm->workqueue_lock); + + RUBY_VM_SET_POSTPONED_JOB_INTERRUPT(GET_EC()); + + return TRUE; +} + void rb_postponed_job_flush(rb_vm_t *vm) { rb_execution_context_t *ec = GET_EC(); - const unsigned long block_mask = POSTPONED_JOB_INTERRUPT_MASK|TRAP_INTERRUPT_MASK; - volatile unsigned long saved_mask = ec->interrupt_mask & block_mask; + const rb_atomic_t block_mask = POSTPONED_JOB_INTERRUPT_MASK|TRAP_INTERRUPT_MASK; + volatile rb_atomic_t saved_mask = ec->interrupt_mask & block_mask; VALUE volatile saved_errno = ec->errinfo; + struct list_head tmp; + + list_head_init(&tmp); + + rb_nativethread_lock_lock(&vm->workqueue_lock); + list_append_list(&tmp, &vm->workqueue); + rb_nativethread_lock_unlock(&vm->workqueue_lock); ec->errinfo = Qnil; /* mask POSTPONED_JOB dispatch */ @@ -1682,16 +1890,33 @@ rb_postponed_job_flush(rb_vm_t *vm) EC_PUSH_TAG(ec); if (EC_EXEC_TAG() == TAG_NONE) { int index; + struct rb_workqueue_job *wq_job; + while ((index = vm->postponed_job_index) > 0) { if (ATOMIC_CAS(vm->postponed_job_index, index, index-1) == index) { rb_postponed_job_t *pjob = &vm->postponed_job_buffer[index-1]; (*pjob->func)(pjob->data); } } + while ((wq_job = list_pop(&tmp, struct rb_workqueue_job, jnode))) { + rb_postponed_job_t pjob = wq_job->job; + + free(wq_job); + (pjob.func)(pjob.data); + } } EC_POP_TAG(); } /* restore POSTPONED_JOB mask */ ec->interrupt_mask &= ~(saved_mask ^ block_mask); ec->errinfo = saved_errno; + + /* don't leak memory if a job threw an exception */ + if (!list_empty(&tmp)) { + rb_nativethread_lock_lock(&vm->workqueue_lock); + list_prepend_list(&vm->workqueue, &tmp); + rb_nativethread_lock_unlock(&vm->workqueue_lock); + + RUBY_VM_SET_POSTPONED_JOB_INTERRUPT(GET_EC()); + } } diff --git a/vsnprintf.c b/vsnprintf.c index d221e757bf567d..04184d7a6afdf1 100644 --- a/vsnprintf.c +++ b/vsnprintf.c @@ -1250,7 +1250,7 @@ cvt(double value, int ndigits, int flags, char *sign, int *decpt, int ch, int *l if (value < 0) { value = -value; *sign = '-'; - } else if (value == 0.0 && 1.0/value < 0) { + } else if (value == 0.0 && signbit(value)) { *sign = '-'; } else { *sign = '\000'; diff --git a/wercker.yml b/wercker.yml index 1a8feb19adb9f3..af7c4ccb15f948 100644 --- a/wercker.yml +++ b/wercker.yml @@ -6,7 +6,28 @@ box: ruby:2.5-stretch no-response-timeout: 30 command-timeout: 60 -test-mjit: +### Code to generate test-all definition with --jit-wait ### +# This aims to relax no-output timeout, and to isolate TracePoint testing since JIT is not supporting trace_* insns for now. + +# allow_failures = [] +# dirs = Dir.glob('test/*/').sort +# index = 1 +# puts <<-EOS +# - script: +# name: make test-all#{index} -- others (JIT wait) +# code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTOPTS="#{dirs.map { |d| "--exclude #{d}" }.join(' ')} --color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" +# EOS +# dirs.each do |dir| +# index += 1 +# puts <<-EOS +# - script: +# name: make test-all#{index} -- #{dir} (JIT wait#{(', allow failure' if allow_failures.include?(dir))}) +# code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="#{dir}" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait"#{(' || true' if allow_failures.include?(dir))} +# EOS +# end + +# --jit + first half of --jit-wait. +mjit-test1: steps: - install-packages: packages: bison sudo @@ -22,24 +43,152 @@ test-mjit: - script: name: make all install code: /usr/bin/sudo -H -u test -- make -j$(nproc) all install + + # --jit - script: name: make test (JIT) code: /usr/bin/sudo -H -u test -- make test RUN_OPTS="--disable-gems --jit --jit-warnings" - - script: - name: make test-all (JIT) - code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit --jit-warnings" TESTOPTS="--color=never --job-status=normal --longest 10 --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/test-mjit" - script: name: make test-spec (JIT) code: /usr/bin/sudo -H -u test -- make test-spec RUN_OPTS="--disable-gems --jit --jit-warnings" + - script: + name: make test-all (JIT) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit --jit-warnings" TESTOPTS="-v --color=never --job-status=normal --longest 10 --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit" + + # --jit-wait + - script: + name: make test (JIT wait) + code: /usr/bin/sudo -H -u test -- make test RUN_OPTS="--disable-gems --jit-wait --jit-warnings" + - script: + name: make test-spec (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-spec RUN_OPTS="--disable-gems --jit-wait --jit-warnings" + + # -- AUTO GENERATED 1st half START (by above code) --- + - script: + name: make test-all1 -- others (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTOPTS="--exclude test/-ext-/ --exclude test/base64/ --exclude test/benchmark/ --exclude test/bigdecimal/ --exclude test/cgi/ --exclude test/coverage/ --exclude test/csv/ --exclude test/date/ --exclude test/dbm/ --exclude test/digest/ --exclude test/drb/ --exclude test/dtrace/ --exclude test/erb/ --exclude test/etc/ --exclude test/excludes/ --exclude test/fiddle/ --exclude test/fileutils/ --exclude test/gdbm/ --exclude test/io/ --exclude test/irb/ --exclude test/json/ --exclude test/lib/ --exclude test/logger/ --exclude test/matrix/ --exclude test/minitest/ --exclude test/misc/ --exclude test/mkmf/ --exclude test/monitor/ --exclude test/net/ --exclude test/nkf/ --exclude test/objspace/ --exclude test/open-uri/ --exclude test/openssl/ --exclude test/optparse/ --exclude test/ostruct/ --exclude test/pathname/ --exclude test/psych/ --exclude test/rdoc/ --exclude test/readline/ --exclude test/resolv/ --exclude test/rexml/ --exclude test/rinda/ --exclude test/ripper/ --exclude test/rss/ --exclude test/ruby/ --exclude test/rubygems/ --exclude test/scanf/ --exclude test/sdbm/ --exclude test/shell/ --exclude test/socket/ --exclude test/stringio/ --exclude test/strscan/ --exclude test/syslog/ --exclude test/testunit/ --exclude test/uri/ --exclude test/webrick/ --exclude test/win32ole/ --exclude test/yaml/ --exclude test/zlib/ --color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all2 -- test/-ext-/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/-ext-/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all3 -- test/base64/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/base64/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all4 -- test/benchmark/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/benchmark/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all5 -- test/bigdecimal/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/bigdecimal/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all6 -- test/cgi/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/cgi/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all7 -- test/coverage/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/coverage/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all8 -- test/csv/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/csv/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all9 -- test/date/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/date/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all10 -- test/dbm/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/dbm/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all11 -- test/digest/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/digest/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all12 -- test/drb/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/drb/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all13 -- test/dtrace/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/dtrace/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all14 -- test/erb/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/erb/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all15 -- test/etc/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/etc/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all16 -- test/excludes/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/excludes/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all17 -- test/fiddle/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/fiddle/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all18 -- test/fileutils/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/fileutils/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all19 -- test/gdbm/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/gdbm/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all20 -- test/io/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/io/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all21 -- test/irb/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/irb/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all22 -- test/json/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/json/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all23 -- test/lib/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/lib/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all24 -- test/logger/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/logger/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all25 -- test/matrix/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/matrix/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all26 -- test/minitest/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/minitest/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all27 -- test/misc/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/misc/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all28 -- test/mkmf/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/mkmf/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all29 -- test/monitor/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/monitor/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all30 -- test/net/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/net/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all31 -- test/nkf/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/nkf/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all32 -- test/objspace/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/objspace/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all33 -- test/open-uri/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/open-uri/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all34 -- test/openssl/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/openssl/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all35 -- test/optparse/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/optparse/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all36 -- test/ostruct/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/ostruct/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all37 -- test/pathname/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/pathname/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all38 -- test/psych/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/psych/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + # -- AUTO GENERATED 1st half END --- after-steps: - wantedly/pretty-slack-notify: webhook_url: $SLACK_WEBHOOK_URL - username: Wercker test-mjit + username: Wercker mjit-test1 channel: alerts notify_on: "failed" branches: ^trunk$ -test-mjit-wait: +# second half of --jit-wait. +mjit-test2: steps: - install-packages: packages: bison sudo @@ -55,23 +204,79 @@ test-mjit-wait: - script: name: make all install code: /usr/bin/sudo -H -u test -- make -j$(nproc) all install + + # -- AUTO GENERATED 2nd half START (by above code) --- - script: - name: make test (JIT wait) - code: /usr/bin/sudo -H -u test -- make test RUN_OPTS="--disable-gems --jit-wait --jit-warnings" - # split test-all to 2 steps to loosen timeout + name: make test-all39 -- test/rdoc/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/rdoc/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" - script: - name: make test-all1 (JIT wait) # only: test/ruby/, test/testunit/, test/rubygems/, test/rss - code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/ruby/ test/testunit/ test/rubygems/ test/rss/" TESTOPTS="--color=never --job-status=normal --longest 10 --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/test-mjit-wait" + name: make test-all40 -- test/readline/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/readline/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" - script: - name: make test-all2 (JIT wait) # except: test/ruby/, test/testunit/, test/rubygems/, test/rss - code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTOPTS="--exclude test/ruby/ --exclude test/testunit/ --exclude test/rubygems/ --exclude test/rss/ --color=never --job-status=normal --longest 10 --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/test-mjit-wait" + name: make test-all41 -- test/resolv/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/resolv/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" - script: - name: make test-spec (JIT wait) - code: /usr/bin/sudo -H -u test -- make test-spec RUN_OPTS="--disable-gems --jit-wait --jit-warnings" + name: make test-all42 -- test/rexml/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/rexml/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all43 -- test/rinda/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/rinda/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all44 -- test/ripper/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/ripper/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all45 -- test/rss/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/rss/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all46 -- test/ruby/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/ruby/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all47 -- test/rubygems/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/rubygems/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all48 -- test/scanf/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/scanf/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all49 -- test/sdbm/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/sdbm/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all50 -- test/shell/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/shell/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all51 -- test/socket/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/socket/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all52 -- test/stringio/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/stringio/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all53 -- test/strscan/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/strscan/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all54 -- test/syslog/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/syslog/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all55 -- test/testunit/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/testunit/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all56 -- test/uri/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/uri/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all57 -- test/webrick/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/webrick/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all58 -- test/win32ole/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/win32ole/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all59 -- test/yaml/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/yaml/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + - script: + name: make test-all60 -- test/zlib/ (JIT wait) + code: /usr/bin/sudo -H -u test -- make test-all RUN_OPTS="--disable-gems --jit-wait --jit-warnings" TESTS="test/zlib/" TESTOPTS="--color=never --job-status=normal --subprocess-timeout-scale=3.0 --excludes=test/excludes/_wercker/jit-wait" + # -- AUTO GENERATED 2nd half END --- after-steps: - wantedly/pretty-slack-notify: webhook_url: $SLACK_WEBHOOK_URL - username: Wercker test-mjit-wait + username: Wercker mjit-test2 channel: alerts notify_on: "failed" branches: ^trunk$ diff --git a/win32/Makefile.sub b/win32/Makefile.sub index f81af2d55cd3f7..7692173140df14 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -14,6 +14,12 @@ PWD = $(MAKEDIR) MFLAGS=-l !endif +!ifndef REVISION_FORCE +!if "$(HAVE_BASERUBY)" == "yes" +REVISION_FORCE = PHONY +!endif +!endif + !ifndef CROSS_COMPILING CROSS_COMPILING = no !endif @@ -283,6 +289,14 @@ MISSING = $(MISSING) acosh.obj cbrt.obj erf.obj nan.obj tgamma.obj MISSING = $(MISSING) explicit_bzero.obj !endif DLNOBJ = dln.obj +!if "$(ARCH)" == "x64" +COROUTINE_H = coroutine/Win64/Context.h +!elseif "$(ARCH)" == "i386" +COROUTINE_H = coroutine/Win32/Context.h +!else +COROUTINE_H = +!endif +COROUTINE_OBJ = $(COROUTINE_H:.h=.obj) ARFLAGS = -machine:$(MACHINE) -out: LD = $(CC) @@ -851,6 +865,9 @@ $(CONFIG_H): $(MKFILES) $(srcdir)/win32/Makefile.sub $(win_srcdir)/Makefile.sub !if "$(MACHINE)" == "x86" || "$(ARCH)" == "x64" || "$(ARCH)" == "ia64" #define STACK_GROW_DIRECTION -1 !endif +!if defined(COROUTINE_H) +#define FIBER_USE_COROUTINE "$(COROUTINE_H)" +!endif #define DEFAULT_KCODE KCODE_NONE #define LOAD_RELATIVE 1 #define DLEXT ".so" @@ -1208,6 +1225,13 @@ $(ruby_pc): $(RBCONFIG) -output=$@ -mode=$(INSTALL_DATA_MODE) -config=rbconfig.rb \ $(srcdir)/template/ruby.pc.in +{$(srcdir)/coroutine/Win32}.asm{coroutine/Win32}.obj: + $(ECHO) assembling $(<:\=/) + $(Q) $(AS) $(ASFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(<:\=/) +{$(srcdir)/coroutine/Win64}.asm{coroutine/Win64}.obj: + $(ECHO) assembling $(<:\=/) + $(Q) $(AS) $(ASFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(<:\=/) + {$(srcdir)/enc/trans}.c.obj: $(ECHO) compiling $(<:\=/) $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(<:\=/) @@ -1352,7 +1376,6 @@ mjit_config.h: #define RUBY_MJIT_CONFIG_H 1 #define MJIT_CONFIG_ESCAPED_EQ "=" -#define MJIT_BUILD_DIR "$(MAKEDIR)" #define MJIT_PRECOMPILED_HEADER_NAME "/$(MJIT_HEADER_INSTALL_DIR)/$(MJIT_PRECOMPILED_HEADER_NAME)" <> $@ @echo #endif /* RUBY_MJIT_CONFIG_H */>> $@ @$(Q:@=: :) type $@ + +!if defined(COROUTINE_H) +cont.$(OBJEXT): {$(VPATH)}$(COROUTINE_H) +!endif diff --git a/win32/setup.mak b/win32/setup.mak index 8caafa4eeabbc5..eb99c6ed1dfbfe 100644 --- a/win32/setup.mak +++ b/win32/setup.mak @@ -20,8 +20,9 @@ MAKE = $(MAKE) -f $(MAKEFILE) MAKEFILE = Makefile !endif CPU = PROCESSOR_LEVEL -CC = cl -nologo +CC = $(CC) -nologo CPP = $(CC) -EP +AS = $(AS) -nologo all: -prologue- -generic- -epilogue- i386-mswin32: -prologue- -i386- -epilogue- @@ -237,7 +238,11 @@ MACHINE = x86 # XLDFLAGS = # RFLAGS = -r # EXTLIBS = -CC = cl -nologo +CC = $(CC) +AS = $(AS) +<< + @(for %I in (cl.exe) do @set MJIT_CC=%~$$PATH:I) && (call echo MJIT_CC = "%MJIT_CC:\=/%" -nologo>>$(MAKEFILE)) + @type << >>$(MAKEFILE) $(BANG)include $$(srcdir)/win32/Makefile.sub << diff --git a/win32/win32.c b/win32/win32.c index 820b16c40b3cdf..d28bd56452de7b 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -4429,11 +4429,11 @@ fcntl(int fd, int cmd, ...) /* License: Ruby's */ int -rb_w32_set_nonblock(int fd) +rb_w32_set_nonblock2(int fd, int nonblock) { SOCKET sock = TO_SOCKET(fd); if (is_socket(sock)) { - return setfl(sock, O_NONBLOCK); + return setfl(sock, nonblock ? O_NONBLOCK : 0); } else if (is_pipe(sock)) { DWORD state; @@ -4441,7 +4441,12 @@ rb_w32_set_nonblock(int fd) errno = map_errno(GetLastError()); return -1; } - state |= PIPE_NOWAIT; + if (nonblock) { + state |= PIPE_NOWAIT; + } + else { + state &= ~PIPE_NOWAIT; + } if (!SetNamedPipeHandleState((HANDLE)sock, &state, NULL, NULL)) { errno = map_errno(GetLastError()); return -1; @@ -4454,6 +4459,12 @@ rb_w32_set_nonblock(int fd) } } +int +rb_w32_set_nonblock(int fd) +{ + return rb_w32_set_nonblock2(fd, TRUE); +} + #ifndef WNOHANG #define WNOHANG -1 #endif