From 84e0bb34f395a7523d06202c4675649075fcb8f2 Mon Sep 17 00:00:00 2001 From: hsbt Date: Fri, 8 Sep 2017 08:45:41 +0000 Subject: Merge bundler to standard libraries. rubygems 2.7.x depends bundler-1.15.x. This is preparation for rubygems and bundler migration. * lib/bundler.rb, lib/bundler/*: files of bundler-1.15.4 * spec/bundler/*: rspec examples of bundler-1.15.4. I applied patches. * https://github.com/bundler/bundler/pull/6007 * Exclude not working examples on ruby repository. * Fake ruby interpriter instead of installed ruby. * Makefile.in: Added test task named `test-bundler`. This task is only working macOS/linux yet. I'm going to support Windows environment later. * tool/sync_default_gems.rb: Added sync task for bundler. [Feature #12733][ruby-core:77172] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@59779 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- .gitignore | 4 + Makefile.in | 14 + bin/bundle | 31 + bin/bundle_ruby | 60 + bin/bundler | 4 + lib/bundler.gemspec | 251 ++++ lib/bundler.rb | 533 ++++++++ lib/bundler/capistrano.rb | 17 + lib/bundler/cli.rb | 658 ++++++++++ lib/bundler/cli/add.rb | 26 + lib/bundler/cli/binstubs.rb | 41 + lib/bundler/cli/cache.rb | 35 + lib/bundler/cli/check.rb | 40 + lib/bundler/cli/clean.rb | 26 + lib/bundler/cli/common.rb | 93 ++ lib/bundler/cli/config.rb | 118 ++ lib/bundler/cli/console.rb | 42 + lib/bundler/cli/doctor.rb | 93 ++ lib/bundler/cli/exec.rb | 104 ++ lib/bundler/cli/gem.rb | 248 ++++ lib/bundler/cli/info.rb | 51 + lib/bundler/cli/init.rb | 35 + lib/bundler/cli/inject.rb | 59 + lib/bundler/cli/install.rb | 214 +++ lib/bundler/cli/issue.rb | 40 + lib/bundler/cli/lock.rb | 64 + lib/bundler/cli/open.rb | 26 + lib/bundler/cli/outdated.rb | 255 ++++ lib/bundler/cli/package.rb | 46 + lib/bundler/cli/platform.rb | 45 + lib/bundler/cli/plugin.rb | 23 + lib/bundler/cli/pristine.rb | 36 + lib/bundler/cli/show.rb | 76 ++ lib/bundler/cli/update.rb | 63 + lib/bundler/cli/viz.rb | 30 + lib/bundler/compact_index_client.rb | 108 ++ lib/bundler/compact_index_client/cache.rb | 119 ++ lib/bundler/compact_index_client/updater.rb | 106 ++ lib/bundler/constants.rb | 6 + lib/bundler/current_ruby.rb | 85 ++ lib/bundler/definition.rb | 940 +++++++++++++ lib/bundler/dep_proxy.rb | 46 + lib/bundler/dependency.rb | 139 ++ lib/bundler/deployment.rb | 69 + lib/bundler/deprecate.rb | 32 + lib/bundler/dsl.rb | 564 ++++++++ lib/bundler/endpoint_specification.rb | 132 ++ lib/bundler/env.rb | 94 ++ lib/bundler/environment_preserver.rb | 38 + lib/bundler/errors.rb | 157 +++ lib/bundler/feature_flag.rb | 32 + lib/bundler/fetcher.rb | 305 +++++ lib/bundler/fetcher/base.rb | 51 + lib/bundler/fetcher/compact_index.rb | 135 ++ lib/bundler/fetcher/dependency.rb | 81 ++ lib/bundler/fetcher/downloader.rb | 78 ++ lib/bundler/fetcher/index.rb | 51 + lib/bundler/friendly_errors.rb | 126 ++ lib/bundler/gem_helper.rb | 193 +++ lib/bundler/gem_helpers.rb | 100 ++ lib/bundler/gem_remote_fetcher.rb | 42 + lib/bundler/gem_tasks.rb | 6 + lib/bundler/gem_version_promoter.rb | 175 +++ lib/bundler/gemdeps.rb | 28 + lib/bundler/graph.rb | 151 +++ lib/bundler/index.rb | 213 +++ lib/bundler/injector.rb | 91 ++ lib/bundler/inline.rb | 76 ++ lib/bundler/installer.rb | 233 ++++ lib/bundler/installer/gem_installer.rb | 76 ++ lib/bundler/installer/parallel_installer.rb | 197 +++ lib/bundler/installer/standalone.rb | 52 + lib/bundler/lazy_specification.rb | 122 ++ lib/bundler/lockfile_parser.rb | 250 ++++ lib/bundler/match_platform.rb | 23 + lib/bundler/mirror.rb | 220 ++++ lib/bundler/plugin.rb | 284 ++++ lib/bundler/plugin/api.rb | 81 ++ lib/bundler/plugin/api/source.rb | 299 +++++ lib/bundler/plugin/dsl.rb | 53 + lib/bundler/plugin/index.rb | 157 +++ lib/bundler/plugin/installer.rb | 95 ++ lib/bundler/plugin/installer/git.rb | 38 + lib/bundler/plugin/installer/rubygems.rb | 27 + lib/bundler/plugin/source_list.rb | 28 + lib/bundler/psyched_yaml.rb | 27 + lib/bundler/remote_specification.rb | 113 ++ lib/bundler/resolver.rb | 410 ++++++ lib/bundler/retry.rb | 65 + lib/bundler/ruby_dsl.rb | 17 + lib/bundler/ruby_version.rb | 151 +++ lib/bundler/rubygems_ext.rb | 209 +++ lib/bundler/rubygems_gem_installer.rb | 76 ++ lib/bundler/rubygems_integration.rb | 862 ++++++++++++ lib/bundler/runtime.rb | 320 +++++ lib/bundler/settings.rb | 340 +++++ lib/bundler/setup.rb | 27 + lib/bundler/shared_helpers.rb | 301 +++++ lib/bundler/similarity_detector.rb | 62 + lib/bundler/source.rb | 58 + lib/bundler/source/gemspec.rb | 17 + lib/bundler/source/git.rb | 324 +++++ lib/bundler/source/git/git_proxy.rb | 252 ++++ lib/bundler/source/path.rb | 249 ++++ lib/bundler/source/path/installer.rb | 72 + lib/bundler/source/rubygems.rb | 462 +++++++ lib/bundler/source/rubygems/remote.rb | 63 + lib/bundler/source_list.rb | 126 ++ lib/bundler/spec_set.rb | 187 +++ lib/bundler/ssl_certs/.document | 1 + lib/bundler/ssl_certs/certificate_manager.rb | 65 + .../index.rubygems.org/GlobalSignRootCA.pem | 21 + .../DigiCertHighAssuranceEVRootCA.pem | 23 + .../rubygems.org/AddTrustExternalCARoot.pem | 25 + lib/bundler/stub_specification.rb | 107 ++ lib/bundler/templates/Executable | 17 + lib/bundler/templates/Executable.standalone | 14 + lib/bundler/templates/Gemfile | 6 + lib/bundler/templates/newgem/.travis.yml.tt | 5 + lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt | 74 ++ lib/bundler/templates/newgem/Gemfile.tt | 6 + lib/bundler/templates/newgem/LICENSE.txt.tt | 21 + lib/bundler/templates/newgem/README.md.tt | 47 + lib/bundler/templates/newgem/Rakefile.tt | 29 + lib/bundler/templates/newgem/bin/console.tt | 14 + lib/bundler/templates/newgem/bin/setup.tt | 8 + lib/bundler/templates/newgem/exe/newgem.tt | 3 + .../templates/newgem/ext/newgem/extconf.rb.tt | 3 + .../templates/newgem/ext/newgem/newgem.c.tt | 9 + .../templates/newgem/ext/newgem/newgem.h.tt | 6 + lib/bundler/templates/newgem/gitignore.tt | 21 + lib/bundler/templates/newgem/lib/newgem.rb.tt | 12 + .../templates/newgem/lib/newgem/version.rb.tt | 7 + lib/bundler/templates/newgem/newgem.gemspec.tt | 46 + lib/bundler/templates/newgem/rspec.tt | 2 + .../templates/newgem/spec/newgem_spec.rb.tt | 11 + .../templates/newgem/spec/spec_helper.rb.tt | 14 + .../templates/newgem/test/newgem_test.rb.tt | 11 + .../templates/newgem/test/test_helper.rb.tt | 4 + lib/bundler/ui.rb | 8 + lib/bundler/ui/rg_proxy.rb | 18 + lib/bundler/ui/shell.rb | 133 ++ lib/bundler/ui/silent.rb | 68 + lib/bundler/uri_credentials_filter.rb | 36 + lib/bundler/vendor/molinillo/lib/molinillo.rb | 10 + .../lib/molinillo/delegates/resolution_state.rb | 50 + .../molinillo/delegates/specification_provider.rb | 80 ++ .../molinillo/lib/molinillo/dependency_graph.rb | 222 ++++ .../lib/molinillo/dependency_graph/action.rb | 35 + .../dependency_graph/add_edge_no_circular.rb | 65 + .../lib/molinillo/dependency_graph/add_vertex.rb | 61 + .../lib/molinillo/dependency_graph/delete_edge.rb | 62 + .../dependency_graph/detach_vertex_named.rb | 60 + .../lib/molinillo/dependency_graph/log.rb | 125 ++ .../lib/molinillo/dependency_graph/set_payload.rb | 45 + .../lib/molinillo/dependency_graph/tag.rb | 35 + .../lib/molinillo/dependency_graph/vertex.rb | 125 ++ .../vendor/molinillo/lib/molinillo/errors.rb | 75 ++ .../vendor/molinillo/lib/molinillo/gem_metadata.rb | 5 + .../molinillo/modules/specification_provider.rb | 100 ++ .../vendor/molinillo/lib/molinillo/modules/ui.rb | 65 + .../vendor/molinillo/lib/molinillo/resolution.rb | 494 +++++++ .../vendor/molinillo/lib/molinillo/resolver.rb | 45 + .../vendor/molinillo/lib/molinillo/state.rb | 54 + .../net-http-persistent/lib/net/http/faster.rb | 27 + .../net-http-persistent/lib/net/http/persistent.rb | 1233 +++++++++++++++++ .../lib/net/http/persistent/ssl_reuse.rb | 129 ++ lib/bundler/vendor/thor/lib/thor.rb | 509 ++++++++ lib/bundler/vendor/thor/lib/thor/actions.rb | 321 +++++ .../vendor/thor/lib/thor/actions/create_file.rb | 104 ++ .../vendor/thor/lib/thor/actions/create_link.rb | 60 + .../vendor/thor/lib/thor/actions/directory.rb | 118 ++ .../thor/lib/thor/actions/empty_directory.rb | 143 ++ .../thor/lib/thor/actions/file_manipulation.rb | 364 ++++++ .../thor/lib/thor/actions/inject_into_file.rb | 109 ++ lib/bundler/vendor/thor/lib/thor/base.rb | 679 ++++++++++ lib/bundler/vendor/thor/lib/thor/command.rb | 135 ++ .../thor/core_ext/hash_with_indifferent_access.rb | 97 ++ .../thor/lib/thor/core_ext/io_binary_read.rb | 12 + .../vendor/thor/lib/thor/core_ext/ordered_hash.rb | 129 ++ lib/bundler/vendor/thor/lib/thor/error.rb | 32 + lib/bundler/vendor/thor/lib/thor/group.rb | 281 ++++ lib/bundler/vendor/thor/lib/thor/invocation.rb | 177 +++ lib/bundler/vendor/thor/lib/thor/line_editor.rb | 17 + .../vendor/thor/lib/thor/line_editor/basic.rb | 37 + .../vendor/thor/lib/thor/line_editor/readline.rb | 88 ++ lib/bundler/vendor/thor/lib/thor/parser.rb | 4 + .../vendor/thor/lib/thor/parser/argument.rb | 70 + .../vendor/thor/lib/thor/parser/arguments.rb | 175 +++ lib/bundler/vendor/thor/lib/thor/parser/option.rb | 146 +++ lib/bundler/vendor/thor/lib/thor/parser/options.rb | 221 ++++ lib/bundler/vendor/thor/lib/thor/rake_compat.rb | 71 + lib/bundler/vendor/thor/lib/thor/runner.rb | 324 +++++ lib/bundler/vendor/thor/lib/thor/shell.rb | 81 ++ lib/bundler/vendor/thor/lib/thor/shell/basic.rb | 437 +++++++ lib/bundler/vendor/thor/lib/thor/shell/color.rb | 149 +++ lib/bundler/vendor/thor/lib/thor/shell/html.rb | 126 ++ lib/bundler/vendor/thor/lib/thor/util.rb | 268 ++++ lib/bundler/vendor/thor/lib/thor/version.rb | 3 + lib/bundler/vendored_molinillo.rb | 3 + lib/bundler/vendored_persistent.rb | 17 + lib/bundler/vendored_thor.rb | 7 + lib/bundler/version.rb | 24 + lib/bundler/version_ranges.rb | 75 ++ lib/bundler/vlad.rb | 12 + lib/bundler/worker.rb | 105 ++ lib/bundler/yaml_serializer.rb | 90 ++ spec/README.md | 13 +- spec/bundler/bundler/bundler_spec.rb | 212 +++ spec/bundler/bundler/cli_spec.rb | 150 +++ .../bundler/compact_index_client/updater_spec.rb | 30 + spec/bundler/bundler/definition_spec.rb | 277 ++++ spec/bundler/bundler/dsl_spec.rb | 268 ++++ .../bundler/bundler/endpoint_specification_spec.rb | 66 + spec/bundler/bundler/env_spec.rb | 96 ++ spec/bundler/bundler/environment_preserver_spec.rb | 80 ++ spec/bundler/bundler/fetcher/base_spec.rb | 77 ++ spec/bundler/bundler/fetcher/compact_index_spec.rb | 94 ++ spec/bundler/bundler/fetcher/dependency_spec.rb | 288 ++++ spec/bundler/bundler/fetcher/downloader_spec.rb | 251 ++++ spec/bundler/bundler/fetcher/index_spec.rb | 100 ++ spec/bundler/bundler/fetcher_spec.rb | 116 ++ spec/bundler/bundler/friendly_errors_spec.rb | 270 ++++ spec/bundler/bundler/gem_helper_spec.rb | 260 ++++ spec/bundler/bundler/gem_version_promoter_spec.rb | 179 +++ spec/bundler/bundler/index_spec.rb | 37 + .../bundler/installer/gem_installer_spec.rb | 28 + .../bundler/installer/parallel_installer_spec.rb | 47 + .../bundler/installer/spec_installation_spec.rb | 62 + spec/bundler/bundler/lockfile_parser_spec.rb | 94 ++ spec/bundler/bundler/mirror_spec.rb | 329 +++++ spec/bundler/bundler/plugin/api/source_spec.rb | 83 ++ spec/bundler/bundler/plugin/api_spec.rb | 84 ++ spec/bundler/bundler/plugin/dsl_spec.rb | 39 + spec/bundler/bundler/plugin/index_spec.rb | 179 +++ spec/bundler/bundler/plugin/installer_spec.rb | 100 ++ spec/bundler/bundler/plugin/source_list_spec.rb | 26 + spec/bundler/bundler/plugin_spec.rb | 292 +++++ spec/bundler/bundler/psyched_yaml_spec.rb | 9 + spec/bundler/bundler/remote_specification_spec.rb | 188 +++ spec/bundler/bundler/retry_spec.rb | 82 ++ spec/bundler/bundler/ruby_dsl_spec.rb | 95 ++ spec/bundler/bundler/ruby_version_spec.rb | 524 ++++++++ spec/bundler/bundler/rubygems_integration_spec.rb | 115 ++ spec/bundler/bundler/settings_spec.rb | 284 ++++ spec/bundler/bundler/shared_helpers_spec.rb | 451 +++++++ spec/bundler/bundler/source/git/git_proxy_spec.rb | 117 ++ spec/bundler/bundler/source/path_spec.rb | 31 + .../bundler/bundler/source/rubygems/remote_spec.rb | 162 +++ spec/bundler/bundler/source/rubygems_spec.rb | 34 + spec/bundler/bundler/source_list_spec.rb | 441 +++++++ spec/bundler/bundler/source_spec.rb | 155 +++ spec/bundler/bundler/spec_set_spec.rb | 43 + .../bundler/ssl_certs/certificate_manager_spec.rb | 141 ++ spec/bundler/bundler/stub_specification_spec.rb | 25 + spec/bundler/bundler/ui_spec.rb | 42 + .../bundler/bundler/uri_credentials_filter_spec.rb | 128 ++ spec/bundler/bundler/version_ranges_spec.rb | 37 + spec/bundler/bundler/worker_spec.rb | 22 + spec/bundler/bundler/yaml_serializer_spec.rb | 193 +++ spec/bundler/cache/cache_path_spec.rb | 34 + spec/bundler/cache/gems_spec.rb | 292 +++++ spec/bundler/cache/git_spec.rb | 215 +++ spec/bundler/cache/path_spec.rb | 140 ++ spec/bundler/cache/platform_spec.rb | 54 + spec/bundler/commands/add_spec.rb | 109 ++ spec/bundler/commands/binstubs_spec.rb | 261 ++++ spec/bundler/commands/check_spec.rb | 348 +++++ spec/bundler/commands/clean_spec.rb | 703 ++++++++++ spec/bundler/commands/config_spec.rb | 385 ++++++ spec/bundler/commands/console_spec.rb | 107 ++ spec/bundler/commands/doctor_spec.rb | 64 + spec/bundler/commands/exec_spec.rb | 736 +++++++++++ spec/bundler/commands/help_spec.rb | 99 ++ spec/bundler/commands/info_spec.rb | 58 + spec/bundler/commands/init_spec.rb | 66 + spec/bundler/commands/inject_spec.rb | 114 ++ spec/bundler/commands/install_spec.rb | 513 ++++++++ spec/bundler/commands/issue_spec.rb | 17 + spec/bundler/commands/licenses_spec.rb | 32 + spec/bundler/commands/lock_spec.rb | 315 +++++ spec/bundler/commands/newgem_spec.rb | 909 +++++++++++++ spec/bundler/commands/open_spec.rb | 93 ++ spec/bundler/commands/outdated_spec.rb | 731 +++++++++++ spec/bundler/commands/package_spec.rb | 306 +++++ spec/bundler/commands/pristine_spec.rb | 121 ++ spec/bundler/commands/show_spec.rb | 191 +++ spec/bundler/commands/update_spec.rb | 657 ++++++++++ spec/bundler/commands/viz_spec.rb | 150 +++ spec/bundler/install/allow_offline_install_spec.rb | 92 ++ spec/bundler/install/binstubs_spec.rb | 50 + spec/bundler/install/bundler_spec.rb | 147 +++ spec/bundler/install/deploy_spec.rb | 301 +++++ spec/bundler/install/failure_spec.rb | 33 + spec/bundler/install/force_spec.rb | 67 + spec/bundler/install/gemfile/eval_gemfile_spec.rb | 67 + spec/bundler/install/gemfile/gemspec_spec.rb | 563 ++++++++ spec/bundler/install/gemfile/git_spec.rb | 1259 ++++++++++++++++++ spec/bundler/install/gemfile/groups_spec.rb | 371 ++++++ spec/bundler/install/gemfile/install_if.rb | 45 + spec/bundler/install/gemfile/path_spec.rb | 595 +++++++++ spec/bundler/install/gemfile/platform_spec.rb | 265 ++++ spec/bundler/install/gemfile/ruby_spec.rb | 109 ++ spec/bundler/install/gemfile/sources_spec.rb | 518 ++++++++ .../install/gemfile/specific_platform_spec.rb | 115 ++ spec/bundler/install/gemfile_spec.rb | 98 ++ spec/bundler/install/gems/compact_index_spec.rb | 805 ++++++++++++ spec/bundler/install/gems/dependency_api_spec.rb | 671 ++++++++++ spec/bundler/install/gems/env_spec.rb | 108 ++ spec/bundler/install/gems/flex_spec.rb | 319 +++++ spec/bundler/install/gems/mirror_spec.rb | 40 + .../bundler/install/gems/native_extensions_spec.rb | 92 ++ spec/bundler/install/gems/post_install_spec.rb | 151 +++ spec/bundler/install/gems/resolving_spec.rb | 195 +++ spec/bundler/install/gems/standalone_spec.rb | 318 +++++ spec/bundler/install/gems/sudo_spec.rb | 179 +++ spec/bundler/install/gems/win32_spec.rb | 27 + spec/bundler/install/gemspecs_spec.rb | 110 ++ spec/bundler/install/git_spec.rb | 66 + spec/bundler/install/path_spec.rb | 178 +++ spec/bundler/install/post_bundle_message_spec.rb | 190 +++ spec/bundler/install/prereleases_spec.rb | 42 + spec/bundler/install/security_policy_spec.rb | 77 ++ spec/bundler/install/yanked_spec.rb | 72 + spec/bundler/lock/git_spec.rb | 35 + spec/bundler/lock/lockfile_spec.rb | 1381 ++++++++++++++++++++ spec/bundler/other/bundle_ruby_spec.rb | 143 ++ spec/bundler/other/cli_dispatch_spec.rb | 22 + spec/bundler/other/ext_spec.rb | 67 + spec/bundler/other/major_deprecation_spec.rb | 248 ++++ spec/bundler/other/platform_spec.rb | 1292 ++++++++++++++++++ spec/bundler/other/ssl_cert_spec.rb | 18 + spec/bundler/plugins/command_spec.rb | 81 ++ spec/bundler/plugins/hook_spec.rb | 28 + spec/bundler/plugins/install_spec.rb | 258 ++++ spec/bundler/plugins/source/example_spec.rb | 446 +++++++ spec/bundler/plugins/source_spec.rb | 109 ++ spec/bundler/quality_spec.rb | 263 ++++ spec/bundler/realworld/dependency_api_spec.rb | 49 + spec/bundler/realworld/edgecases_spec.rb | 382 ++++++ .../realworld/gemfile_source_header_spec.rb | 53 + spec/bundler/realworld/mirror_probe_spec.rb | 143 ++ spec/bundler/realworld/parallel_spec.rb | 81 ++ spec/bundler/resolver/basic_spec.rb | 258 ++++ spec/bundler/resolver/platform_spec.rb | 101 ++ spec/bundler/runtime/executable_spec.rb | 149 +++ spec/bundler/runtime/gem_tasks_spec.rb | 43 + spec/bundler/runtime/inline_spec.rb | 268 ++++ spec/bundler/runtime/load_spec.rb | 115 ++ spec/bundler/runtime/platform_spec.rb | 123 ++ spec/bundler/runtime/require_spec.rb | 442 +++++++ spec/bundler/runtime/setup_spec.rb | 1289 ++++++++++++++++++ spec/bundler/runtime/with_clean_env_spec.rb | 135 ++ spec/bundler/spec_helper.rb | 156 +++ spec/bundler/support/artifice/compact_index.rb | 121 ++ .../support/artifice/compact_index_api_missing.rb | 17 + .../artifice/compact_index_basic_authentication.rb | 14 + .../artifice/compact_index_checksum_mismatch.rb | 15 + .../artifice/compact_index_concurrent_download.rb | 31 + .../artifice/compact_index_creds_diff_host.rb | 38 + .../support/artifice/compact_index_extra.rb | 36 + .../support/artifice/compact_index_extra_api.rb | 51 + .../artifice/compact_index_extra_missing.rb | 16 + .../support/artifice/compact_index_forbidden.rb | 12 + .../artifice/compact_index_host_redirect.rb | 20 + .../artifice/compact_index_partial_update.rb | 37 + .../support/artifice/compact_index_redirects.rb | 20 + .../compact_index_strict_basic_authentication.rb | 19 + .../artifice/compact_index_wrong_dependencies.rb | 16 + .../artifice/compact_index_wrong_gem_checksum.rb | 19 + .../endopint_marshal_fail_basic_authentication.rb | 14 + spec/bundler/support/artifice/endpoint.rb | 73 ++ spec/bundler/support/artifice/endpoint_500.rb | 18 + .../support/artifice/endpoint_api_forbidden.rb | 12 + .../support/artifice/endpoint_api_missing.rb | 17 + .../artifice/endpoint_basic_authentication.rb | 14 + .../support/artifice/endpoint_creds_diff_host.rb | 38 + spec/bundler/support/artifice/endpoint_extra.rb | 32 + .../bundler/support/artifice/endpoint_extra_api.rb | 33 + .../support/artifice/endpoint_extra_missing.rb | 16 + spec/bundler/support/artifice/endpoint_fallback.rb | 18 + .../support/artifice/endpoint_host_redirect.rb | 16 + .../support/artifice/endpoint_marshal_fail.rb | 12 + .../support/artifice/endpoint_mirror_source.rb | 14 + spec/bundler/support/artifice/endpoint_redirect.rb | 16 + .../endpoint_strict_basic_authentication.rb | 19 + spec/bundler/support/artifice/endpoint_timeout.rb | 14 + spec/bundler/support/artifice/fail.rb | 39 + spec/bundler/support/artifice/windows.rb | 48 + spec/bundler/support/builders.rb | 806 ++++++++++++ spec/bundler/support/code_climate.rb | 25 + spec/bundler/support/hax.rb | 47 + spec/bundler/support/helpers.rb | 504 +++++++ spec/bundler/support/indexes.rb | 365 ++++++ spec/bundler/support/less_than_proc.rb | 19 + spec/bundler/support/matchers.rb | 222 ++++ spec/bundler/support/path.rb | 134 ++ spec/bundler/support/permissions.rb | 11 + spec/bundler/support/platforms.rb | 98 ++ spec/bundler/support/rubygems_ext.rb | 64 + spec/bundler/support/silent_logger.rb | 9 + spec/bundler/support/sometimes.rb | 56 + spec/bundler/support/streams.rb | 14 + spec/bundler/support/sudo.rb | 17 + spec/bundler/support/the_bundle.rb | 36 + spec/bundler/update/gems/post_install_spec.rb | 77 ++ spec/bundler/update/git_spec.rb | 333 +++++ spec/bundler/update/path_spec.rb | 19 + tool/sync_default_gems.rb | 8 + 409 files changed, 60699 insertions(+), 1 deletion(-) create mode 100755 bin/bundle create mode 100755 bin/bundle_ruby create mode 100755 bin/bundler create mode 100644 lib/bundler.gemspec create mode 100644 lib/bundler.rb create mode 100644 lib/bundler/capistrano.rb create mode 100644 lib/bundler/cli.rb create mode 100644 lib/bundler/cli/add.rb create mode 100644 lib/bundler/cli/binstubs.rb create mode 100644 lib/bundler/cli/cache.rb create mode 100644 lib/bundler/cli/check.rb create mode 100644 lib/bundler/cli/clean.rb create mode 100644 lib/bundler/cli/common.rb create mode 100644 lib/bundler/cli/config.rb create mode 100644 lib/bundler/cli/console.rb create mode 100644 lib/bundler/cli/doctor.rb create mode 100644 lib/bundler/cli/exec.rb create mode 100644 lib/bundler/cli/gem.rb create mode 100644 lib/bundler/cli/info.rb create mode 100644 lib/bundler/cli/init.rb create mode 100644 lib/bundler/cli/inject.rb create mode 100644 lib/bundler/cli/install.rb create mode 100644 lib/bundler/cli/issue.rb create mode 100644 lib/bundler/cli/lock.rb create mode 100644 lib/bundler/cli/open.rb create mode 100644 lib/bundler/cli/outdated.rb create mode 100644 lib/bundler/cli/package.rb create mode 100644 lib/bundler/cli/platform.rb create mode 100644 lib/bundler/cli/plugin.rb create mode 100644 lib/bundler/cli/pristine.rb create mode 100644 lib/bundler/cli/show.rb create mode 100644 lib/bundler/cli/update.rb create mode 100644 lib/bundler/cli/viz.rb create mode 100644 lib/bundler/compact_index_client.rb create mode 100644 lib/bundler/compact_index_client/cache.rb create mode 100644 lib/bundler/compact_index_client/updater.rb create mode 100644 lib/bundler/constants.rb create mode 100644 lib/bundler/current_ruby.rb create mode 100644 lib/bundler/definition.rb create mode 100644 lib/bundler/dep_proxy.rb create mode 100644 lib/bundler/dependency.rb create mode 100644 lib/bundler/deployment.rb create mode 100644 lib/bundler/deprecate.rb create mode 100644 lib/bundler/dsl.rb create mode 100644 lib/bundler/endpoint_specification.rb create mode 100644 lib/bundler/env.rb create mode 100644 lib/bundler/environment_preserver.rb create mode 100644 lib/bundler/errors.rb create mode 100644 lib/bundler/feature_flag.rb create mode 100644 lib/bundler/fetcher.rb create mode 100644 lib/bundler/fetcher/base.rb create mode 100644 lib/bundler/fetcher/compact_index.rb create mode 100644 lib/bundler/fetcher/dependency.rb create mode 100644 lib/bundler/fetcher/downloader.rb create mode 100644 lib/bundler/fetcher/index.rb create mode 100644 lib/bundler/friendly_errors.rb create mode 100644 lib/bundler/gem_helper.rb create mode 100644 lib/bundler/gem_helpers.rb create mode 100644 lib/bundler/gem_remote_fetcher.rb create mode 100644 lib/bundler/gem_tasks.rb create mode 100644 lib/bundler/gem_version_promoter.rb create mode 100644 lib/bundler/gemdeps.rb create mode 100644 lib/bundler/graph.rb create mode 100644 lib/bundler/index.rb create mode 100644 lib/bundler/injector.rb create mode 100644 lib/bundler/inline.rb create mode 100644 lib/bundler/installer.rb create mode 100644 lib/bundler/installer/gem_installer.rb create mode 100644 lib/bundler/installer/parallel_installer.rb create mode 100644 lib/bundler/installer/standalone.rb create mode 100644 lib/bundler/lazy_specification.rb create mode 100644 lib/bundler/lockfile_parser.rb create mode 100644 lib/bundler/match_platform.rb create mode 100644 lib/bundler/mirror.rb create mode 100644 lib/bundler/plugin.rb create mode 100644 lib/bundler/plugin/api.rb create mode 100644 lib/bundler/plugin/api/source.rb create mode 100644 lib/bundler/plugin/dsl.rb create mode 100644 lib/bundler/plugin/index.rb create mode 100644 lib/bundler/plugin/installer.rb create mode 100644 lib/bundler/plugin/installer/git.rb create mode 100644 lib/bundler/plugin/installer/rubygems.rb create mode 100644 lib/bundler/plugin/source_list.rb create mode 100644 lib/bundler/psyched_yaml.rb create mode 100644 lib/bundler/remote_specification.rb create mode 100644 lib/bundler/resolver.rb create mode 100644 lib/bundler/retry.rb create mode 100644 lib/bundler/ruby_dsl.rb create mode 100644 lib/bundler/ruby_version.rb create mode 100644 lib/bundler/rubygems_ext.rb create mode 100644 lib/bundler/rubygems_gem_installer.rb create mode 100644 lib/bundler/rubygems_integration.rb create mode 100644 lib/bundler/runtime.rb create mode 100644 lib/bundler/settings.rb create mode 100644 lib/bundler/setup.rb create mode 100644 lib/bundler/shared_helpers.rb create mode 100644 lib/bundler/similarity_detector.rb create mode 100644 lib/bundler/source.rb create mode 100644 lib/bundler/source/gemspec.rb create mode 100644 lib/bundler/source/git.rb create mode 100644 lib/bundler/source/git/git_proxy.rb create mode 100644 lib/bundler/source/path.rb create mode 100644 lib/bundler/source/path/installer.rb create mode 100644 lib/bundler/source/rubygems.rb create mode 100644 lib/bundler/source/rubygems/remote.rb create mode 100644 lib/bundler/source_list.rb create mode 100644 lib/bundler/spec_set.rb create mode 100644 lib/bundler/ssl_certs/.document create mode 100644 lib/bundler/ssl_certs/certificate_manager.rb create mode 100644 lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem create mode 100644 lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem create mode 100644 lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem create mode 100644 lib/bundler/stub_specification.rb create mode 100644 lib/bundler/templates/Executable create mode 100644 lib/bundler/templates/Executable.standalone create mode 100644 lib/bundler/templates/Gemfile create mode 100644 lib/bundler/templates/newgem/.travis.yml.tt create mode 100644 lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt create mode 100644 lib/bundler/templates/newgem/Gemfile.tt create mode 100644 lib/bundler/templates/newgem/LICENSE.txt.tt create mode 100644 lib/bundler/templates/newgem/README.md.tt create mode 100644 lib/bundler/templates/newgem/Rakefile.tt create mode 100644 lib/bundler/templates/newgem/bin/console.tt create mode 100644 lib/bundler/templates/newgem/bin/setup.tt create mode 100644 lib/bundler/templates/newgem/exe/newgem.tt create mode 100644 lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt create mode 100644 lib/bundler/templates/newgem/ext/newgem/newgem.c.tt create mode 100644 lib/bundler/templates/newgem/ext/newgem/newgem.h.tt create mode 100644 lib/bundler/templates/newgem/gitignore.tt create mode 100644 lib/bundler/templates/newgem/lib/newgem.rb.tt create mode 100644 lib/bundler/templates/newgem/lib/newgem/version.rb.tt create mode 100644 lib/bundler/templates/newgem/newgem.gemspec.tt create mode 100644 lib/bundler/templates/newgem/rspec.tt create mode 100644 lib/bundler/templates/newgem/spec/newgem_spec.rb.tt create mode 100644 lib/bundler/templates/newgem/spec/spec_helper.rb.tt create mode 100644 lib/bundler/templates/newgem/test/newgem_test.rb.tt create mode 100644 lib/bundler/templates/newgem/test/test_helper.rb.tt create mode 100644 lib/bundler/ui.rb create mode 100644 lib/bundler/ui/rg_proxy.rb create mode 100644 lib/bundler/ui/shell.rb create mode 100644 lib/bundler/ui/silent.rb create mode 100644 lib/bundler/uri_credentials_filter.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/errors.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb create mode 100644 lib/bundler/vendor/molinillo/lib/molinillo/state.rb create mode 100644 lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb create mode 100644 lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb create mode 100644 lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb create mode 100644 lib/bundler/vendor/thor/lib/thor.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/actions.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/actions/create_file.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/actions/create_link.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/actions/directory.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/base.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/command.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/error.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/group.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/invocation.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/line_editor.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/parser.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/parser/argument.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/parser/arguments.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/parser/option.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/parser/options.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/rake_compat.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/runner.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/shell.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/shell/basic.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/shell/color.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/shell/html.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/util.rb create mode 100644 lib/bundler/vendor/thor/lib/thor/version.rb create mode 100644 lib/bundler/vendored_molinillo.rb create mode 100644 lib/bundler/vendored_persistent.rb create mode 100644 lib/bundler/vendored_thor.rb create mode 100644 lib/bundler/version.rb create mode 100644 lib/bundler/version_ranges.rb create mode 100644 lib/bundler/vlad.rb create mode 100644 lib/bundler/worker.rb create mode 100644 lib/bundler/yaml_serializer.rb create mode 100644 spec/bundler/bundler/bundler_spec.rb create mode 100644 spec/bundler/bundler/cli_spec.rb create mode 100644 spec/bundler/bundler/compact_index_client/updater_spec.rb create mode 100644 spec/bundler/bundler/definition_spec.rb create mode 100644 spec/bundler/bundler/dsl_spec.rb create mode 100644 spec/bundler/bundler/endpoint_specification_spec.rb create mode 100644 spec/bundler/bundler/env_spec.rb create mode 100644 spec/bundler/bundler/environment_preserver_spec.rb create mode 100644 spec/bundler/bundler/fetcher/base_spec.rb create mode 100644 spec/bundler/bundler/fetcher/compact_index_spec.rb create mode 100644 spec/bundler/bundler/fetcher/dependency_spec.rb create mode 100644 spec/bundler/bundler/fetcher/downloader_spec.rb create mode 100644 spec/bundler/bundler/fetcher/index_spec.rb create mode 100644 spec/bundler/bundler/fetcher_spec.rb create mode 100644 spec/bundler/bundler/friendly_errors_spec.rb create mode 100644 spec/bundler/bundler/gem_helper_spec.rb create mode 100644 spec/bundler/bundler/gem_version_promoter_spec.rb create mode 100644 spec/bundler/bundler/index_spec.rb create mode 100644 spec/bundler/bundler/installer/gem_installer_spec.rb create mode 100644 spec/bundler/bundler/installer/parallel_installer_spec.rb create mode 100644 spec/bundler/bundler/installer/spec_installation_spec.rb create mode 100644 spec/bundler/bundler/lockfile_parser_spec.rb create mode 100644 spec/bundler/bundler/mirror_spec.rb create mode 100644 spec/bundler/bundler/plugin/api/source_spec.rb create mode 100644 spec/bundler/bundler/plugin/api_spec.rb create mode 100644 spec/bundler/bundler/plugin/dsl_spec.rb create mode 100644 spec/bundler/bundler/plugin/index_spec.rb create mode 100644 spec/bundler/bundler/plugin/installer_spec.rb create mode 100644 spec/bundler/bundler/plugin/source_list_spec.rb create mode 100644 spec/bundler/bundler/plugin_spec.rb create mode 100644 spec/bundler/bundler/psyched_yaml_spec.rb create mode 100644 spec/bundler/bundler/remote_specification_spec.rb create mode 100644 spec/bundler/bundler/retry_spec.rb create mode 100644 spec/bundler/bundler/ruby_dsl_spec.rb create mode 100644 spec/bundler/bundler/ruby_version_spec.rb create mode 100644 spec/bundler/bundler/rubygems_integration_spec.rb create mode 100644 spec/bundler/bundler/settings_spec.rb create mode 100644 spec/bundler/bundler/shared_helpers_spec.rb create mode 100644 spec/bundler/bundler/source/git/git_proxy_spec.rb create mode 100644 spec/bundler/bundler/source/path_spec.rb create mode 100644 spec/bundler/bundler/source/rubygems/remote_spec.rb create mode 100644 spec/bundler/bundler/source/rubygems_spec.rb create mode 100644 spec/bundler/bundler/source_list_spec.rb create mode 100644 spec/bundler/bundler/source_spec.rb create mode 100644 spec/bundler/bundler/spec_set_spec.rb create mode 100644 spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb create mode 100644 spec/bundler/bundler/stub_specification_spec.rb create mode 100644 spec/bundler/bundler/ui_spec.rb create mode 100644 spec/bundler/bundler/uri_credentials_filter_spec.rb create mode 100644 spec/bundler/bundler/version_ranges_spec.rb create mode 100644 spec/bundler/bundler/worker_spec.rb create mode 100644 spec/bundler/bundler/yaml_serializer_spec.rb create mode 100644 spec/bundler/cache/cache_path_spec.rb create mode 100644 spec/bundler/cache/gems_spec.rb create mode 100644 spec/bundler/cache/git_spec.rb create mode 100644 spec/bundler/cache/path_spec.rb create mode 100644 spec/bundler/cache/platform_spec.rb create mode 100644 spec/bundler/commands/add_spec.rb create mode 100644 spec/bundler/commands/binstubs_spec.rb create mode 100644 spec/bundler/commands/check_spec.rb create mode 100644 spec/bundler/commands/clean_spec.rb create mode 100644 spec/bundler/commands/config_spec.rb create mode 100644 spec/bundler/commands/console_spec.rb create mode 100644 spec/bundler/commands/doctor_spec.rb create mode 100644 spec/bundler/commands/exec_spec.rb create mode 100644 spec/bundler/commands/help_spec.rb create mode 100644 spec/bundler/commands/info_spec.rb create mode 100644 spec/bundler/commands/init_spec.rb create mode 100644 spec/bundler/commands/inject_spec.rb create mode 100644 spec/bundler/commands/install_spec.rb create mode 100644 spec/bundler/commands/issue_spec.rb create mode 100644 spec/bundler/commands/licenses_spec.rb create mode 100644 spec/bundler/commands/lock_spec.rb create mode 100644 spec/bundler/commands/newgem_spec.rb create mode 100644 spec/bundler/commands/open_spec.rb create mode 100644 spec/bundler/commands/outdated_spec.rb create mode 100644 spec/bundler/commands/package_spec.rb create mode 100644 spec/bundler/commands/pristine_spec.rb create mode 100644 spec/bundler/commands/show_spec.rb create mode 100644 spec/bundler/commands/update_spec.rb create mode 100644 spec/bundler/commands/viz_spec.rb create mode 100644 spec/bundler/install/allow_offline_install_spec.rb create mode 100644 spec/bundler/install/binstubs_spec.rb create mode 100644 spec/bundler/install/bundler_spec.rb create mode 100644 spec/bundler/install/deploy_spec.rb create mode 100644 spec/bundler/install/failure_spec.rb create mode 100644 spec/bundler/install/force_spec.rb create mode 100644 spec/bundler/install/gemfile/eval_gemfile_spec.rb create mode 100644 spec/bundler/install/gemfile/gemspec_spec.rb create mode 100644 spec/bundler/install/gemfile/git_spec.rb create mode 100644 spec/bundler/install/gemfile/groups_spec.rb create mode 100644 spec/bundler/install/gemfile/install_if.rb create mode 100644 spec/bundler/install/gemfile/path_spec.rb create mode 100644 spec/bundler/install/gemfile/platform_spec.rb create mode 100644 spec/bundler/install/gemfile/ruby_spec.rb create mode 100644 spec/bundler/install/gemfile/sources_spec.rb create mode 100644 spec/bundler/install/gemfile/specific_platform_spec.rb create mode 100644 spec/bundler/install/gemfile_spec.rb create mode 100644 spec/bundler/install/gems/compact_index_spec.rb create mode 100644 spec/bundler/install/gems/dependency_api_spec.rb create mode 100644 spec/bundler/install/gems/env_spec.rb create mode 100644 spec/bundler/install/gems/flex_spec.rb create mode 100644 spec/bundler/install/gems/mirror_spec.rb create mode 100644 spec/bundler/install/gems/native_extensions_spec.rb create mode 100644 spec/bundler/install/gems/post_install_spec.rb create mode 100644 spec/bundler/install/gems/resolving_spec.rb create mode 100644 spec/bundler/install/gems/standalone_spec.rb create mode 100644 spec/bundler/install/gems/sudo_spec.rb create mode 100644 spec/bundler/install/gems/win32_spec.rb create mode 100644 spec/bundler/install/gemspecs_spec.rb create mode 100644 spec/bundler/install/git_spec.rb create mode 100644 spec/bundler/install/path_spec.rb create mode 100644 spec/bundler/install/post_bundle_message_spec.rb create mode 100644 spec/bundler/install/prereleases_spec.rb create mode 100644 spec/bundler/install/security_policy_spec.rb create mode 100644 spec/bundler/install/yanked_spec.rb create mode 100644 spec/bundler/lock/git_spec.rb create mode 100644 spec/bundler/lock/lockfile_spec.rb create mode 100644 spec/bundler/other/bundle_ruby_spec.rb create mode 100644 spec/bundler/other/cli_dispatch_spec.rb create mode 100644 spec/bundler/other/ext_spec.rb create mode 100644 spec/bundler/other/major_deprecation_spec.rb create mode 100644 spec/bundler/other/platform_spec.rb create mode 100644 spec/bundler/other/ssl_cert_spec.rb create mode 100644 spec/bundler/plugins/command_spec.rb create mode 100644 spec/bundler/plugins/hook_spec.rb create mode 100644 spec/bundler/plugins/install_spec.rb create mode 100644 spec/bundler/plugins/source/example_spec.rb create mode 100644 spec/bundler/plugins/source_spec.rb create mode 100644 spec/bundler/quality_spec.rb create mode 100644 spec/bundler/realworld/dependency_api_spec.rb create mode 100644 spec/bundler/realworld/edgecases_spec.rb create mode 100644 spec/bundler/realworld/gemfile_source_header_spec.rb create mode 100644 spec/bundler/realworld/mirror_probe_spec.rb create mode 100644 spec/bundler/realworld/parallel_spec.rb create mode 100644 spec/bundler/resolver/basic_spec.rb create mode 100644 spec/bundler/resolver/platform_spec.rb create mode 100644 spec/bundler/runtime/executable_spec.rb create mode 100644 spec/bundler/runtime/gem_tasks_spec.rb create mode 100644 spec/bundler/runtime/inline_spec.rb create mode 100644 spec/bundler/runtime/load_spec.rb create mode 100644 spec/bundler/runtime/platform_spec.rb create mode 100644 spec/bundler/runtime/require_spec.rb create mode 100644 spec/bundler/runtime/setup_spec.rb create mode 100644 spec/bundler/runtime/with_clean_env_spec.rb create mode 100644 spec/bundler/spec_helper.rb create mode 100644 spec/bundler/support/artifice/compact_index.rb create mode 100644 spec/bundler/support/artifice/compact_index_api_missing.rb create mode 100644 spec/bundler/support/artifice/compact_index_basic_authentication.rb create mode 100644 spec/bundler/support/artifice/compact_index_checksum_mismatch.rb create mode 100644 spec/bundler/support/artifice/compact_index_concurrent_download.rb create mode 100644 spec/bundler/support/artifice/compact_index_creds_diff_host.rb create mode 100644 spec/bundler/support/artifice/compact_index_extra.rb create mode 100644 spec/bundler/support/artifice/compact_index_extra_api.rb create mode 100644 spec/bundler/support/artifice/compact_index_extra_missing.rb create mode 100644 spec/bundler/support/artifice/compact_index_forbidden.rb create mode 100644 spec/bundler/support/artifice/compact_index_host_redirect.rb create mode 100644 spec/bundler/support/artifice/compact_index_partial_update.rb create mode 100644 spec/bundler/support/artifice/compact_index_redirects.rb create mode 100644 spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb create mode 100644 spec/bundler/support/artifice/compact_index_wrong_dependencies.rb create mode 100644 spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb create mode 100644 spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb create mode 100644 spec/bundler/support/artifice/endpoint.rb create mode 100644 spec/bundler/support/artifice/endpoint_500.rb create mode 100644 spec/bundler/support/artifice/endpoint_api_forbidden.rb create mode 100644 spec/bundler/support/artifice/endpoint_api_missing.rb create mode 100644 spec/bundler/support/artifice/endpoint_basic_authentication.rb create mode 100644 spec/bundler/support/artifice/endpoint_creds_diff_host.rb create mode 100644 spec/bundler/support/artifice/endpoint_extra.rb create mode 100644 spec/bundler/support/artifice/endpoint_extra_api.rb create mode 100644 spec/bundler/support/artifice/endpoint_extra_missing.rb create mode 100644 spec/bundler/support/artifice/endpoint_fallback.rb create mode 100644 spec/bundler/support/artifice/endpoint_host_redirect.rb create mode 100644 spec/bundler/support/artifice/endpoint_marshal_fail.rb create mode 100644 spec/bundler/support/artifice/endpoint_mirror_source.rb create mode 100644 spec/bundler/support/artifice/endpoint_redirect.rb create mode 100644 spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb create mode 100644 spec/bundler/support/artifice/endpoint_timeout.rb create mode 100644 spec/bundler/support/artifice/fail.rb create mode 100644 spec/bundler/support/artifice/windows.rb create mode 100644 spec/bundler/support/builders.rb create mode 100644 spec/bundler/support/code_climate.rb create mode 100644 spec/bundler/support/hax.rb create mode 100644 spec/bundler/support/helpers.rb create mode 100644 spec/bundler/support/indexes.rb create mode 100644 spec/bundler/support/less_than_proc.rb create mode 100644 spec/bundler/support/matchers.rb create mode 100644 spec/bundler/support/path.rb create mode 100644 spec/bundler/support/permissions.rb create mode 100644 spec/bundler/support/platforms.rb create mode 100644 spec/bundler/support/rubygems_ext.rb create mode 100644 spec/bundler/support/silent_logger.rb create mode 100644 spec/bundler/support/sometimes.rb create mode 100644 spec/bundler/support/streams.rb create mode 100644 spec/bundler/support/sudo.rb create mode 100644 spec/bundler/support/the_bundle.rb create mode 100644 spec/bundler/update/gems/post_install_spec.rb create mode 100644 spec/bundler/update/git_spec.rb create mode 100644 spec/bundler/update/path_spec.rb diff --git a/.gitignore b/.gitignore index 5517f80fad..c72e0bbca3 100644 --- a/.gitignore +++ b/.gitignore @@ -179,6 +179,10 @@ lcov*.info /gems/*.gem /gems/*-* +# /spec/bundler +/.rspec_status +/spec/rspec + # /tool/ /tool/config.guess /tool/config.sub diff --git a/Makefile.in b/Makefile.in index b0f7975cc5..abb8f71991 100644 --- a/Makefile.in +++ b/Makefile.in @@ -474,6 +474,20 @@ ext/extinit.$(OBJEXT): ext/extinit.c $(SETUP) enc/encinit.$(OBJEXT): enc/encinit.c $(SETUP) +test-bundler-precheck: $(arch)-fake.rb programs + +test-bundler-prepare: + GEM_HOME=$(srcdir)/spec/rspec GEM_PATH=$(srcdir)/spec/rspec \ + $(XRUBY) "$(srcdir)/bin/gem" install --no-ri --no-rdoc --conservative 'rspec:~> 3.5' +test-bundler: $(TEST_RUNNABLE)-test-bundler +yes-test-bundler: test-bundler-precheck test-bundler-prepare + $(gnumake_recursive)$(Q) \ + GEM_HOME=spec/rspec GEM_PATH=spec/rspec \ + BUNDLE_RUBY="$(abspath ./ruby) -I$(abspath $(srcdir)/lib) -I$(abspath .) -I$(abspath $(EXTOUT)/common) -I$(abspath $(EXTOUT)/$(arch))" \ + BUNDLE_GEM="$(abspath ./ruby) -I$(abspath $(srcdir)/lib) -I$(abspath .) -I$(abspath $(EXTOUT)/common) -I$(abspath $(EXTOUT)/$(arch)) -rubygems $(abspath $(srcdir)/bin/gem) --backtrace" \ + $(XRUBY) -C $(srcdir) -Ispec/bundler "spec/rspec/bin/rspec" --format progress spec/bundler +no-test-bundler: + update-src:: @$(CHDIR) "$(srcdir)" && LC_TIME=C exec $(VCSUP) diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000000..cf03a523ab --- /dev/null +++ b/bin/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/bin/bundle_ruby b/bin/bundle_ruby new file mode 100755 index 0000000000..df6f8cc8a1 --- /dev/null +++ b/bin/bundle_ruby @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/shared_helpers" + +Bundler::SharedHelpers.major_deprecation(2, "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/bin/bundler b/bin/bundler new file mode 100755 index 0000000000..d9131fe834 --- /dev/null +++ b/bin/bundler @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +load File.expand_path("../bundle", __FILE__) diff --git a/lib/bundler.gemspec b/lib/bundler.gemspec new file mode 100644 index 0000000000..65713ebd57 --- /dev/null +++ b/lib/bundler.gemspec @@ -0,0 +1,251 @@ +# coding: utf-8 +# frozen_string_literal: true +lib = File.expand_path("../lib/", __FILE__) +$:.unshift lib unless $:.include?(lib) +require "bundler/version" + +Gem::Specification.new do |s| + s.name = "bundler" + s.version = Bundler::VERSION + s.license = "MIT" + s.authors = [ + "André Arko", "Samuel Giddins", "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 + + s.required_ruby_version = ">= 1.8.7" + s.required_rubygems_version = ">= 1.3.6" + + 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.5" + + s.files = [ + "lib/bundler.gemspec", + "bin/bundle", + "bin/bundle_ruby", + "bin/bundler", + "lib/bundler.rb", + "lib/bundler/capistrano.rb", + "lib/bundler/cli.rb", + "lib/bundler/cli/add.rb", + "lib/bundler/cli/binstubs.rb", + "lib/bundler/cli/cache.rb", + "lib/bundler/cli/check.rb", + "lib/bundler/cli/clean.rb", + "lib/bundler/cli/common.rb", + "lib/bundler/cli/config.rb", + "lib/bundler/cli/console.rb", + "lib/bundler/cli/doctor.rb", + "lib/bundler/cli/exec.rb", + "lib/bundler/cli/gem.rb", + "lib/bundler/cli/info.rb", + "lib/bundler/cli/init.rb", + "lib/bundler/cli/inject.rb", + "lib/bundler/cli/install.rb", + "lib/bundler/cli/issue.rb", + "lib/bundler/cli/lock.rb", + "lib/bundler/cli/open.rb", + "lib/bundler/cli/outdated.rb", + "lib/bundler/cli/package.rb", + "lib/bundler/cli/platform.rb", + "lib/bundler/cli/plugin.rb", + "lib/bundler/cli/pristine.rb", + "lib/bundler/cli/show.rb", + "lib/bundler/cli/update.rb", + "lib/bundler/cli/viz.rb", + "lib/bundler/compact_index_client.rb", + "lib/bundler/compact_index_client/cache.rb", + "lib/bundler/compact_index_client/updater.rb", + "lib/bundler/constants.rb", + "lib/bundler/current_ruby.rb", + "lib/bundler/definition.rb", + "lib/bundler/dep_proxy.rb", + "lib/bundler/dependency.rb", + "lib/bundler/deployment.rb", + "lib/bundler/deprecate.rb", + "lib/bundler/dsl.rb", + "lib/bundler/endpoint_specification.rb", + "lib/bundler/env.rb", + "lib/bundler/environment_preserver.rb", + "lib/bundler/errors.rb", + "lib/bundler/feature_flag.rb", + "lib/bundler/fetcher.rb", + "lib/bundler/fetcher/base.rb", + "lib/bundler/fetcher/compact_index.rb", + "lib/bundler/fetcher/dependency.rb", + "lib/bundler/fetcher/downloader.rb", + "lib/bundler/fetcher/index.rb", + "lib/bundler/friendly_errors.rb", + "lib/bundler/gem_helper.rb", + "lib/bundler/gem_helpers.rb", + "lib/bundler/gem_remote_fetcher.rb", + "lib/bundler/gem_tasks.rb", + "lib/bundler/gem_version_promoter.rb", + "lib/bundler/gemdeps.rb", + "lib/bundler/graph.rb", + "lib/bundler/index.rb", + "lib/bundler/injector.rb", + "lib/bundler/inline.rb", + "lib/bundler/installer.rb", + "lib/bundler/installer/gem_installer.rb", + "lib/bundler/installer/parallel_installer.rb", + "lib/bundler/installer/standalone.rb", + "lib/bundler/lazy_specification.rb", + "lib/bundler/lockfile_parser.rb", + "lib/bundler/match_platform.rb", + "lib/bundler/mirror.rb", + "lib/bundler/plugin.rb", + "lib/bundler/plugin/api.rb", + "lib/bundler/plugin/api/source.rb", + "lib/bundler/plugin/dsl.rb", + "lib/bundler/plugin/index.rb", + "lib/bundler/plugin/installer.rb", + "lib/bundler/plugin/installer/git.rb", + "lib/bundler/plugin/installer/rubygems.rb", + "lib/bundler/plugin/source_list.rb", + "lib/bundler/psyched_yaml.rb", + "lib/bundler/remote_specification.rb", + "lib/bundler/resolver.rb", + "lib/bundler/retry.rb", + "lib/bundler/ruby_dsl.rb", + "lib/bundler/ruby_version.rb", + "lib/bundler/rubygems_ext.rb", + "lib/bundler/rubygems_gem_installer.rb", + "lib/bundler/rubygems_integration.rb", + "lib/bundler/runtime.rb", + "lib/bundler/settings.rb", + "lib/bundler/setup.rb", + "lib/bundler/shared_helpers.rb", + "lib/bundler/similarity_detector.rb", + "lib/bundler/source.rb", + "lib/bundler/source/gemspec.rb", + "lib/bundler/source/git.rb", + "lib/bundler/source/git/git_proxy.rb", + "lib/bundler/source/path.rb", + "lib/bundler/source/path/installer.rb", + "lib/bundler/source/rubygems.rb", + "lib/bundler/source/rubygems/remote.rb", + "lib/bundler/source_list.rb", + "lib/bundler/spec_set.rb", + "lib/bundler/ssl_certs/.document", + "lib/bundler/ssl_certs/certificate_manager.rb", + "lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem", + "lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem", + "lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem", + "lib/bundler/stub_specification.rb", + "lib/bundler/templates/Executable", + "lib/bundler/templates/Executable.standalone", + "lib/bundler/templates/Gemfile", + "lib/bundler/templates/newgem/.travis.yml.tt", + "lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt", + "lib/bundler/templates/newgem/Gemfile.tt", + "lib/bundler/templates/newgem/LICENSE.txt.tt", + "lib/bundler/templates/newgem/README.md.tt", + "lib/bundler/templates/newgem/Rakefile.tt", + "lib/bundler/templates/newgem/bin/console.tt", + "lib/bundler/templates/newgem/bin/setup.tt", + "lib/bundler/templates/newgem/exe/newgem.tt", + "lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt", + "lib/bundler/templates/newgem/ext/newgem/newgem.c.tt", + "lib/bundler/templates/newgem/ext/newgem/newgem.h.tt", + "lib/bundler/templates/newgem/gitignore.tt", + "lib/bundler/templates/newgem/lib/newgem.rb.tt", + "lib/bundler/templates/newgem/lib/newgem/version.rb.tt", + "lib/bundler/templates/newgem/newgem.gemspec.tt", + "lib/bundler/templates/newgem/rspec.tt", + "lib/bundler/templates/newgem/spec/newgem_spec.rb.tt", + "lib/bundler/templates/newgem/spec/spec_helper.rb.tt", + "lib/bundler/templates/newgem/test/newgem_test.rb.tt", + "lib/bundler/templates/newgem/test/test_helper.rb.tt", + "lib/bundler/ui.rb", + "lib/bundler/ui/rg_proxy.rb", + "lib/bundler/ui/shell.rb", + "lib/bundler/ui/silent.rb", + "lib/bundler/uri_credentials_filter.rb", + "lib/bundler/vendor/molinillo/lib/molinillo.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/errors.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb", + "lib/bundler/vendor/molinillo/lib/molinillo/state.rb", + "lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb", + "lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb", + "lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb", + "lib/bundler/vendor/thor/lib/thor.rb", + "lib/bundler/vendor/thor/lib/thor/actions.rb", + "lib/bundler/vendor/thor/lib/thor/actions/create_file.rb", + "lib/bundler/vendor/thor/lib/thor/actions/create_link.rb", + "lib/bundler/vendor/thor/lib/thor/actions/directory.rb", + "lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb", + "lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb", + "lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb", + "lib/bundler/vendor/thor/lib/thor/base.rb", + "lib/bundler/vendor/thor/lib/thor/command.rb", + "lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb", + "lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb", + "lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb", + "lib/bundler/vendor/thor/lib/thor/error.rb", + "lib/bundler/vendor/thor/lib/thor/group.rb", + "lib/bundler/vendor/thor/lib/thor/invocation.rb", + "lib/bundler/vendor/thor/lib/thor/line_editor.rb", + "lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb", + "lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb", + "lib/bundler/vendor/thor/lib/thor/parser.rb", + "lib/bundler/vendor/thor/lib/thor/parser/argument.rb", + "lib/bundler/vendor/thor/lib/thor/parser/arguments.rb", + "lib/bundler/vendor/thor/lib/thor/parser/option.rb", + "lib/bundler/vendor/thor/lib/thor/parser/options.rb", + "lib/bundler/vendor/thor/lib/thor/rake_compat.rb", + "lib/bundler/vendor/thor/lib/thor/runner.rb", + "lib/bundler/vendor/thor/lib/thor/shell.rb", + "lib/bundler/vendor/thor/lib/thor/shell/basic.rb", + "lib/bundler/vendor/thor/lib/thor/shell/color.rb", + "lib/bundler/vendor/thor/lib/thor/shell/html.rb", + "lib/bundler/vendor/thor/lib/thor/util.rb", + "lib/bundler/vendor/thor/lib/thor/version.rb", + "lib/bundler/vendored_molinillo.rb", + "lib/bundler/vendored_persistent.rb", + "lib/bundler/vendored_thor.rb", + "lib/bundler/version.rb", + "lib/bundler/version_ranges.rb", + "lib/bundler/vlad.rb", + "lib/bundler/worker.rb", + "lib/bundler/yaml_serializer.rb" + ] + + 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 0000000000..88822f8f1a --- /dev/null +++ b/lib/bundler.rb @@ -0,0 +1,533 @@ +# frozen_string_literal: true +require "fileutils" +require "pathname" +require "rbconfig" +require "thread" +require "tmpdir" + +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" + +module Bundler + environment_preserver = EnvironmentPreserver.new(ENV, %w(PATH GEM_PATH)) + 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 :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 + attr_writer :bundle_path + + 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(settings.path).expand_path(root) + 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 "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 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 + + warning = if home.nil? + "Your home directory is not set." + elsif !File.directory?(home) + "`#{home}` is not a directory." + elsif !File.writable?(home) + "`#{home}` is not writable." + end + + if warning + 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" + 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 => e + raise e.exception("#{warning}\nBundler also failed to create a temporary home directory at `#{path}':\n#{e}") + end + + def user_bundle_path + Pathname.new(user_home).join(".bundle") + end + + def home + bundle_path.join("bundler") + end + + def install_path + home.join("gems") + end + + def specs_path + bundle_path.join("specifications") + end + + def cache + bundle_path.join("cache/bundler") + end + + def user_cache + user_bundle_path.join("cache") + end + + def root + @root ||= begin + default_gemfile.dirname.expand_path + 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 ENV["BUNDLE_APP_CONFIG"] + Pathname.new(ENV["BUNDLE_APP_CONFIG"]).expand_path(root) + else + root.join(".bundle") + end + end + + def app_cache(custom_path = nil) + path = custom_path || root + path.join(settings.app_cache_path) + end + + def tmp(name = Process.pid.to_s) + 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 = path.read + 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! + @root = nil + @settings = nil + @definition = nil + @setup = nil + @load = nil + @locked_gems = nil + @bundle_path = nil + @bin_path = 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) + # 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, 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, settings = self.settings) + blank_home = env["GEM_HOME"].nil? || env["GEM_HOME"].empty? + if settings[:disable_shared_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 || Bundler.rubygems.gem_dir != bundle_path.to_s + 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 + # TODO: This mkdir_p is only needed for JRuby <= 1.5 and should go away (GH #602) + begin + FileUtils.mkdir_p bundle_path.to_s + rescue + nil + end + + 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/capistrano.rb b/lib/bundler/capistrano.rb new file mode 100644 index 0000000000..7b0bbbd6d2 --- /dev/null +++ b/lib/bundler/capistrano.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +# 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 0000000000..03e08e25a1 --- /dev/null +++ b/lib/bundler/cli.rb @@ -0,0 +1,658 @@ +# frozen_string_literal: true +require "bundler" +require "bundler/vendored_thor" + +module Bundler + class CLI < Thor + 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? + ENV["BUNDLE_GEMFILE"] = File.expand_path(custom_gemfile) + Bundler.reset_paths! + end + + Bundler.settings[:retry] = options[:retry] if 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 ||= {} + Bundler.settings.cli_flags_given = !options.empty? + 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 + + check_unknown_options!(:except => [:config, :exec]) + stop_on_unknown_option! :exec + + default_task :install + 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 + method_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" + map "c" => "check" + def check + require "bundler/cli/check" + Check.new(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 + method_option "binstubs", :type => :string, :lazy_default => "bin", :banner => + "Generate bin stubs for bundled gems to ./bin" + method_option "clean", :type => :boolean, :banner => + "Run bundle clean automatically after install" + method_option "deployment", :type => :boolean, :banner => + "Install using defaults tuned for deployment environments" + method_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" + method_option "no-cache", :type => :boolean, :banner => + "Don't update the existing gem cache." + method_option "force", :type => :boolean, :banner => + "Force downloading every gem." + 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 "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" + method_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("|") + method_option "without", :type => :array, :banner => + "Exclude gems that are part of the specified named group." + method_option "with", :type => :array, :banner => + "Include gems that are part of the specified named group." + map "i" => "install" + def install + 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 "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 "force", :type => :boolean, :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." + def update(*gems) + 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) + Bundler::SharedHelpers.major_deprecation("use `bundle show` instead of `bundle list`") if ARGV[0] == "list" + require "bundler/cli/show" + Show.new(options, gem_name).run + end + # TODO: 2.0 remove `bundle list` + map %w(list) => "show" + + 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 "standalone", :type => :boolean, :banner => + "Make binstubs that can work without the Bundler runtime" + 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 + + def add(gem_name) + require "bundler/cli/add" + Add.new(options.dup, gem_name).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", :aliases => "--group", :type => :string, :banner => "List gems from a specific group" + method_option "groups", :aliases => "--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" + def outdated(*gems) + require "bundler/cli/outdated" + Outdated.new(options, gems).run + end + + desc "cache [OPTIONS]", "Cache all the gems to vendor/cache", :hide => true + method_option "all", :type => :boolean, :banner => "Include all sources (including path and git)." + 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 + + desc "package [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" + method_option "all", :type => :boolean, :banner => "Include all sources (including path and git)." + 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 + 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 + + desc "console [GROUP]", "Opens an IRB session with the bundle pre-loaded" + def console(group = nil) + # TODO: Remove for 2.0 + require "bundler/cli/console" + Console.new(options, group).run + end + + desc "version", "Prints the bundler's version information" + def version + Bundler.ui.info "Bundler version #{Bundler::VERSION}" + 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 + + desc "viz [OPTIONS]", "Generates a visual dependency graph" + 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 + require "bundler/cli/viz" + Viz.new(options.dup).run + end + + old_gem = instance_method(:gem) + + desc "gem GEM [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" + 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" + 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 "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.new.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", "Restores installed gems to pristine condition from files located in the gem cache. Gem installed from a git repository will be issued `git checkout --force`." + def pristine + require "bundler/cli/pristine" + Pristine.new.run + end + + if Bundler.feature_flag.plugins? + require "bundler/cli/plugin" + desc "plugin SUBCOMMAND ...ARGS", "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 print_command + return unless Bundler.ui.debug? + _, _, config = @_initializer + current_command = config[:current_command] + command_name = current_command.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 = current_command.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] + + _, _, config = @_initializer + current_command = config[:current_command] + command_name = current_command.name + return if PARSEABLE_COMMANDS.include?(command_name) + + 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 + + Bundler.ui.warn "The latest bundler is #{latest}, but you are currently running #{current}.\nTo update, run `gem install bundler#{" --pre" if latest.prerelease?}`" + rescue + nil + end + end +end diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb new file mode 100644 index 0000000000..e80c775433 --- /dev/null +++ b/lib/bundler/cli/add.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Add + def initialize(options, gem_name) + @gem_name = gem_name + @options = options + @options[:group] = @options[:group].split(",").map(&:strip) if !@options[:group].nil? && !@options[:group].empty? + end + + def run + 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 + dependency = Bundler::Dependency.new(@gem_name, version, @options) + + Injector.inject([dependency], :conservative_versioning => @options[:version].nil?) # Perform conservative versioning only when version is not specified + Installer.install(Bundler.root, Bundler.definition) + end + end +end diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb new file mode 100644 index 0000000000..95103b7dd8 --- /dev/null +++ b/lib/bundler/cli/binstubs.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Binstubs + attr_reader :options, :gems + def initialize(options, gems) + @options = options + @gems = gems + end + + def run + Bundler.definition.validate_runtime! + Bundler.settings[:bin] = options["path"] if options["path"] + Bundler.settings[:bin] = nil if options["path"] && options["path"].empty? + installer = Installer.new(Bundler.root, Bundler.definition) + + if 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 spec.name == "bundler" + Bundler.ui.warn "Sorry, Bundler can only be run via Rubygems." + elsif options[:standalone] + installer.generate_standalone_bundler_executable_stubs(spec) + else + installer.generate_bundler_executable_stubs(spec, :force => options[:force], :binstubs_cmd => true) + end + end + end + end +end diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb new file mode 100644 index 0000000000..5ba105a31d --- /dev/null +++ b/lib/bundler/cli/cache.rb @@ -0,0 +1,35 @@ +# 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[:cache_all_platforms] = options["all-platforms"] if options.key?("all-platforms") + Bundler.load.cache + Bundler.settings[:no_prune] = true if 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[:cache_all] = options[:all] if options.key?("all") + + if Bundler.definition.has_local_dependencies? && !Bundler.settings[: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 0000000000..057a7e5695 --- /dev/null +++ b/lib/bundler/cli/check.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +module Bundler + class CLI::Check + attr_reader :options + + def initialize(options) + @options = options + end + + def run + if options[:path] + Bundler.settings[:path] = File.expand_path(options[:path]) + Bundler.settings[:disable_shared_gems] = true + end + + 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.settings[:frozen] + 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 0000000000..5eba09c6bc --- /dev/null +++ b/lib/bundler/cli/clean.rb @@ -0,0 +1,26 @@ +# 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 + if !Bundler.settings[:path] && !options[:force] + Bundler.ui.error "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`." + exit 1 + end + end + end +end diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb new file mode 100644 index 0000000000..bacbb2edc5 --- /dev/null +++ b/lib/bundler/cli/common.rb @@ -0,0 +1,93 @@ +# 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 unless Bundler.settings.without.any? + 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 + end +end diff --git a/lib/bundler/cli/config.rb b/lib/bundler/cli/config.rb new file mode 100644 index 0000000000..e8f13620ec --- /dev/null +++ b/lib/bundler/cli/config.rb @@ -0,0 +1,118 @@ +# 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 0000000000..715abf2554 --- /dev/null +++ b/lib/bundler/cli/console.rb @@ -0,0 +1,42 @@ +# 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 "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 0000000000..ae27983240 --- /dev/null +++ b/lib/bundler/cli/doctor.rb @@ -0,0 +1,93 @@ +# 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] + 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 + + 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 + else + Bundler.ui.info "No issues found with the installed bundle" + end + end + end +end diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb new file mode 100644 index 0000000000..62f7bc26cb --- /dev/null +++ b/lib/bundler/cli/exec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true +require "bundler/current_ruby" + +module Bundler + class CLI::Exec + attr_reader :options, :args, :cmd + + RESERVED_SIGNALS = %w(SEGV BUS ILL FPE VTALRM KILL STOP).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" + signals = Signal.list.keys - RESERVED_SIGNALS + signals.each {|s| trap(s, "DEFAULT") } + Kernel.load(file) + rescue SystemExit + raise + rescue Exception => e # rubocop:disable Lint/RescueException + Bundler.ui = ui + Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})" + 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", + "#!#{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 0000000000..45782d71a3 --- /dev/null +++ b/lib/bundler/cli/gem.rb @@ -0,0 +1,248 @@ +# 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 http://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 http://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] + 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 0000000000..4465fba9d4 --- /dev/null +++ b/lib/bundler/cli/info.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +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 0000000000..8ffd1db41a --- /dev/null +++ b/lib/bundler/cli/init.rb @@ -0,0 +1,35 @@ +# 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 #{SharedHelpers.pwd}/Gemfile" + 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) + + puts "Writing new Gemfile to #{SharedHelpers.pwd}/Gemfile" + File.open("Gemfile", "wb") do |file| + file << "# Generated from #{gemspec}\n" + file << spec.to_gemfile + end + else + puts "Writing new Gemfile to #{SharedHelpers.pwd}/Gemfile" + FileUtils.cp(File.expand_path("../../templates/Gemfile", __FILE__), "Gemfile") + end + end + end +end diff --git a/lib/bundler/cli/inject.rb b/lib/bundler/cli/inject.rb new file mode 100644 index 0000000000..b17292643f --- /dev/null +++ b/lib/bundler/cli/inject.rb @@ -0,0 +1,59 @@ +# 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 0000000000..ff6bedd9fd --- /dev/null +++ b/lib/bundler/cli/install.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +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 + + [:with, :without].each do |option| + if options[option] + options[option] = options[option].join(":").tr(" ", ":").split(":") + end + end + + check_for_group_conflicts + + normalize_groups + + 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] + unless Bundler.default_lockfile.exist? + flag = options[:deployment] ? "--deployment" : "--frozen" + raise ProductionError, "The #{flag} 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? + + Bundler.settings[:frozen] = "1" + end + + # When install is called with --no-deployment, disable deployment mode + if options[:deployment] == false + Bundler.settings.delete(:frozen) + options[:system] = true + end + + normalize_settings + + Bundler::Fetcher.disable_endpoint = options["full-index"] + + if options["binstubs"] + Bundler::SharedHelpers.major_deprecation \ + "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.settings[:frozen] + + Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}." + Bundler::CLI::Common.output_without_groups_message + + if Bundler.settings[:path] + absolute_path = File.expand_path(Bundler.settings[:path]) + relative_path = absolute_path.sub(File.expand_path(".") + File::SEPARATOR, "." + File::SEPARATOR) + Bundler.ui.confirm "Bundled gems are installed into #{relative_path}." + else + Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed." + end + + Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + + warn_ambiguous_gems + + if Bundler.settings[:clean] && Bundler.settings[:path] + 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 + if options[:without] && options[:with] + conflicting_groups = options[:without] & options[:with] + unless conflicting_groups.empty? + Bundler.ui.error "You can't list a group in both, --with and --without." \ + " The offending groups are: #{conflicting_groups.join(", ")}." + exit 1 + end + end + 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 + if options["trust-policy"] + unless Bundler.rubygems.security_policies.keys.include?(options["trust-policy"]) + Bundler.ui.error "Rubygems doesn't know about trust policy '#{options["trust-policy"]}'. " \ + "The known policies are: #{Bundler.rubygems.security_policies.keys.join(", ")}." + exit 1 + end + Bundler.settings["trust-policy"] = options["trust-policy"] + else + Bundler.settings["trust-policy"] = nil if Bundler.settings["trust-policy"] + end + end + + def normalize_groups + Bundler.settings.with = [] if options[:with] && options[:with].empty? + Bundler.settings.without = [] if options[:without] && options[:without].empty? + + 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[:path] = nil if options[:system] + Bundler.settings[:path] = "vendor/bundle" if options[:deployment] + Bundler.settings[:path] = options["path"] if options["path"] + Bundler.settings[:path] ||= "bundle" if options["standalone"] + + Bundler.settings[:bin] = options["binstubs"] if options["binstubs"] + Bundler.settings[:bin] = nil if options["binstubs"] && options["binstubs"].empty? + + Bundler.settings[:shebang] = options["shebang"] if options["shebang"] + + Bundler.settings[:jobs] = options["jobs"] if options["jobs"] + + Bundler.settings[:no_prune] = true if options["no-prune"] + + Bundler.settings[:no_install] = true if options["no-install"] + + Bundler.settings[:clean] = options["clean"] if options["clean"] + + Bundler.settings.without = options[:without] + Bundler.settings.with = options[:with] + + Bundler.settings[:disable_shared_gems] = Bundler.settings[:path] ? true : nil + 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 0000000000..ace0f985a9 --- /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.new.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/lock.rb b/lib/bundler/cli/lock.rb new file mode 100644 index 0000000000..223db9419f --- /dev/null +++ b/lib/bundler/cli/lock.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +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 0000000000..9a21f6811c --- /dev/null +++ b/lib/bundler/cli/open.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "bundler/cli/common" +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_clean_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 0000000000..863d0dd388 --- /dev/null +++ b/lib/bundler/cli/outdated.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +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 + + (gemfile_specs + dependency_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 + if Bundler.settings[:frozen] + 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 `bundle install --no-deployment`." + end + 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 0000000000..cf65e8a68c --- /dev/null +++ b/lib/bundler/cli/package.rb @@ -0,0 +1,46 @@ +# 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[:path] = File.expand_path(options[:path]) if options[:path] + Bundler.settings[:cache_all_platforms] = options["all-platforms"] if options.key?("all-platforms") + Bundler.settings[:cache_path] = options["cache-path"] if options.key?("cache-path") + + setup_cache_all + install + + # TODO: move cache contents here now that all bundles are locked + custom_path = Pathname.new(options[: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 + Bundler.settings[:cache_all] = options[:all] if options.key?("all") + + if Bundler.definition.has_local_dependencies? && !Bundler.settings[: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 0000000000..9fdab0a53c --- /dev/null +++ b/lib/bundler/cli/platform.rb @@ -0,0 +1,45 @@ +# 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 0000000000..277822dafc --- /dev/null +++ b/lib/bundler/cli/plugin.rb @@ -0,0 +1,23 @@ +# 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 0000000000..10d03b4b41 --- /dev/null +++ b/lib/bundler/cli/pristine.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Pristine + def run + 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 + + 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 + when Source::Git + source.remote! + else + Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.") + next + end + FileUtils.rm_rf spec.full_gem_path + + Bundler::GemInstaller.new(spec, installer, false, 0, true).install_from_spec + end + end + end +end diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb new file mode 100644 index 0000000000..47d4470aec --- /dev/null +++ b/lib/bundler/cli/show.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +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 0000000000..df7524f004 --- /dev/null +++ b/lib/bundler/cli/update.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +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) + + if gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !options[:bundler] + # 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? + specs = Bundler.definition.specs_for groups + gems.concat(specs.map(&:name)) + end + + Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby], + :lock_shared_dependencies => options[:conservative]) + 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[:jobs] = opts["jobs"] if opts["jobs"] + + Bundler.definition.validate_runtime! + installer = Installer.install Bundler.root, Bundler.definition, opts + Bundler.load.cache if Bundler.app_cache.exist? + + if Bundler.settings[:clean] && Bundler.settings[:path] + require "bundler/cli/clean" + Bundler::CLI::Clean.new(options).run + 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 0000000000..767fe8f3de --- /dev/null +++ b/lib/bundler/cli/viz.rb @@ -0,0 +1,30 @@ +# 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 0000000000..3ed05ca484 --- /dev/null +++ b/lib/bundler/compact_index_client.rb @@ -0,0 +1,108 @@ +# 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 0000000000..e44f05dc7e --- /dev/null +++ b/lib/bundler/compact_index_client/cache.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true +require "digest/md5" + +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 += "-#{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 0000000000..dc26095040 --- /dev/null +++ b/lib/bundler/compact_index_client/updater.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true +require "fileutils" +require "stringio" +require "tmpdir" +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 + 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? + FileUtils.cp local_path, local_temp_path + 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 + 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 + Digest::MD5.hexdigest(IO.read(path)) + end + end + end + end +end diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb new file mode 100644 index 0000000000..5b1c0a8cb1 --- /dev/null +++ b/lib/bundler/constants.rb @@ -0,0 +1,6 @@ +# 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 0000000000..cca40100ad --- /dev/null +++ b/lib/bundler/current_ruby.rb @@ -0,0 +1,85 @@ +# 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 + ).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 + x64_mingw + ).freeze + + def ruby? + !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev") + 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 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 0000000000..3e5b1bc447 --- /dev/null +++ b/lib/bundler/definition.rb @@ -0,0 +1,940 @@ +# frozen_string_literal: true +require "bundler/lockfile_parser" +require "digest/sha1" +require "set" + +module Bundler + class Definition + include GemHelpers + + attr_reader( + :dependencies, + :gem_version_promoter, + :locked_deps, + :locked_gems, + :platforms, + :requires, + :ruby_version + ) + + # 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 = []) + @unlocking = unlock == true || !unlock.empty? + + @dependencies = dependencies + @sources = sources + @unlock = unlock + @optional_groups = optional_groups + @remote = false + @specs = nil + @ruby_version = ruby_version + + @lockfile = lockfile + @lockfile_contents = String.new + @locked_bundler_version = nil + @locked_ruby_version = nil + + 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.settings[:frozen] + + 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]) + @unlock[:gems] = @locked_specs.for(eager_unlock).map(&:name) + end + + @gem_version_promoter = create_gem_version_promoter + + @dependency_changes = converge_dependencies + @local_changes = converge_locals + + @requires = compute_requires + end + + def create_gem_version_promoter + 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 + + 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 different version of #{locked_gem} that hasn't been removed in order to install." + end + unless specs["bundler"].any? + local = Bundler.settings[:frozen] ? rubygems_index : index + bundler = local.search(Gem::Dependency.new("bundler", VERSION)).last + specs["bundler"] = bundler if 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_dependencies + missing = [] + resolve.materialize(current_dependencies, missing) + missing + 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 + if Bundler.settings[:frozen] || (!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 + 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.dup + idx.add_source source.specs + dependency_names -= pinned_spec_names(source.specs) + dependency_names.concat(source.unmet_deps).uniq! + end + idx << Gem::Specification.new("ruby\0", RubyVersion.system.to_gem_version_with_patchlevel) + idx << Gem::Specification.new("rubygems\0", Gem::VERSION) + end + end + + # used when frozen is enabled so we can find the bundler + # spec, even if (say) a git gem is not checked out. + def rubygems_index + @rubygems_index ||= Index.build do |idx| + sources.rubygems_sources.each do |rubygems| + idx.add_source rubygems.specs + end + end + end + + 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 + 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.settings[:frozen] || !unlocking?) + return if lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) + + if Bundler.settings[:frozen] + 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 + out = String.new + + sources.lock_sources.each do |source| + # Add the source header + out << source.to_lock + # Find all specs for this source + resolve. + select {|s| source.can_lock?(s) }. + # This needs to be sorted by full name so that + # gems with the same name, but different platform + # are ordered consistently + sort_by(&:full_name). + each do |spec| + next if spec.name == "bundler" + out << spec.to_lock + end + out << "\n" + end + + out << "PLATFORMS\n" + + platforms.map(&:to_s).sort.each do |p| + out << " #{p}\n" + end + + out << "\n" + out << "DEPENDENCIES\n" + + handled = [] + dependencies.sort_by(&:to_s).each do |dep| + next if handled.include?(dep.name) + out << dep.to_lock + handled << dep.name + end + + if locked_ruby_version + out << "\nRUBY VERSION\n" + out << " #{locked_ruby_version}\n" + end + + # Record the version of Bundler that was used to create the lockfile + out << "\nBUNDLED WITH\n" + out << " #{locked_bundler_version}\n" + + out + 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 = Bundler.settings.locations("frozen")[:global] == "1" ? "bundle config --delete frozen" : "bundle install --no-deployment" + 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.is_a_path? && source.path.exist? } + deleted_sources.reject! {|source| source.is_a_path? && source.path.exist? } + 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.settings[: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 + 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"], + ].select(&:first).map(&:last).join(", ") + end + + def pretty_dep(dep, source = false) + msg = String.new(dep.name) + msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default + msg << " from the `#{dep.source}` source" if source && dep.source + msg + 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 + 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_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) + end + end + + # 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.settings[:frozen] + (@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 + + 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[: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 = s.source.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) + resolve = resolve.for(expand_dependencies(deps, true), @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 + 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 + + metadata_dependencies = [ + Dependency.new("ruby\0", ruby_versions), + Dependency.new("rubygems\0", Gem::VERSION), + ] + expand_dependencies(dependencies + metadata_dependencies, @remote) + 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) + 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(@platforms) + if platforms.empty? + 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) + source_requirements = {} + dependencies.each do |dep| + next unless dep.source + source_requirements[dep.name] = dep.source.specs + end + source_requirements + end + + def pinned_spec_names(specs) + names = [] + specs.each do |s| + # TODO: when two sources without blocks is an error, we can change + # this check to !s.source.is_a?(Source::LocalRubygems). For now, + # we need to ask every Rubygems for every gem name. + if s.source.is_a?(Source::Git) || s.source.is_a?(Source::Path) + names << s.name + end + end + names.uniq! + 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? + @locked_gems.specs.reduce({}) do |requirements, locked_spec| + dep = Gem::Dependency.new(locked_spec.name, ">= #{locked_spec.version}") + requirements[locked_spec.name] = DepProxy.new(dep, locked_spec.platform) + requirements + end.values + end + end +end diff --git a/lib/bundler/dep_proxy.rb b/lib/bundler/dep_proxy.rb new file mode 100644 index 0000000000..998975bbaf --- /dev/null +++ b/lib/bundler/dep_proxy.rb @@ -0,0 +1,46 @@ +# 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.hash + end + + def ==(other) + 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 0000000000..d2bac66cdb --- /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 + attr_reader :platforms + + 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, + :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) + + @autorequire = Array(options["require"] || []) if options.key?("require") + end + + def gem_platforms(valid_platforms) + return valid_platforms if @platforms.empty? + + platforms = [] + @platforms.each do |p| + platform = PLATFORM_MAP[p] + next unless valid_platforms.include?(platform) + platforms |= [platform] + end + 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 0000000000..94f2fac620 --- /dev/null +++ b/lib/bundler/deployment.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "bundler/shared_helpers" +Bundler::SharedHelpers.major_deprecation "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 0000000000..b978c0df6c --- /dev/null +++ b/lib/bundler/deprecate.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +module Bundler + if 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 + 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 0000000000..e4c257d267 --- /dev/null +++ b/lib/bundler/dsl.rb @@ -0,0 +1,564 @@ +# 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 + + 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 + add_git_sources + end + + def eval_gemfile(gemfile, contents = nil) + expanded_gemfile_path = Pathname.new(gemfile).expand_path + original_gemfile = @gemfile + @gemfile = 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 : {} + 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 } + if current.requirement != dep.requirement + if current.type == :development + @dependencies.delete current + else + return if dep.type == :development + 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})" + 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" \ + "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 + if current.type == :development + @dependencies.delete current + else + 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) + if options.key?("type") + options["type"] = options["type"].to_s + unless Plugin.source?(options["type"]) + raise "No 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? + source = normalize_source(source) + with_source(@sources.add_rubygems_source("remotes" => source), &blk) + else + source = normalize_source(source) + check_primary_source_safety(@sources) + @sources.add_rubygems_remote(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) + 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? + 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) + end + + def group(*args, &blk) + opts = Hash === args.last ? args.pop.dup : {} + normalize_group_options(opts, args) + + @groups.concat args + + if opts["optional"] + optional_groups = args - @optional_groups + @optional_groups.concat optional_groups + end + + yield + ensure + args.each { @groups.pop } + end + + def install_if(*args, &blk) + @install_conditionals.concat args + blk.call + 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 + git_source(:github) do |repo_name| + # 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"] + "https://github.com/#{repo_name}.git" + else + warn_github_source_change(repo_name) + "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| + user_name, repo_name = repo_name.split "/" + warn_deprecated_git_source(:bitbucket, 'https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git') + 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 ||= %w(group groups git path glob name branch ref tag require submodules platform platforms type source install_if) + 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 + + 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 + + if 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 + end + + def normalize_source(source) + case source + when :gemcutter, :rubygems, :rubyforge + Bundler::SharedHelpers.major_deprecation "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) + return unless source.rubygems_primary_remotes.any? + + # TODO: 2.0 upgrade from setting to default + if Bundler.settings[:disable_multisource] + raise GemfileError, "Warning: this Gemfile contains multiple primary sources. " \ + "Each source after the first must include a block to indicate which gems " \ + "should come from that source. To downgrade this error to a warning, run " \ + "`bundle config --delete disable_multisource`" + else + Bundler::SharedHelpers.major_deprecation "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_github_source_change(repo_name) + # TODO: 2.0 remove deprecation + Bundler::SharedHelpers.major_deprecation "The :github option uses the git: protocol, which is not secure. " \ + "Bundler 2.0 will use the https: protocol, which is secure. Enable this change now by " \ + "running `bundle config github.https true`." + end + + def warn_deprecated_git_source(name, repo_string) + # TODO: 2.0 remove deprecation + Bundler::SharedHelpers.major_deprecation <<-EOS +The :#{name} git source is deprecated, and will be removed in Bundler 2.0. Add this code to your Gemfile to ensure it continues to work: + git_source(:#{name}) do |repo_name| + "#{repo_string}" + end + 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 0000000000..5a1deeea47 --- /dev/null +++ b/lib/bundler/endpoint_specification.rb @@ -0,0 +1,132 @@ +# 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 0000000000..8b990baf40 --- /dev/null +++ b/lib/bundler/env.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +require "bundler/rubygems_integration" +require "bundler/source/git/git_proxy" + +module Bundler + class Env + def write(io) + io.write report + end + + def report(options = {}) + print_gemfile = options.delete(:print_gemfile) { true } + print_gemspecs = options.delete(:print_gemspecs) { true } + + out = String.new("## Environment\n\n```\n") + out << "Bundler #{Bundler::VERSION}\n" + out << "Rubygems #{Gem::VERSION}\n" + out << "Ruby #{ruby_version}" + out << "GEM_HOME #{ENV["GEM_HOME"]}\n" unless ENV["GEM_HOME"].nil? || ENV["GEM_HOME"].empty? + out << "GEM_PATH #{ENV["GEM_PATH"]}\n" unless ENV["GEM_PATH"] == ENV["GEM_HOME"] + out << "RVM #{ENV["rvm_version"]}\n" if ENV["rvm_version"] + out << "Git #{git_version}\n" + out << "Platform #{Gem::Platform.local}\n" + out << "OpenSSL #{OpenSSL::OPENSSL_VERSION}\n" if defined?(OpenSSL::OPENSSL_VERSION) + %w(rubygems-bundler open_gem).each do |name| + specs = Bundler.rubygems.find_name(name) + out << "#{name} (#{specs.map(&:version).join(",")})\n" unless specs.empty? + end + + out << "```\n" + + 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 + out << "\n## Gemfile\n" + out << "\n### #{Bundler.default_gemfile.relative_path_from(SharedHelpers.pwd)}\n\n" + out << "```ruby\n" << read_file(Bundler.default_gemfile).chomp << "\n```\n" + + 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 + + private + + def read_file(filename) + File.read(filename.to_s).strip + rescue Errno::ENOENT + "" + rescue => e + "#{e.class}: #{e.message}" + end + + def 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}]\n" + else + str << "p#{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL + str << " (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{RUBY_PLATFORM}]\n" + end + end + + def git_version + Bundler::Source::Git::GitProxy.new(nil, nil, nil).full_version + rescue Bundler::Source::Git::GitNotInstalledError + "not installed" + end + end +end diff --git a/lib/bundler/environment_preserver.rb b/lib/bundler/environment_preserver.rb new file mode 100644 index 0000000000..a891f4854d --- /dev/null +++ b/lib/bundler/environment_preserver.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +module Bundler + class EnvironmentPreserver + # @param env [ENV] + # @param keys [Array] + def initialize(env, keys) + @original = env.to_hash + @keys = keys + @prefix = "BUNDLER_ORIG_" + end + + # @return [Hash] + def backup + env = @original.clone + @keys.each do |key| + value = env[key] + original_value = env[@prefix + key] + if !value.nil? && !value.empty? && original_value.nil? + env[@prefix + key] = value + end + end + env + end + + # @return [Hash] + def restore + env = @original.clone + @keys.each do |key| + value_original = env[@prefix + key] + unless value_original.nil? || value_original.empty? + env[key] = value_original + env.delete(@prefix + key) + end + end + env + end + end +end diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb new file mode 100644 index 0000000000..6ce8493ea7 --- /dev/null +++ b/lib/bundler/errors.rb @@ -0,0 +1,157 @@ +# 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 0000000000..150cac1e67 --- /dev/null +++ b/lib/bundler/feature_flag.rb @@ -0,0 +1,32 @@ +# 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 + define_method("#{flag}?") do + value = Bundler.settings[flag] + value = instance_eval(&default) if value.nil? && !default.nil? + value + end + end + + (1..10).each {|v| define_method("bundler_#{v}_mode?") { major_version >= v } } + + settings_flag(:allow_offline_install) { bundler_2_mode? } + settings_flag(:only_update_to_newer_versions) { bundler_2_mode? } + settings_flag(:plugins) { @bundler_version >= Gem::Version.new("1.14") } + + def initialize(bundler_version) + @bundler_version = Gem::Version.create(bundler_version) + end + + def major_version + @bundler_version.segments.first + end + private :major_version + + class << self; private :settings_flag; end + end +end diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb new file mode 100644 index 0000000000..9e208e4957 --- /dev/null +++ b/lib/bundler/fetcher.rb @@ -0,0 +1,305 @@ +# 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 Gem.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 Gem.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 + "???" + 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 = Bundler::Persistent::Net::HTTP::Persistent.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 + + if Bundler.settings[:ssl_client_cert] + pem = File.read(Bundler.settings[: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, + Bundler::Persistent::Net::HTTP::Persistent::Error, Zlib::BufError, Errno::EHOSTUNREACH + ].freeze + + def bundler_cert_store + store = OpenSSL::X509::Store.new + if Bundler.settings[:ssl_ca_cert] + if File.directory? Bundler.settings[:ssl_ca_cert] + store.add_path Bundler.settings[:ssl_ca_cert] + else + store.add_file Bundler.settings[: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 0000000000..271729a534 --- /dev/null +++ b/lib/bundler/fetcher/base.rb @@ -0,0 +1,51 @@ +# 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 0000000000..97de88101b --- /dev/null +++ b/lib/bundler/fetcher/compact_index.rb @@ -0,0 +1,135 @@ +# 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 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 + + def md5_available? + require "openssl" + OpenSSL::Digest::MD5.digest("") + true + rescue LoadError + true + rescue OpenSSL::Digest::DigestError + false + end + end + end +end diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb new file mode 100644 index 0000000000..445b0f2332 --- /dev/null +++ b/lib/bundler/fetcher/dependency.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true +require "bundler/fetcher/base" +require "cgi" + +module Bundler + class Fetcher + class Dependency < Base + def 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 0000000000..453e4645eb --- /dev/null +++ b/lib/bundler/fetcher/downloader.rb @@ -0,0 +1,78 @@ +# 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, options = {}, counter = 0) + raise HTTPError, "Too many redirects" if counter >= redirect_limit + + response = request(uri, options) + 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, options, counter + 1) + 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, options) + validate_uri_scheme!(uri) + + Bundler.ui.debug "HTTP GET #{uri}" + req = Net::HTTP::Get.new uri.request_uri, options + 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 0000000000..d8e212989e --- /dev/null +++ b/lib/bundler/fetcher/index.rb @@ -0,0 +1,51 @@ +# 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 Gem.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 Gem.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 0000000000..3ba3dcdd91 --- /dev/null +++ b/lib/bundler/friendly_errors.rb @@ -0,0 +1,126 @@ +# 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 + 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.new.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 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 0000000000..936d1361fa --- /dev/null +++ b/lib/bundler/gem_helper.rb @@ -0,0 +1,193 @@ +# 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 Rubygems\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) + allowed_push_host = nil + gem_command = "gem push '#{path}'" + gem_command += " --key #{gem_key}" if gem_key + if @gemspec.respond_to?(:metadata) + allowed_push_host = @gemspec.metadata["allowed_push_host"] + gem_command += " --host #{allowed_push_host}" if allowed_push_host + end + 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 #{allowed_push_host ? allowed_push_host : "rubygems.org."}" + 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 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 + 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 0000000000..955834ff01 --- /dev/null +++ b/lib/bundler/gem_helpers.rb @@ -0,0 +1,100 @@ +# 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 0000000000..481838a5e2 --- /dev/null +++ b/lib/bundler/gem_remote_fetcher.rb @@ -0,0 +1,42 @@ +# 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 0000000000..230e7f28f2 --- /dev/null +++ b/lib/bundler/gem_tasks.rb @@ -0,0 +1,6 @@ +# 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 0000000000..d60d823d9c --- /dev/null +++ b/lib/bundler/gem_version_promoter.rb @@ -0,0 +1,175 @@ +# 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 + 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 + + # 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 = {} + 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 ENV["DEBUG_RESOLVER"] + + @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 ENV["DEBUG_RESOLVER"] + 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 + 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 0000000000..8595b8c7ea --- /dev/null +++ b/lib/bundler/gemdeps.rb @@ -0,0 +1,28 @@ +# 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 0000000000..e145590430 --- /dev/null +++ b/lib/bundler/graph.rb @@ -0,0 +1,151 @@ +# 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 0000000000..5f54796fa2 --- /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) } + 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 + + wants_prerelease = dependency.requirement.prerelease? + wants_prerelease ||= base && base.any? {|base_spec| base_spec.version.prerelease? } + only_prerelease = specs.all? {|spec| spec.version.prerelease? } + + unless wants_prerelease || only_prerelease + found.reject! {|spec| spec.version.prerelease? } + 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 0000000000..cba1b3d5e5 --- /dev/null +++ b/lib/bundler/injector.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true +module Bundler + class Injector + def self.inject(new_deps, options = {}) + injector = new(new_deps, options) + injector.inject(Bundler.default_gemfile, Bundler.default_lockfile) + end + + def initialize(new_deps, options = {}) + @new_deps = new_deps + @options = options + end + + def inject(gemfile_path, lockfile_path) + if Bundler.settings[:frozen] + # ensure the lock and Gemfile are synced + Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) + # temporarily remove frozen while we inject + frozen = Bundler.settings.delete(:frozen) + end + + # 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 + @new_deps -= builder.dependencies + + # add new deps to the end of the in-memory Gemfile + # Set conservative versioining to false because we want to let the resolver resolve the version first + builder.eval_gemfile("injected gems", build_gem_lines(false)) if @new_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 @new_deps.any? + + # since we resolved successfully, write out the lockfile + @definition.lock(Bundler.default_lockfile) + + # return an array of the deps that we added + return @new_deps + ensure + Bundler.settings[:frozen] = "1" if frozen + 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? + "~> #{segments[0..seg_end_index].join(".")}#{prerelease_suffix}" + end + + def build_gem_lines(conservative_versioning) + @new_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.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 + if @options["timestamp"] || @options["timestamp"].nil? + f.puts "# Added at #{Time.now} by #{`whoami`.chomp}:" + end + f.puts new_gem_lines + end + end + end +end diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb new file mode 100644 index 0000000000..38dcda6b5b --- /dev/null +++ b/lib/bundler/inline.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +# 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 + 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 + begin + !definition.missing_specs.empty? + rescue Bundler::GemNotFound, Bundler::GitError + definition.instance_variable_set(:@index, nil) + true + end + 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 0000000000..bce0e46393 --- /dev/null +++ b/lib/bundler/installer.rb @@ -0,0 +1,233 @@ +# 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("before-install-all", definition.dependencies) + installer.run(options) + 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 will create it. This is usually the location of gems + # on the system, be it RVM or at a system path. + # + # 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 using + # Bundler::Environment#dependencies. If there are no dependencies specified then + # Bundler returns a warning message stating so and this method returns. + # + # Fourthly, Bundler checks if the default lockfile (Gemfile.lock) exists, and if so + # then proceeds to set up a definition based on the default gemfile (Gemfile) and the + # default lock file (Gemfile.lock). However, this is not the case if the platform is different + # to that which is specified in Gemfile.lock, or if there are any missing specs for the gems. + # + # 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: TODO add documentation for how the standalone process works. + def run(options) + create_bundle_path + + if Bundler.settings[:frozen] + @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) + end + + if @definition.dependencies.empty? + Bundler.ui.warn "The Gemfile specifies no dependencies" + lock + return + end + + resolve_if_need(options) + ensure_specs_are_compatible! + install(options) + + lock unless Bundler.settings[:frozen] + Standalone.new(options[:standalone], @definition).generate if options[:standalone] + 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 = bin_path = Bundler.bin_path + template = template = File.read(File.expand_path("../templates/Executable", __FILE__)) + relative_gemfile_path = relative_gemfile_path = Bundler.default_gemfile.relative_path_from(bin_path) + ruby_command = ruby_command = Thor::Util.ruby_command + + exists = [] + spec.executables.each do |executable| + next if executable == "bundle" + + 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| + f.puts ERB.new(template, nil, "-").result(binding) + 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 + standalone_path = standalone_path = Bundler.root.join(Bundler.settings[:path]).relative_path_from(bin_path) + template = File.read(File.expand_path("../templates/Executable.standalone", __FILE__)) + ruby_command = ruby_command = Thor::Util.ruby_command + + spec.executables.each do |executable| + next if executable == "bundle" + executable_path = executable_path = Pathname(spec.full_gem_path).join(spec.bindir, executable).relative_path_from(bin_path) + File.open "#{bin_path}/#{executable}", "w", 0o755 do |f| + f.puts ERB.new(template, nil, "-").result(binding) + 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) + Bundler.rubygems.load_plugins + force = options["force"] + jobs = 1 + jobs = [Bundler.settings[:jobs].to_i - 1, 1].max if can_install_in_parallel? + install_in_parallel jobs, options[:standalone], force + 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 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.settings[:path]}` " \ + "because a file already exists at that path. Either remove or rename the file so the directory can be created." + end + + def resolve_if_need(options) + if !options["update"] && !options["force"] && !Bundler.settings[:inline] && Bundler.default_lockfile.file? + local = Bundler.ui.silence do + begin + tmpdef = Definition.build(Bundler.default_gemfile, Bundler.default_lockfile, nil) + true unless tmpdef.new_platform? || tmpdef.missing_dependencies.any? + rescue BundlerError + end + end + end + + return if local + options["local"] ? @definition.resolve_with_cache! : @definition.resolve_remotely! + 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 0000000000..a4d9bcaa07 --- /dev/null +++ b/lib/bundler/installer/gem_installer.rb @@ -0,0 +1,76 @@ +# 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 => 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 + "Make sure that `gem install #{spec.name} -v '#{spec.version}'` succeeds before bundling." + 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.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 0000000000..97c124e015 --- /dev/null +++ b/lib/bundler/installer/parallel_installer.rb @@ -0,0 +1,197 @@ +# 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 + + # Returns max number of threads machine can handle with a min of 1 + def self.max_threads + [Bundler.settings[:jobs].to_i - 1, 1].max + 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 + 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 + enqueue_specs + process_specs until @specs.all?(&:installed?) || @specs.any?(&:failed?) + handle_error if @specs.any?(&:failed?) + @specs + ensure + worker_pool && worker_pool.stop + end + + def worker_pool + @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda { |spec_install, worker_num| + gem_installer = Bundler::GemInstaller.new( + spec_install.spec, @installer, @standalone, worker_num, @force + ) + success, message = gem_installer.install_from_spec + if success && !message.nil? + spec_install.post_install_message = message + elsif !success + spec_install.state = :failed + spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}" + end + 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 + spec = worker_pool.deq + spec.state = :installed unless spec.failed? + enqueue_specs + 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 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 + + 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| + 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 0000000000..03411d85e2 --- /dev/null +++ b/lib/bundler/installer/standalone.rb @@ -0,0 +1,52 @@ +# 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 0000000000..8d9a02c2b8 --- /dev/null +++ b/lib/bundler/lazy_specification.rb @@ -0,0 +1,122 @@ +# 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.settings[: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.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_parser.rb b/lib/bundler/lockfile_parser.rb new file mode 100644 index 0000000000..dbf8926690 --- /dev/null +++ b/lib/bundler/lockfile_parser.rb @@ -0,0 +1,250 @@ +# 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 + @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 + Array(@opts["remote"]).each do |url| + @rubygems_aggregate.add_remote(url) + end + @current_source = @rubygems_aggregate + 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 0000000000..050cd0efd3 --- /dev/null +++ b/lib/bundler/match_platform.rb @@ -0,0 +1,23 @@ +# 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 0000000000..97a6776adb --- /dev/null +++ b/lib/bundler/mirror.rb @@ -0,0 +1,220 @@ +# 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] = @mirrors[config.uri] || Mirror.new) + end + config.update_mirror(mirror) + end + + private + + def fetch_valid_mirror_for(uri) + mirror = (@mirrors[URI(uri.to_s.downcase)] || @mirrors[URI(uri.to_s).host] || Mirror.new(uri)).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{^mirror\.(all|.+?)(\.fallback_timeout)?\/?$}).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 # 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 # 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 0000000000..66f485ef8e --- /dev/null +++ b/lib/bundler/plugin.rb @@ -0,0 +1,284 @@ +# frozen_string_literal: true +require "bundler/plugin/api" + +module Bundler + module Plugin + autoload :DSL, "bundler/plugin/dsl" + 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 => 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 + # + # Points to root in app_config_path if ran in an app else points to the one + # in user_bundle_path + 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.join("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) + @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? + + 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 => 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 0000000000..a2d5cbb4ac --- /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 0000000000..5d3f58df92 --- /dev/null +++ b/lib/bundler/plugin/api/source.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true +require "uri" +require "digest/sha1" + +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 + 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 + end + end + end +end diff --git a/lib/bundler/plugin/dsl.rb b/lib/bundler/plugin/dsl.rb new file mode 100644 index 0000000000..4bfc8437e0 --- /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/index.rb b/lib/bundler/plugin/index.rb new file mode 100644 index 0000000000..8dde072f16 --- /dev/null +++ b/lib/bundler/plugin/index.rb @@ -0,0 +1,157 @@ +# 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 = {} + + load_index(global_index_file, true) + 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 + @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 0000000000..a50d0ceedd --- /dev/null +++ b/lib/bundler/plugin/installer.rb @@ -0,0 +1,95 @@ +# 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"] + + if options[:git] + install_git(names, version, options) + else + sources = options[:source] || Bundler.rubygems.sources + install_rubygems(names, version, sources) + 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 0000000000..fbb6c5e40e --- /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 0000000000..7ae74fa93b --- /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 0000000000..33f5e5afbd --- /dev/null +++ b/lib/bundler/plugin/source_list.rb @@ -0,0 +1,28 @@ +# 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 initialize + @path_sources = [] + @git_sources = [] + @rubygems_aggregate = Plugin::Installer::Rubygems.new + @rubygems_sources = [] + end + + 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 + end + end + end +end diff --git a/lib/bundler/psyched_yaml.rb b/lib/bundler/psyched_yaml.rb new file mode 100644 index 0000000000..69d2ae78c5 --- /dev/null +++ b/lib/bundler/psyched_yaml.rb @@ -0,0 +1,27 @@ +# 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 diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb new file mode 100644 index 0000000000..208ee1d4b7 --- /dev/null +++ b/lib/bundler/remote_specification.rb @@ -0,0 +1,113 @@ +# 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 0000000000..db2ae496a4 --- /dev/null +++ b/lib/bundler/resolver.rb @@ -0,0 +1,410 @@ +# frozen_string_literal: true +module Bundler + class Resolver + require "bundler/vendored_molinillo" + + class Molinillo::VersionConflict + def printable_dep(dep) + if dep.is_a?(Bundler::Dependency) + DepProxy.new(dep, dep.platforms.join(", ")).to_s.strip + else + dep.to_s + end + end + + def message + conflicts.sort.reduce(String.new) do |o, (name, conflict)| + o << %(\nBundler could not find compatible versions for gem "#{name}":\n) + if conflict.locked_requirement + o << %( In snapshot (#{Bundler.default_lockfile.basename}):\n) + o << %( #{printable_dep(conflict.locked_requirement)}\n) + o << %(\n) + end + o << %( In Gemfile:\n) + trees = conflict.requirement_trees + + 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 + + o << trees.sort_by {|t| t.reverse.map(&:name) }.map do |tree| + t = String.new + depth = 2 + tree.each do |req| + t << " " * depth << req.to_s + unless tree.last == req + if spec = conflict.activated_by_name[req.name] + t << %( was resolved to #{spec.version}, which) + end + t << %( depends on) + end + t << %(\n) + depth += 1 + end + t + end.join("\n") + + 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" + if conflict.requirement_trees.first.size > 1 + o << "Could not find gem '#{conflict.requirement}', which is required by " + o << "gem '#{conflict.requirement_trees.first[-2]}', in any of the sources." + else + o << "Could not find gem '#{conflict.requirement}' in any of the sources\n" + end + end + o + end.strip + end + end + + class SpecGroup < Array + include GemHelpers + + attr_reader :activated + + def initialize(a) + super + @required_by = [] + @activated_platforms = [] + @dependencies = nil + @specs = Hash.new do |specs, platform| + specs[platform] = select_best_platform_match(self, platform) + end + end + + def initialize_copy(o) + super + @activated_platforms = o.activated.dup + 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 name + @name ||= first.name + end + + def version + @version ||= first.version + end + + def source + @source ||= first.source + end + + def for?(platform) + spec = @specs[platform] + !spec.nil? + end + + def 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 platforms_for_dependency_named(dependency) + __dependencies.select {|_, deps| deps.map(&:name).include? dependency }.keys + 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 + 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 + + # 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 + end + + def start(requirements) + 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 + raise VersionConflict.new(e.conflicts.keys.uniq, e.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 + 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) + groups << SpecGroup.new(specs) + 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 @gem_version_promoter.major? + 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_requirements[dependency.name] || @index + 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 sort_dependencies(dependencies, activated, conflicts) + dependencies.sort_by do |dependency| + name = name_for(dependency) + [ + @base_dg.vertex_named(name) ? 0 : 1, + activated.vertex_named(name).payload ? 0 : 1, + amount_constrained(dependency), + conflicts[name] ? 0 : 1, + activated.vertex_named(name).payload ? 0 : search_for(dependency).count, + ] + end + 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).size + search - all + end + end + end + end + + def verify_gemfile_dependencies_are_found!(requirements) + requirements.each do |requirement| + next if requirement.name == "bundler" + next unless search_for(requirement).empty? + if (base = @base[requirement.name]) && !base.empty? + version = base.first.version + message = "You have requested:\n" \ + " #{requirement.name} #{requirement.requirement}\n\n" \ + "The bundle currently has #{requirement.name} locked at #{version}.\n" \ + "Try running `bundle update #{requirement.name}`\n\n" \ + "If you are updating multiple gems in your Gemfile at once,\n" \ + "try passing them all to `bundle update`" + elsif requirement.source + name = requirement.name + specs = @source_requirements[name][name] + versions_with_platforms = specs.map {|s| [s.version, s.platform] } + message = String.new("Could not find gem '#{requirement}' in #{requirement.source}.\n") + message << if versions_with_platforms.any? + "Source contains '#{name}' at: #{formatted_versions_with_platforms(versions_with_platforms)}" + else + "Source does not contain any versions of '#{requirement}'" + end + else + cache_message = begin + " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? + rescue GemfileNotFound + nil + end + 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? + end + version_platform_strs.join(", ") + end + end +end diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb new file mode 100644 index 0000000000..092fb866b3 --- /dev/null +++ b/lib/bundler/retry.rb @@ -0,0 +1,65 @@ +# 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 0000000000..a410b7f3d7 --- /dev/null +++ b/lib/bundler/ruby_dsl.rb @@ -0,0 +1,17 @@ +# 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 0000000000..f0a001d296 --- /dev/null +++ b/lib/bundler/ruby_version.rb @@ -0,0 +1,151 @@ +# 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 + raise BundlerError, "RUBY_ENGINE value #{RUBY_ENGINE} is not recognized" + 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 0000000000..a0f8fa848b --- /dev/null +++ b/lib/bundler/rubygems_ext.rb @@ -0,0 +1,209 @@ +# 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 + + 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"].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 0000000000..977e13d948 --- /dev/null +++ b/lib/bundler/rubygems_gem_installer.rb @@ -0,0 +1,76 @@ +# 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 + + 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 = 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 0000000000..c3e16e086c --- /dev/null +++ b/lib/bundler/rubygems_integration.rb @@ -0,0 +1,862 @@ +# 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 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) + Gem.inflate(obj) + 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 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 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 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" + 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 + + # TODO: delete this in 2.0, it's a backwards compatibility shim + # see https://github.com/bundler/bundler/issues/5102 + kernel_class.send(:public, :gem) + 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 == name + Bundler::SharedHelpers.major_deprecation \ + "Bundler is using a binstub that was created for a different gem.\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 + post_reset_hooks.reject! do |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 + 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 0000000000..5540509d74 --- /dev/null +++ b/lib/bundler/runtime.rb @@ -0,0 +1,320 @@ +# frozen_string_literal: true +require "digest/sha1" + +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.settings[:frozen] + + 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 => 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"] + 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 = [] + 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_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 + + 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_git_cache_dirs.each do |cache_dir| + SharedHelpers.filesystem_access(cache_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 + # Store original MANPATH for restoration later in with_clean_env() + ENV["BUNDLER_ORIG_MANPATH"] = ENV["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? + 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 0000000000..1898738b7c --- /dev/null +++ b/lib/bundler/settings.rb @@ -0,0 +1,340 @@ +# frozen_string_literal: true +require "uri" + +module Bundler + class Settings + autoload :Mirror, "bundler/mirror" + autoload :Mirrors, "bundler/mirror" + + BOOL_KEYS = %w( + allow_offline_install + auto_install + cache_all + cache_all_platforms + disable_checksum_validation + disable_exec_load + disable_local_branch_check + disable_shared_gems + disable_version_check + force_ruby_platform + frozen + gem.coc + gem.mit + ignore_messages + major_deprecations + no_install + no_prune + only_update_to_newer_versions + plugins + silence_root_warning + ).freeze + + NUMBER_KEYS = %w( + redirect + retry + ssl_verify_mode + timeout + ).freeze + + DEFAULT_CONFIG = { + :redirect => 5, + :retry => 3, + :timeout => 10, + }.freeze + + attr_accessor :cli_flags_given + + def initialize(root = nil) + @root = root + @local_config = load_config(local_config_file) + @global_config = load_config(global_config_file) + @cli_flags_given = false + @temporary = {} + end + + def [](name) + key = key_for(name) + value = @temporary.fetch(name) 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 []=(key, value) + if cli_flags_given + command = if value.nil? + "bundle config --delete #{key}" + else + "bundle config #{key} #{Array(value).join(":")}" + end + + Bundler::SharedHelpers.major_deprecation \ + "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}`" + end + local_config_file || raise(GemfileNotFound, "Could not locate Gemfile") + set_key(key, value, @local_config, local_config_file) + end + alias_method :set_local, :[]= + + def temporary(update) + existing = Hash[update.map {|k, _| [k, @temporary[k]] }] + @temporary.update(update) + return unless block_given? + begin + yield + ensure + existing.each {|k, v| v.nil? ? @temporary.delete(k) : @temporary[k] = v } + end + end + + def delete(key) + @local_config.delete(key_for(key)) + end + + def set_global(key, value) + set_key(key, value, @global_config, global_config_file) + end + + def all + env_keys = ENV.keys.select {|k| k =~ /BUNDLE_.*/ } + + 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 =~ /^mirror\./ + mirrors + end + end + + def locations(key) + key = key_for(key) + locations = {} + 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 @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 + + def without=(array) + set_array(:without, array) + end + + def with=(array) + set_array(:with, array) + end + + def without + get_array(:without) + end + + def with + get_array(:with) + end + + # @local_config["BUNDLE_PATH"] should be prioritized over ENV["BUNDLE_PATH"] + def path + key = key_for(:path) + path = ENV[key] || @global_config[key] + return path if path && !@local_config.key?(key) + + if path = self[:path] + "#{path}/#{Bundler.ruby_scope}" + else + Bundler.rubygems.gem_dir + end + end + + def allow_sudo? + !@local_config.key?(key_for(:path)) + end + + def ignore_config? + ENV["BUNDLE_IGNORE_CONFIG"] + end + + def app_cache_path + @app_cache_path ||= begin + path = self[:cache_path] || "vendor/cache" + raise InvalidOption, "Cache path must be relative to the bundle path" if path.start_with?("/") + path + end + end + + private + + 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 + + def parent_setting_for(name) + split_specfic_setting_for(name)[0] + end + + def specfic_gem_for(name) + split_specfic_setting_for(name)[1] + end + + def split_specfic_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(value) + NUMBER_KEYS.include?(value.to_s) + end + + def get_array(key) + self[key] ? self[key].split(":").map(&:to_sym) : [] + end + + def set_array(key, array) + self[key] = (array.empty? ? nil : array.join(":")) if array + end + + def set_key(key, value, hash, file) + key = key_for(key) + + unless hash[key] == value + hash[key] = value + hash.delete(key) if value.nil? + 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 + + value + end + + def converted_value(value, key) + if value.nil? + nil + elsif is_bool(key) || value == "false" + to_bool(value) + elsif is_num(key) + value.to_i + else + value + 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.join("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 + + # TODO: duplicates Rubygems#normalize_uri + # TODO: is this the correct place to validate mirror URIs? + def self.normalize_uri(uri) + uri = uri.to_s + uri = "#{uri}/" unless uri =~ %r{/\Z} + uri = URI(uri) + unless uri.absolute? + raise ArgumentError, format("Gem sources must be absolute. You provided '%s'.", uri) + end + uri + end + end +end diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb new file mode 100644 index 0000000000..9aae6478cd --- /dev/null +++ b/lib/bundler/setup.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +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 0000000000..a9141a1346 --- /dev/null +++ b/lib/bundler/shared_helpers.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true +require "pathname" +require "rubygems" + +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 default_gemfile + gemfile = find_gemfile + raise GemfileNotFound, "Could not locate Gemfile" unless gemfile + Pathname.new(gemfile).untaint + 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(message) + 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 #{Bundler::VERSION.split(".").first.to_i + 1}.0] #{message}") + end + + def print_major_deprecations! + deprecate_gemfile(find_gemfile) if find_gemfile == find_file("Gemfile") + if RUBY_VERSION < "2" + major_deprecation("Bundler will only support ruby >= 2.0, you are running #{RUBY_VERSION}") + end + return if Bundler.rubygems.provides?(">= 2") + major_deprecation("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 + + private + + def validate_bundle_path + return unless Bundler.bundle_path.to_s.include?(File::PATH_SEPARATOR) + message = "Your bundle path contains a '#{File::PATH_SEPARATOR}', " \ + "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 include '#{File::PATH_SEPARATOR}'." \ + "\nYour current bundle path is '#{Bundler.bundle_path}'." + raise Bundler::PathError, message + end + + def find_gemfile + given = ENV["BUNDLE_GEMFILE"] + return given if given && !given.empty? + find_file("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 + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + gemspec = "lib/bundler.gemspec" + else + gemspec = "bundler.gemspec" + end + 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_bundle_variables + begin + ENV["BUNDLE_BIN_PATH"] = Bundler.rubygems.bin_path("bundler", "bundle", VERSION) + rescue Gem::GemNotFoundException + if File.exist?(File.expand_path("../../../exe/bundle", __FILE__)) + ENV["BUNDLE_BIN_PATH"] = File.expand_path("../../../exe/bundle", __FILE__) + else + ENV["BUNDLE_BIN_PATH"] = File.expand_path("../../../../bin/bundle", __FILE__) + end + end + + # Set BUNDLE_GEMFILE + ENV["BUNDLE_GEMFILE"] = find_gemfile.to_s + 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" + 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) + ENV["RUBYOPT"] = rubyopt.join(" ") + end + + def set_rubylib + rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR) + rubylib.unshift bundler_ruby_lib + ENV["RUBYLIB"] = rubylib.uniq.join(File::PATH_SEPARATOR) + end + + def bundler_ruby_lib + 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 File.expand_path(p).start_with?(bundler_lib) + loaded_gem_paths.delete(p) + end + $LOAD_PATH.uniq! + 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 + + def deprecate_gemfile(gemfile) + return unless gemfile && File.basename(gemfile) == "Gemfile" + Bundler::SharedHelpers.major_deprecation \ + "gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock." + end + + extend self + end +end diff --git a/lib/bundler/similarity_detector.rb b/lib/bundler/similarity_detector.rb new file mode 100644 index 0000000000..e9c1413ea3 --- /dev/null +++ b/lib/bundler/similarity_detector.rb @@ -0,0 +1,62 @@ +# 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 0000000000..cf56ed1cc1 --- /dev/null +++ b/lib/bundler/source.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true +module Bundler + class Source + autoload :Gemspec, "bundler/source/gemspec" + autoload :Git, "bundler/source/git" + 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 + + def include?(other) + other == self + end + + def inspect + "#<#{self.class}:0x#{object_id} #{self}>" + 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 + end +end diff --git a/lib/bundler/source/gemspec.rb b/lib/bundler/source/gemspec.rb new file mode 100644 index 0000000000..05e613277f --- /dev/null +++ b/lib/bundler/source/gemspec.rb @@ -0,0 +1,17 @@ +# 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 0000000000..b3e218e390 --- /dev/null +++ b/lib/bundler/source/git.rb @@ -0,0 +1,324 @@ +# frozen_string_literal: true +require "fileutils" +require "uri" +require "digest/sha1" + +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"] || "" + @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 + + "#{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] + + Bundler.ui.info "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 + elsif force + git_proxy.copy_to(install_path, submodules) + 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.settings[: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 + git_scope = "#{base_name}-#{uri_hash}" + + if Bundler.requires_sudo? + Bundler.user_bundle_path.join("cache/git", git_scope) + else + Bundler.cache.join("git", git_scope) + end + 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 + 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 + raise unless Bundler.feature_flag.allow_offline_install? + Bundler.ui.warn "Using cached git data because of network errors" + 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 + 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 0000000000..c05d7a5afa --- /dev/null +++ b/lib/bundler/source/git/git_proxy.rb @@ -0,0 +1,252 @@ +# 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, 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 + if path.exist? + return if has_revision_cached? + Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" + in_path do + git_retry %(fetch --force --quiet --tags #{uri_escaped_with_configured_credentials} "refs/heads/*:refs/heads/*") + end + else + Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" + 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) + 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}") + git "reset --hard #{@revision}" + + 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 #{command}`", 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? + 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/path.rb b/lib/bundler/source/path.rb new file mode 100644 index 0000000000..8dd0763cc1 --- /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! + @allow_remote = true + end + + def cached! + @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 = {}) + Bundler.ui.info "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.settings[: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 is_a_path? + instance_of?(Path) + 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] + ) + 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 0000000000..9c2f74a31b --- /dev/null +++ b/lib/bundler/source/path/installer.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +module Bundler + class Source + class Path + class Installer < Bundler::RubyGemsGemInstaller + attr_reader :spec + + def initialize(spec, 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 0000000000..353194f53f --- /dev/null +++ b/lib/bundler/source/rubygems.rb @@ -0,0 +1,462 @@ +# 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! + @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) + 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 + remote_names = remotes.map(&:to_s).join(", ") + "rubygems repository #{remote_names}" + 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 || spec.name.eql?("bundler")) + Bundler.ui.info "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 + + 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 + ).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 replace_remotes(other_remotes) + return false if other_remotes == @remotes + + @remotes = [] + other_remotes.reverse_each do |r| + add_remote r.to_s + end + 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 + + 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 = remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s + if remote.userinfo && remote.userinfo == Bundler.settings[remote_nouser] + remote_nouser + else + remote + end + end + + def installed_specs + @installed_specs ||= begin + idx = Index.new + have_bundler = false + Bundler.rubygems.all_specs.reverse_each do |spec| + if spec.name == "bundler" + next unless spec.version.to_s == VERSION + have_bundler = true + end + 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 + + # Always have bundler locally + unless have_bundler + # We're running bundler directly from the source + # so, let's create a fake gemspec for it (it's a path) + # gemspec + bundler = Gem::Specification.new do |s| + s.name = "bundler" + s.version = VERSION + s.platform = Gem::Platform::RUBY + s.source = self + s.authors = ["bundler team"] + s.loaded_from = File.expand_path("..", __FILE__) + end + idx << bundler + end + idx + 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 + end + + idx + 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 + index_fetchers.each do |f| + Bundler.ui.info "Fetching source index from #{f.uri}" + idx.use f.specs_with_retry(nil, self) + end + + # 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 100 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 + + if allow_api + api_fetchers.each do |f| + Bundler.ui.info "Fetching gem metadata from #{f.uri}", Bundler.ui.debug? + idx.use f.specs_with_retry(dependency_names, self) + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + 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. + loop do + idxcount = idx.size + api_fetchers.each do |f| + Bundler.ui.info "Fetching version metadata from #{f.uri}", Bundler.ui.debug? + idx.use f.specs_with_retry(idx.dependency_names, self), true + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + end + break if idxcount == idx.size + end + + if api_fetchers.any? + # 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. + unmet = idx.unmet_dependency_names + + # if there are any cross-site gems we missed, get them now + api_fetchers.each do |f| + Bundler.ui.info "Fetching dependency metadata from #{f.uri}", Bundler.ui.debug? + idx.use f.specs_with_retry(unmet, self) + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + end if unmet.any? + else + allow_api = false + end + end + + unless allow_api + api_fetchers.each do |f| + Bundler.ui.info "Fetching source index from #{f.uri}" + idx.use f.specs_with_retry(nil, self) + end + end + end + end + + def fetch_gem(spec) + return false unless spec.remote + uri = spec.remote.uri + spec.fetch_platform + Bundler.ui.confirm("Fetching #{version_message(spec)}") + + 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 + Bundler.rubygems.download_gem(spec, uri, 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 + end + end +end diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb new file mode 100644 index 0000000000..b49e645506 --- /dev/null +++ b/lib/bundler/source/rubygems/remote.rb @@ -0,0 +1,63 @@ +# 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 + cache_uri = original_uri || uri + + uri_parts = [cache_uri.host, cache_uri.user, cache_uri.port, cache_uri.path] + uri_digest = 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 0000000000..b6ce6029c8 --- /dev/null +++ b/lib/bundler/source_list.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true +module Bundler + class SourceList + attr_reader :path_sources, + :git_sources, + :plugin_sources + + def initialize + @path_sources = [] + @git_sources = [] + @plugin_sources = [] + @rubygems_aggregate = Source::Rubygems.new + @rubygems_sources = [] + 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 add_rubygems_remote(uri) + @rubygems_aggregate.add_remote(uri) + @rubygems_aggregate + end + + def rubygems_sources + @rubygems_sources + [@rubygems_aggregate] + end + + def rubygems_remotes + rubygems_sources.map(&:remotes).flatten.uniq + end + + def all_sources + path_sources + git_sources + plugin_sources + rubygems_sources + end + + def get(source) + source_list_for(source).find {|s| source == s } + end + + def lock_sources + lock_sources = (path_sources + git_sources + plugin_sources).sort_by(&:to_s) + lock_sources << combine_rubygems_sources + end + + 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 = + replacement_sources.detect {|s| s.is_a?(Source::Rubygems) } + @rubygems_aggregate = replacement_rubygems if replacement_rubygems + + # Return true if there were changes + lock_sources.to_set != replacement_sources.to_set || + rubygems_remotes.to_set != replacement_rubygems.remotes.to_set + 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 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 + end +end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb new file mode 100644 index 0000000000..9642633578 --- /dev/null +++ b/lib/bundler/spec_set.rb @@ -0,0 +1,187 @@ +# 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 + raise "Unable to find a spec satisfying #{dep} in the set. Perhaps the lockfile is corrupted?" + 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 |s| + next if arr.any? {|s2| s2.name == s.name && s2.version == s.version && s2.platform == s.platform } + arr << s + 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 0000000000..fb66f13c33 --- /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 0000000000..a5e5d84b64 --- /dev/null +++ b/lib/bundler/ssl_certs/certificate_manager.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true +require "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 0000000000..f4ce4ca43d --- /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 0000000000..9e6810ab70 --- /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 0000000000..20585f1c01 --- /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 0000000000..aeacf245a3 --- /dev/null +++ b/lib/bundler/stub_specification.rb @@ -0,0 +1,107 @@ +# 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/Executable b/lib/bundler/templates/Executable new file mode 100644 index 0000000000..fe22de0a6d --- /dev/null +++ b/lib/bundler/templates/Executable @@ -0,0 +1,17 @@ +#!/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) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("<%= spec.name %>", "<%= executable %>") diff --git a/lib/bundler/templates/Executable.standalone b/lib/bundler/templates/Executable.standalone new file mode 100644 index 0000000000..4bf0753f44 --- /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 0000000000..21c6283123 --- /dev/null +++ b/lib/bundler/templates/Gemfile @@ -0,0 +1,6 @@ +# 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/newgem/.travis.yml.tt b/lib/bundler/templates/newgem/.travis.yml.tt new file mode 100644 index 0000000000..fe0761cc23 --- /dev/null +++ b/lib/bundler/templates/newgem/.travis.yml.tt @@ -0,0 +1,5 @@ +sudo: false +language: ruby +rvm: + - <%= RUBY_VERSION %> +before_install: gem install bundler -v <%= Bundler::VERSION %> 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 0000000000..a3833d29d7 --- /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 0000000000..c114bd6665 --- /dev/null +++ b/lib/bundler/templates/newgem/Gemfile.tt @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# 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 0000000000..76ef4b0191 --- /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 0000000000..edbe55dabe --- /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](http://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 0000000000..099da6f3ec --- /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 0000000000..a27f82430f --- /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 0000000000..dce67d860a --- /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 0000000000..a8339bb79f --- /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 0000000000..8cfc828f94 --- /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 0000000000..8177c4d202 --- /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 0000000000..c6e420b66e --- /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 0000000000..573d76b4c2 --- /dev/null +++ b/lib/bundler/templates/newgem/gitignore.tt @@ -0,0 +1,21 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_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 0000000000..7d8ad90ab0 --- /dev/null +++ b/lib/bundler/templates/newgem/lib/newgem.rb.tt @@ -0,0 +1,12 @@ +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 %># 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 0000000000..389daf5048 --- /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 0000000000..caea7fe7be --- /dev/null +++ b/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -0,0 +1,46 @@ +# coding: utf-8 +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'" + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end + + spec.files = `git ls-files -z`.split("\x0").reject do |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 0000000000..8c18f1abdd --- /dev/null +++ b/lib/bundler/templates/newgem/rspec.tt @@ -0,0 +1,2 @@ +--format documentation +--color 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 0000000000..b7ef7f9e4a --- /dev/null +++ b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt @@ -0,0 +1,11 @@ +require "spec_helper" + +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 0000000000..805cf57e01 --- /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 0000000000..f2af9f90e0 --- /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 0000000000..725e3e4647 --- /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/ui.rb b/lib/bundler/ui.rb new file mode 100644 index 0000000000..794c000dc4 --- /dev/null +++ b/lib/bundler/ui.rb @@ -0,0 +1,8 @@ +# 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 0000000000..95a1ecdf0c --- /dev/null +++ b/lib/bundler/ui/rg_proxy.rb @@ -0,0 +1,18 @@ +# 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 0000000000..87a92471fb --- /dev/null +++ b/lib/bundler/ui/shell.rb @@ -0,0 +1,133 @@ +# 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 if @warning_history.include? msg + @warning_history << msg + tell_me(msg, :yellow, newline) if level("warn") + end + + def error(msg, newline = nil) + tell_me(msg, :red, newline) if level("error") + 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) + 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 0000000000..48390b7198 --- /dev/null +++ b/lib/bundler/ui/silent.rb @@ -0,0 +1,68 @@ +# 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 0000000000..997a307533 --- /dev/null +++ b/lib/bundler/uri_credentials_filter.rb @@ -0,0 +1,36 @@ +# 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/molinillo/lib/molinillo.rb b/lib/bundler/vendor/molinillo/lib/molinillo.rb new file mode 100644 index 0000000000..134bf1d720 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +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/delegates/resolution_state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb new file mode 100644 index 0000000000..253c18764f --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb @@ -0,0 +1,50 @@ +# 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 + 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 0000000000..29f48d5b3c --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb @@ -0,0 +1,80 @@ +# 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 0000000000..76e84ab7e6 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb @@ -0,0 +1,222 @@ +# 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_node = vertex_named(parent_name) + add_edge(parent_node, 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 0000000000..e0dfe6cbbd --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb @@ -0,0 +1,35 @@ +# 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 0000000000..9092e4d546 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb @@ -0,0 +1,65 @@ +# 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 0000000000..eda4251801 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.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#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 0000000000..e9125a59c6 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.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#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 0000000000..d20b2cb0e0 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb @@ -0,0 +1,60 @@ +# 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 0000000000..72a705e023 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb @@ -0,0 +1,125 @@ +# 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 0000000000..8d8e10fedf --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb @@ -0,0 +1,45 @@ +# 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 0000000000..53524d36ad --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb @@ -0,0 +1,35 @@ +# 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 0000000000..eab989e7bc --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb @@ -0,0 +1,125 @@ +# 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 + 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 += vertices.map(&:recursive_predecessors).flatten(1) + 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 += vertices.map(&:recursive_successors).flatten(1) + 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) + equal?(other) || successors.any? { |v| v.path_to?(other) } + end + + alias descendent? 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 0000000000..f904bd0814 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb @@ -0,0 +1,75 @@ +# 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 + 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] nodes the nodes in the dependency + # that caused the error + def initialize(nodes) + super "There is a circular dependency between #{nodes.map(&:name).join(' and ')}" + @dependencies = nodes.map(&:payload).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 + + # Initializes a new error with the given version conflicts. + # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} + def initialize(conflicts) + pairs = [] + conflicts.values.flatten.map(&:requirements).flatten.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 + 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 0000000000..a4fb6dd68e --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +module Bundler::Molinillo + # The version of Bundler::Molinillo. + VERSION = '0.5.7'.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 0000000000..0f1ad195f2 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb @@ -0,0 +1,100 @@ +# 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 0000000000..d47cfa2928 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb @@ -0,0 +1,65 @@ +# 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) + output.puts debug_info.split("\n").map { |s| ' ' * depth + s } + 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 0000000000..1845966a75 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb @@ -0,0 +1,494 @@ +# 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 the spec 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. + Conflict = Struct.new( + :requirement, + :requirements, + :existing, + :possibility, + :locked_requirement, + :requirement_trees, + :activated_by_name + ) + + # @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 unless state.requirements.any? || state.requirement + 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 + + activated.freeze + 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 + + # 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 if state.is_a? PossibilityState + unwind_for_conflict until possibility && state.is_a?(DependencyState) + end + 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 { |r| dg.add_vertex(name_for(r), nil, true).tap { |v| v.explicit_requirements << r } } + 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, + initial_requirement && search_for(initial_requirement), + 0, + {} + ) + end + + # Unwinds the states stack because a conflict has been encountered + # @return [void] + def unwind_for_conflict + debug(depth) { "Unwinding for conflict: #{requirement} to #{state_index_for_unwind / 2}" } + conflicts.tap do |c| + sliced_states = states.slice!((state_index_for_unwind + 1)..-1) + raise VersionConflict.new(c) unless state + activated.rewind_to(sliced_states.first || :initial_state) if sliced_states + state.conflicts = c + index = states.size - 1 + @parents_of.each { |_, a| a.reject! { |i| i >= index } } + end + end + + # @return [Integer] The index to which the resolution should unwind in the + # case of conflict. + def state_index_for_unwind + current_requirement = requirement + existing_requirement = requirement_for_existing_name(name) + index = -1 + [current_requirement, existing_requirement].each do |r| + until r.nil? + current_state = find_state_for(r) + if state_any?(current_state) + current_index = states.index(current_state) + index = current_index if current_index > index + break + end + r = parent_of(r) + end + end + + index + 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 activated.vertex_named(name).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.reverse_each.find { |i| requirement == i.requirement && i.is_a?(DependencyState) } + end + + # @return [Boolean] whether or not the given state has any possibilities + # left. + def state_any?(state) + state && state.possibilities.any? + end + + # @return [Conflict] a {Conflict} that reflects the failure to activate + # the {#possibility} in conjunction with the current {#state} + def create_conflict + 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 { |edge| (requirements[edge.origin.payload] ||= []).unshift(edge.requirement) } + + activated_by_name = {} + activated.each { |v| activated_by_name[v.name] = v.payload if v.payload } + conflicts[name] = Conflict.new( + requirement, + requirements, + vertex.payload, + possibility, + locked_requirement, + requirement_trees, + activated_by_name + ) + 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_node = activated.vertex_named(name) + if existing_node.payload + debug(depth) { "Found existing spec (#{existing_node.payload})" } + attempt_to_activate_existing_spec(existing_node) + else + attempt_to_activate_new_spec + end + end + + # Attempts to activate the current {#possibility} (given that it has + # already been activated) + # @return [void] + def attempt_to_activate_existing_spec(existing_node) + existing_spec = existing_node.payload + if requirement_satisfied_by?(requirement, activated, existing_spec) + new_requirements = requirements.dup + push_state_for_requirements(new_requirements, false) + else + return if attempt_to_swap_possibility + create_conflict + debug(depth) { "Unsatisfied by existing spec (#{existing_node.payload})" } + unwind_for_conflict + end + end + + # Attempts to swp the current {#possibility} with the already-activated + # spec with the given name + # @return [Boolean] Whether the possibility was swapped into {#activated} + def attempt_to_swap_possibility + activated.tag(:swap) + vertex = activated.vertex_named(name) + activated.set_payload(name, possibility) + if !vertex.requirements. + all? { |r| requirement_satisfied_by?(r, activated, possibility) } || + !new_spec_satisfied? + activated.rewind_to(:swap) + return + end + fixup_swapped_children(vertex) + activate_spec + end + + # Ensures there are no orphaned successors to the given {vertex}. + # @param [DependencyGraph::Vertex] vertex the vertex to fix up. + # @return [void] + def fixup_swapped_children(vertex) # rubocop:disable Metrics/CyclomaticComplexity + payload = vertex.payload + deps = dependencies_for(payload).group_by(&method(:name_for)) + vertex.outgoing_edges.each do |outgoing_edge| + requirement = outgoing_edge.requirement + parent_index = @parents_of[requirement].last + succ = outgoing_edge.destination + matching_deps = Array(deps[succ.name]) + dep_matched = matching_deps.include?(requirement) + + # only push the current index when it was originally required by the + # same named spec + if parent_index && states[parent_index].name == name + @parents_of[requirement].push(states.size - 1) + end + + if matching_deps.empty? && !succ.root? && succ.predecessors.to_a == [vertex] + debug(depth) { "Removing orphaned spec #{succ.name} after swapping #{name}" } + succ.requirements.each { |r| @parents_of.delete(r) } + + removed_names = activated.detach_vertex_named(succ.name).map(&:name) + requirements.delete_if do |r| + # the only removed vertices are those with no other requirements, + # so it's safe to delete only based upon name here + removed_names.include?(name_for(r)) + end + elsif !dep_matched + debug(depth) { "Removing orphaned dependency #{requirement} after swapping #{name}" } + # also reset if we're removing the edge, but only if its parent has + # already been fixed up + @parents_of[requirement].push(states.size - 1) if @parents_of[requirement].empty? + + activated.delete_edge(outgoing_edge) + requirements.delete(requirement) + end + end + end + + # Attempts to activate the current {#possibility} (given that it hasn't + # already been activated) + # @return [void] + def attempt_to_activate_new_spec + if new_spec_satisfied? + activate_spec + else + create_conflict + unwind_for_conflict + end + end + + # @return [Boolean] whether the current spec is satisfied as a new + # possibility. + def new_spec_satisfied? + unless requirement_satisfied_by?(requirement, activated, possibility) + debug(depth) { 'Unsatisfied by requested spec' } + return false + end + + locked_requirement = locked_requirement_named(name) + + locked_spec_satisfied = !locked_requirement || + requirement_satisfied_by?(locked_requirement, activated, possibility) + debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied + + locked_spec_satisfied + 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_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_spec the specification that has just been + # activated + # @return [void] + def require_nested_dependencies_for(activated_spec) + nested_dependencies = dependencies_for(activated_spec) + debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } + nested_dependencies.each do |d| + activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], 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 = new_requirements.shift + new_name = new_requirement ? name_for(new_requirement) : ''.freeze + possibilities = new_requirement ? search_for(new_requirement) : [] + handle_missing_or_push_dependency_state DependencyState.new( + new_name, new_requirements, new_activated, + new_requirement, possibilities, depth, conflicts.dup + ) + 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 node 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 0000000000..50d853b146 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb @@ -0,0 +1,45 @@ +# 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 0000000000..3a8107cf1a --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb @@ -0,0 +1,54 @@ +# 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 [Set] conflicts unresolved conflicts + ResolutionState = Struct.new( + :name, + :requirements, + :activated, + :requirement, + :possibilities, + :depth, + :conflicts + ) + + class ResolutionState + # Returns an empty resolution state + # @return [ResolutionState] an empty state + def self.empty + new(nil, [], DependencyGraph.new, nil, nil, 0, Set.new) + 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 + ).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 0000000000..e5e09080c2 --- /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 0000000000..c872a79c13 --- /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 recieved. + # + # 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 0000000000..1b6b789f6d --- /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 0000000000..999e8b7e61 --- /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 0000000000..e6698572a9 --- /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 0000000000..97d22d9bbd --- /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 0000000000..3a664401b4 --- /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 0000000000..f555f7b7e0 --- /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 0000000000..284d92c19a --- /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 0000000000..4c83bebc86 --- /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 0000000000..349b26ff65 --- /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 0000000000..9bd1077170 --- /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 0000000000..c636948e5d --- /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 0000000000..c167aa33b8 --- /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 0000000000..0f6e2e0af2 --- /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 0000000000..76f1e43c65 --- /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 0000000000..2f816081f3 --- /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 0000000000..05ddc10cd3 --- /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 0000000000..866d2212a7 --- /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 0000000000..ce81a17484 --- /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 0000000000..0adb2b3137 --- /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 0000000000..dd39cff35d --- /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 0000000000..08f80e565d --- /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 0000000000..dfe7398583 --- /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 0000000000..1fd790f4b7 --- /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 0000000000..85169b56c8 --- /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 0000000000..70f6366842 --- /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 0000000000..60282e2991 --- /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 0000000000..65ae422d7f --- /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/md5" +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 0000000000..e945549324 --- /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 0000000000..5162390efd --- /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 0000000000..da289cb50c --- /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 0000000000..83d2054988 --- /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 0000000000..5d03177a28 --- /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 0000000000..df8f18821a --- /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_molinillo.rb b/lib/bundler/vendored_molinillo.rb new file mode 100644 index 0000000000..7b231263cb --- /dev/null +++ b/lib/bundler/vendored_molinillo.rb @@ -0,0 +1,3 @@ +# 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 0000000000..729ac6b6f5 --- /dev/null +++ b/lib/bundler/vendored_persistent.rb @@ -0,0 +1,17 @@ +# 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" diff --git a/lib/bundler/vendored_thor.rb b/lib/bundler/vendored_thor.rb new file mode 100644 index 0000000000..4a5d0cf6bb --- /dev/null +++ b/lib/bundler/vendored_thor.rb @@ -0,0 +1,7 @@ +# 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 0000000000..b2dad6dfb6 --- /dev/null +++ b/lib/bundler/version.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# 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 = "1.15.4" 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 +end diff --git a/lib/bundler/version_ranges.rb b/lib/bundler/version_ranges.rb new file mode 100644 index 0000000000..1ee8440edd --- /dev/null +++ b/lib/bundler/version_ranges.rb @@ -0,0 +1,75 @@ +# 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 0000000000..db78f84baa --- /dev/null +++ b/lib/bundler/vlad.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +# 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 0000000000..b73a7ed04a --- /dev/null +++ b/lib/bundler/worker.rb @@ -0,0 +1,105 @@ +# 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 0000000000..3c9eccafc2 --- /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, _, val = match.captures + key = convert_to_backward_compatible_key(key) + depth = indent.scan(/ /).length + if 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/spec/README.md b/spec/README.md index 3c32a6727e..a17a93f8cc 100644 --- a/spec/README.md +++ b/spec/README.md @@ -1,4 +1,15 @@ -# ruby/spec +# 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/rubyspec ruby/spec (https://github.com/ruby/spec/) is a test suite for the Ruby language. diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb new file mode 100644 index 0000000000..268c0d99ac --- /dev/null +++ b/spec/bundler/bundler/bundler_spec.rb @@ -0,0 +1,212 @@ +# encoding: utf-8 +# frozen_string_literal: true +require "spec_helper" +require "bundler" + +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 + 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 = {} + settings = { :disable_shared_gems => true } + Bundler.send(:configure_gem_path, env, settings) + 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(FileUtils).to receive(:remove_entry_secure).and_raise(ArgumentError) + allow(File).to receive(:world_writable?).and_return(true) + message = <" do + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env 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 "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(out).to start_with("Running `bundle info bundler --no-color --verbose` with bundler #{Bundler::VERSION}") + end + + it "doesn't print defaults" do + install_gemfile! "", :verbose => true + expect(out).to start_with("Running `bundle install --no-color --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(err + out).to eq("Could not find command \"fail\".") + end + end + + let(:bundler_version) { "1.1" } + let(:latest_version) { nil } + before do + 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) { "2.0" } + it "prints the version warning" do + bundle "fail" + expect(err + out).to eq(<<-EOS.strip) +The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. +To update, run `gem install bundler` +Could not find command "fail". + 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(out).to eq "" + + bundle "platform --ruby" + expect(out).to eq "Could not locate Gemfile" + end + end + + context "and is a pre-release" do + let(:latest_version) { "2.0.0.pre.4" } + it "prints the version warning" do + bundle "fail" + expect(err + out).to eq(<<-EOS.strip) +The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. +To update, run `gem install bundler --pre` +Could not find command "fail". + EOS + end + end + end + end +end + +RSpec.describe "bundler executable" do + it "shows the bundler version just as the `bundle` executable does" do + bundler "--version" + expect(out).to eq("Bundler version #{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 0000000000..c1cae31956 --- /dev/null +++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +require "spec_helper" +require "net/http" +require "bundler/compact_index_client" +require "bundler/compact_index_client/updater" + +RSpec.describe Bundler::CompactIndexClient::Updater do + subject(:updater) { described_class.new(fetcher) } + + let(:fetcher) { double(:fetcher) } + + context "when the ETag header is missing" do + # Regression test for https://github.com/bundler/bundler/issues/5463 + + let(:response) { double(:response, :body => "") } + let(:local_path) { Pathname("/tmp/localpath") } + let(:remote_path) { double(:remote_path) } + + 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 +end diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb new file mode 100644 index 0000000000..73d44a93ab --- /dev/null +++ b/spec/bundler/bundler/definition_spec.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true +require "spec_helper" +require "bundler/definition" + +RSpec.describe Bundler::Definition do + describe "#lock" do + before do + allow(Bundler).to receive(:settings) { Bundler::Settings.new(".") } + allow(Bundler).to receive(:default_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" do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://#{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:#{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" 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://#{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:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a rubygems gem" do + install_gemfile <<-G + source "file://#{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:#{gem_repo1}/ + specs: + foo (1.0) + + PLATFORMS + ruby + + 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 + 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, + Bundler::SourceList.new, + unlock_hash_for_bundle_install + ) + locked = definition.send(:converge_locked_specs).map(&:name) + expect(locked.include?("shared_dep")).to be_truthy + 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, + Bundler::SourceList.new, + :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/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb new file mode 100644 index 0000000000..4f5eb6dc92 --- /dev/null +++ b/spec/bundler/bundler/dsl_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "default hosts (git, gist)" do + 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 + end + + describe "#method_missing" do + it "raises an error for unknown DSL methods" do + expect(Bundler).to receive(:read_file).with("Gemfile"). + 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("Gemfile").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("Gemfile").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].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 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 specifiying 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 specifiying 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" 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 + 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 0000000000..0b8da840d2 --- /dev/null +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Bundler::EndpointSpecification do + let(:name) { "foo" } + let(:version) { "1.0.0" } + let(:platform) { Gem::Platform::RUBY } + let(:dependencies) { [] } + let(:metadata) { nil } + + subject { 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 +end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb new file mode 100644 index 0000000000..269c323ac6 --- /dev/null +++ b/spec/bundler/bundler/env_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true +require "spec_helper" +require "bundler/settings" + +RSpec.describe Bundler::Env do + let(:env) { described_class.new } + let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil, nil) } + + describe "#report" do + it "prints the environment" do + out = env.report + + expect(out).to include("Environment") + expect(out).to include(Bundler::VERSION) + expect(out).to include(Gem::VERSION) + expect(out).to include(env.send(:ruby_version)) + expect(out).to include(env.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) { env.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) { env.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 = env.report(:print_gemspecs => true) + + expect(output).to include("foo.gemspec") + expect(output).to include(gemspec) + 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(env.report).to include("Git 1.2.3 (Apple Git-BS)") + end + 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 0000000000..41d2650055 --- /dev/null +++ b/spec/bundler/bundler/environment_preserver_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true +require "spec_helper" + +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.key?("BUNDLER_ORIG_foo")).to eq(false) + 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 0000000000..38b69429bc --- /dev/null +++ b/spec/bundler/bundler/fetcher/base_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..e653c1ea43 --- /dev/null +++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + before { stub_const("OpenSSL::OPENSSL_FIPS", true) } + + 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 0000000000..134ca1bc57 --- /dev/null +++ b/spec/bundler/bundler/fetcher/dependency_spec.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..4dcd94b1b2 --- /dev/null +++ b/spec/bundler/bundler/fetcher/downloader_spec.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..b17e0d1727 --- /dev/null +++ b/spec/bundler/bundler/fetcher/index_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..585768343f --- /dev/null +++ b/spec/bundler/bundler/fetcher_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true +require "spec_helper" +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 + 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 0000000000..19799d5495 --- /dev/null +++ b/spec/bundler/bundler/friendly_errors_spec.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true +require "spec_helper" +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(err).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 = ""') + 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 + 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 0000000000..c7620e2620 --- /dev/null +++ b/spec/bundler/bundler/gem_version_promoter_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true +require "spec_helper" + +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_group(name, version) + Bundler::Resolver::SpecGroup.new(build_spec(name, version)) + end + + # Rightmost (highest array index) in result is most preferred. + # Leftmost (lowest array index) in result is least preferred. + # `build_spec_group` has all version 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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("foo", "1.2.0"), + build_spec_group("foo", "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 0000000000..09b09e08fa --- /dev/null +++ b/spec/bundler/bundler/index_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..e2f30cdd70 --- /dev/null +++ b/spec/bundler/bundler/installer/gem_installer_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require "spec_helper" +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("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 0000000000..7d2c441399 --- /dev/null +++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..1e368ab7c5 --- /dev/null +++ b/spec/bundler/bundler/installer/spec_installation_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..17bb447194 --- /dev/null +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +require "spec_helper" +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 +end diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb new file mode 100644 index 0000000000..9051a80465 --- /dev/null +++ b/spec/bundler/bundler/mirror_spec.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true +require "spec_helper" +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" 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 0000000000..4dbb993b89 --- /dev/null +++ b/spec/bundler/bundler/plugin/api/source_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..e40b9adb0f --- /dev/null +++ b/spec/bundler/bundler/plugin/api_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true +require "spec_helper" + +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_unkown_function).once + + api.an_unkown_function + end + + it "includes Bundler::SharedHelpers' functions" do + expect(Bundler::SharedHelpers).to receive(:an_unkown_helper).once + + api.an_unkown_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 0000000000..cd15b6ea9d --- /dev/null +++ b/spec/bundler/bundler/plugin/dsl_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require "spec_helper" + +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/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb new file mode 100644 index 0000000000..24b9a408ff --- /dev/null +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true +require "spec_helper" + +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 +end diff --git a/spec/bundler/bundler/plugin/installer_spec.rb b/spec/bundler/bundler/plugin/installer_spec.rb new file mode 100644 index 0000000000..e8d5941e33 --- /dev/null +++ b/spec/bundler/bundler/plugin/installer_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Bundler::Plugin::Installer do + subject(:installer) { Bundler::Plugin::Installer.new } + + describe "cli install" do + it "uses Gem.sources when non of the source is provided" do + sources = double(:sources) + 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 0000000000..86cc4ac4ed --- /dev/null +++ b/spec/bundler/bundler/plugin/source_list_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..5bbb7384c8 --- /dev/null +++ b/spec/bundler/bundler/plugin_spec.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "#hook" do + before do + path = lib_path("foo-plugin") + build_lib "foo-plugin", :path => path do |s| + s.write "plugins.rb", code + end + + allow(index).to receive(:hook_plugins).with(event). + 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 + + let(:event) { "event-1" } + + it "executes the hook" do + out = capture(:stdout) do + Plugin.hook("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("event-1") {} + Bundler::Plugin::API.hook("event-2") {} + puts "loaded" + RUBY + + let(:event) { /event-1|event-2/ } + + it "evals plugins.rb once" do + out = capture(:stdout) do + Plugin.hook("event-1") + Plugin.hook("event-2") + end.strip + + expect(out).to eq("loaded") + end + end + + context "a block is passed" do + let(:code) { <<-RUBY } + Bundler::Plugin::API.hook("#{event}") { |&blk| blk.call } + RUBY + + it "is passed to the hook" do + out = capture(:stdout) do + Plugin.hook("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 0000000000..18e40d6b5a --- /dev/null +++ b/spec/bundler/bundler/psyched_yaml_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..644814c563 --- /dev/null +++ b/spec/bundler/bundler/remote_specification_spec.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..525f05d327 --- /dev/null +++ b/spec/bundler/bundler/retry_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..3e0ec9d7f0 --- /dev/null +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..f77ec606fc --- /dev/null +++ b/spec/bundler/bundler/ruby_version_spec.rb @@ -0,0 +1,524 @@ +# frozen_string_literal: true +require "spec_helper" +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") + allow(Bundler).to receive(:ruby_engine).and_return("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") + allow(Bundler).to receive(:ruby_engine).and_return("not_supported_ruby_engine") + end + + it "should raise a BundlerError with a 'not recognized' message" do + expect { bundler_system_ruby_version.engine_versions }.to raise_error(Bundler::BundlerError, "RUBY_ENGINE value not_supported_ruby_engine is not recognized") + 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 0000000000..38ff9dae7e --- /dev/null +++ b/spec/bundler/bundler/rubygems_integration_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "succesfully 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_spec.rb b/spec/bundler/bundler/settings_spec.rb new file mode 100644 index 0000000000..7302da5421 --- /dev/null +++ b/spec/bundler/bundler/settings_spec.rb @@ -0,0 +1,284 @@ +# frozen_string_literal: true +require "spec_helper" +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[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[:frozen] = "true" + expect(settings[:frozen]).to be true + end + context "when specific gem is configured" do + it "returns a boolean" do + settings["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[: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(FileUtils).to receive(:mkdir_p).with(settings.send(:local_config_file).dirname). + and_raise(Errno::EACCES) + expect { settings[:frozen] = "1" }. + to raise_error(Bundler::PermissionError, /config/) + end + end + end + + describe "#temporary" do + it "reset after used" do + Bundler.settings[: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 + 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(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[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["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 + 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["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["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["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["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["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["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["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["local.httpsmarty"] = home("httpsmarty") + expect(settings.all).to include("local.httpsmarty") + end + + it "reads older keys without trailing slashes" do + settings["mirror.https://rubygems.org"] = "http://rubygems-mirror.org" + expect(settings.mirror_for("https://rubygems.org/")).to eq( + URI("http://rubygems-mirror.org/") + ) + 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 0000000000..d3b93b56d0 --- /dev/null +++ b/spec/bundler/bundler/shared_helpers_spec.rb @@ -0,0 +1,451 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + 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 path seperator" do + stub_const("File::PATH_SEPARATOR", ":".freeze) + 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 a ':', 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 include ':'.\nYour current bundle " \ + "path is '#{Bundler.bundle_path}'." + ) + end + + context "ENV['PATH'] does not exist" do + before { ENV.delete("PATH") } + + it_behaves_like "ENV['PATH'] gets set correctly" + end + + context "ENV['PATH'] is empty" do + before { ENV["PATH"] = "" } + + it_behaves_like "ENV['PATH'] gets set correctly" + end + + context "ENV['PATH'] exists" do + before { ENV["PATH"] = "/some_path/bin" } + + it_behaves_like "ENV['PATH'] gets set correctly" + end + + context "ENV['PATH'] already contains the bundle bin path" do + let(:bundle_path) { "#{Bundler.bundle_path}/bin" } + + before do + ENV["PATH"] = bundle_path + end + + it_behaves_like "ENV['PATH'] gets set correctly" + + it "ENV['PATH'] should only contain one instance of bundle bin path" do + subject.set_bundle_environment + paths = (ENV["PATH"]).split(File::PATH_SEPARATOR) + expect(paths.count(bundle_path)).to eq(1) + end + end + + context "ENV['RUBYOPT'] does not exist" do + before { ENV.delete("RUBYOPT") } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end + + context "ENV['RUBYOPT'] exists without -rbundler/setup" do + before { ENV["RUBYOPT"] = "-I/some_app_path/lib" } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end + + context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do + before { ENV["RUBYOPT"] = "-rbundler/setup" } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end + + context "ENV['RUBYLIB'] does not exist" do + before { ENV.delete("RUBYLIB") } + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + end + + context "ENV['RUBYLIB'] is empty" do + before { ENV["PATH"] = "" } + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + end + + context "ENV['RUBYLIB'] exists" do + before { ENV["PATH"] = "/some_path/bin" } + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + end + + context "ENV['RUBYLIB'] already contains the bundler's ruby version lib path" do + let(:ruby_lib_path) { "stubbed_ruby_lib_dir" } + + before do + ENV["RUBYLIB"] = ruby_lib_path + end + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + + it "ENV['RUBYLIB'] should only contain one instance of bundler's ruby version lib path" do + subject.set_bundle_environment + paths = (ENV["RUBYLIB"]).split(File::PATH_SEPARATOR) + expect(paths.count(ruby_lib_path)).to eq(1) + end + end + end + + describe "#filesystem_access" do + context "system has proper permission access" do + let(:file_op_block) { proc {|path| FileUtils.mkdir_p(path) } } + + it "performs the operation in the passed block" do + subject.filesystem_access("./test_dir", &file_op_block) + expect(Pathname.new("test_dir")).to exist + end + end + + context "system throws Errno::EACESS" do + let(:file_op_block) { proc {|_path| raise Errno::EACCES } } + + it "raises a PermissionError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::PermissionError + ) + end + end + + context "system throws Errno::EAGAIN" do + let(:file_op_block) { proc {|_path| raise Errno::EAGAIN } } + + it "raises a TemporaryResourceError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::TemporaryResourceError + ) + end + end + + context "system throws Errno::EPROTO" do + let(:file_op_block) { proc {|_path| raise Errno::EPROTO } } + + it "raises a VirtualProtocolError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::VirtualProtocolError + ) + end + end + + context "system throws Errno::ENOTSUP", :ruby => "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 0000000000..34fe21e9fb --- /dev/null +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Bundler::Source::Git::GitProxy do + let(:uri) { "https://github.com/bundler/bundler.git" } + subject { described_class.new(Pathname("path"), uri, "HEAD") } + + context "with configured credentials" do + it "adds username and password to URI" do + Bundler.settings[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["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["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["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 +end diff --git a/spec/bundler/bundler/source/path_spec.rb b/spec/bundler/bundler/source/path_spec.rb new file mode 100644 index 0000000000..1d13e03ec1 --- /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 0000000000..54394fc0ca --- /dev/null +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true +require "spec_helper" +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[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[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[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[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[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[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["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["mirror.https://rubygems.org/"] = mirror_uri_no_auth.to_s + Bundler.settings[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 0000000000..b8f9f09c20 --- /dev/null +++ b/spec/bundler/bundler/source/rubygems_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..6a23c8bcbf --- /dev/null +++ b/spec/bundler/bundler/source_list_spec.rb @@ -0,0 +1,441 @@ +# frozen_string_literal: true +require "spec_helper" + +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 } + + 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["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" do + before do + @returned_source = source_list.add_rubygems_remote("https://rubygems.org/") + end + + 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.first).to eq(URI("https://othersource.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, + ] + 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 + it "combines the rubygems sources into a single instance, removing duplicate remotes from the end" 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") + + 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 + 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_remote("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_remote("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 0000000000..08d1698fcd --- /dev/null +++ b/spec/bundler/bundler/source_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..8f7c27f065 --- /dev/null +++ b/spec/bundler/bundler/spec_set_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "#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 0000000000..66853a6815 --- /dev/null +++ b/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true +require "spec_helper" +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 !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + + FileUtils.mkdir_p(rubygems_certs_dir) + FileUtils.touch(stub_cert) + end + + after do + rubygems_dir = File.join(root.to_s, "lib", "rubygems") + FileUtils.rm_rf(rubygems_certs_dir) + + # Restore rubygems certificates + FileUtils.mv(rubygems_certs_dir + ".back", rubygems_certs_dir) if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + 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(FileUtils).to receive(:rm) + allow(FileUtils).to receive(:cp) + end + + it "should remove the current bundler certs" do + expect(FileUtils).to receive(:rm).with(subject.bundler_certs) + subject.update! + end + + it "should copy the rubygems certs into bundler certs" do + expect(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 0000000000..f1ddf43bb4 --- /dev/null +++ b/spec/bundler/bundler/stub_specification_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require "spec_helper" + +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_spec.rb b/spec/bundler/bundler/ui_spec.rb new file mode 100644 index 0000000000..fc76eb1ee7 --- /dev/null +++ b/spec/bundler/bundler/ui_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..1dd01b4be0 --- /dev/null +++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true +require "spec_helper" + +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/version_ranges_spec.rb b/spec/bundler/bundler/version_ranges_spec.rb new file mode 100644 index 0000000000..f746aa88ad --- /dev/null +++ b/spec/bundler/bundler/version_ranges_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..fbfe6ddab3 --- /dev/null +++ b/spec/bundler/bundler/worker_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..c28db59223 --- /dev/null +++ b/spec/bundler/bundler/yaml_serializer_spec.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true +require "spec_helper" +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", + }, + "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 0000000000..ec6d6e312a --- /dev/null +++ b/spec/bundler/cache/cache_path_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "when given an absolute path" do + it "exits with non-zero status" do + bundle :package, "cache-path" => "/tmp/cache-foo" + expect(out).to match(/must be relative/) + expect(exitstatus).to eq(15) if exitstatus + end + end +end diff --git a/spec/bundler/cache/gems_spec.rb b/spec/bundler/cache/gems_spec.rb new file mode 100644 index 0000000000..7828c87fec --- /dev/null +++ b/spec/bundler/cache/gems_spec.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle cache" do + describe "when there are only gemsources" do + before :each do + gemfile <<-G + gem 'rack' + G + + system_gems "rack-1.0.0" + 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 [] + 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" + + 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" + 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 + + 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" do + 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 + 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" + 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 0000000000..31b3816a3b --- /dev/null +++ b/spec/bundler/cache/git_spec.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: true +require "spec_helper" + +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} --all" + 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} --all" + + 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} --all" + bundle "#{cmd} --all" + + expect(err).to lack_errors + 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} --all" + + 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" + bundle "#{cmd} --all" + + 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} --all" + + 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} --all" + + 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} --all" + + 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" 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} --all" + 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} --all" + + 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 0000000000..bbce448759 --- /dev/null +++ b/spec/bundler/cache/path_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true +require "spec_helper" + +%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} --all" + 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} --all" + 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} --all" + 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} --all" + + build_lib "foo" do |s| + s.write "lib/foo.rb", "puts :CACHE" + end + + bundle "#{cmd} --all" + + 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} --all" + + install_gemfile <<-G + gem "bar", :path => '#{lib_path("bar-1.0")}' + G + + bundle "#{cmd} --all" + expect(bundled_app("vendor/cache/bar-1.0")).not_to exist + end + + it "raises a warning without --all" 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} --all" + 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} --all" + 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 0000000000..ed80c949aa --- /dev/null +++ b/spec/bundler/cache/platform_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +require "spec_helper" + +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(exitstatus).to eq 0 if exitstatus + + 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" + + expect(exitstatus).to eq 0 if exitstatus + + 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 0000000000..4931402c33 --- /dev/null +++ b/spec/bundler/commands/add_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + + 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 + bundle "add 'foo' --version='< 3.0, > 1.1'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "< 3.0", "> 1.1"/) + 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 + + 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 match(%r{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 any of the gem sources listed in your Gemfile.") + + 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 +end diff --git a/spec/bundler/commands/binstubs_spec.rb b/spec/bundler/commands/binstubs_spec.rb new file mode 100644 index 0000000000..cb0999348e --- /dev/null +++ b/spec/bundler/commands/binstubs_spec.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "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 "does not bundle the bundler binary" do + install_gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle "binstubs bundler" + + expect(bundled_app("bin/bundle")).not_to exist + expect(out).to include("Sorry, Bundler can only be run via Rubygems.") + 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 + 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" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle "binstubs rack --path exec" + bundle :install + + expect(bundled_app("exec/rails")).to exist + end + end + + context "after installing with --standalone" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "install --standalone" + end + + it "includes the standalone path" do + bundle "binstubs rack --standalone" + 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 + + 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 0000000000..532be07c3f --- /dev/null +++ b/spec/bundler/commands/check_spec.rb @@ -0,0 +1,348 @@ +# frozen_string_literal: true +require "spec_helper" + +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" do + gemfile <<-G + source "file://#{gem_repo1}" + group :foo do + gem "rack" + end + G + + bundle "install --without foo" + bundle "check" + expect(exitstatus).to eq(0) if exitstatus + 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 --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 + system_gems "rack-1.0.0" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + platforms :#{not_local_tag} do + gem "activesupport" + end + G + + 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 + system_gems "rack-1.0.0" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + env :NOT_GOING_TO_BE_SET do + gem "activesupport" + end + G + + 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) + expect(err).to lack_errors + end + end + + it "fails when there's no lock file and frozen is set" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "foo" + G + + bundle "install" + bundle "install --deployment" + FileUtils.rm(bundled_app("Gemfile.lock")) + + bundle :check + expect(exitstatus).not_to eq(0) if exitstatus + end + + context "--path" 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(exitstatus).to eq(0) if exitstatus + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "should write to .bundle/config" do + bundle "check --path vendor/bundle" + bundle "check" + expect(exitstatus).to eq(0) if exitstatus + 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 + #{generic_local_platform} + + 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(out).to include("the running version of Bundler (#{Bundler::VERSION}) is older than the version that created the lockfile (#{Bundler::VERSION.succ})") + expect(err).to lack_errors + 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 0000000000..02d96a0ff7 --- /dev/null +++ b/spec/bundler/commands/clean_spec.rb @@ -0,0 +1,703 @@ +# frozen_string_literal: true +require "spec_helper" + +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 --path vendor/bundle --no-clean" + + 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 --path vendor/bundle --no-clean" + + 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 --path vendor/bundle --no-clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "0.9.1" + gem "foo" + G + bundle "install" + + 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 --path vendor/bundle" + bundle "install --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 --path vendor/bundle" + + bundle :clean + + digest = Digest::SHA1.hexdigest(git_path.to_s) + expect(vendored_gems("cache/bundler/git/foo-1.0-#{digest}")).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 --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 --path vendor/bundle" + + update_git "foo", :path => lib_path("foo-bar") + revision2 = revision_for(lib_path("foo-bar")) + + bundle "update" + 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 --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 --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 --path vendor/bundle --without development" + + bundle :clean + expect(exitstatus).to eq(0) if exitstatus + end + + it "displays an error when used without --path" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + + bundle :clean + + expect(exitstatus).to eq(1) 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 --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 + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle :install + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle :install + + sys_exec "gem list" + expect(out).to include("rack (1.0.0)") + expect(out).to include("thin (1.0)") + end + + it "--clean should override the bundle setting on install" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle "install --path vendor/bundle --clean" + + 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" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + bundle "install --path vendor/bundle --clean" + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle "update" + + should_have_gems "foo-1.0.1" + should_not_have_gems "foo-1.0" + end + + it "does not clean automatically on --path" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle "install --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 --path vendor/bundle" + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle :update + should_have_gems "foo-1.0", "foo-1.0.1" + end + + it "does not clean on bundle update when using --system" do + 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 + + sys_exec "gem list" + expect(out).to include("foo (1.0.1, 1.0)") + end + + it "cleans system gems when --force is used" 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 + 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 + after do + FileUtils.chmod(0o755, default_bundle_path("cache")) + 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 + + system_cache_path = default_bundle_path("cache") + 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 --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 --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 + 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 --path vendor/bundle" + bundle :clean + + expect(exitstatus).to eq(0) if exitstatus + 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 --path vendor/bundle --no-clean" + + 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 --path vendor/bundle --no-clean" + + 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 --path vendor/bundle --no-clean" + 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 --path vendor/bundle --no-clean" + + 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 --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 +end diff --git a/spec/bundler/commands/config_spec.rb b/spec/bundler/commands/config_spec.rb new file mode 100644 index 0000000000..a3ca696ec1 --- /dev/null +++ b/spec/bundler/commands/config_spec.rb @@ -0,0 +1,385 @@ +# frozen_string_literal: true +require "spec_helper" + +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 --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 --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 "preferes 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 0000000000..de14b6db5f --- /dev/null +++ b/spec/bundler/commands/console_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle console" 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 0000000000..7c6e48ce19 --- /dev/null +++ b/spec/bundler/commands/doctor_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "spec_helper" +require "stringio" +require "bundler/cli" +require "bundler/cli/doctor" + +RSpec.describe "bundle doctor" do + before(:each) do + @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 + + it "exits with no message if the installed gem has no C extensions" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + Bundler::CLI::Doctor.new({}).run + expect(@stdout.string).to be_empty + end + + it "exits with no message if the installed gem's C extension dylib breakage is fine" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + 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) + doctor.run + expect(@stdout.string).to be_empty + end + + it "exits with a message if one of the linked libraries is missing" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + 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 + 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 diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb new file mode 100644 index 0000000000..7736adefe1 --- /dev/null +++ b/spec/bundler/commands/exec_spec.rb @@ -0,0 +1,736 @@ +# frozen_string_literal: true +require "spec_helper" + +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) + 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'" + + 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}'" + 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 "" + sys_exec("#{Gem.ruby} #{command.path}") + + 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, :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", :ruby_repo 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 + 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(err).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" + expect(err).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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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" do + before(:each) do + install_gemfile <<-G + gem "rack" + G + end + + it "works when unlocked", :ruby_repo do + bundle "exec 'cd #{tmp("gems")} && rackup'" + expect(out).to eq("1.0.0") + expect(out).to include("1.0.0") + end + + it "works when locked", :ruby_repo do + expect(the_bundle).to be_locked + bundle "exec 'cd #{tmp("gems")} && rackup'" + 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" 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?'" + 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" } + + shared_examples_for "it runs" do + it "like a normally executed executable" do + subject + expect(exitstatus).to eq(exit_code) if exitstatus + expect(err).to eq(expected_err) + expect(out).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 is empty" do + let(:executable) { "" } + + let(:exit_code) { 0 } + let(:expected) { "#{path} is empty" } + let(:expected_err) { "" } + if LessThanProc.with(RUBY_VERSION).call("1.9") + # Kernel#exec in ruby < 1.9 will raise Errno::ENOEXEC if the command content is empty, + # even if the command is set as an executable. + pending "Kernel#exec is different" + else + it_behaves_like "it runs" + end + end + + context "the executable raises" do + let(:executable) { super() << "\nraise 'ERROR'" } + let(:exit_code) { 1 } + let(:expected) { super() << "\nbundler: failed to load command: #{path} (#{path})" } + let(:expected_err) do + "RuntimeError: ERROR\n #{path}:10" + + (Bundler.current_ruby.ruby_18? ? "" : ":in `'") + end + 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" 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 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 "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" do + skip "popen3 doesn't provide a way to get pid " unless RUBY_VERSION >= "1.9.3" + + 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 + end + + context "nested bundle exec", :ruby_repo 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 + 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}", :system_bundler => true, :artifice => nil)).to eq(expected) + expect(bundle!("exec bundle exec #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) + expect(bundle!("exec ruby #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) + expect(run!(file.read, :no_lib => true, :artifice => nil)).to eq(expected) + end + + # sanity check that we get the newer, custom version without bundler + sys_exec("#{Gem.ruby} #{file}") + expect(err).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 0000000000..6faeed058e --- /dev/null +++ b/spec/bundler/commands/help_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true +require "spec_helper" + +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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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", :ruby_repo 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 0000000000..cdfea983dc --- /dev/null +++ b/spec/bundler/commands/info_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..6ab7e25cc3 --- /dev/null +++ b/spec/bundler/commands/init_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle init" do + it "generates a Gemfile" do + bundle :init + expect(bundled_app("Gemfile")).to exist + end + + context "when a Gemfile already exists" do + before do + 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 "given --gemspec option" 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 = bundled_app("Gemfile").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 + + 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(out).to include("There was an error while loading `test.gemspec`") + 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 0000000000..dd0f1348cc --- /dev/null +++ b/spec/bundler/commands/inject_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle inject" 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" + bundle "config --local frozen 1" + 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_FROZEN"]).to eq("1") + 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 0000000000..2d67a39f1e --- /dev/null +++ b/spec/bundler/commands/install_spec.rb @@ -0,0 +1,513 @@ +# frozen_string_literal: true +require "spec_helper" + +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(err).to lack_errors + expect(out).to match(/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" 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 "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" + + 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_system => 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_system => 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 --path vendor" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "allows running bundle install --system without deleting foo" do + bundle "install --path vendor" + bundle "install --system" + 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" do + bundle "install --path vendor" + FileUtils.rm_rf(bundled_app("vendor")) + bundle "install --system" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + it "finds gems in multiple sources" 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 + + it "gracefully handles error when rubygems server is unavailable" do + install_gemfile <<-G, :artifice => nil + source "file://#{gem_repo1}" + source "http://localhost:9384" + + gem 'foo' + 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://#{gem_repo2}" + + gem "ajp-rails", "0.0.0" + G + + expect(out).not_to match(/Error Report/i) + expect(err).not_to match(/Error Report/i) + expect(out).to include("An error occurred while installing ajp-rails (0.0.0), and Bundler cannot continue."). + and include("Make sure that `gem install ajp-rails -v '0.0.0'` 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 = '1.8.7' + ruby '~> 2.1' + G + expect(out).to include("Your Ruby version is 1.8.7, but your Gemfile specified ~> 2.1") + 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 + ruby + + 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 + ruby + + 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, :path => "vendor" + expect(out).to include(bundled_app("vendor").to_s) + expect(out).to include("grant write permissions") + end + end + + describe "when bundle install is executed with unencoded authentication" do + before do + gemfile <<-G + source 'https://rubygems.org/' + gem 'bundler' + 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 0000000000..056ef0f300 --- /dev/null +++ b/spec/bundler/commands/issue_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..0ee1a46945 --- /dev/null +++ b/spec/bundler/commands/licenses_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require "spec_helper" + +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" + + expect(out).to include("bundler: Unknown") + 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/lock_spec.rb b/spec/bundler/commands/lock_spec.rb new file mode 100644 index 0000000000..5c15b6a7f6 --- /dev/null +++ b/spec/bundler/commands/lock_spec.rb @@ -0,0 +1,315 @@ +# frozen_string_literal: true +require "spec_helper" + +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://#{repo}" + gem "rails" + gem "with_license" + gem "foo" + G + + @lockfile = strip_lockfile <<-L + GEM + remote: file:#{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 + #{local} + + 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 include(@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 include("sources listed in your Gemfile") + 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 "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 eq([java, local, mingw]) + end + + it "supports adding the `ruby` platform" do + bundle! "lock --add-platform ruby" + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to eq([local, "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 eq([java, local, mingw]) + + bundle! "lock --remove-platform java" + + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to eq([local, mingw]) + end + + it "errors when removing all platforms" do + bundle "lock --remove-platform #{local}" + expect(out).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:#{gem_repo4}" + + gem "mixlib-shellout" + gem "gssapi" + G + + simulate_platform(mingw) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + GEM + remote: file:#{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(strip_whitespace(<<-G)) + GEM + remote: file:#{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 0000000000..e9c19005eb --- /dev/null +++ b/spec/bundler/commands/newgem_spec.rb @@ -0,0 +1,909 @@ +# frozen_string_literal: true +require "spec_helper" + +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 avaiable" do + let(:gem_name) { "test_gem" } + + # This spec cannot have `git` avaiable 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 + system_gems ["rake-10.0.2"] + + 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'\]/, /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 + bundle "exec rake build" + end + + expect(exitstatus).to be_zero if exitstatus + expect(out).not_to include("ERROR") + expect(err).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 "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(out).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(out).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") + end + + it "fails gracefully with a space" do + bundle "gem 'foo bar'" + expect(out).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(out).to eq(<<-E.strip) +ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] +Usage: "bundle gem GEM [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 + output = bundle "gem conflict-foobar" + expect(output).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(out).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 0000000000..6872e859d2 --- /dev/null +++ b/spec/bundler/commands/open_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..c6b6c9f59e --- /dev/null +++ b/spec/bundler/commands/outdated_spec.rb @@ -0,0 +1,731 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + + 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" do + before do + install_gemfile <<-G, :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(exitstatus).to_not be_zero if exitstatus + 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 "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 +end diff --git a/spec/bundler/commands/package_spec.rb b/spec/bundler/commands/package_spec.rb new file mode 100644 index 0000000000..86c09db3ca --- /dev/null +++ b/spec/bundler/commands/package_spec.rb @@ -0,0 +1,306 @@ +# frozen_string_literal: true +require "spec_helper" + +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 --all" + + 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 --all" + + 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 --all" + + 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 --all" + + 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" do + it "sets root directory for gems" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle "package --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 --frozen" } + + it "tries to install with frozen" do + 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") + 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 --deployment" + 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 0000000000..3aca313e0f --- /dev/null +++ b/spec/bundler/commands/pristine_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true +require "spec_helper" +require "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_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 "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", :ruby_repo do + system_gems :bundler + bundle! "install" + bundle! "pristine", :system_bundler => true + bundle! "-v", :system_bundler => true + expect(out).to end_with(Bundler::VERSION) + 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 "#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 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 "#Pristine spec changes" } + 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 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 +end diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb new file mode 100644 index 0000000000..0391ddec52 --- /dev/null +++ b/spec/bundler/commands/show_spec.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle show" 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 + + it "warns if path no longer exists on disk" do + FileUtils.rm_rf("#{system_gem_path}/gems/rails-2.3.2") + + bundle "show rails" + + expect(out).to match(/has been deleted/i) + expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints the path to the running bundler", :ruby_repo 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" + + 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: No website available.") + 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 0000000000..4992e428da --- /dev/null +++ b/spec/bundler/commands/update_spec.rb @@ -0,0 +1,657 @@ +# frozen_string_literal: true +require "spec_helper" + +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" 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 "--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 "a" do |s| + s.add_dependency "b" + s.add_dependency "c" + end + build_gem "b" + build_gem "c" + build_gem "c", "2.0" + end + + install_gemfile! <<-G + source "file:#{gem_repo4}" + gem "a" + G + + expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + + update_repo4 do + build_gem "b", "2.0" do |s| + s.add_dependency "c", "< 2" + end + end + + bundle! "update" + + expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + end + end + end + + describe "with --local option" do + it "doesn't hit repo2" do + FileUtils.rm_rf(gem_repo2) + + bundle "update --local" + expect(out).not_to match(/Fetching source index/) + end + end + + describe "with --group option" do + it "should update only specifed 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 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 + end + + describe "in a frozen bundle" do + it "should fail loudly" do + bundle "install --deployment" + bundle "update" + + 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) + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "should suggest different command when frozen is set globally" do + bundler "config --global frozen 1" + bundle "update" + 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 config --delete frozen`./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" do + pending("Allowed to fail to preserve backwards-compatibility") + + 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 + + it "should 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).to include_gems "activesupport 3.0" + 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" 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 + 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" 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 + 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 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 +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" + + 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 not explode" do + bundle "update" + expect(err).to lack_errors + end + + it "should explain that bundler conflicted" do + bundle "update" + expect(out).not_to match(/in snapshot/i) + expect(out).to match(/current Bundler version/i) + expect(out).to match(/perhaps you need to update bundler/i) + end +end + +RSpec.describe "bundle update" do + it "shows the previous version of the gem when updated from rubygems source" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + + bundle "update" + expect(out).to include("Using activesupport 2.3.5") + + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update" + expect(out).to include("Installing activesupport 3.0 (was 2.3.5)") + 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 + ruby + + 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 + ruby + + 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 + ruby + + DEPENDENCIES + + RUBY VERSION + ruby 1.8.3p55 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + 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" + + 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" + + 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" + + expect(out).to eq "Provide only one of the following options: minor, patch" + end + end +end diff --git a/spec/bundler/commands/viz_spec.rb b/spec/bundler/commands/viz_spec.rb new file mode 100644 index 0000000000..77112aace4 --- /dev/null +++ b/spec/bundler/commands/viz_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle viz", :ruby => "1.9.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 0000000000..1bca055c9f --- /dev/null +++ b/spec/bundler/install/allow_offline_install_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +require "spec_helper" + +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"] + + 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" + expect(out).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 } + 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 0000000000..a1a9ab167d --- /dev/null +++ b/spec/bundler/install/binstubs_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" 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" 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 "prints a deprecation notice" do + bundle "config major_deprecations true" + gembin("rackup") + expect(out).to include("Bundler is using a binstub that was created for a different gem.") + 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 0000000000..c1ce57e60e --- /dev/null +++ b/spec/bundler/install/bundler_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true +require "spec_helper" + +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}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + 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 of the sources + E + expect(out).to eq(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}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + 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(out).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}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + 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(out).to include(nice_error) + end + + it "can install dependencies with newer bundler version" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + simulate_bundler_version "10.0.0" + + bundle "check" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + end +end diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb new file mode 100644 index 0000000000..b66135c6b0 --- /dev/null +++ b/spec/bundler/install/deploy_spec.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "install with --deployment or --frozen" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + 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(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "rack 1.0" + end + + it "still works if you are not in the app directory and specify --gemfile" do + bundle "install" + Dir.chdir tmp + simulate_new_machine + bundle "install --gemfile #{tmp}/bundled_app/Gemfile --deployment" + Dir.chdir bundled_app + 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 --deployment --without test" + expect(exitstatus).to eq(0) if exitstatus + end + + it "works when you bundle exec bundle", :ruby_repo do + bundle :install + bundle "install --deployment" + bundle "exec bundle check" + expect(exitstatus).to eq(0) if exitstatus + 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 --deployment" + 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 --deployment", :artifice => "endpoint_strict_basic_authentication" + + expect(exitstatus).to eq(0) if exitstatus + end + + it "works with sources given by a block" do + install_gemfile <<-G + source "file://#{gem_repo1}" do + gem "rack" + end + G + + bundle "install --deployment" + + expect(exitstatus).to eq(0) if exitstatus + 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" do + bundle "install --deployment" + expect(exitstatus).to eq(0) if exitstatus + end + + it "works with the --frozen flag if you didn't change anything" do + bundle "install --frozen" + expect(exitstatus).to eq(0) if exitstatus + 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 --deployment" + 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 via an environment variable" 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 --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" + 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" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama", "1.1" + G + + bundle "install --frozen" + 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 --deployment" + 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 --deployment" + 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 --deployment" + 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 --deployment" + 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 + + it "remembers that the bundle is frozen at runtime" do + bundle "install --deployment" + + 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 --all" + expect(bundled_app("vendor/cache/foo")).to be_directory + + bundle! "install --local" + expect(out).to include("Using foo 1.0 from source at") + expect(out).to include("vendor/cache/foo") + + simulate_new_machine + bundle! "install --deployment --verbose" + 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("Using foo 1.0 from source at") + 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 0000000000..738b2cf1bd --- /dev/null +++ b/spec/bundler/install/failure_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require "spec_helper" + +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:#{gem_repo2}" + gem "rails" + G + expect(out).to end_with(<<-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'` 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/force_spec.rb b/spec/bundler/install/force_spec.rb new file mode 100644 index 0000000000..dc4956a7ae --- /dev/null +++ b/spec/bundler/install/force_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + describe "with --force" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + 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 --force" + + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include "Using bundler" + 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 --force" + + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include "Using bundler" + 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 --force" + + expect(out).to include "Using bundler" + expect(out).to include "Using foo 1.0 from #{lib_path("foo-1.0")} (at master@#{ref[0, 7]})" + 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 --force" + + expect(out).to include "Using bundler" + expect(out).to include "Using foo 1.0 from #{lib_path("foo-1.0")} (at master@#{ref[0, 7]})" + expect(the_bundle).to include_gems "foo 1.0" + end + 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 0000000000..f02223d34d --- /dev/null +++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +require "spec_helper" + +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("Using gunks 0.0.1 from source at `gems/gunks`") + expect(out).to include("Bundle complete") + 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 --deployment" + 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("Using gunks 0.0.1 from source at `gems/gunks`") + expect(out).to include("Bundle complete") + 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 0000000000..1ea613c9d2 --- /dev/null +++ b/spec/bundler/install/gemfile/gemspec_spec.rb @@ -0,0 +1,563 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install from an existing gemspec" do + before(:each) do + build_gem "bar", :to_system => true + build_gem "bar-dev", :to_system => true + 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 + build_gem "baz", "1.0", :to_system => true + build_gem "baz", "1.1", :to_system => true + + 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) + + error = install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + expect(error).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 + + error = install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + expect(error).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 + expect(out).to include("Found no changes, using resolution from the lockfile") + 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 + expect(out).to include("Found no changes, using resolution from the lockfile") + 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 + + install_gem "platform_specific-1.0-java" + + 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(@err).not_to match(/ahh/) + end + + it "allows the gemspec to activate other gems" do + # 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_system => true do |s| + s.add_dependency "foo", "= 0.0.1" + end + build_gem "foo", "0.0.1", :to_system => 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 + build_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 --deployment" + + 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://#{gem_repo2}" + gemspec + G + end + end + end + + context "on ruby" 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 strip_whitespace(<<-L) + PATH + remote: . + specs: + foo (1.0) + platform_specific + + GEM + remote: file:#{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 strip_whitespace(<<-L) + PATH + remote: . + specs: + foo (1.0) + + GEM + remote: file:#{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 strip_whitespace(<<-L) + PATH + remote: . + specs: + foo (1.0) + + GEM + remote: file:#{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 + 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, :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 0000000000..5868c76570 --- /dev/null +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -0,0 +1,1259 @@ +# frozen_string_literal: true +require "spec_helper" + +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" do + expect(Dir["#{default_bundle_path}/cache/bundler/git/foo-1.0-*"].size).to eq(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("Source contains 'foo' at: 1.0 ruby") + 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("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("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 + 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 + expect(out).to match(/at #{lib_path('local-rack')}/) + + 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" + expect(the_bundle).to include_gems "forced 1.1" + + Dir.chdir(lib_path("forced-1.0")) do + `git reset --hard HEAD^` + end + + bundle "update" + 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.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 --deployment" + expect(exitstatus).to eq(0) if exitstatus + 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(system_gem_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_sha = git_reader.ref_for("HEAD") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_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(out).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" + end + expect(out).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 --all" + 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.") + expect(exitstatus).to be_zero if exitstatus + end + end + + describe "when the git source is overriden 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(out).to_not include("password1") + expect(err).to_not include("password1") + expect(out).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(out).to_not include("oauth_token") + expect(err).to_not include("oauth_token") + expect(out).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 0000000000..a3a5eeefdf --- /dev/null +++ b/spec/bundler/install/gemfile/groups_spec.rb @@ -0,0 +1,371 @@ +# frozen_string_literal: true +require "spec_helper" + +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, :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, :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, :without => "emo" + expect(out).not_to include("activesupport") + end + + it "allows Bundler.setup for specific groups" do + bundle :install, :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, :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, :without => "emo" + + simulate_new_machine + bundle :install, :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, :without => "emo" + + bundle 'install --without ""' + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "doesn't clear without when nothing is passed" do + bundle :install, :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, :with => "debugging" + expect(the_bundle).to include_gems "thin 1.0" + end + + it "does install gems from the previously requested group" do + bundle :install, :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, :with => "debugging" + bundle 'install --with ""' + expect(the_bundle).not_to include_gems "thin 1.0" + end + + it "does remove groups from without when passed at with" do + bundle :install, :without => "emo" + bundle :install, :with => "emo" + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "does remove groups from with when passed at without" do + bundle :install, :with => "debugging" + bundle :install, :without => "debugging" + expect(the_bundle).not_to include_gems "thin 1.0" + end + + it "errors out when passing a group to with and without" do + bundle :install, :with => "emo debugging", :without => "emo" + expect(out).to include("The offending groups are: emo") + end + + it "can add and remove a group at the same time" do + bundle :install, :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, :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, :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, :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 --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 --without emo" + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "installs the gem w/ option --without lolercoaster" do + bundle "install --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 --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 --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, :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 --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, :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 --without rack" + expect(err).to lack_errors + 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 0000000000..b1717ad583 --- /dev/null +++ b/spec/bundler/install/gemfile/install_if.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require "spec_helper" + +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/path_spec.rb b/spec/bundler/install/gemfile/path_spec.rb new file mode 100644 index 0000000000..a1c41aebbb --- /dev/null +++ b/spec/bundler/install/gemfile/path_spec.rb @@ -0,0 +1,595 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with explicit source paths" do + it "fetches gems" 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 "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 --frozen" + 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")}" + gem 'foo' + 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 + 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 0000000000..c6eaec7ca6 --- /dev/null +++ b/spec/bundler/install/gemfile/platform_spec.rb @@ -0,0 +1,265 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "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" 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 --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 --path vendor/bundle" + 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 :show + 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 +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 0000000000..b9d9683758 --- /dev/null +++ b/spec/bundler/install/gemfile/ruby_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..c5375b4abf --- /dev/null +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -0,0 +1,518 @@ +# frozen_string_literal: true +require "spec_helper" + +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://#{gem_repo3}" + source "file://#{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" 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("Installed from: file:#{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://#{gem_repo3}" + source "file://#{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" 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("Installed from: file:#{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 + 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 --deployment" + + expect(exitstatus).to eq(0) if exitstatus + 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 + 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 "with 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 + + 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 + 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://#{gem_repo1}" + source "file://#{gem_repo2}" + source "file://#{gem_repo3}" do + gem "depends_on_rack" + end + G + end + + it "installs from the other source and warns about ambiguous gems" 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("Installed from: file:#{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" 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 "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" + + 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"') + + expect(out).to include("OK") + expect(exitstatus).to eq(0) if exitstatus + 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 --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") + expect(the_bundle).to include_gems("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 0000000000..cc6c82c0ff --- /dev/null +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..bc49053081 --- /dev/null +++ b/spec/bundler/install/gemfile_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +require "spec_helper" + +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") + + 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 "show" + + 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 "show" + + 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 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 +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 0000000000..e9e671105a --- /dev/null +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -0,0 +1,805 @@ +# frozen_string_literal: true +require "spec_helper" + +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(out).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 --deployment", :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 --deployment", :artifice => "compact_index" + + expect(exitstatus).to eq(0) if exitstatus + 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"] = nil + + 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" + 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" 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" + 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" 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 "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" + gem "back_deps" + 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" + gem "back_deps" + G + + bundle! :install, :artifice => "compact_index_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 => "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" 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 "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" 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" 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" 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" 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", :ruby_repo 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 "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: `#{system_gem_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 0000000000..d495490745 --- /dev/null +++ b/spec/bundler/install/gems/dependency_api_spec.rb @@ -0,0 +1,671 @@ +# frozen_string_literal: true +require "spec_helper" + +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 --deployment", :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 --deployment", :artifice => "endpoint" + + expect(exitstatus).to eq(0) if exitstatus + 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" + 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" 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" + 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" 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 "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" + gem "back_deps" + 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" 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 "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" 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 "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" 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" 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" 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" 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", :ruby_repo 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 0000000000..9b1d8e5424 --- /dev/null +++ b/spec/bundler/install/gems/env_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..2c2d3c16a1 --- /dev/null +++ b/spec/bundler/install/gems/flex_spec.rb @@ -0,0 +1,319 @@ +# frozen_string_literal: true +require "spec_helper" + +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}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + 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(out).to eq(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" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + install_gemfile <<-G + source "file://#{gem_repo1}" + source "file://#{gem_repo2}" + gem "rack" + G + + lockfile_should_be <<-L + GEM + remote: file:#{gem_repo1}/ + remote: file:#{gem_repo2}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + 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 0000000000..798156fb12 --- /dev/null +++ b/spec/bundler/install/gems/mirror_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +require "spec_helper" + +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://#{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("Fetching source index from file:#{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://#{gem_repo2}" + + gem "rack" + G + bundle "config --local mirror.file://#{gem_repo2} file://#{gem_repo1}" + end + + it "installs the gem from the mirror" do + bundle :install + expect(out).to include("Fetching source index from file:#{gem_repo1}") + expect(out).not_to include("Fetching source index from file:#{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 0000000000..dcf67e976e --- /dev/null +++ b/spec/bundler/install/gems/native_extensions_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +require "spec_helper" + +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") + expect(out).to include("Using c_extension 1.0") + + 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 0000000000..c3ea3e7c51 --- /dev/null +++ b/spec/bundler/install/gems/post_install_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..7a341fd14f --- /dev/null +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true +require "spec_helper" + +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(Gem.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") + expect(err).to include(" net_build_extensions (1.0)") + 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 + Fetching gem metadata from http://localgemserver.test/. + Fetching version metadata from http://localgemserver.test/ + Resolving dependencies... + 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 sources. + E + expect(out).to eq(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(:ruby_requirement) { %("< 5000", "> 0.1") } + let(:error_message_requirement) { "< 5000, > 0.1" } + + 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 0000000000..9a79a05b32 --- /dev/null +++ b/spec/bundler/install/gems/standalone_spec.rb @@ -0,0 +1,318 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + install_gemfile <<-G, :standalone => true + source "file://#{gem_repo1}" + gem "rails" + G + 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, :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 + extension_line = File.read(bundled_app("bundle/bundler/setup.rb")).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, :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" + + install_gemfile <<-G, :standalone => true + source "file://#{gem_repo1}" + gem "rails" + gem "devise", :git => "#{lib_path("devise-1.0")}" + G + 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" + + install_gemfile <<-G, :standalone => true + source "file://#{gem_repo1}" + gem "rails" + + group :test do + gem "rspec" + gem "rack-test" + end + G + 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 --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(out).to eq("2.3.2") + expect(err).to eq("ZOMG LOAD ERROR") + end + + it "allows --without to limit the groups used in a standalone" do + bundle "install --standalone --without test" + + 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(out).to eq("2.3.2") + expect(err).to eq("ZOMG LOAD ERROR") + end + + it "allows --path to change the location of the standalone bundle" do + bundle "install --standalone --path path/to/bundle" + + 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(out).to eq("2.3.2") + end + + it "allows remembered --without to limit the groups used in a standalone" do + bundle "install --without test" + bundle "install --standalone" + + 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(out).to eq("2.3.2") + expect(err).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 --standalone", :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" do + before do + install_gemfile <<-G, :standalone => true, :binstubs => true + source "file://#{gem_repo1}" + gem "rails" + G + 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 + subdir = bundled_app("bob") + FileUtils.mkdir_p(subdir) + Dir.chdir(subdir) + 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 0000000000..13abffc14e --- /dev/null +++ b/spec/bundler/install/gems/sudo_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + 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 + 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_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 + 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 + 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 is passed as an option" do + it "skips the warning" do + bundle :install, :sudo => true, :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, :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 0000000000..cdad9a8821 --- /dev/null +++ b/spec/bundler/install/gems/win32_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..97eaf149c1 --- /dev/null +++ b/spec/bundler/install/gemspecs_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "#{tmp}/gems/system/specifications" + File.open("#{tmp}/gems/system/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 + + 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 0000000000..04f2380b45 --- /dev/null +++ b/spec/bundler/install/git_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + context "git sources" do + it "displays the revision hash of the gem repository" 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" 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 + 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 check out git repos that are missing but not being installed" do + build_git "foo" + + gemfile <<-G + gem "foo", :git => "file://#{lib_path("foo-1.0")}", :group => :development + G + + lockfile <<-L + GIT + remote: file://#{lib_path("foo-1.0")} + specs: + foo (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + L + + bundle "install --path=vendor/bundle --without development" + + expect(out).to include("Bundle complete!") + expect(vendored_gems("bundler/gems/foo-1.0-#{revision_for(lib_path("foo-1.0"))[0..11]}")).to be_directory + end + end +end diff --git a/spec/bundler/install/path_spec.rb b/spec/bundler/install/path_spec.rb new file mode 100644 index 0000000000..7a501d42b3 --- /dev/null +++ b/spec/bundler/install/path_spec.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true +require "spec_helper" + +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" do + bundle "install --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 --path 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 --path vendor/bundle" + expect(out).to include("gems are installed into ./vendor") + end + + it "disallows --path vendor/bundle --system" 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" 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 + 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| + it "installs gems to a path if one is specified" do + set_bundle_path(type, bundled_app("vendor2").to_s) + bundle "install --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 + + 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 + + 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 --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 --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 --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 --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 + `touch /tmp/idontexist bundle` + end + end + + it "reports the file exists" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "install --path bundle" + expect(out).to match(/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 0000000000..4453e4190f --- /dev/null +++ b/spec/bundler/install/post_bundle_message_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true +require "spec_helper" + +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_show_message) { "Use `bundle info [gemname]` to see where a bundled gem is installed." } + let(:bundle_deployment_message) { "Bundled gems are installed into ./vendor" } + let(:bundle_complete_message) { "Bundle complete!" } + let(:bundle_updated_message) { "Bundle updated!" } + let(:installed_gems_stats) { "4 Gemfile dependencies, 5 gems now installed." } + + 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 --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 --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 --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 + it "without any options" do + bundle "install --path vendor" + expect(out).to include(bundle_deployment_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 --without emo --path vendor" + expect(out).to include(bundle_deployment_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 --without emo test --path vendor" + expect(out).to include(bundle_deployment_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 --without emo obama test --path vendor" + expect(out).to include(bundle_deployment_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 --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 --path #{bundled_app}_cache" + expect(out).to include("Bundled gems are installed into #{bundled_app}_cache") + 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" do + install_gemfile <<-G + source "file://#{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 with reference to cache if available" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + install_gemfile <<-G + source "file://#{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 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 --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 --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 --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 + 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, :without => :emo + bundle :update + 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 --without emo test" + bundle :update + 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 --without emo obama test" + bundle :update + 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 0000000000..6c32094d90 --- /dev/null +++ b/spec/bundler/install/prereleases_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require "spec_helper" + +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/security_policy_spec.rb b/spec/bundler/install/security_policy_spec.rb new file mode 100644 index 0000000000..ab531bdad6 --- /dev/null +++ b/spec/bundler/install/security_policy_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..d42978ce4c --- /dev/null +++ b/spec/bundler/install/yanked_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +require "spec_helper" + +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 any of the gem sources") + 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 0000000000..b36f61338d --- /dev/null +++ b/spec/bundler/lock/git_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +require "spec_helper" + +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("show foo")) + end +end diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb new file mode 100644 index 0000000000..968c969a55 --- /dev/null +++ b/spec/bundler/lock/lockfile_spec.rb @@ -0,0 +1,1381 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "the lockfile format" do + include Bundler::GemHelpers + + it "generates a simple lockfile for a single source, gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{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://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + G + end + + it "updates the lockfile's bundler version if not present" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "> 0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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:#{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://#{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:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_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:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source "file://#{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:#{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://#{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:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_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://#{gem_repo1}" + + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + + gem "rack-obama", ">= 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + gem "net-sftp" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{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://#{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 --all" + bundle! "install --local" + + 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://#{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:#{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://#{gem_repo1}" + + gem "thin" + gem "actionpack" + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + + gem "rails" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + gem 'double_deps' + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + + gem "rack-obama", ">= 1.0", :require => "rack/obama" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + + gem "rack-obama", ">= 1.0", :group => :test + G + + lockfile_should_be <<-G + GEM + remote: file:#{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:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + java + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + platforms = ["java", generic_local_platform.to_s].sort + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + gem "platform_specific" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + gem "rack" + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + gem "rack", "1.0" + gem "rack", "1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + gem "rack", "1.0", :group => :one + gem "rack", "1.0", :group => :two + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{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://#{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://#{gem_repo1}" + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{gem_repo1}" + ruby '#{RUBY_VERSION}' + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{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://#{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:#{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 + expect(File.read(bundled_app("Gemfile.lock"))).to eq(strip_whitespace(<<-L)) + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GEM + remote: file:#{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:#{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://#{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" }.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" }.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://#{gem_repo1}/ + specs: + <<<<<<< + rack (1.0.0) + ======= + rack (1.0.1) + >>>>>>> + + PLATFORMS + ruby + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + + error = install_gemfile(<<-G) + source "file://#{gem_repo1}" + gem "rack" + G + + expect(error).to match(/your Gemfile.lock contains merge conflicts/i) + expect(error).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 0000000000..09fa2c223b --- /dev/null +++ b/spec/bundler/other/bundle_ruby_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle_ruby" 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 "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 0000000000..8b34a457ef --- /dev/null +++ b/spec/bundler/other/cli_dispatch_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle command names" do + it "work when given fully" do + bundle "install" + expect(err).to lack_errors + expect(out).not_to match(/Ambiguous command/) + end + + it "work when not ambiguous" do + bundle "ins" + expect(err).to lack_errors + expect(out).not_to match(/Ambiguous command/) + end + + it "print a friendly error when ambiguous" do + bundle "in" + expect(err).to lack_errors + expect(out).to match(/Ambiguous command/) + end +end diff --git a/spec/bundler/other/ext_spec.rb b/spec/bundler/other/ext_spec.rb new file mode 100644 index 0000000000..2d6ab941b8 --- /dev/null +++ b/spec/bundler/other/ext_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..465d769538 --- /dev/null +++ b/spec/bundler/other/major_deprecation_spec.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "major deprecations" do + let(:warnings) { out } # change to err in 2.0 + + 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" + + install_gemfile <<-G + source "file:#{gem_repo1}" + ruby #{RUBY_VERSION.dump} + gem "rack" + G + end + + describe "bundle_ruby" do + it "prints a deprecation" do + bundle_ruby + out.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).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(err).not_to have_major_deprecation + expect(out).not_to have_major_deprecation + end + + it "should print a Gemfile deprecation warning" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(warnings).to have_major_deprecation("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 + 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).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 = "The :github option uses the git: protocol, which is not secure. " \ + "Bundler 2.0 will use the https: protocol, which is secure. Enable this change now by " \ + "running `bundle config github.https true`." + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(msg) + subject.gem("sparks", :github => "indirect/sparks") + end + + it "upgrades to https on request" do + Bundler.settings["github.https"] = true + subject.gem("sparks", :github => "indirect/sparks") + expect(Bundler::SharedHelpers).to receive(:major_deprecation).never + 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 = "The :bitbucket git source is deprecated, and will be removed " \ + "in Bundler 2.0. Add this code to your Gemfile to ensure it " \ + "continues to work:\n git_source(:bitbucket) do |repo_name|\n " \ + " \"https://\#{user_name}@bitbucket.org/\#{user_name}/\#{repo_name}" \ + ".git\"\n end\n" + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(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 2.0. Add this code to your Gemfile to ensure it " \ + "continues to work:\n git_source(:gist) do |repo_name|\n " \ + " \"https://gist.github.com/\#{repo_name}.git\"\n" \ + " end\n" + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(msg) + subject.gem("not-really-a-gem", :gist => "1234") + end + end + end + + context "bundle list" do + it "prints a deprecation warning" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :list + + out.gsub!(/gems included.*?\[DEPRECATED/im, "[DEPRECATED") + + expect(warnings).to have_major_deprecation("use `bundle show` instead of `bundle list`") + end + end + + context "bundle console" do + it "prints a deprecation warning" do + bundle "console" + + expect(warnings).to have_major_deprecation \ + "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 0000000000..6adbcef111 --- /dev/null +++ b/spec/bundler/other/platform_spec.rb @@ -0,0 +1,1292 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle platform" do + context "without flags" do + 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: +* ruby + +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: +* ruby + +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: +* ruby + +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: +* ruby + +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 "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" + 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" + 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 + 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 + 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 + 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 + should_be_patchlevel_incorrect + end + end + + context "bundle show" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "prints path if ruby version is correct" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_correct} + G + + bundle "show rails" + 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 + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_correct_engineless} + G + + bundle "show rails" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + end + + it "fails if ruby version doesn't match" 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" 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" 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" 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 + gemfile <<-G + gem 'rack' + G + + system_gems "rack-1.0.0" + 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 + gemfile <<-G + 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 + gemfile <<-G + gem 'rack' + G + + system_gems "rack-1.0.0" + 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 + gemfile <<-G + 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" + 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 + 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" 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 + 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 0000000000..2de4dfdd0c --- /dev/null +++ b/spec/bundler/other/ssl_cert_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require "spec_helper" +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 0000000000..6ad782b758 --- /dev/null +++ b/spec/bundler/plugins/command_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true +require "spec_helper" + +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", "no-color" => false + 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 0000000000..9850d850ac --- /dev/null +++ b/spec/bundler/plugins/hook_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "hook plugins" do + before do + build_repo2 do + build_plugin "before-install-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook "before-install-all" do |deps| + puts "gems to be installed \#{deps.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install before-install-plugin --source file://#{gem_repo2}" + end + + it "runs after a rubygem is 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 diff --git a/spec/bundler/plugins/install_spec.rb b/spec/bundler/plugins/install_spec.rb new file mode 100644 index 0000000000..e2d351181c --- /dev/null +++ b/spec/bundler/plugins/install_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..2ae34caf73 --- /dev/null +++ b/spec/bundler/plugins/source/example_spec.rb @@ -0,0 +1,446 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "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) + + post_install(spec) + + nil + end + end + RUBY + end # build_plugin + end + + build_lib "a-path-gem" + + gemfile <<-G + source "file://#{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" 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:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + 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("show a-path-gem")) + 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 --all" + + 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 --path vendor/bundle" + bundle "cache --all" + + 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 --path vendor/bundle" + bundle "package --all" + + 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 + + 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://#{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" 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:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + 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 --all" + 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 0000000000..0448bc409a --- /dev/null +++ b/spec/bundler/plugins/source_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..b87b4a0731 --- /dev/null +++ b/spec/bundler/quality_spec.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true +require "spec_helper" + +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_spec_defs_with_single_quotes(filename) + failing_lines = [] + + File.readlines(filename).each_with_index do |line, number| + failing_lines << number + 1 if line =~ /^ *(describe|it|context) {1}'{1}/ + end + + return if failing_lines.empty? + "#{filename} uses inconsistent single quotes on lines #{failing_lines.join(", ")}" + end + + 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} has '#{word_found}' on line #{number + 1}. 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} has '#{word_found}' on line #{number + 1}. 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", :ruby_repo do + exempt = /\.gitmodules|\.marshal|fixtures|vendor|ssl_certs|LICENSE/ + error_messages = [] + Dir.chdir(File.expand_path("../..", __FILE__)) do + `git ls-files -z`.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 "uses double-quotes consistently in specs", :ruby_repo do + included = /spec/ + error_messages = [] + Dir.chdir(File.expand_path("../", __FILE__)) do + `git ls-files -z`.split("\x0").each do |filename| + next unless filename =~ included + error_messages << check_for_spec_defs_with_single_quotes(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "does not include any leftover debugging or development mechanisms", :ruby_repo do + exempt = %r{quality_spec.rb|support/helpers} + error_messages = [] + Dir.chdir(File.expand_path("../", __FILE__)) do + `git ls-files -z`.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", :ruby_repo do + error_messages = [] + exempt = %r{lock/lockfile_spec|quality_spec} + Dir.chdir(File.expand_path("../", __FILE__)) do + `git ls-files -z`.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", :ruby_repo do + included = /ronn/ + error_messages = [] + Dir.chdir(File.expand_path("../../man", __FILE__)) do + `git ls-files -z`.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", :ruby_repo do + error_messages = [] + exempt = /vendor/ + Dir.chdir(File.expand_path("../../lib", __FILE__)) do + `git ls-files -z`.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", :ruby_repo do + exemptions = %w( + gem.coc + gem.mit + inline + warned_version + ) + + all_settings = Hash.new {|h, k| h[k] = [] } + documented_settings = exemptions + + 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" } + + Dir.chdir(File.expand_path("../../lib", __FILE__)) do + key_pattern = /([a-z\._-]+)/i + `git ls-files -z`.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 `lib/#{filename}:#{number.succ}`" } + end + end + documented_settings = File.read("../man/bundle-config.ronn").scan(/^\* `#{key_pattern}`/).flatten + end + + documented_settings.each {|s| all_settings.delete(s) } + exemptions.each {|s| all_settings.delete(s) } + 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 + end + + it "can still be built", :ruby_repo do + Dir.chdir(root) do + begin + gem_command! :build, "bundler.gemspec" + if Bundler.rubygems.provides?(">= 2.4") + # older rubygems have weird warnings, and we won't actually be using them + # to build the gem for releases anyways + expect(err).to be_empty, "bundler should build as a gem without warnings, but\n#{err}" + end + ensure + # clean up the .gem generated + FileUtils.rm("bundler-#{Bundler::VERSION}.gem") + end + end + end + + it "does not contain any warnings", :ruby_repo do + Dir.chdir(root.join("lib")) do + exclusions = %w( + bundler/capistrano.rb + bundler/gem_tasks.rb + bundler/vlad.rb + ) + lib_files = `git ls-files -z`.split("\x0").grep(/\.rb$/) - exclusions + lib_files.reject! {|f| f.start_with?("bundler/vendor") } + lib_files.map! {|f| f.chomp(".rb") } + sys_exec!("ruby -w -I.") do |input, _, _| + lib_files.each do |f| + input.puts "require '#{f}'" + end + end + + expect(@err.split("\n")).to be_well_formed + expect(@out.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 0000000000..468fa3644c --- /dev/null +++ b/spec/bundler/realworld/dependency_api_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require "spec_helper" + +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) + end + + after do + Artifice.deactivate + @t.kill + @t.join + end + + it "times out and falls back on the modern index" do + gemfile <<-G + source "#{@server_uri}" + gem "rack" + + old_v, $VERBOSE = $VERBOSE, nil + Bundler::Fetcher.api_timeout = 1 + $VERBOSE = old_v + G + + bundle :install + 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/edgecases_spec.rb b/spec/bundler/realworld/edgecases_spec.rb new file mode 100644 index 0000000000..302fd57cf0 --- /dev/null +++ b/spec/bundler/realworld/edgecases_spec.rb @@ -0,0 +1,382 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "real world edgecases", :realworld => true, :sometimes => true do + def rubygems_version(name, requirement) + 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], nil) + rubygem = index.search(Gem::Dependency.new(name, requirement)).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})" + 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) + + BUNDLED WITH + 1.13.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 --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" + + 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: git://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: git://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: git://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(err).to eq("") + expect(exitstatus).to eq(0) if exitstatus + 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 0000000000..ba888d43bd --- /dev/null +++ b/spec/bundler/realworld/gemfile_source_header_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +require "spec_helper" +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 + + 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 0000000000..93dca0c173 --- /dev/null +++ b/spec/bundler/realworld/mirror_probe_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true +require "spec_helper" +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 + + 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 + + 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 + + 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 + + expect(out).to eq "Fetching source index from #{mirror}/ + +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}/" + 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 + + 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 0000000000..6950bead19 --- /dev/null +++ b/spec/bundler/realworld/parallel_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "show activesupport" + expect(out).to match(/activesupport/) + + bundle "show faker" + expect(out).to match(/faker/) + + bundle "config jobs" + expect(out).to match(/: "4"/) + 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" } + + if Bundler.rubygems.provides?(">= 2.1.0") + expect(out).to match(/[1-3]: /) + else + expect(out).to include("is not threadsafe") + end + + bundle "show activesupport" + expect(out).to match(/activesupport-3\.2\.\d+/) + + bundle "show faker" + expect(out).to match(/faker/) + + bundle "config jobs" + expect(out).to match(/: "4"/) + 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 0000000000..9e93847ab5 --- /dev/null +++ b/spec/bundler/resolver/basic_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "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 "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 + # the strict option removes the version required to match, so a version conflict results + expect do + should_conservative_resolve_and_include [:patch, :strict], [], %w(foo-1.4.3 bar-2.1.1) + end.to raise_error Bundler::VersionConflict, /#{Regexp.escape("Could not find gem 'bar (~> 2.1.0)'")}/ + 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 + # the strict option removes the version required to match, so a version conflict results + expect do + should_conservative_resolve_and_include [:minor, :strict], [], %w(foo-1.4.3 bar-2.1.1) + end.to raise_error Bundler::VersionConflict, /#{Regexp.escape("Could not find gem 'bar (~> 2.0.0)'")}/ + 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 0000000000..90d6f637ce --- /dev/null +++ b/spec/bundler/resolver/platform_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true +require "spec_helper" + +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 0000000000..ff27d0b415 --- /dev/null +++ b/spec/bundler/runtime/executable_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Running bin/* commands" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "runs the bundled command when in the bundle" do + bundle "install --binstubs" + + 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 "install --binstubs 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 "install --binstubs #{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 "install --binstubs" + 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 "install --binstubs --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 "install --binstubs" + + 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 "install --binstubs" + + build_gem "rack", "2.0", :to_system => true do |s| + s.executables = "rackup" + end + + gembin "rackup" + expect(out).to eq("1.0") + end + + it "don't bundle da bundla" 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 "install --binstubs" + + expect(bundled_app("bin/bundle")).not_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" 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" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + bundle "install --binstubs" + + 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)" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "install --binstubs 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 +end diff --git a/spec/bundler/runtime/gem_tasks_spec.rb b/spec/bundler/runtime/gem_tasks_spec.rb new file mode 100644 index 0000000000..7cb0f32c0c --- /dev/null +++ b/spec/bundler/runtime/gem_tasks_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "require 'bundler/gem_tasks'" 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 + require "bundler/gem_tasks" + expect(CLOBBER).to include("pkg") + end +end diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb new file mode 100644 index 0000000000..e816799d08 --- /dev/null +++ b/spec/bundler/runtime/inline_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + end + + it "requires the gems" do + script <<-RUBY + gemfile do + path "#{lib_path}" + gem "two" + end + RUBY + + expect(out).to eq("two") + expect(exitstatus).to be_zero if exitstatus + + script <<-RUBY + gemfile do + path "#{lib_path}" + gem "eleven" + 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}" + gem "two" + 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(exitstatus).to eq(0) if exitstatus + expect(out).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 0000000000..d0e308ed3e --- /dev/null +++ b/spec/bundler/runtime/load_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Bundler.load" do + before :each do + system_gems "rack-1.0.0" + end + + describe "with a gemfile" do + before(:each) do + 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 + 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 + system_gems "rack-1.0.0", "activesupport-2.3.5" + gemfile <<-G + 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 + system_gems "activerecord-2.3.2", "activesupport-2.3.2" + gemfile <<-G + 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 0000000000..4df934e71f --- /dev/null +++ b/spec/bundler/runtime/platform_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true +require "spec_helper" + +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 targetted 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 + + system_gems "nokogiri-1.4.2" + + simulate_platform "x86-darwin-10" + 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" + + system_gems "nokogiri-1.4.2", "platform_specific-1.0-x86-darwin-100" + + 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 +end diff --git a/spec/bundler/runtime/require_spec.rb b/spec/bundler/runtime/require_spec.rb new file mode 100644 index 0000000000..b68313726b --- /dev/null +++ b/spec/bundler/runtime/require_spec.rb @@ -0,0 +1,442 @@ +# frozen_string_literal: true +require "spec_helper" + +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}" + 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" } + 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}" + gem "two", :require => 'fail' + 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}" + gem "faulty" + 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}" + gem "loadfuuu" + 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}' + gem 'jquery-rails' + 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}" + gem "bcrypt-ruby" + 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}" + gem 'jquery-rails', :require => 'jquery-rails' + 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}" + gem "load-fuuu" + 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}" + gem "load-fuuu" + 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'" + expect(out).to eq("two") + + bundle "exec ruby -e 'Bundler.require(:bar)'" + expect(out).to eq("baz\nqux") + + bundle "exec ruby -e 'Bundler.require(:default, :bar)'" + 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}" + gem "two" + gem "one" + 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_system => 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}" + gem "one" + gem "two" + 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_system => 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 0000000000..dc7af07188 --- /dev/null +++ b/spec/bundler/runtime/setup_spec.rb @@ -0,0 +1,1289 @@ +# frozen_string_literal: true +require "spec_helper" + +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(/^#{system_gem_path}/, "") } + 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", :ruby_repo 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")) + + 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 + + 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 + + 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 + path "#{lib_path("rack-1.0.0")}" + source "file://#{gem_repo1}" + gem "rack" + 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 --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 --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 + expect(out).to match(/at #{lib_path('local-rack')}/) + + 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 + expect(out).to match(/at #{lib_path('local-rack')}/) + + 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 + expect(out).to match(/at #{lib_path('local-rack')}/) + + 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, :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, :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, :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'") + + expect(err).to lack_errors + 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 + + 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 + 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) + expect(paths).to include(default_bundle_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") + 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 + #{generic_local_platform} + + 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:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + L + + if ruby_version + lock += "\n RUBY VERSION\n ruby #{ruby_version}\n" + end + + lock += <<-L + + BUNDLED WITH + #{Bundler::VERSION} + L + + lock + end + + before do + install_gemfile <<-G + ruby ">= 0" + source "file:#{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", :ruby_repo 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(:code) { strip_whitespace(<<-RUBY) } + 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 + + 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) + expect(err).to eq("") + expect(out).to eq("{}") + end + + it "activates no gems with bundle exec" do + install_gemfile! "" + create_file("script.rb", code) + bundle! "exec ruby ./script.rb" + expect(err).to eq("") + expect(out).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 + expect(err).to eq("") + expect(out).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 + + 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" 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 + 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 0000000000..d18a0de485 --- /dev/null +++ b/spec/bundler/runtime/with_clean_env_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Bundler.with_env helpers" do + describe "Bundler.original_env" do + before do + gemfile "" + bundle "install --path vendor/bundle" + end + + it "should return the PATH present before bundle was activated", :ruby_repo do + code = "print Bundler.original_env['PATH']" + path = `getconf PATH`.strip + "#{File::PATH_SEPARATOR}/foo" + with_path_as(path) do + result = bundle("exec ruby -e #{code.dump}") + expect(result).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 + result = bundle("exec ruby -e #{code.inspect}") + expect(result).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("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 ruby #{bundled_app("exe.rb")} 2") + end + expect(err).to eq <<-EOS.strip +2 false +1 true +0 true + EOS + end + end + + describe "Bundler.clean_env" do + before do + gemfile "" + bundle "install --path vendor/bundle" + end + + it "should delete BUNDLE_PATH" do + code = "print Bundler.clean_env.has_key?('BUNDLE_PATH')" + ENV["BUNDLE_PATH"] = "./foo" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).to eq("false") + end + + it "should remove '-rbundler/setup' from RUBYOPT" do + code = "print Bundler.clean_env['RUBYOPT']" + ENV["RUBYOPT"] = "-W2 -rbundler/setup" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).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" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).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" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).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" 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" 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" 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 0000000000..7293f0e57b --- /dev/null +++ b/spec/bundler/spec_helper.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true +$:.unshift File.expand_path("..", __FILE__) +$:.unshift File.expand_path("../../lib", __FILE__) + +require "bundler/psyched_yaml" +require "fileutils" +require "uri" +require "digest/sha1" +require File.expand_path("../support/path.rb", __FILE__) + +begin + require "rubygems" + 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" +rescue LoadError + abort "Run rake spec:deps to install development dependencies" +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" + +# Require the correct version of popen for the current platform +if RbConfig::CONFIG["host_os"] =~ /mingw|mswin/ + begin + require "win32/open3" + rescue LoadError + abort "Run `gem install win32-open3` to be able to run specs" + end +else + require "open3" +end + +Dir["#{File.expand_path("../support", __FILE__)}/*.rb"].each do |file| + require file unless file.end_with?("hax.rb") +end + +$debug = false + +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" +ENV["BUNDLE_PLUGINS"] = "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["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 :ruby_repo => !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + + config.filter_run_when_matching :focus unless ENV["CI"] + + original_wd = Dir.pwd + original_env = ENV.to_hash + + config.expect_with :rspec do |c| + c.syntax = :expect + end + + config.before :suite do + @orig_ruby = if ENV['BUNDLE_RUBY'] + ruby = Gem.ruby + Gem.ruby = ENV['BUNDLE_RUBY'] + ruby + end + end + + config.before :all do + build_repo1 + # HACK: necessary until rspec-mocks > 3.5.0 is used + # see https://github.com/bundler/bundler/pull/5363#issuecomment-278089256 + if RUBY_VERSION < "1.9" + FileUtils.module_eval do + alias_method :mkpath, :mkdir_p + module_function :mkpath + end + end + end + + config.before :each do + reset! + system_gems [] + in_app_root + @all_output = String.new + end + + config.after :each do |example| + @all_output.strip! + 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 + Gem.ruby = @orig_ruby + 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 0000000000..9111ed8211 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index.rb @@ -0,0 +1,121 @@ +# 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(Gem.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 0000000000..6d15b54b85 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -0,0 +1,17 @@ +# 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 0000000000..bffb5b9e2b --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -0,0 +1,14 @@ +# 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 0000000000..4ac328736c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -0,0 +1,15 @@ +# 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 0000000000..b788a852cf --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -0,0 +1,31 @@ +# 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 0000000000..0c417f0580 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -0,0 +1,38 @@ +# 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 0000000000..8a87fc4343 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -0,0 +1,36 @@ +# 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 0000000000..844a9ca9f2 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -0,0 +1,51 @@ +# 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_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb new file mode 100644 index 0000000000..2af5ce9c27 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -0,0 +1,16 @@ +# 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 0000000000..b25eea94e7 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -0,0 +1,12 @@ +# 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 0000000000..6c1ab2def6 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -0,0 +1,20 @@ +# 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_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb new file mode 100644 index 0000000000..bf6feab877 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -0,0 +1,37 @@ +# 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_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb new file mode 100644 index 0000000000..ff1d3e43bc --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -0,0 +1,20 @@ +# 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 0000000000..49a072d2b9 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -0,0 +1,19 @@ +# 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 0000000000..25935f5e5d --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -0,0 +1,16 @@ +# 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 0000000000..3a12a59ae7 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -0,0 +1,19 @@ +# 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 0000000000..f1f8dc5700 --- /dev/null +++ b/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb @@ -0,0 +1,14 @@ +# 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 0000000000..771d431f22 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint.rb @@ -0,0 +1,73 @@ +# 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" + +class Endpoint < Sinatra::Base + GEM_REPO = Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + set :raise_errors, true + set :show_exceptions, false + + 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(Gem.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 0000000000..993630b79e --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -0,0 +1,18 @@ +# 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 0000000000..21ad9117ed --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -0,0 +1,12 @@ +# 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 0000000000..6f5b5f1323 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_missing.rb @@ -0,0 +1,17 @@ +# 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 0000000000..9fafd51a3d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -0,0 +1,14 @@ +# 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 0000000000..cd152848fe --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -0,0 +1,38 @@ +# 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 0000000000..ed4e87e65f --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -0,0 +1,32 @@ +# 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 0000000000..1e9e1dc60d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -0,0 +1,33 @@ +# 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 0000000000..dc79705a26 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -0,0 +1,16 @@ +# 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 0000000000..8a85a41784 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -0,0 +1,18 @@ +# 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 0000000000..250416d8cc --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -0,0 +1,16 @@ +# 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 0000000000..0fb75ebf31 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -0,0 +1,12 @@ +# 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 0000000000..9fb58ecb29 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -0,0 +1,14 @@ +# 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 0000000000..f80d7600c2 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -0,0 +1,16 @@ +# 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 0000000000..4b32cbbf5b --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -0,0 +1,19 @@ +# 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 0000000000..b15650f226 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_fallback", __FILE__) + +Artifice.deactivate + +class EndpointTimeout < EndpointFallback + SLEEP_TIMEOUT = 15 + + 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 0000000000..1059c6df4e --- /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/windows.rb b/spec/bundler/support/artifice/windows.rb new file mode 100644 index 0000000000..c18ca454ec --- /dev/null +++ b/spec/bundler/support/artifice/windows.rb @@ -0,0 +1,48 @@ +# 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 0000000000..db128d497b --- /dev/null +++ b/spec/bundler/support/builders.rb @@ -0,0 +1,806 @@ +# 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-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 "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, 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 = "very_simple_binary_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/very_simple_binary.c", <<-C + #include "ruby.h" + + void Init_very_simple_binary_c() { + rb_define_module("VerySimpleBinaryInC"); + } + 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])) + Dir.chdir(lib_path) do + destination = opts[:path] || _default_path + FileUtils.mkdir_p(destination) + + @spec.authors = ["that guy"] if !@spec.authors || @spec.authors.empty? + + Bundler.rubygems.build(@spec, opts[:skip_validation]) + if opts[:to_system] + `gem install --ignore-dependencies --no-ri --no-rdoc #{@spec.full_name}.gem` + else + FileUtils.mv("#{@spec.full_name}.gem", opts[:path] || _default_path) + end + 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 0000000000..8f1fb35bcd --- /dev/null +++ b/spec/bundler/support/code_climate.rb @@ -0,0 +1,25 @@ +# 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/hax.rb b/spec/bundler/support/hax.rb new file mode 100644 index 0000000000..663d3527c5 --- /dev/null +++ b/spec/bundler/support/hax.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require "rubygems" + +module Gem + class Platform + @local = new(ENV["BUNDLER_SPEC_PLATFORM"]) if ENV["BUNDLER_SPEC_PLATFORM"] + end + @platforms = [Gem::Platform::RUBY, Gem::Platform.local] +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 diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb new file mode 100644 index 0000000000..1a3fec1960 --- /dev/null +++ b/spec/bundler/support/helpers.rb @@ -0,0 +1,504 @@ +# frozen_string_literal: true + +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 + if exitstatus && exitstatus != 0 + error = out + "\n" + err + error.strip! + raise RuntimeError, + "Invoking #{method}!(#{args.map(&:inspect).join(", ")}) failed:\n#{error}", + caller.drop_while {|bt| bt.start_with?(__FILE__) } + end + end + end + end + + attr_reader :out, :err, :exitstatus + + def the_bundle(*args) + TheBundle.new(*args) + 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" + @out = 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 + + options["no-color"] = true unless options.key?("no-color") || cmd.to_s =~ /\A(e|ex|exe|exec|conf|confi|config)(\s|\z)/ + + bundle_bin = options.delete("bundle_bin") || bindir.join("bundle") + + if system_bundler = options.delete(:system_bundler) + bundle_bin = "-S bundle" + end + + requires = options.delete(:requires) || [] + if artifice = options.delete(:artifice) { "fail" unless RSpec.current_example.metadata[:realworld] } + requires << File.expand_path("../artifice/#{artifice}.rb", __FILE__) + end + requires << "support/hax" + 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 = (options.delete(:env) || {}).map {|k, v| "#{k}='#{v}'" }.join(" ") + env["PATH"].gsub!("#{Path.root}/exe", "") if env["PATH"] && system_bundler + args = options.map do |k, v| + v == true ? " --#{k}" : " --#{k} #{v}" if v + 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 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} -rubygems -S gem --backtrace" + sys_exec("#{gem} #{command} #{args}") + end + bang :gem_command + + def rake + if ENV['BUNDLE_RUBY'] && ENV['BUNDLE_GEM'] + "#{ENV['BUNDLE_RUBY']} #{ENV['GEM_PATH']}/bin/rake" + else + 'rake' + end + end + + def sys_exec(cmd) + Open3.popen3(cmd.to_s) do |stdin, stdout, stderr, wait_thr| + yield stdin, stdout, wait_thr if block_given? + stdin.close + + @exitstatus = wait_thr && wait_thr.value.exitstatus + @out = Thread.new { stdout.read }.value.strip + @err = Thread.new { stderr.read }.value.strip + end + + (@all_output ||= String.new) << [ + "$ #{cmd.to_s.strip}", + out, + err, + @exitstatus ? "# $? => #{@exitstatus}" : "", + "\n", + ].reject(&:empty?).join("\n") + + @out + 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 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 = root + "bundler-#{Bundler::VERSION}.gem" + 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) + + gem_command! :install, "--no-rdoc --no-ri --ignore-dependencies '#{path}'" + 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) + gems = gems.flatten + + FileUtils.rm_rf(system_gem_path) + FileUtils.mkdir_p(system_gem_path) + + Gem.clear_paths + + env_backup = ENV.to_hash + ENV["GEM_HOME"] = system_gem_path.to_s + ENV["GEM_PATH"] = system_gem_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 default_bundle_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_windows + old = ENV["BUNDLER_SPEC_WINDOWS"] + ENV["BUNDLER_SPEC_WINDOWS"] = "true" + simulate_platform mswin 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 + end +end diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb new file mode 100644 index 0000000000..29780014fc --- /dev/null +++ b/spec/bundler/support/indexes.rb @@ -0,0 +1,365 @@ +# 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 = [] + @deps.each do |d| + @platforms.each do |p| + deps << Bundler::DepProxy.new(d, p) + end + end + Bundler::Resolver.resolve(deps, @index, *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) + 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) + + # --- 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 + 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 + 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 0000000000..27966aa6ed --- /dev/null +++ b/spec/bundler/support/less_than_proc.rb @@ -0,0 +1,19 @@ +# 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/matchers.rb b/spec/bundler/support/matchers.rb new file mode 100644 index 0000000000..9248360639 --- /dev/null +++ b/spec/bundler/support/matchers.rb @@ -0,0 +1,222 @@ +# 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| + actual.split(MAJOR_DEPRECATION).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 + + 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 + out.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + actual_version, actual_platform = out.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 + out.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + unless out.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 out == "WIN" + next "expected #{name} to not be installed, but it was" if version.nil? + if Gem::Version.new(out) == 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(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 0000000000..f28d660e83 --- /dev/null +++ b/spec/bundler/support/path.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true +require "pathname" + +module Spec + module Path + def root + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + root_path = File.expand_path("../../../..", __FILE__) + else + root_path = File.expand_path("../../..", __FILE__) + end + @root ||= Pathname.new(root_path) + end + + def gemspec + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + gemspec_path = File.expand_path(root.join("lib/bundler.gemspec"), __FILE__) + else + gemspec_path = File.expand_path(root.join("bundler.gemspec"), __FILE__) + end + @gemspec ||= Pathname.new(gemspec_path) + end + + def bindir + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + bin_path = File.expand_path(root.join("bin"), __FILE__) + else + bin_path = File.expand_path(root.join("exe"), __FILE__) + end + @bindir ||= Pathname.new(bin_path) + end + + def spec_dir + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + spec_path = File.expand_path(root.join("spec/bundler"), __FILE__) + else + spec_path = File.expand_path(root.join("spec"), __FILE__) + end + @spec_dir ||= Pathname.new(spec_path) + end + + def tmp(*path) + root.join("tmp", *path) + end + + def home(*path) + tmp.join("home", *path) + end + + def default_bundle_path(*path) + system_gem_path(*path) + 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 + + extend self + end +end diff --git a/spec/bundler/support/permissions.rb b/spec/bundler/support/permissions.rb new file mode 100644 index 0000000000..f5636dd70a --- /dev/null +++ b/spec/bundler/support/permissions.rb @@ -0,0 +1,11 @@ +# 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 0000000000..a2a3afba00 --- /dev/null +++ b/spec/bundler/support/platforms.rb @@ -0,0 +1,98 @@ +# 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 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 + raise BundlerError, "That RUBY_ENGINE is not recognized" + 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 + end +end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb new file mode 100644 index 0000000000..b484d63eab --- /dev/null +++ b/spec/bundler/support/rubygems_ext.rb @@ -0,0 +1,64 @@ +# 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. + "rack" => "< 2", + # 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", + "bundler" => "1.12.0", + } + # 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.root}/exe", "#{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? } + reqs = reqs.sort_by {|name, _| name == "rack" ? 0 : 1 } # TODO: remove when we drop ruby 1.8.7 support + no_reqs.map!(&:first) + reqs.map! {|name, req| "'#{name}:#{req}'" } + deps = reqs.concat(no_reqs).join(" ") + cmd = "gem install #{deps} --no-rdoc --no-ri --conservative" + 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 0000000000..1a8f91b3ba --- /dev/null +++ b/spec/bundler/support/silent_logger.rb @@ -0,0 +1,9 @@ +# 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 0000000000..6a50f5ff4c --- /dev/null +++ b/spec/bundler/support/sometimes.rb @@ -0,0 +1,56 @@ +# 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 0000000000..561b29092b --- /dev/null +++ b/spec/bundler/support/streams.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require "stringio" + +def capture(*streams) + streams.map!(&:to_s) + begin + result = StringIO.new + streams.each {|stream| eval "$#{stream} = result" } + yield + ensure + streams.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 0000000000..8c82bb8c0f --- /dev/null +++ b/spec/bundler/support/sudo.rb @@ -0,0 +1,17 @@ +# 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 0000000000..742d393425 --- /dev/null +++ b/spec/bundler/support/the_bundle.rb @@ -0,0 +1,36 @@ +# 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/gems/post_install_spec.rb b/spec/bundler/update/gems/post_install_spec.rb new file mode 100644 index 0000000000..5a4fe7f321 --- /dev/null +++ b/spec/bundler/update/gems/post_install_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + 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 + 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 0000000000..021c8c942b --- /dev/null +++ b/spec/bundler/update/git_spec.rb @@ -0,0 +1,333 @@ +# frozen_string_literal: true +require "spec_helper" + +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" + + 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(out).to include("Using activesupport 3.0 from #{lib_path("rails")} (at master@#{revision_for(lib_path("rails"))[0..6]})") + 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_system => 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" + expect(exitstatus).to eq(0) if exitstatus + end + + describe "with submodules" do + before :each do + build_gem "submodule", :to_system => true do |s| + s.write "lib/submodule.rb", "puts 'GEM'" + 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 + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + + run "require 'submodule'" + expect(out).to eq("GEM") + + install_gemfile <<-G + 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 + 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 + 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 + expect(out).to include(lib_path("foo-1.0").to_s) + 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" + 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://#{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" 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:#{gem_repo2}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + 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 0000000000..5ac4f7b1fe --- /dev/null +++ b/spec/bundler/update/path_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require "spec_helper" + +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/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index f533ad61bb..189048570b 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 @@ -25,6 +26,7 @@ $repositories = { rubygems: 'rubygems/rubygems', + bundler: 'bundler/bundler', rdoc: 'ruby/rdoc', json: 'flori/json', psych: 'ruby/psych', @@ -63,6 +65,12 @@ def sync_default_gems(gem) `cp -r ../../rubygems/rubygems/lib/ubygems.rb ./lib` `cp -r ../../rubygems/rubygems/test/rubygems ./test` `cp ../../rubygems/rubygems/LICENSE.txt ./lib/rubygems` + when "bundler" + `rm -rf lib/bundler* bin/bundler bin/bundle bin/bundle_ruby spec/bundler` + `cp -r ../../bundler/bundler/lib/bundler* ./lib` + `cp -r ../../bundler/bundler/exe/bundle* ./bin` + `cp ../../bundler/bundler/bundler.gemspec ./lib` + `cp ../../bundler/bundler/spec spec/bundler when "rdoc" `rm -rf lib/rdoc* test/rdoc` `cp -rf ../rdoc/lib/rdoc* ./lib` -- cgit v1.2.3