diff options
author | John Keiser <john@johnkeiser.com> | 2016-04-15 12:10:40 -0700 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2016-04-18 14:21:02 -0700 |
commit | ea2593fe58037739c5c2ada517e3d988dec290ff (patch) | |
tree | 55142ceaef43080201a8d9e54f79868368241f60 | |
parent | bbc1d6ebccbd7f22fea72a9c2b76ef19f3b5beb3 (diff) | |
download | chef-ea2593fe58037739c5c2ada517e3d988dec290ff.tar.gz |
Make gemfile_util capable of copying groups over
-rw-r--r-- | .travis.yml | 26 | ||||
-rw-r--r-- | Gemfile | 12 | ||||
-rw-r--r-- | Gemfile.lock | 25 | ||||
-rw-r--r-- | Gemfile.windows | 11 | ||||
-rw-r--r-- | Gemfile.windows.lock | 80 | ||||
-rw-r--r-- | Rakefile | 4 | ||||
-rw-r--r-- | acceptance/Gemfile.lock | 8 | ||||
-rw-r--r-- | appveyor.yml | 2 | ||||
-rw-r--r-- | chef-config/lib/chef-config/package_task.rb | 7 | ||||
-rw-r--r-- | kitchen-tests/Berksfile.lock | 4 | ||||
-rw-r--r-- | kitchen-tests/Gemfile.lock | 8 | ||||
-rw-r--r-- | omnibus/.kitchen.yml | 37 | ||||
-rw-r--r-- | omnibus/Gemfile.lock | 8 | ||||
-rw-r--r-- | omnibus/config/software/chef.rb | 7 | ||||
-rw-r--r-- | omnibus/files/chef-appbundle/build-chef-appbundle.rb | 9 | ||||
-rw-r--r-- | omnibus/files/chef-gem/build-chef-gem/gem-install-software-def.rb | 21 | ||||
-rw-r--r-- | omnibus/files/chef/build-chef.rb | 15 | ||||
-rw-r--r-- | pedant.gemfile | 25 | ||||
-rw-r--r-- | tasks/bin/bundle-platform.bat | 2 | ||||
-rwxr-xr-x | tasks/bin/create-override-gemfile | 110 | ||||
-rwxr-xr-x | tasks/bin/run_external_test | 11 | ||||
-rw-r--r-- | tasks/bundle.rb | 7 | ||||
-rw-r--r-- | tasks/bundle_util.rb | 3 | ||||
-rw-r--r-- | tasks/dependencies.rb | 2 | ||||
-rw-r--r-- | tasks/gemfile_util.rb | 476 |
25 files changed, 604 insertions, 316 deletions
diff --git a/.travis.yml b/.travis.yml index 7d630f154f..ec07e3b456 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ before_install: - gem install bundler -v $(grep bundler omnibus_overrides.rb | cut -d'"' -f2) - rm -f .bundle/config -bundler_args: --without docgen maintenance omnibus_package aix bsd solaris windows --frozen +bundler_args: --without docgen maintenance omnibus_package aix bsd mac_os_x solaris windows --frozen # do not run expensive spec tests on PRs, only on branches branches: @@ -26,29 +26,29 @@ matrix: - rvm: 2.1 sudo: true script: tasks/bin/run_chef_tests - bundler_args: --without docgen maintenance integration aix bsd solaris windows --frozen + bundler_args: --without docgen maintenance integration aix bsd mac_os_x solaris windows --frozen - rvm: 2.2 sudo: true script: tasks/bin/run_chef_tests - bundler_args: --without docgen maintenance integration aix bsd solaris windows --frozen + bundler_args: --without docgen maintenance integration aix bsd mac_os_x solaris windows --frozen - rvm: 2.3.0 sudo: true script: tasks/bin/run_chef_tests - bundler_args: --without docgen maintenance integration aix bsd solaris windows --frozen + bundler_args: --without docgen maintenance integration aix bsd mac_os_x solaris windows --frozen - rvm: rbx sudo: true script: tasks/bin/run_chef_tests - bundler_args: --without docgen maintenance integration aix bsd solaris windows --frozen + bundler_args: --without docgen maintenance integration aix bsd mac_os_x solaris windows --frozen # # External tests # - env: TEST_GEM: chef-provisioning - script: tasks/bin/run_external_test $TEST_GEM "rake spec" + script: tasks/bin/run_external_test $TEST_GEM rake spec rvm: 2.2 - env: TEST_GEM: chef-provisioning-aws - script: tasks/bin/run_external_test $TEST_GEM "rake spec" + script: tasks/bin/run_external_test $TEST_GEM rake spec rvm: 2.2 # requires vagrant # - env: TEST_GEM=chef-rewind @@ -61,11 +61,11 @@ matrix: rvm: 2.2 - env: - TEST_GEM: chef-zero - script: tasks/bin/run_external_test $TEST_GEM "rake spec" "rake cheffs" + script: tasks/bin/run_external_test $TEST_GEM rake spec cheffs rvm: 2.2 - env: TEST_GEM: cheffish - script: tasks/bin/run_external_test $TEST_GEM "rake spec" + script: tasks/bin/run_external_test $TEST_GEM rake spec rvm: 2.2 - env: TEST_GEM: chefspec @@ -78,19 +78,19 @@ matrix: rvm: 2.2.0 - env: TEST_GEM: foodcritic - script: tasks/bin/run_external_test $TEST_GEM "rake test" + script: tasks/bin/run_external_test $TEST_GEM rake test rvm: 2.2 - env: TEST_GEM: halite - script: tasks/bin/run_external_test $TEST_GEM "rake spec" + script: tasks/bin/run_external_test $TEST_GEM rake spec rvm: 2.2 - env: TEST_GEM: knife-windows - script: tasks/bin/run_external_test $TEST_GEM "rake unit_spec" + script: tasks/bin/run_external_test $TEST_GEM rake unit_spec rvm: 2.2 - env: TEST_GEM: poise - script: tasks/bin/run_external_test $TEST_GEM "rake spec" + script: tasks/bin/run_external_test $TEST_GEM rake spec rvm: 2.2 # ### START TEST KITCHEN ONLY ### @@ -3,7 +3,7 @@ require_relative "tasks/gemfile_util" extend GemfileUtil source "https://rubygems.org" -gemspec name: "chef" +gemspec name: ($chef_platform ? "chef-#{$chef_platform}" : "chef") gem "activesupport", "< 4.0.0", group: :compat_testing, platform: "ruby" gem "chef-config", path: File.expand_path("../chef-config", __FILE__) if File.exist?(File.expand_path("../chef-config", __FILE__)) @@ -31,7 +31,7 @@ group(:integration) do gem "chef-sugar" gem "chefspec" gem "halite" - gem "poise", git: "https://github.com/poise/poise" # until released poise's tests succeed against chef master + gem "poise" gem "knife-windows" gem "foodcritic" end @@ -53,9 +53,11 @@ group(:linux, :bsd, :mac_os_x, :solaris, :windows, :ruby_prof) do # may need to disable this in insolation on fussy builds like AIX, RHEL4, etc gem "ruby-prof" end -# Everything except AIX and Windows -group(:linux, :bsd, :mac_os_x, :solaris) do - gem "ruby-shadow" +unless $chef_platform == "windows" + # Everything except AIX and Windows + group(:linux, :bsd, :mac_os_x, :solaris) do + gem "ruby-shadow" + end end group(:development, :test) do diff --git a/Gemfile.lock b/Gemfile.lock index 9c30299306..a2aec12cd6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,13 +7,6 @@ GIT rubocop (= 0.39.0) GIT - remote: https://github.com/poise/poise - revision: a76980685a92283c08f5b2e4526ae1f08f87de79 - specs: - poise (2.6.2.pre) - halite (~> 1.0) - -GIT remote: https://github.com/rubysec/bundler-audit.git revision: 4e32fca89d75f0e249671431ff38aadc02bfb28b ref: 4e32fca @@ -25,9 +18,9 @@ GIT PATH remote: . specs: - chef (12.10.4) + chef (12.10.5) bundler (>= 1.10) - chef-config (= 12.10.4) + chef-config (= 12.10.5) chef-zero (~> 4.5) diff-lcs (~> 1.2, >= 1.2.4) erubis (~> 2.7) @@ -55,7 +48,7 @@ PATH PATH remote: chef-config specs: - chef-config (12.10.4) + chef-config (12.10.5) fuzzyurl (~> 0.8.0) mixlib-config (~> 2.0) mixlib-shellout (~> 2.0) @@ -202,10 +195,10 @@ GEM rspec-mocks (~> 3.2) mixlib-cli (1.5.0) mixlib-config (2.2.1) - mixlib-install (1.0.6) - artifactory (>= 2.3.0) - mixlib-shellout (>= 2.2.6) - mixlib-versioning (>= 1.1.0) + mixlib-install (1.0.7) + artifactory + mixlib-shellout + mixlib-versioning mixlib-log (1.6.0) mixlib-shellout (2.2.6) mixlib-versioning (1.1.0) @@ -253,6 +246,8 @@ GEM parser (2.3.0.7) ast (~> 2.2) plist (3.2.0) + poise (2.7.0) + halite (~> 1.0) polyglot (0.3.5) powerpack (0.1.1) proxifier (1.0.3) @@ -373,7 +368,7 @@ DEPENDENCIES netrc nokogiri octokit - poise! + poise pry pry-byebug pry-remote diff --git a/Gemfile.windows b/Gemfile.windows index cf7b6efdb0..def7a0bff6 100644 --- a/Gemfile.windows +++ b/Gemfile.windows @@ -29,4 +29,13 @@ # require_relative "tasks/gemfile_util" -GemfileUtil.include_locked_gemfile(self, "Gemfile", without_groups: [ :linux, :solaris, :aix ]) +extend GemfileUtil + +# Honor the main gem's lockfile +include_locked_gemfile("Gemfile", without_groups: [ :aix, :bsd, :mac_os_x, :linux, :solaris ], copy_groups: true) + +# We have the same deps as the main Gemfile (except since we're Windows, we read the +# chef-windows.gemspec instead of chef.gemspec) +$chef_platform = "windows" +chef_gemfile = File.expand_path("../Gemfile", __FILE__) +instance_eval(IO.read(chef_gemfile), chef_gemfile) diff --git a/Gemfile.windows.lock b/Gemfile.windows.lock index 79c946b30a..8eadf2b475 100644 --- a/Gemfile.windows.lock +++ b/Gemfile.windows.lock @@ -7,14 +7,6 @@ GIT rubocop (= 0.39.0) GIT - remote: https://github.com/poise/poise - revision: a76980685a92283c08f5b2e4526ae1f08f87de79 - ref: a76980685a92283c08f5b2e4526ae1f08f87de79 - specs: - poise (2.6.2.pre) - halite (~> 1.0) - -GIT remote: https://github.com/rubysec/bundler-audit.git revision: 4e32fca89d75f0e249671431ff38aadc02bfb28b ref: 4e32fca89d75f0e249671431ff38aadc02bfb28b @@ -24,19 +16,37 @@ GIT thor (~> 0.18) PATH - remote: chef-config - specs: - chef-config (12.10.4) - fuzzyurl (~> 0.8.0) - mixlib-config (~> 2.0) - mixlib-shellout (~> 2.0) - -PATH remote: . specs: - chef (12.10.4-universal-mingw32) + chef (12.10.5) + bundler (>= 1.10) + chef-config (= 12.10.5) + chef-zero (~> 4.5) + diff-lcs (~> 1.2, >= 1.2.4) + erubis (~> 2.7) + ffi-yajl (~> 2.2) + highline (~> 1.6, >= 1.6.9) + mixlib-authentication (~> 1.4) + mixlib-cli (~> 1.4) + mixlib-log (~> 1.3) + mixlib-shellout (~> 2.0) + net-sftp (~> 2.1, >= 2.1.2) + net-ssh (>= 2.9, < 4.0) + net-ssh-multi (~> 1.1) + ohai (>= 8.6.0.alpha.1, < 9) + plist (~> 3.2) + proxifier (~> 1.0) + rspec-core (~> 3.4) + rspec-expectations (~> 3.4) + rspec-mocks (~> 3.4) + rspec_junit_formatter (~> 0.2.0) + serverspec (~> 2.7) + specinfra (~> 2.10) + syslog-logger (~> 1.6) + uuidtools (~> 2.1.5) + chef (12.10.5-universal-mingw32) bundler (>= 1.10) - chef-config (= 12.10.4) + chef-config (= 12.10.5) chef-zero (~> 4.5) diff-lcs (~> 1.2, >= 1.2.4) erubis (~> 2.7) @@ -72,6 +82,14 @@ PATH windows-api (~> 0.4.4) wmi-lite (~> 1.0) +PATH + remote: chef-config + specs: + chef-config (12.10.5) + fuzzyurl (~> 0.8.0) + mixlib-config (~> 2.0) + mixlib-shellout (~> 2.0) + GEM remote: https://rubygems.org/ specs: @@ -145,6 +163,7 @@ GEM multipart-post (>= 1.2, < 3) fauxhai (3.3.0) net-ssh + ffi (1.9.10) ffi (1.9.10-x86-mingw32) ffi-yajl (2.2.3) libyajl2 (~> 1.2) @@ -214,11 +233,12 @@ GEM rspec-mocks (~> 3.2) mixlib-cli (1.5.0) mixlib-config (2.2.1) - mixlib-install (1.0.6) - artifactory (>= 2.3.0) - mixlib-shellout (>= 2.2.6) - mixlib-versioning (>= 1.1.0) + mixlib-install (1.0.7) + artifactory + mixlib-shellout + mixlib-versioning mixlib-log (1.6.0) + mixlib-shellout (2.2.6) mixlib-shellout (2.2.6-universal-mingw32) win32-process (~> 0.8.2) wmi-lite (~> 1.0) @@ -238,6 +258,8 @@ GEM net-ssh-gateway (>= 1.2.0) net-telnet (0.1.1) netrc (0.11.0) + nokogiri (1.6.7.2) + mini_portile2 (~> 2.0.0.rc2) nokogiri (1.6.7.2-x86-mingw32) mini_portile2 (~> 2.0.0.rc2) nori (2.6.0) @@ -267,6 +289,8 @@ GEM parser (2.3.0.7) ast (~> 2.2) plist (3.2.0) + poise (2.7.0) + halite (~> 1.0) polyglot (0.3.5) powerpack (0.1.1) proxifier (1.0.3) @@ -315,7 +339,6 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-prof (0.15.9) ruby-progressbar (1.7.5) - ruby-shadow (2.5.0) rubyntlm (0.6.0) rufus-lru (1.0.5) sawyer (0.7.0) @@ -383,6 +406,7 @@ GEM yard (0.8.7.6) PLATFORMS + ruby x86-mingw32 DEPENDENCIES @@ -397,7 +421,8 @@ DEPENDENCIES aws-sdk-v1 (= 1.66.0)! binding_of_caller (= 0.7.2)! builder (= 3.2.2)! - bundler-audit! + bundler + bundler-audit (= 0.4.0)! byebug (= 8.2.4)! chef! chef-api (= 0.5.0)! @@ -409,7 +434,7 @@ DEPENDENCIES chef-zero (= 4.6.1)! cheffish (= 2.0.4)! chefspec (= 4.6.1)! - chefstyle! + chefstyle (= 0.3.1)! childprocess (= 0.5.9)! coderay (= 1.1.1)! colorize (= 0.7.7)! @@ -455,7 +480,7 @@ DEPENDENCIES mixlib-authentication (= 1.4.0)! mixlib-cli (= 1.5.0)! mixlib-config (= 2.2.1)! - mixlib-install (= 1.0.6)! + mixlib-install (= 1.0.7)! mixlib-log (= 1.6.0)! mixlib-shellout (= 2.2.6)! mixlib-versioning (= 1.1.0)! @@ -477,7 +502,7 @@ DEPENDENCIES overcommit (= 0.33.0)! parser (= 2.3.0.7)! plist (= 3.2.0)! - poise! + poise (= 2.7.0)! polyglot (= 0.3.5)! powerpack (= 0.1.1)! proxifier (= 1.0.3)! @@ -500,7 +525,6 @@ DEPENDENCIES rubocop (= 0.39.0)! ruby-prof (= 0.15.9)! ruby-progressbar (= 1.7.5)! - ruby-shadow (= 2.5.0)! rubyntlm (= 0.6.0)! rufus-lru (= 1.0.5)! sawyer (= 0.7.0)! @@ -34,7 +34,9 @@ ChefConfig::PackageTask.new(File.expand_path("..", __FILE__), "Chef") do |packag package.generate_version_class = true end # Add a conservative dependency update to version:bump (which was created by PackageTask) -task "version:bump" => %w{version:bump_patch version:update dependencies:update[conservative]} +task "version:bump" => %w{version:bump_patch version:update} do + Rake::Task["dependencies:update"].invoke("conservative") +end task :pedant, :chef_zero_spec diff --git a/acceptance/Gemfile.lock b/acceptance/Gemfile.lock index 618acb42f8..fe7ffe9513 100644 --- a/acceptance/Gemfile.lock +++ b/acceptance/Gemfile.lock @@ -124,10 +124,10 @@ GEM rspec-expectations (~> 3.2) rspec-mocks (~> 3.2) mixlib-config (2.2.1) - mixlib-install (1.0.6) - artifactory (>= 2.3.0) - mixlib-shellout (>= 2.2.6) - mixlib-versioning (>= 1.1.0) + mixlib-install (1.0.7) + artifactory + mixlib-shellout + mixlib-versioning mixlib-log (1.6.0) mixlib-shellout (2.2.6) mixlib-versioning (1.1.0) diff --git a/appveyor.yml b/appveyor.yml index 5a24059131..17282f9414 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -32,7 +32,7 @@ install: build_script: - bundle config --local frozen 1 - - bundle config --local without docgen:maintenance:omnibus_package:integration:aix:bsd:linux:solaris + - bundle config --local without docgen:maintenance:omnibus_package:integration:aix:bsd:linux:mac_os_x:solaris - bundle install || bundle install || bundle install test_script: diff --git a/chef-config/lib/chef-config/package_task.rb b/chef-config/lib/chef-config/package_task.rb index 43d01f53ef..5971510337 100644 --- a/chef-config/lib/chef-config/package_task.rb +++ b/chef-config/lib/chef-config/package_task.rb @@ -222,7 +222,12 @@ end task :install => [:package] do with_clean_env do full_module_path = File.join(full_package_dir, module_path) - sh %{gem install #{full_module_path}-#{version}.gem --no-rdoc --no-ri} + # Install the windows version on windows + if Gem.win_platform? && File.exist?("#{full_module_path}-#{version}-universal-mingw32.gem") + sh %{gem install #{full_module_path}-#{version}-universal-mingw32.gem --no-rdoc --no-ri} + else + sh %{gem install #{full_module_path}-#{version}.gem --no-rdoc --no-ri} + end end end diff --git a/kitchen-tests/Berksfile.lock b/kitchen-tests/Berksfile.lock index dcdafe06f3..ee5027e4b7 100644 --- a/kitchen-tests/Berksfile.lock +++ b/kitchen-tests/Berksfile.lock @@ -18,7 +18,7 @@ GRAPH mysql-chef_gem (~> 0.0) postgresql (>= 1.0.0) xfs (>= 0.0.0) - iis (4.1.6) + iis (4.1.7) windows (>= 1.34.6) mysql (5.6.3) yum-mysql-community (>= 0.0.0) @@ -35,7 +35,7 @@ GRAPH windows (>= 0.0.0) xml (>= 0.0.0) yum-epel (>= 0.0.0) - postgresql (4.0.4) + postgresql (4.0.6) apt (>= 1.9.0) build-essential (>= 0.0.0) openssl (~> 4.0) diff --git a/kitchen-tests/Gemfile.lock b/kitchen-tests/Gemfile.lock index 0fad64e53d..70f40c5235 100644 --- a/kitchen-tests/Gemfile.lock +++ b/kitchen-tests/Gemfile.lock @@ -82,10 +82,10 @@ GEM rspec-expectations (~> 3.2) rspec-mocks (~> 3.2) mixlib-config (2.2.1) - mixlib-install (1.0.6) - artifactory (>= 2.3.0) - mixlib-shellout (>= 2.2.6) - mixlib-versioning (>= 1.1.0) + mixlib-install (1.0.7) + artifactory + mixlib-shellout + mixlib-versioning mixlib-log (1.6.0) mixlib-shellout (2.2.6) mixlib-versioning (1.1.0) diff --git a/omnibus/.kitchen.yml b/omnibus/.kitchen.yml index 6a56b92dcb..e64cadca26 100644 --- a/omnibus/.kitchen.yml +++ b/omnibus/.kitchen.yml @@ -11,9 +11,7 @@ driver: cpus: 4 memory: 4096 synced_folders: - - ['..', '/home/vagrant/chef'] - - ['../../omnibus', '/home/vagrant/omnibus'] - - ['../../omnibus-software', '/home/vagrant/omnibus-software'] + - ['../..', '/home/vagrant'] provisioner: name: chef_zero @@ -21,6 +19,11 @@ provisioner: # This is not the version of chef that we're building - this is the version # of chef that omnibus needs to build chef/chef. require_chef_omnibus: true + attributes: + vagrant: + this_key_exists_so_we_have_a_vagrant_key: true + chef_omnibus_install_options: -P angrychef + chef_omnibus_root: /opt/angrychef platforms: - name: centos-5.11 @@ -73,6 +76,8 @@ platforms: - name: windows-2012r2-standard driver: box: chef/windows-server-2012r2-standard # private + provisioner: + chef_omnibus_root: /opscode/angrychef attribute_defaults: &attribute_defaults build_user: vagrant @@ -80,27 +85,17 @@ attribute_defaults: &attribute_defaults build_user_password: vagrant suites: - - name: angrychef - attributes: - omnibus: - <<: *attribute_defaults - install_dir: /opt/angrychef - run_list: - - omnibus::default - - name: chef-fips - attributes: - omnibus: - <<: *attribute_defaults - install_dir: /opt/chef-fips - run_list: - - omnibus::default + # - name: angrychef + # attributes: + # omnibus: + # <<: *attribute_defaults + # install_dir: /opt/angrychef + # run_list: + # - omnibus::default - name: chef - provisioner: - chef_omnibus_install_options: -P angrychef - chef_omnibus_root: /opt/angrychef attributes: omnibus: <<: *attribute_defaults - install_dir: /opt/chef + install_dir: /opscode/chef run_list: - omnibus::default diff --git a/omnibus/Gemfile.lock b/omnibus/Gemfile.lock index 7e92537e3c..90bf93fa1c 100644 --- a/omnibus/Gemfile.lock +++ b/omnibus/Gemfile.lock @@ -130,10 +130,10 @@ GEM rspec-mocks (~> 3.2) mixlib-cli (1.5.0) mixlib-config (2.2.1) - mixlib-install (1.0.6) - artifactory (>= 2.3.0) - mixlib-shellout (>= 2.2.6) - mixlib-versioning (>= 1.1.0) + mixlib-install (1.0.7) + artifactory + mixlib-shellout + mixlib-versioning mixlib-log (1.6.0) mixlib-shellout (2.2.6) mixlib-versioning (1.1.0) diff --git a/omnibus/config/software/chef.rb b/omnibus/config/software/chef.rb index 02f4162313..866694c894 100644 --- a/omnibus/config/software/chef.rb +++ b/omnibus/config/software/chef.rb @@ -62,8 +62,11 @@ build do # Prepare to install: build config, retries, job, frozen=true # TODO Windows install seems to sometimes install already-installed gems such - # as gherkin (and fail as a result) if you use jobs: 4. - create_bundle_config(chef_gemfile, retries: 4, jobs: 1, frozen: true) + # as gherkin (and fail as a result) if you use jobs > 1. + # TODO Windows cannot be frozen, because Bundler doesn't understand platform-specific + # versions. However, on Windows we have explicit version pins for most things, so + # we will *probably* get the exact versions of everything we want. + create_bundle_config(chef_gemfile, retries: 4, jobs: windows? ? 1 : 7, frozen: !windows?) # Install all the things. Arguments are specified in .bundle/config (see create_bundle_config) block { log.info(log_key) { "" } } diff --git a/omnibus/files/chef-appbundle/build-chef-appbundle.rb b/omnibus/files/chef-appbundle/build-chef-appbundle.rb index 97a94ef5a7..5e4a6afdfd 100644 --- a/omnibus/files/chef-appbundle/build-chef-appbundle.rb +++ b/omnibus/files/chef-appbundle/build-chef-appbundle.rb @@ -19,12 +19,12 @@ module BuildChefAppbundle # times in the file. # distribution_gemfile = Pathname(shared_gemfile).relative_path_from(Pathname(installed_gemfile)).to_s - gemfile_text = IO.read(installed_gemfile) - gemfile_text << <<-EOM.gsub(/^\s+/, "") + gemfile_text = <<-EOM.gsub(/^\s+/, "") # Lock gems that are part of the distribution distribution_gemfile = File.expand_path(#{distribution_gemfile.inspect}, __FILE__) instance_eval(IO.read(distribution_gemfile), distribution_gemfile) EOM + gemfile_text << IO.read(installed_gemfile) create_file(installed_gemfile) { gemfile_text } # Remove the gemfile.lock @@ -50,7 +50,10 @@ module BuildChefAppbundle # "test", "changelog" and "guard" come from berkshelf, "maintenance" comes from chef # "tools" and "integration" come from inspec shellout!("#{bundle_bin} config --local without #{without_groups.join(":")}", env: env, cwd: installed_path) - shellout!("#{bundle_bin} config --local frozen 1") + # TODO Windows cannot be frozen, because Bundler doesn't understand platform-specific + # versions. However, on Windows we have explicit version pins for most things, so + # we will *probably* get the exact versions of everything we want. + shellout!("#{bundle_bin} config --local frozen 1") unless windows? shellout!("#{bundle_bin} check", env: env, cwd: installed_path) diff --git a/omnibus/files/chef-gem/build-chef-gem/gem-install-software-def.rb b/omnibus/files/chef-gem/build-chef-gem/gem-install-software-def.rb index ee096b8ed9..5d77ebf7f2 100644 --- a/omnibus/files/chef-gem/build-chef-gem/gem-install-software-def.rb +++ b/omnibus/files/chef-gem/build-chef-gem/gem-install-software-def.rb @@ -80,7 +80,7 @@ module BuildChefGem end def lockfile_path - @lockfile_path ||= "#{gemfile_path}.lock" + "#{gemfile_path}.lock" end def gem_name @@ -98,16 +98,19 @@ module BuildChefGem def gem_metadata @gem_metadata ||= begin - selected = GemfileUtil.select_gems(gemfile_path, without_groups: without_groups) - result = GemfileUtil.locked_gems(lockfile_path, selected)[gem_name] + bundle = GemfileUtil::Bundle.parse(gemfile_path, lockfile_path) + result = bundle.gems[gem_name] if result - log.info(software.name) { "Using #{gem_name} version #{result[:version]} from #{gemfile_path}" } - result - elsif GemfileUtil.locked_gems(lockfile_path, GemfileUtil.select_gems(gemfile_path))[gem_name] - log.info(software.name) { "#{gem_name} not loaded from #{gemfile_path} because it was only in groups #{without_groups.join(", ")}, skipping" } - nil + if bundle.select_gems(without_groups: without_groups).include?(gem_name) + log.info(software.name) { "Using #{gem_name} version #{result[:version]} from #{gemfile_path}" } + result + else + log.info(software.name) { "#{gem_name} not loaded from #{gemfile_path} because it was only in groups #{without_groups.join(", ")}. Skipping ..." } + nil + end else - raise "#{gem_name} not found in #{gemfile_path} or #{lockfile_path}" + log.info(software.name) { "#{gem_name} was not found in #{lockfile_path}. Skipping ..." } + nil end end end diff --git a/omnibus/files/chef/build-chef.rb b/omnibus/files/chef/build-chef.rb index 21bd6c2250..dd2f76e049 100644 --- a/omnibus/files/chef/build-chef.rb +++ b/omnibus/files/chef/build-chef.rb @@ -9,12 +9,6 @@ module BuildChef include BuildChefGem def create_bundle_config(gemfile, without: without_groups, retries: nil, jobs: nil, frozen: nil) - if without - without = without.dup - # no_aix, no_windows groups - without << "no_#{Omnibus::Ohai["platform"]}" - end - bundle_config = File.expand_path("../.bundle/config", gemfile) block "Put build config into #{bundle_config}: #{ { without: without, retries: retries, jobs: jobs, frozen: frozen } }" do @@ -84,7 +78,7 @@ module BuildChef # Emit blank line to separate different tasks log.info(log_key) { "" } log.info(log_key) { "Properly installing git or path sourced gem #{gem_path} using rake install" } - shellout!("#{bundle_bin} exec #{rake_bin} install", env: chef_env, cwd: gem_path) + shellout!("#{bundle_bin} exec #{rake_bin} install --trace", env: chef_env, cwd: gem_path) end end end @@ -113,7 +107,7 @@ module BuildChef end # Find the installed chef gem by looking for lib/chef.rb - chef_gem = File.expand_path("../..", shellout!("#{gem_bin} which chef").stdout.chomp) + chef_gem = File.expand_path("../..", shellout!("#{gem_bin} which chef", env: chef_env).stdout.chomp) # Figure out the path to gemfile_util from there gemfile_util = Pathname.new(File.join(chef_gem, "tasks", "gemfile_util")) gemfile_util = gemfile_util.relative_path_from(Pathname.new(shared_gemfile).dirname) @@ -136,7 +130,10 @@ module BuildChef bundle "lock", env: shared_gemfile_env # Freeze the location's Gemfile.lock. - create_bundle_config(shared_gemfile, frozen: true) + # TODO Windows cannot be frozen, because Bundler doesn't understand platform-specific + # versions. However, on Windows we have explicit version pins for most things, so + # we will *probably* get the exact versions of everything we want. + create_bundle_config(shared_gemfile, frozen: !windows?) # Clear the now-unnecessary git caches, cached gems, and git-checked-out gems block "Delete bundler git cache and git installs" do diff --git a/pedant.gemfile b/pedant.gemfile deleted file mode 100644 index 3302bccfe1..0000000000 --- a/pedant.gemfile +++ /dev/null @@ -1,25 +0,0 @@ -source "https://rubygems.org" -gemspec :name => "chef" - -# TODO figure out how to grab this stuff from the main Gemfile -gem "activesupport", "< 4.0.0", :group => :compat_testing, :platform => "ruby" - -# We are pinning chef-zero to 4.2.x until ChefFS can deal -# with V1 api calls or chef-zero supports both v0 and v1 -gem "chef-zero", "~> 4.2.3" - -group(:docgen) do - gem "tomlrb" - gem "yard" -end - -group(:development, :test) do - gem "simplecov" - gem 'rack', "~> 1.5.1" - - gem 'ruby-shadow', :platforms => :ruby unless RUBY_PLATFORM.downcase.match(/(aix|cygwin)/) -end - -# If you want to load debugging tools into the bundle exec sandbox, -# add these additional dependencies into chef/Gemfile.local -eval(IO.read(__FILE__ + '.local'), binding) if File.exists?(__FILE__ + '.local') diff --git a/tasks/bin/bundle-platform.bat b/tasks/bin/bundle-platform.bat new file mode 100644 index 0000000000..d193eb0c05 --- /dev/null +++ b/tasks/bin/bundle-platform.bat @@ -0,0 +1,2 @@ +@ECHO OFF +ruby "%~dpn0" %* diff --git a/tasks/bin/create-override-gemfile b/tasks/bin/create-override-gemfile new file mode 100755 index 0000000000..b67da025d2 --- /dev/null +++ b/tasks/bin/create-override-gemfile @@ -0,0 +1,110 @@ +#!/usr/bin/env ruby + +require "rubygems" +require "bundler" + +Bundler.with_clean_env do + require_relative "../gemfile_util" + + options = {} + opts = OptionParser.new do |opts| + opts.banner = "Usage: create-override-gemfile [OPTIONS]" + + opts.on("--gemfile GEMFILE", "The Gemfile to read (default: Gemfile).") { |path| options[:gemfile_path] = path } + opts.on("--lockfile GEMFILE", "The lockfile to read (default: <gemfile>.lock).") { |path| options[:lockfile_path] = path } + + opts.on("--group GROUP", "Groups to include (whitelist).") do |group| + options[:groups] ||= [] + options[:groups] << group.to_sym + end + + opts.on("--without GROUP", "Groups to exclude.") do |group| + options[:without_groups] ||= [] + options[:without_groups] << group.to_sym + end + + opts.on("--gem GEM", "Gems to include regardless of groups.") do |name| + options[:gems] ||= [] + options[:gems] << name + end + + opts.on("--relative-to PATH", "A path to prepend to any relative paths in the Gemfile.") do |path| + unless Pathname.new(path).absolute? + puts opts + raise "--relative-to #{path} was not an absolute path!" + end + options[:relative_to] = path + end + + opts.on("--[no-]copy-groups", "Whether to copy groups over from the original Gemfile or not (default: false).") { |val| options[:copy_groups] = val } + + opts.on("--[no-]override", "Whether to emit override: true on each gem line (default: false).") { |val| options[:override] = val } + + opts.on("-h", "--help", "Print this message.") do + puts opts + exit(0) + end + end + + args = opts.parse(ARGV) + + if args.size > 0 + puts opts + raise "Invalid arguments #{args}" + end + + def create_override_gemfile(gemfile_path: "Gemfile", lockfile_path: "#{gemfile_path}.lock", groups: nil, without_groups: nil, gems: [], copy_groups: false, relative_to: ".", override: false) + relative_to = Pathname.new(relative_to).realpath + # Select the gems we want + bundle = GemfileUtil::Bundle.parse(gemfile_path, lockfile_path) + gems_to_include = bundle.select_gems(groups: groups, without_groups: without_groups) + gems.each do |name| + raise "Requested gem #{name} is not in #{gemfile_path}.lock!" if !bundle.gems[name] + gems_to_include[name] ||= bundle.gems[name] + gems_to_include[name][:dependencies].each do |dep| + gems_to_include[name] ||= bundle.gems[dep] + end + end + + # Add the gems to the Gemfile + gem_root = Pathname.new(gemfile_path).dirname.realpath + gems_to_include.sort_by { |name, options| options[:declared_groups].empty? ? 1 : 0 }.each do |name, options| + comment = nil + options = options.dup + version = options.delete(:version) + if copy_groups + # Some dependencies have no groups (are not in the Gemfile--just runtime + # dependencies). If we actually record that they have no groups, they + # will *always* be installed (or perhaps never). We only want them to + # install if their other deps do, so we mark them with the groups of the + # things that brought them in (the gems that depended on them). To do + # this, we just leave :groups intact. + if options[:declared_groups].empty? + options.delete(:declared_groups) + comment = " # Transitive dependency, not actually in original Gemfile" + else + # For other things, we want to copy the actual declared_groups--the + # ones that were in the Gemfile. We want the same --with and --without + # options to include and exclude them as worked with the original + # Gemfile. + options[:groups] = options.delete(:declared_groups) + end + else + options.delete(:groups) + options.delete(:declared_groups) + end + options.delete(:dependencies) + options.delete(:development_dependencies) + options[:override] = true if override + options[:path] = Pathname.new(options[:path]).expand_path(gem_root).relative_path_from(relative_to).to_s if options[:path] + line = "gem #{name.inspect}, #{version.inspect}" + options.each do |name, value| + line << ", #{name}: #{value.inspect}" + end + line << comment if comment + puts line + end + end + + create_override_gemfile(options) +end diff --git a/tasks/bin/run_external_test b/tasks/bin/run_external_test index f1cefb9138..74f76d3229 100755 --- a/tasks/bin/run_external_test +++ b/tasks/bin/run_external_test @@ -20,7 +20,7 @@ TEST_GEMFILE=$TEST_GEM_ROOT/Gemfile MODIFIED_TEST_GEMFILE=$TEST_GEMFILE.externaltest cat <<EOM > $MODIFIED_TEST_GEMFILE require_relative "$PROJECT_ROOT/tasks/gemfile_util" -GemfileUtil.include_locked_gemfile(self, "$PROJECT_ROOT/Gemfile", groups: [:default], gems: ["$TEST_GEM"] + "$TEST_WITH_GEMS".split(/\s+/)) +GemfileUtil.include_locked_gemfile(self, "$PROJECT_ROOT/Gemfile", gems: ["$TEST_GEM"] + "$TEST_WITH_GEMS".split(/\s+/)) $TEST_GEM_OVERRIDES EOM cat $TEST_GEMFILE >> $MODIFIED_TEST_GEMFILE @@ -38,17 +38,10 @@ export BUNDLE_IGNORE_CONFIG=true # Use the top level bundle cache so we don't have to reinstall their packages if [ -n "$PROJECT_BUNDLE_PATH" ]; then export BUNDLE_PATH=$PROJECT_BUNDLE_PATH - export BUNDLE_DISABLE_SHARED_GEMS=1 - export BUNDLE_NO_PRUNE=true fi export BUNDLE_FROZEN= bundle install export BUNDLE_FROZEN=true bundle config - -# Iterate through the remaining arguments as commands -while test ${#} -gt 0; do - bundle exec $1 - shift -done +bundle exec $@ diff --git a/tasks/bundle.rb b/tasks/bundle.rb index b0cec5f580..349f83124c 100644 --- a/tasks/bundle.rb +++ b/tasks/bundle.rb @@ -17,6 +17,7 @@ require_relative "bundle_util" require_relative "../version_policy" +require "fileutils" desc "Tasks to work with the main Gemfile and Gemfile.<platform>" namespace :bundle do @@ -35,7 +36,9 @@ namespace :bundle do puts "-------------------------------------------------------------------" puts "Updating Gemfile.#{platform}.lock ..." puts "-------------------------------------------------------------------" - bundle "lock", gemfile: "Gemfile.#{platform}", platform: platform, delete_gemfile_lock: true + puts "Copy Gemfile.lock to Gemfile.#{platform}.lock ..." + FileUtils.cp(File.join(project_root, "Gemfile.lock"), File.join(project_root, "Gemfile.#{platform}.lock")) + bundle "lock", gemfile: "Gemfile.#{platform}", platform: platform end end end @@ -55,6 +58,8 @@ namespace :bundle do puts "-------------------------------------------------------------------" puts "Updating Gemfile.#{platform}.lock (conservatively) ..." puts "-------------------------------------------------------------------" + puts "Copy Gemfile.lock to Gemfile.#{platform}.lock ..." + FileUtils.cp(File.join(project_root, "Gemfile.lock"), File.join(project_root, "Gemfile.#{platform}.lock")) bundle "lock", gemfile: "Gemfile.#{platform}", platform: platform end end diff --git a/tasks/bundle_util.rb b/tasks/bundle_util.rb index 91ffa1f317..ee75610cac 100644 --- a/tasks/bundle_util.rb +++ b/tasks/bundle_util.rb @@ -1,3 +1,4 @@ +require "bundler" require "shellwords" module BundleUtil @@ -56,7 +57,7 @@ module BundleUtil # Run the bundle command ruby_platforms = platform ? PLATFORMS[platform].join(" ") : "ruby" - cmd = Shellwords.join([bundle_platform, ruby_platforms, *args]) + cmd = Shellwords.join([Gem.ruby, "-S", bundle_platform, ruby_platforms, *args]) puts "#{prefix}#{Shellwords.join(["bundle", *args])}#{platform ? " for #{platform} platform" : ""}:" with_gemfile(gemfile) do puts "#{prefix}BUNDLE_GEMFILE=#{gemfile}" diff --git a/tasks/dependencies.rb b/tasks/dependencies.rb index b2eabffb09..0fe3907ce1 100644 --- a/tasks/dependencies.rb +++ b/tasks/dependencies.rb @@ -29,7 +29,6 @@ namespace :dependencies do dependencies:update_omnibus_gemfile_lock dependencies:update_acceptance_gemfile_lock dependencies:update_kitchen_tests_gemfile_lock - dependencies:update_omnibus_berksfile_lock dependencies:update_kitchen_tests_berksfile_lock } @@ -81,7 +80,6 @@ namespace :dependencies do end gemfile_lock_task :update_omnibus_gemfile_lock, dirs: %w{omnibus} - berksfile_lock_task :update_omnibus_berksfile_lock, dirs: %w{omnibus} gemfile_lock_task :update_acceptance_gemfile_lock, dirs: %w{ acceptance acceptance/fips/test/integration/fips/serverspec diff --git a/tasks/gemfile_util.rb b/tasks/gemfile_util.rb index 96dfcd78a2..62d8cfdf0b 100644 --- a/tasks/gemfile_util.rb +++ b/tasks/gemfile_util.rb @@ -1,4 +1,6 @@ +require "rubygems" require "bundler" +require "shellwords" require "set" module GemfileUtil @@ -7,212 +9,376 @@ module GemfileUtil # gem statement about the same gem in the Gemfile. # def gem(name, *args) - Bundler.ui.debug "gem #{name}, #{args.join(", ")}" - current_dep = dependencies.find { |dep| dep.name == name } - - # Set path to absolute in case this is an included Gemfile in bundler 1.11.2 and below options = args[-1].is_a?(Hash) ? args[-1] : {} - if options[:path] - # path sourced gems are assumed to be overrides. - options[:override] = true - # options[:path] = File.expand_path(options[:path], Bundler.default_gemfile.dirname) - end - # Handle override - if options[:override] - override = true - options.delete(:override) - if current_dep - dependencies.delete(current_dep) - end - else - # If an override gem already exists, and we're not an override gem, - # ignore this gem in favor of the override (but warn if they don't match) - if overridden_gems.include?(name) - args.pop if args[-1].is_a?(Hash) - version = args || [">=0"] - desired_dep = Bundler::Dependency.new(name, version, options.dup) - if desired_dep =~ current_dep - Bundler.ui.debug "Replaced Gemfile dependency #{desired_dep} (#{desired_dep.source}) with override gem #{current_dep} (#{current_dep.source})" - else - Bundler.ui.warn "Replaced Gemfile dependency #{desired_dep} (#{desired_dep.source}) with incompatible override gem #{current_dep} (#{current_dep.source})" - end + + # Unless we're finished with everything, ignore gems that are being overridden + unless overridden_gems == :finished + # If it's a path or override gem, it overrides whatever else is there. + if options[:path] || options[:override] + options.delete(:override) + warn_if_replacing(name, overridden_gems[name], args) + overridden_gems[name] = args + return + + # If there's an override gem, and we're *not* an override gem, don't do anything + elsif overridden_gems[name] + warn_if_replacing(name, args, overridden_gems[name]) return end end - # Add the gem normally + # Otherwise, add the gem normally super + rescue + puts $!.backtrace + raise + end - overridden_gems << name if override + def overridden_gems + @overridden_gems ||= {} + end - # Emit a warning if we're replacing a dep that doesn't match - if current_dep && override - added_dep = dependencies.find { |dep| dep.name == name } - if added_dep =~ current_dep - Bundler.ui.debug "Replaced Gemfile dependency #{current_dep} (#{current_dep.source}) with override gem #{added_dep} (#{added_dep.source})" - else - Bundler.ui.warn "Replaced Gemfile dependency #{current_dep} (#{current_dep.source}) with incompatible override gem #{added_dep} (#{added_dep.source})" - end - end + # + # Just before we finish the Gemfile, finish up the override gems + # + def to_definition(*args) + complete_overrides + super end - def overridden_gems - @overridden_gems ||= Set.new + def complete_overrides + to_override = overridden_gems + unless to_override == :finished + @overridden_gems = :finished + to_override.each do |name, args| + gem name, *args + end + end end # # Include all gems in the locked gemfile. # - # @param gemfile Path to the Gemfile to load (relative to your Gemfile) + # @param gemfile_path Path to the Gemfile to load (relative to your Gemfile) + # @param lockfile_path Path to the Gemfile to load (relative to your Gemfile). + # Defaults to <gemfile_path>.lock. # @param groups A list of groups to include (whitelist). If not passed (or set - # to nil), all gems will be selected. + # to nil), all gems will be selected. # @param without_groups A list of groups to ignore. Gems will be excluded from - # the results if all groups they belong to are ignored. - # This matches bundler's `without` behavior. + # the results if all groups they belong to are ignored. This matches + # bundler's `without` behavior. # @param gems A list of gems to include above and beyond the given groups. - # Gems in this list must be explicitly included in the Gemfile - # with a `gem "gem_name", ...` line or they will be silently - # ignored. + # Gems in this list must be explicitly included in the Gemfile + # with a `gem "gem_name", ...` line or they will be silently + # ignored. + # @param copy_groups Whether to copy the groups over from the old lockfile to + # the new. Use this when the new lockfile has the same convention for + # groups as the old. Defaults to `false`. # - def include_locked_gemfile(gemfile, groups: nil, without_groups: nil, gems: []) - gemfile = File.expand_path(gemfile, Bundler.default_gemfile.dirname) - gems = Set.new(gems) + GemfileUtil.select_gems(gemfile, groups: nil, without_groups: nil) - specs = GemfileUtil.locked_gems("#{gemfile}.lock", gems) - specs.each do |name, version: nil, **options| - options = options.merge(override: true) - Bundler.ui.debug("Adding gem #{name}, #{version}, #{options} from #{gemfile}") - gem name, version, options + def include_locked_gemfile(gemfile_path, lockfile_path = "#{gemfile_path}.lock", groups: nil, without_groups: nil, gems: [], copy_groups: false) + # Parse the desired lockfile + gemfile_path = Pathname.new(gemfile_path).expand_path(Bundler.default_gemfile.dirname).realpath + lockfile_path = Pathname.new(lockfile_path).expand_path(Bundler.default_gemfile.dirname).realpath + + # Calculate relative_to + relative_to = Bundler.default_gemfile.dirname.realpath + + # Call out to create-override-gemfile to read the Gemfile+Gemfile.lock (bundler does not work well if you do two things in one process) + create_override_gemfile_bin = File.expand_path("../bin/create-override-gemfile", __FILE__) + arguments = [ + "--gemfile", gemfile_path, + "--lockfile", lockfile_path, + "--override" + ] + arguments += [ "--relative-to", relative_to ] if relative_to != "." + arguments += Array(groups).flat_map { |group| [ "--group", group ] } + arguments += Array(without_groups).flat_map { |without| [ "--without", without ] } + arguments += Array(gems).flat_map { |name| [ "--gem", name ] } + arguments << "--copy-groups" if copy_groups + cmd = Shellwords.join([ Gem.ruby, "-S", create_override_gemfile_bin, *arguments ]) + output = nil + Bundler.ui.info("> #{cmd}") + Bundler.with_clean_env do + output = `#{cmd}` end - rescue - puts "ERROR: #{$!}" - puts $!.backtrace - raise + instance_eval(output, cmd, 1) end # # Include all gems in the locked gemfile. # # @param current_gemfile The Gemfile you are currently loading (`self`). - # @param gemfile Path to the Gemfile to load (relative to your Gemfile) + # @param gemfile_path Path to the Gemfile to load (relative to your Gemfile) + # @param lockfile_path Path to the Gemfile to load (relative to your Gemfile). + # Defaults to <gemfile_path>.lock. # @param groups A list of groups to include (whitelist). If not passed (or set - # to nil), all gems will be selected. + # to nil), all gems will be selected. # @param without_groups A list of groups to ignore. Gems will be excluded from - # the results if all groups they belong to are ignored. - # This matches bundler's `without` behavior. + # the results if all groups they belong to are ignored. This matches + # bundler's `without` behavior. # @param gems A list of gems to include above and beyond the given groups. - # Gems in this list must be explicitly included in the Gemfile - # with a `gem "gem_name", ...` line or they will be silently - # ignored. + # Gems in this list must be explicitly included in the Gemfile + # with a `gem "gem_name", ...` line or they will be silently + # ignored. + # @param copy_groups Whether to copy the groups over from the old lockfile to + # the new. Use this when the new lockfile has the same convention for + # groups as the old. Defaults to `false`. # - def self.include_locked_gemfile(current_gemfile, gemfile, groups: nil, without_groups: nil, gems: []) + def self.include_locked_gemfile(current_gemfile, gemfile_path, lockfile_path = "#{gemfile_path}.lock", groups: nil, without_groups: nil, gems: [], copy_groups: false) current_gemfile.instance_eval do extend GemfileUtil - include_locked_gemfile(gemfile, groups: groups, without_groups: without_groups, gems: []) + include_locked_gemfile(gemfile_path, lockfile_path, groups: groups, without_groups: without_groups, gems: gems, copy_groups: copy_groups) end end + def warn_if_replacing(name, old_args, new_args) + return if !old_args || !new_args + if args_to_dep(name, *old_args) =~ args_to_dep(name, *new_args) + Bundler.ui.debug "Replaced Gemfile dependency #{name} (#{old_args}) with (#{new_args})" + else + Bundler.ui.warn "Replaced Gemfile dependency #{name} (#{old_args}) with (#{new_args})" + end + end + + def args_to_dep(name, *version, **options) + version = [">= 0"] if version.empty? + Bundler::Dependency.new(name, version, options) + end + # - # Select the desired gems, sans dependencies, from the gemfile. - # - # @param gemfile Path to the Gemfile to load - # @param groups A list of groups to include (whitelist). If not passed (or set - # to nil), all gems will be selected. - # @param without_groups A list of groups to ignore. Gems will be excluded from - # the results if all groups they belong to are ignored. - # This matches bundler's `without` behavior. + # Reads a bundle, including a gemfile and lockfile. # - # @return An array of strings with the names of the given gems. + # Does no validation, does not update the lockfile or its gems in any way. # - def self.select_gems(gemfile, groups: nil, without_groups: nil) - Bundler.with_clean_env do - # Set BUNDLE_GEMFILE to the new gemfile temporarily so all bundler's things work - # This works around some issues in bundler 1.11.2. - ENV["BUNDLE_GEMFILE"] = gemfile - - parsed_gemfile = Bundler::Dsl.new - parsed_gemfile.eval_gemfile(gemfile) - deps = parsed_gemfile.dependencies.select do |dep| - dep_groups = dep.groups + class Bundle + # + # Parse the given gemfile/lockfile pair. + # + # @return [Bundle] The parsed bundle. + # + def self.parse(gemfile_path, lockfile_path = "#{gemfile_path}.lock") + result = new(gemfile_path, lockfile_path) + result.gems + result + end + + # + # Create a new Bundle to parse the given gemfile/lockfile pair. + # + def initialize(gemfile_path, lockfile_path = "#{gemfile_path}.lock") + @gemfile_path = gemfile_path + @lockfile_path = lockfile_path + end + + # + # The path to the Gemfile + # + attr_reader :gemfile_path + + # + # The path to the Lockfile + # + attr_reader :lockfile_path + + # + # The list of gems. + # + # @return [Hash<String, Hash>] The resulting gems, where key = gem_name, and the + # hash has: + # - version: version of the gem. + # - source info (:source/:git/:ref/:path) from the lockfile + # - dependencies: A list of gem names this gem has a runtime + # dependency on. Dependencies are transitive: if A depends on B, + # and B depends on C, then A has C in its :dependencies list. + # - development_dependencies: - A list of gem names this gem has a + # development dependency on. Dependencies are transitive: if A + # depends on B, and B depends on C, then A has C in its + # :development_dependencies list. development dependencies *include* + # runtime dependencies. + # - groups: The list of groups (symbols) this gem is in. Groups + # are transitive: if A has a runtime dependency on B, and A is + # in group X, then B is also in group X. + # - declared_groups: The list of groups (symbols) this gem was + # declared in the Gemfile. + # + def gems + @gems ||= begin + gems = locks.dup + gems.each do |name, g| + if gem_declarations.has_key?(name) + g[:declared_groups] = gem_declarations[name][:groups] + else + g[:declared_groups] = [] + end + g[:groups] = g[:declared_groups].dup + end + # Transitivize groups (since dependencies are already transitive, this is easy) + gems.each do |name, g| + g[:dependencies].each do |dep| + gems[dep][:groups] |= gems[name][:declared_groups].dup + end + end + gems + end + end + + # + # Get the gems (and their deps) in the given group. + # + # @param groups A list of groups to include (whitelist). If not passed (or set + # to nil), all gems will be selected. + # @param without_groups A list of groups to ignore. Gems will be excluded from + # the results if all groups they belong to are ignored. + # This matches bundler's `without` behavior. + # @param gems A list of gems to include regardless of what groups are included. + # + # @return Hash[String, Hash] The resulting gems, where key = gem_name, and the + # hash has: + # - version: version of the gem. + # - source info (:source/:git/:ref/:path) from the lockfile + # - dependencies: A list of gem names this gem has a runtime + # dependency on. Dependencies are transitive: if A depends on B, + # and B depends on C, then A has C in its :dependencies list. + # - development_dependencies: - A list of gem names this gem has a + # development dependency on. Dependencies are transitive: if A + # depends on B, and B depends on C, then A has C in its + # :development_dependencies list. development dependencies + # *include* runtime dependencies. + # - groups: The list of groups (symbols) this gem is in. Groups + # are transitive: if A has a runtime dependency on B, and A is + # in group X, then B is also in group X. + # - declared_groups: The list of groups (symbols) this gem was + # declared in the Gemfile. + # + def select_gems(groups: nil, without_groups: nil) + # First, select the gems that match + result = {} + gems.each do |name, g| + dep_groups = g[:declared_groups] - [ :only_a_runtime_dependency_of_other_gems ] dep_groups = dep_groups & groups if groups dep_groups = dep_groups - without_groups if without_groups - dep_groups.any? + if dep_groups.any? + result[name] ||= g + g[:dependencies].each do |dep| + result[dep] ||= gems[dep] + end + end end - deps.map { |dep| dep.name } + result end - end - # - # Get all gems in the locked gemfile that start from the given gem set. - # - # @param lockfile Path to the Gemfile to load - # @param groups A list of groups to include (whitelist). If not passed (or set - # to nil), all gems will be selected. - # @param without_groups A list of groups to ignore. Gems will be excluded from - # the results if all groups they belong to are ignored. - # This matches bundler's `without` behavior. - # @param gems A list of gems to include above and beyond the given groups. - # Gems in this list must be explicitly included in the Gemfile - # with a `gem "gem_name", ...` line or they will be silently - # ignored. - # @param include_development_deps Whether to include development dependencies - # or runtime only. - # - # @return Hash[String, Hash] A hash from gem_name -> { version: <version>, source: <source>, git: <git>, path: <path>, ref: <ref> } - # - def self.locked_gems(lockfile, gems, include_development_deps: false) - # Grab all the specs from the lockfile - parsed_lockfile = Bundler::LockfileParser.new(IO.read(lockfile)) - specs = {} - parsed_lockfile.specs.each { |s| specs[s.name] = s } - - # Select the desired gems, as well as their dependencies - to_process = Array(gems) - results = {} - while to_process.any? - gem_name = to_process.pop - next if gem_name == "bundler" # can't be bundled. Messes things up. Stop it. - # Only process each gem once - unless results.has_key?(gem_name) - spec = specs[gem_name] - unless spec - raise "Gem #{gem_name.inspect} was requested but was not in #{lockfile}! Gems in lockfile: #{specs.keys}" + # + # Get all locks from the given lockfile. + # + # @return Hash[String, Hash] The resulting gems, where key = gem_name, and the + # hash has: + # - version: version of the gem. + # - source info (:source/:git/:ref/:path) + # - dependencies: A list of gem names this gem has a runtime + # dependency on. Dependencies are transitive: if A depends on B, + # and B depends on C, then A has C in its :dependencies list. + # - development_dependencies: - A list of gem names this gem has a + # development dependency on. Dependencies are transitive: if A + # depends on B, and B depends on C, then A has C in its + # :development_dependencies list. development dependencies *include* + # runtime dependencies. + # + def locks + @locks ||= begin + # Grab all the specs from the lockfile + locks = {} + parsed_lockfile = Bundler::LockfileParser.new(IO.read(lockfile_path)) + parsed_lockfile.specs.each do |spec| + # Never include bundler, it can't be bundled and doesn't put itself in + # the lockfile correctly anyway + next if spec.name == "bundler" + lock = lock_source_metadata(spec) + lock[:version] = spec.version.to_s + runtime = spec.dependencies.select { |dep| dep.type == :runtime } + lock[:dependencies] = Set.new(runtime.map { |dep| dep.name }) + lock[:development_dependencies] = Set.new(spec.dependencies.map { |dep| dep.name }) + lock[:dependencies].delete("bundler") + lock[:development_dependencies].delete("bundler") + locks[spec.name] = lock + end + + # Transitivize the deps. + locks.each do |name, lock| + lock[:dependencies] = transitive_dependencies(locks, name, :dependencies) + lock[:development_dependencies] = transitive_dependencies(locks, name, :development_dependencies) end - results[gem_name] = gem_metadata(spec, lockfile) - spec.dependencies.each do |dep| - if dep.type == :runtime || include_development_deps - to_process << dep.name + + locks + end + end + + # + # Get all desired gems, sans dependencies, from the gemfile. + # + # @param gemfile Path to the Gemfile to load + # + # @return Hash<String, Hash> An array of hashes where key = gem name and value + # has :groups (an array of symbols representing the groups the gem + # is in). :groups are not transitive, since we don't know the + # dependency tree yet. + # + def gem_declarations + @gem_declarations ||= begin + Bundler.with_clean_env do + # Set BUNDLE_GEMFILE to the new gemfile temporarily so all bundler's things work + # This works around some issues in bundler 1.11.2. + ENV["BUNDLE_GEMFILE"] = gemfile_path + + parsed_gemfile = Bundler::Dsl.new + parsed_gemfile.eval_gemfile(gemfile_path) + parsed_gemfile.complete_overrides if parsed_gemfile.respond_to?(:complete_overrides) + + result = {} + parsed_gemfile.dependencies.each do |dep| + groups = dep.groups.empty? ? [:default] : dep.groups + result[dep.name] = { groups: groups, platforms: dep.platforms } end + result end end end - results - end + private - private + # + # Given a bunch of locks (name -> { dependencies: [name,name] }) and a + # dependency name, add its dependencies to the result transitively. + # + def transitive_dependencies(locks, name, dep_key, result = Set.new) + locks[name][dep_key].each do |dep| + # Only ever add a dep once, so we don't infinitely recurse + if result.add?(dep) + transitive_dependencies(locks, dep, dep_key, result) + end + end + result + end - # - # Get metadata for the given Bundler spec (coming from a lockfile). - # - # @return Hash { version: <version>, git: <git>, path: <path>, source: <source>, ref: <ref> } - # - def self.gem_metadata(spec, lockfile) - # Copy source information from included Gemfile - result = {} - case spec.source - when Bundler::Source::Rubygems - result[:source] = spec.source.remotes.first.to_s - result[:version] = spec.version.to_s - when Bundler::Source::Git - result[:git] = spec.source.uri.to_s - result[:ref] = spec.source.revision - when Bundler::Source::Path - # Path is relative to the lockfile (if it's relative at all) - result[:path] = File.expand_path(spec.source.path.to_s, File.dirname(lockfile)) - else - raise "Unknown source #{spec.source} for gem #{spec.name}" + # + # Get source and version metadata for the given Bundler spec (coming from a lockfile). + # + # @return Hash { version: <version>, git: <git>, path: <path>, source: <source>, ref: <ref> } + # + def lock_source_metadata(spec) + # Copy source information from included Gemfile + result = {} + case spec.source + when Bundler::Source::Rubygems + result[:source] = spec.source.remotes.first.to_s + when Bundler::Source::Git + result[:git] = spec.source.uri.to_s + result[:ref] = spec.source.revision + when Bundler::Source::Path + result[:path] = spec.source.path.to_s + else + raise "Unknown source #{spec.source} for gem #{spec.name}" + end + result end - result end - end |