8d0d2219 by Jeff Balicki

deploy

Signed-off-by: Jeff <jeff@gotenzing.com>
1 parent 23cbefe0
Showing 1000 changed files with 4197 additions and 13 deletions

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

1 ---
2 BUNDLE_PATH: "vendor/bundle"
File mode changed
1 source 'https://rubygems.org' 1 source 'https://rubygems.org'
2 2
3 gem 'capistrano', '~> 3.4.0' 3 gem "capistrano", "~> 3.10"
4 gem 'capistrano-composer' 4 gem 'capistrano-composer'
......
1 GEM 1 GEM
2 remote: https://rubygems.org/ 2 remote: https://rubygems.org/
3 specs: 3 specs:
4 capistrano (3.4.0) 4 airbrussh (1.4.1)
5 sshkit (>= 1.6.1, != 1.7.0)
6 capistrano (3.17.1)
7 airbrussh (>= 1.0.0)
5 i18n 8 i18n
6 rake (>= 10.0.0) 9 rake (>= 10.0.0)
7 sshkit (~> 1.3) 10 sshkit (>= 1.9.0)
8 capistrano-composer (0.0.6) 11 capistrano-composer (0.0.6)
9 capistrano (>= 3.0.0.pre) 12 capistrano (>= 3.0.0.pre)
10 i18n (0.7.0) 13 concurrent-ruby (1.1.10)
14 i18n (1.12.0)
15 concurrent-ruby (~> 1.0)
11 net-scp (1.2.1) 16 net-scp (1.2.1)
12 net-ssh (>= 2.6.5) 17 net-ssh (>= 2.6.5)
13 net-ssh (3.0.2) 18 net-ssh (7.0.1)
14 rake (10.5.0) 19 rake (13.0.6)
15 sshkit (1.8.1) 20 sshkit (1.21.2)
16 net-scp (>= 1.1.2) 21 net-scp (>= 1.1.2)
17 net-ssh (>= 2.8.0) 22 net-ssh (>= 2.8.0)
18 23
19 PLATFORMS 24 PLATFORMS
20 ruby 25 x86_64-darwin-21
26 x86_64-darwin-22
21 27
22 DEPENDENCIES 28 DEPENDENCIES
23 capistrano (~> 3.4.0) 29 capistrano (~> 3.10)
24 capistrano-composer 30 capistrano-composer
31
32 BUNDLED WITH
33 2.3.21
......
1 set :application, 'tenzing-web-2023' 1 set :application, 'tenzing-web-2019-staging'
2 set :repo_url, 'git@git.gotenzing.com:tenzing/Tenzing-Web-2023.git' 2 set :repo_url, 'git@git.gotenzing.com:tenzing/Tenzing-Web-2023.git'
3 3
4 # Branch options 4 # Branch options
...@@ -33,7 +33,7 @@ namespace :deploy do ...@@ -33,7 +33,7 @@ namespace :deploy do
33 desc 'Install composer packages' 33 desc 'Install composer packages'
34 task :install_composer_packages do 34 task :install_composer_packages do
35 on roles(:web), in: :sequence, wait: 5 do 35 on roles(:web), in: :sequence, wait: 5 do
36 execute("cd '#{release_path}'; composer install --no-dev --prefer-dist --no-interaction --quiet --optimize-autoloader") 36 execute("cd '#{release_path}'; composer update --no-dev --prefer-dist --no-interaction --quiet --optimize-autoloader")
37 end 37 end
38 end 38 end
39 end 39 end
......
1 set :application, 'tenzing-web-2023-staging' 1 set :application, 'tenzing-web-2019-staging'
2 set :stage, :staging 2 set :stage, :staging
3 set :branch, :staging 3 set :branch, :main
4 4
5 # Simple Role Syntax 5 # Simple Role Syntax
6 # ================== 6 # ==================
......
1 #!/usr/bin/env ruby
2 #
3 # This file was generated by RubyGems.
4 #
5 # The application 'bundler' is installed as part of a gem, and
6 # this file is here to facilitate running it.
7 #
8
9 require 'rubygems'
10
11 version = ">= 0.a"
12
13 str = ARGV.first
14 if str
15 str = str.b[/\A_(.*)_\z/, 1]
16 if str and Gem::Version.correct?(str)
17 version = str
18 ENV['BUNDLER_VERSION'] = str
19
20 ARGV.shift
21 end
22 end
23
24 if Gem.respond_to?(:activate_bin_path)
25 load Gem.activate_bin_path('bundler', 'bundle', version)
26 else
27 gem "bundler", version
28 load Gem.bin_path("bundler", "bundle", version)
29 end
1 #!/usr/bin/env ruby
2 #
3 # This file was generated by RubyGems.
4 #
5 # The application 'bundler' is installed as part of a gem, and
6 # this file is here to facilitate running it.
7 #
8
9 require 'rubygems'
10
11 version = ">= 0.a"
12
13 str = ARGV.first
14 if str
15 str = str.b[/\A_(.*)_\z/, 1]
16 if str and Gem::Version.correct?(str)
17 version = str
18 ENV['BUNDLER_VERSION'] = str
19
20 ARGV.shift
21 end
22 end
23
24 if Gem.respond_to?(:activate_bin_path)
25 load Gem.activate_bin_path('bundler', 'bundler', version)
26 else
27 gem "bundler", version
28 load Gem.bin_path("bundler", "bundler", version)
29 end
1 #!/usr/bin/env ruby
2 #
3 # This file was generated by RubyGems.
4 #
5 # The application 'capistrano' is installed as part of a gem, and
6 # this file is here to facilitate running it.
7 #
8
9 require 'rubygems'
10
11 Gem.use_gemdeps
12
13 version = ">= 0.a"
14
15 str = ARGV.first
16 if str
17 str = str.b[/\A_(.*)_\z/, 1]
18 if str and Gem::Version.correct?(str)
19 version = str
20 ARGV.shift
21 end
22 end
23
24 if Gem.respond_to?(:activate_bin_path)
25 load Gem.activate_bin_path('capistrano', 'cap', version)
26 else
27 gem "capistrano", version
28 load Gem.bin_path("capistrano", "cap", version)
29 end
1 #!/usr/bin/env ruby
2 #
3 # This file was generated by RubyGems.
4 #
5 # The application 'capistrano' is installed as part of a gem, and
6 # this file is here to facilitate running it.
7 #
8
9 require 'rubygems'
10
11 Gem.use_gemdeps
12
13 version = ">= 0.a"
14
15 str = ARGV.first
16 if str
17 str = str.b[/\A_(.*)_\z/, 1]
18 if str and Gem::Version.correct?(str)
19 version = str
20 ARGV.shift
21 end
22 end
23
24 if Gem.respond_to?(:activate_bin_path)
25 load Gem.activate_bin_path('capistrano', 'capify', version)
26 else
27 gem "capistrano", version
28 load Gem.bin_path("capistrano", "capify", version)
29 end
1 #!/usr/bin/env ruby
2 #
3 # This file was generated by RubyGems.
4 #
5 # The application 'rake' is installed as part of a gem, and
6 # this file is here to facilitate running it.
7 #
8
9 require 'rubygems'
10
11 Gem.use_gemdeps
12
13 version = ">= 0.a"
14
15 str = ARGV.first
16 if str
17 str = str.b[/\A_(.*)_\z/, 1]
18 if str and Gem::Version.correct?(str)
19 version = str
20 ARGV.shift
21 end
22 end
23
24 if Gem.respond_to?(:activate_bin_path)
25 load Gem.activate_bin_path('rake', 'rake', version)
26 else
27 gem "rake", version
28 load Gem.bin_path("rake", "rake", version)
29 end
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
1 version: 2.1
2
3 executors:
4 ruby:
5 parameters:
6 version:
7 description: "Ruby version number"
8 default: "3.1"
9 type: string
10 docker:
11 - image: ruby:<< parameters.version >>
12
13 commands:
14 bundle_install:
15 description: Install Ruby dependencies with Bundler
16 parameters:
17 key:
18 description: "Cache key"
19 default: "3.1"
20 type: string
21 steps:
22 - restore_cache:
23 keys:
24 - bundle-v1-{{ arch }}-<< parameters.key >>
25 - run:
26 name: Install Ruby Dependencies
27 command: |
28 gem install bundler --conservative --no-document || \
29 gem install rubygems-update -v '<3' && update_rubygems && gem install bundler -v '<2' --no-document
30 bundle config --local path vendor/bundle
31 bundle check || (bundle install --jobs=4 --retry=3 && bundle clean)
32 - save_cache:
33 paths:
34 - ./vendor/bundle
35 key: bundle-v1-{{ arch }}-<< parameters.key >>-{{ checksum "Gemfile.lock" }}
36
37 jobs:
38 rubocop:
39 executor:
40 name: ruby
41 version: "2.7"
42 steps:
43 - checkout
44 - bundle_install:
45 key: "2.7"
46 - run: bundle exec rubocop
47 spec:
48 parameters:
49 ruby:
50 description: "Ruby version number"
51 default: "3.1"
52 type: string
53 executor:
54 name: ruby
55 version: << parameters.ruby >>
56 steps:
57 - checkout
58 - run: echo sshkit=master >> $BASH_ENV
59 - bundle_install:
60 key: << parameters.ruby >>
61 - run: bundle exec rake test
62 spec_legacy_ruby:
63 parameters:
64 ruby:
65 description: "Ruby version number"
66 default: "1.9"
67 type: string
68 sshkit:
69 description: "sshkit version number"
70 default: "1.6.1"
71 type: string
72 executor:
73 name: ruby
74 version: << parameters.ruby >>
75 steps:
76 - checkout
77 - run: |
78 echo "export sshkit=<< parameters.sshkit >>" >> $BASH_ENV
79 if [ "<< parameters.ruby >>" == "1.9" ]; then
80 echo "export RUBYOPT=-Ku" >> $BASH_ENV
81 fi
82 - bundle_install:
83 key: << parameters.ruby >>-<< parameters.sshkit >>
84 - run: bundle exec rake test
85
86 workflows:
87 version: 2
88 commit-workflow:
89 jobs:
90 - rubocop
91 - spec:
92 matrix: &matrix
93 parameters:
94 ruby:
95 - "2.4"
96 - "2.5"
97 - "2.6"
98 - "2.7"
99 - "3.0"
100 - "3.1"
101 - spec_legacy_ruby:
102 matrix: &legacy_ruby_matrix
103 parameters:
104 ruby:
105 - "1.9"
106 - "2.0"
107 - "2.1"
108 - "2.2"
109 - "2.3"
110 sshkit:
111 - "1.6.1"
112 - "1.7.1"
113 - master
114 exclude:
115 - ruby: "1.9"
116 sshkit: master
117 cron-workflow:
118 jobs:
119 - rubocop
120 - spec:
121 matrix:
122 <<: *matrix
123 - spec_legacy_ruby:
124 matrix:
125 <<: *legacy_ruby_matrix
126 triggers:
127 - schedule:
128 cron: "0 13 * * 6"
129 filters:
130 branches:
131 only:
132 - main
1 name-template: "$RESOLVED_VERSION"
2 tag-template: "v$RESOLVED_VERSION"
3 categories:
4 - title: "⚠️ Breaking Changes"
5 label: "⚠️ Breaking"
6 - title: " New Features"
7 label: " Feature"
8 - title: "🐛 Bug Fixes"
9 label: "🐛 Bug Fix"
10 - title: "📚 Documentation"
11 label: "📚 Docs"
12 - title: "🏠 Housekeeping"
13 label: "🏠 Housekeeping"
14 version-resolver:
15 minor:
16 labels:
17 - "⚠️ Breaking"
18 - " Feature"
19 default: patch
20 change-template: "- $TITLE (#$NUMBER) @$AUTHOR"
21 no-changes-template: "- No changes"
22 template: |
23 $CHANGES
24
25 **Full Changelog:** https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
1 name: Release Drafter
2 on:
3 push:
4 branches:
5 - main
6 jobs:
7 update_release_draft:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: release-drafter/release-drafter@v5
11 env:
12 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 /.bundle/
2 /.yardoc
3 /Gemfile.lock
4 /_yardoc/
5 /coverage/
6 /doc/
7 /log/
8 /pkg/
9 /spec/reports/
10 /tmp/
11 *.bundle
12 *.so
13 *.o
14 *.a
15 mkmf.log
1 AllCops:
2 DisplayCopNames: true
3 DisplayStyleGuide: true
4 TargetRubyVersion: 1.9
5 Exclude:
6 - "*.gemspec"
7 - "vendor/**/*"
8
9 Layout/EndOfLine:
10 Enabled: false
11
12 Layout/SpaceAroundEqualsInParameterDefault:
13 EnforcedStyle: no_space
14
15 Metrics/AbcSize:
16 Exclude:
17 - "test/**/*"
18
19 Metrics/MethodLength:
20 Exclude:
21 - "test/**/*"
22
23 Metrics/ClassLength:
24 Exclude:
25 - "test/**/*"
26
27 Style/BarePercentLiterals:
28 EnforcedStyle: percent_q
29
30 Style/ClassAndModuleChildren:
31 Enabled: false
32
33 Style/Documentation:
34 Enabled: false
35
36 Style/DoubleNegation:
37 Enabled: false
38
39 Style/Encoding:
40 Enabled: false
41
42 Style/HashSyntax:
43 EnforcedStyle: hash_rockets
44
45 Style/StringLiterals:
46 EnforcedStyle: double_quotes
47
48 Style/TrivialAccessors:
49 AllowPredicates: true
1 Release notes for this project are kept here: https://github.com/mattbrictson/airbrussh/releases
1 Contributor Code of Conduct
2
3 As contributors and maintainers of this project, we pledge to respect all
4 people who contribute through reporting issues, posting feature requests,
5 updating documentation, submitting pull requests or patches, and other
6 activities.
7
8 We are committed to making participation in this project a harassment-free
9 experience for everyone, regardless of level of experience, gender, gender
10 identity and expression, sexual orientation, disability, personal appearance,
11 body size, race, ethnicity, age, or religion.
12
13 Examples of unacceptable behavior by participants include the use of sexual
14 language or imagery, derogatory comments or personal attacks, trolling, public
15 or private harassment, insults, or other unprofessional conduct.
16
17 Project maintainers have the right and responsibility to remove, edit, or
18 reject comments, commits, code, wiki edits, issues, and other contributions
19 that are not aligned to this Code of Conduct. Project maintainers who do not
20 follow the Code of Conduct may be removed from the project team.
21
22 This code of conduct applies both within project spaces and in public spaces
23 when an individual is representing the project or its community.
24
25 Instances of abusive, harassing, or otherwise unacceptable behavior may be
26 reported by opening an issue or contacting one or more of the project
27 maintainers.
28
29 This Code of Conduct is adapted from the Contributor Covenant
30 (http://contributor-covenant.org), version 1.1.0, available at
31 http://contributor-covenant.org/version/1/1/0/
1 # Contributing to airbrussh
2
3 Have a feature idea, bug fix, or refactoring suggestion? Contributions are welcome!
4
5 ## Pull requests
6
7 1. Check [Issues][] to see if your contribution has already been discussed and/or implemented.
8 2. If not, open an issue to discuss your contribution. I won't accept all changes and do not want to waste your time.
9 3. Once you have the :thumbsup:, fork the repo, make your changes, and open a PR.
10
11 ## Coding guidelines
12
13 * This project has a coding style enforced by [RuboCop][]. Use hash rockets and double-quoted strings, and otherwise try to follow the [Ruby style guide][style].
14 * Writing tests is strongly encouraged! This project uses Minitest.
15
16 ## Getting started
17
18 Note that Bundler 1.10 is required for development. Run `gem update bundler` to get the latest version.
19
20 After checking out the airbrussh repo, run `bin/setup` to install dependencies. Run `rake` to execute airbrussh's tests and RuboCop checks.
21
22 Airbrussh is designed to work against multiple versions of SSHKit and Ruby. In order to test this, we use the environment variable `sshkit` in order to run the tests against a specific version. The combinations of sshkit and ruby we support are specified in [.circleci/config.yml](.circleci/config.yml).
23
24 A Guardfile is also present, so if you'd like to use Guard to do a TDD workflow, then:
25
26 1. Run `bundle install --with extras` to get the optional guard dependencies
27 2. Run `guard` to monitor the filesystem and automatically run tests as you work
28
29 [Issues]: https://github.com/mattbrictson/airbrussh/issues
30 [RuboCop]: https://github.com/bbatsov/rubocop
31 [style]: https://github.com/bbatsov/ruby-style-guide
1 source "https://rubygems.org"
2
3 # Specify your gem's dependencies in airbrussh.gemspec
4 gemspec
5
6 if RUBY_VERSION == "1.9.3"
7 # These gems need specific version for Ruby 1.9
8 gem "json", "~> 1.8"
9 gem "minitest", "~> 5.11.3"
10 gem "net-ssh", "~> 2.8"
11 gem "rake", "< 12.3"
12 gem "term-ansicolor", "~> 1.3.2"
13 gem "tins", "~> 1.6.0"
14 end
15
16 if RUBY_VERSION >= "2.5"
17 # These gems need at least Ruby 2.5
18 gem "coveralls_reborn", "~> 0.24.0"
19 end
20
21 if RUBY_VERSION >= "2.1"
22 # These gems need at least Ruby 2.1
23 gem "rubocop", "0.50.0"
24
25 # Optional development dependencies; requires bundler >= 1.10.
26 # Note that these gems assume a Ruby 2.2 environment. Install them using:
27 #
28 # bundle install --with extras
29 #
30 group :extras, :optional => true do
31 gem "guard", ">= 2.2.2"
32 gem "guard-minitest"
33 gem "rb-fsevent"
34 gem "terminal-notifier-guard"
35 end
36 end
37
38 if (sshkit_version = ENV["sshkit"])
39 requirement = begin
40 Gem::Dependency.new("sshkit", sshkit_version).requirement
41 rescue ArgumentError
42 user, branch =
43 if sshkit_version.include?("#")
44 sshkit_version.split("#")
45 else
46 ["capistrano", sshkit_version]
47 end
48 { :git => "https://github.com/#{user}/sshkit.git", :branch => branch }
49 end
50 gem "sshkit", requirement
51 end
1 guard :minitest do
2 # with Minitest::Unit
3 watch(%r{^test/(.*)\/?(.*)_test\.rb$})
4 watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
5 watch(%r{^test/minitest_helper\.rb$}) { "test" }
6 end
1 The MIT License (MIT)
2
3 Copyright (c) 2020 Matt Brictson
4
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included in
13 all copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 THE SOFTWARE.
1 # Airbrussh
2
3 [![Gem Version](https://badge.fury.io/rb/airbrussh.svg)](http://badge.fury.io/rb/airbrussh)
4 [![Build Status](https://circleci.com/gh/mattbrictson/airbrussh/tree/main.svg?style=shield)](https://app.circleci.com/pipelines/github/mattbrictson/airbrussh?branch=main)
5 [![Build status](https://ci.appveyor.com/api/projects/status/h052rlq54sne3md6/branch/main?svg=true)](https://ci.appveyor.com/project/mattbrictson/airbrussh/branch/main)
6 [![Code Climate](https://codeclimate.com/github/mattbrictson/airbrussh/badges/gpa.svg)](https://codeclimate.com/github/mattbrictson/airbrussh)
7 [![Coverage Status](https://coveralls.io/repos/mattbrictson/airbrussh/badge.svg?branch=main)](https://coveralls.io/r/mattbrictson/airbrussh?branch=main)
8
9
10 Airbrussh is a concise log formatter for Capistrano and SSHKit. It displays well-formatted, useful log output that is easy to read. Airbrussh also saves Capistrano's verbose output to a separate log file just in case you need additional details for troubleshooting.
11
12 **As of April 2016, Airbrussh is bundled with Capistrano 3.5, and is Capistrano's default formatter! There is nothing additional to install or enable.** Continue reading to learn more about Airbrussh's features and configuration options.
13
14 If you aren't yet using Capistrano 3.5 (or wish to use Airbrussh with SSHKit directly), refer to the [advanced/legacy usage](#advancedlegacy-usage) section for installation instructions.
15
16 ![Sample output](https://raw.github.com/mattbrictson/airbrussh/HEAD/demo.gif)
17
18 For more details on how exactly Airbrussh affects Capistrano's output and the reasoning behind it, check out the blog post: [Introducing Airbrussh](https://mattbrictson.com/airbrussh).
19
20 -----
21
22 * [Usage](#usage)
23 * [Configuration](#configuration)
24 * [FAQ](#faq)
25 * [Advanced/legacy usage](#advancedlegacy-usage)
26
27 ## Usage
28
29 Airbrussh is enabled by default in Capistrano 3.5 and newer. To manually enable Airbrussh (for example, when upgrading an existing project), set the Capistrano format like this:
30
31 ```ruby
32 # In deploy.rb
33 set :format, :airbrussh
34 ```
35
36 ### What's displayed
37
38 When you run a Capistrano command, Airbrussh provides the following information in its output:
39
40 ![Sample output](https://raw.github.com/mattbrictson/airbrussh/HEAD/formatting.png)
41
42 * Name of Capistrano task being executed
43 * When each task started (minutes:seconds elapsed since the deploy began)
44 * The SSH command-line strings that are executed; for Capistrano tasks that involve running multiple commands, the numeric prefix indicates the command in the sequence, starting from `01`
45 * Stdout and stderr output from each command
46 * The duration of each command execution, per server
47
48 ### What's *not* displayed
49
50 For brevity, Airbrussh does not show *everything* that Capistrano is doing. For example, it will omit Capistrano's `test` commands, which can be noisy and confusing. Airbrussh also hides things like environment variables, as well as `cd` and `env` invocations. To see a full audit of Capistrano's execution, including *exactly* what commands were run on each server, look at `log/capistrano.log`.
51
52 ## Configuration
53
54 You can customize many aspects of Airbrussh's output. In Capistrano 3.5 and newer, this is done via the `:format_options` variable, like this:
55
56 ```ruby
57 # Pass options to Airbrussh
58 set :format_options, color: false, truncate: 80
59 ```
60
61 Here are the options you can use, and their effects (note that the defaults may be different depending on where Airbrussh is used; these are the defaults used by Capistrano 3.5):
62
63 |Option|Default|Usage|
64 |---|---|---|
65 |`banner`|`nil`|Provide a string (e.g. "Capistrano started!") that will be printed when Capistrano starts up.|
66 |`color`|`:auto`|Use `true` or `false` to enable or disable ansi color. If set to `:auto`, Airbrussh automatically uses color based on whether the output is a TTY, or if the SSHKIT_COLOR environment variable is set.|
67 |`command_output`|`true`|Set to `:stdout`, `:stderr`, or `true` to display the SSH output received via stdout, stderr, or both, respectively. Set to `false` to not show any SSH output, for a minimal look.|
68 |`context`|`Airbrussh::Rake::Context`|Defines the execution context. Targeted towards uses of Airbrussh outside of Rake/Capistrano. Alternate implementations should provide the definition for `current_task_name`, `register_new_command`, and `position`.|
69 |`log_file`|`log/capistrano.log`|Capistrano's verbose output is saved to this file to facilitate debugging. Set to `nil` to disable completely.|
70 |`truncate`|`:auto`|Set to a number (e.g. 80) to truncate the width of the output to that many characters, or `false` to disable truncation. If `:auto`, output is automatically truncated to the width of the terminal window, if it can be determined.|
71 |`task_prefix`|`nil`|A string to prefix to task output. Handy for output collapsing like [buildkite](https://buildkite.com/docs/builds/managing-log-output)'s `---` prefix|
72
73 ## FAQ
74
75 **Airbrussh is not displaying the output of my commands! For example, I run `tail` in one of my capistrano tasks and airbrussh doesn't show anything. How do I fix this?**
76
77 Make sure Airbrussh is configured to show SSH output.
78
79 ```ruby
80 set :format_options, command_output: true
81 ```
82
83 **I haven't upgraded to Capistrano 3.5 yet. Can I still use Airbrussh?**
84
85 Yes! Capistrano 3.4.x is also supported. Refer to the [advanced/legacy usage](#advancedlegacy-usage) section for installation instructions.
86
87 **Does Airbrussh work with Capistrano 2?**
88
89 No, Capistrano 3 is required. We recommend Capistrano 3.4.0 or higher. Capistrano 3.5.0 and higher have Airbrussh enabled by default, with no installation needed.
90
91 **Does Airbrussh work with JRuby?**
92
93 JRuby is not officially supported or tested, but may work. You must disable automatic truncation to work around a known bug in the JRuby 9.0 standard library. See [#62](https://github.com/mattbrictson/airbrussh/issues/62) for more details.
94
95 ```ruby
96 set :format_options, truncate: false
97 ```
98
99 **I have a question that’s not answered here or elsewhere in the README.**
100
101 Please [open a GitHub issue](https://github.com/mattbrictson/airbrussh/issues/new) and we’ll be happy to help!
102
103 ## Advanced/legacy usage
104
105 Although Airbrussh is built into Capistrano 3.5.0 and higher, it is also available as a plug-in for older versions. Airbrussh has been tested with MRI 1.9+, Capistrano 3.4.0+, and SSHKit 1.6.1+.
106
107 ### Capistrano 3.4.x
108
109 Add this line to your application's Gemfile:
110
111 ```ruby
112 gem "airbrussh", require: false
113 ```
114
115 And then execute:
116
117 $ bundle
118
119 Finally, add this line to your application's Capfile:
120
121 ```ruby
122 require "airbrussh/capistrano"
123 ```
124
125 **Important:** explicitly setting Capistrano's `:format` option in your deploy.rb will override airbrussh. Remove this line if you have it:
126
127 ```ruby
128 # Remove this
129 set :format, :pretty
130 ```
131
132 Capistrano 3.4.x doesn't have the `:format_options` configuration system, so you will need to configure Airbrussh using this technique:
133
134 ```ruby
135 Airbrussh.configure do |config|
136 config.color = false
137 config.command_output = true
138 # etc.
139 end
140 ```
141
142 Refer to the [configuration](#configuration) section above for the list of supported options.
143
144 ### SSHKit
145
146 If you are using SSHKit directly (i.e. without Capistrano), you can use Airbrussh like this:
147
148 ```ruby
149 require "airbrussh"
150 SSHKit.config.output = Airbrussh::Formatter.new($stdout)
151
152 # You can also pass configuration options like this
153 SSHKit.config.output = Airbrussh::Formatter.new($stdout, color: false)
154 ```
155
156 ## History
157
158 Airbrussh started life as custom logging code within the [capistrano-mb][] collection of opinionated Capistrano recipes. In February 2015, the logging code was refactored into a standalone gem with its own configuration and documentation, and renamed `airbrussh`. In February 2016, Airbrussh was added as the default formatter in Capistrano 3.5.0.
159
160 ## Roadmap
161
162 Airbrussh now has a stable feature set, excellent test coverage, is being used for production deployments, and has reached 1.0.0! If you have ideas for improvements to Airbrussh, please open a [GitHub issue](https://github.com/mattbrictson/airbrussh/issues/new).
163
164 ## Contributing
165
166 Contributions are welcome! Read [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
167
168 [capistrano-mb]: https://github.com/mattbrictson/capistrano-mb
1 require "bundler/gem_tasks"
2
3 require "rake/testtask"
4 Rake::TestTask.new(:test) do |t|
5 t.libs << "test"
6 t.libs << "lib"
7 t.test_files = FileList["test/**/*_test.rb"]
8 end
9
10 begin
11 require "rubocop/rake_task"
12 RuboCop::RakeTask.new
13 task :default => [:test, :rubocop]
14 rescue LoadError
15 task :default => :test
16 end
17
18 Rake::Task["release"].enhance do
19 puts "Don't forget to publish the release on GitHub!"
20 system "open https://github.com/mattbrictson/airbrussh/releases"
21 end
1 # Capistrano 3.5 upgrade guide for existing Airbrussh users
2
3 If you have been using Airbrussh with Capistrano, and are upgrading to Capistrano 3.5, then this guide is for you.
4
5 ## What changed?
6
7 * Airbrussh is built into Capistrano starting with Capistrano 3.5.0, and is enabled by default.
8 * Capistrano 3.5 initializes Airbrussh with a default configuration that is different than Airbrussh+Capistrano 3.4.0.
9 * In Capistrano 3.5, `set :format_options, ...` is now the preferred mechanism for configuring Airbrussh.
10
11 ## How to upgrade
12
13 1. Remove `gem "airbrussh"` from your Gemfile. Airbrussh is now included automatically by Capistrano.
14 2. Likewise, remove `require "capistrano/airbrussh"` from your Capfile. Capistrano now does this internally.
15 3. Remove `Airbrussh.configure do ... end` from your deploy.rb and replace with `set :format_options, ...` (see below).
16
17 ## New configuration system
18
19 In Capistrano 3.5, Airbrussh is configured by assigning a configuration Hash to the `:format_options` variable. Here is a comparison of the old and new syntaxes:
20
21 ```ruby
22 # Old syntax
23 Airbrussh.configure do |config|
24 config.color = false
25 end
26
27 # New syntax
28 set :format_options, color: false
29 ```
30
31 Although the syntax is different, the names of the configuration keys have not changed.
32
33 ## New defaults
34
35 Capistrano 3.5.0 changes Airbrussh defaults as follows:
36
37 * `banner: false`
38 * `command_output: true`
39
40 Therefore, after upgrading to Capistrano 3.5.0, you may notice Airbrussh's output has changed and become more verbose. To restore the defaults to what you were used to in older versions, do this:
41
42 ```ruby
43 set :format_options, banner: :auto, command_output: false
44 ```
45
46 ## Trouble?
47
48 If you have any Airbrussh-related trouble with the upgrade, please [open an issue](https://github.com/mattbrictson/airbrussh/issues).
1 # coding: utf-8
2 lib = File.expand_path("../lib", __FILE__)
3 $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 require "airbrussh/version"
5
6 Gem::Specification.new do |spec|
7 spec.name = "airbrussh"
8 spec.version = Airbrussh::VERSION
9 spec.license = "MIT"
10 spec.authors = ["Matt Brictson"]
11 spec.email = ["airbrussh@mattbrictson.com"]
12 spec.summary = "Airbrussh pretties up your SSHKit and Capistrano output"
13 spec.description = "A replacement log formatter for SSHKit that makes "\
14 "Capistrano output much easier on the eyes. Just add "\
15 "Airbrussh to your Capfile and enjoy concise, useful "\
16 "log output that is easy to read."
17 spec.homepage = "https://github.com/mattbrictson/airbrussh"
18 spec.metadata = {
19 "bug_tracker_uri" => "https://github.com/mattbrictson/airbrussh/issues",
20 "changelog_uri" => "https://github.com/mattbrictson/airbrussh/releases",
21 "source_code_uri" => "https://github.com/mattbrictson/airbrussh",
22 "homepage_uri" => "https://github.com/mattbrictson/airbrussh"
23 }
24
25 spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/|\.(gif|png)$}) }
26 spec.bindir = "exe"
27 spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28 spec.require_paths = ["lib"]
29
30 spec.add_dependency "sshkit", [">= 1.6.1", "!= 1.7.0"]
31
32 spec.add_development_dependency "bundler"
33 spec.add_development_dependency "rake", "~> 12.0"
34 spec.add_development_dependency "minitest", "~> 5.10"
35 spec.add_development_dependency "minitest-reporters", "~> 1.1"
36 spec.add_development_dependency "mocha", "~> 1.2"
37 end
1 version: "{build}"
2 skip_tags: true
3 skip_branch_with_pr: true
4 install:
5 - set PATH=C:\Ruby26-x64\bin;%PATH%
6 - bundle install --retry=3
7 test_script:
8 - bundle exec rake
9 build: off
1 #!/usr/bin/env ruby
2
3 require "bundler/setup"
4 require "airbrussh"
5
6 # You can add fixtures and/or initialization code here to make experimenting
7 # with your gem easier. You can also use a different console, if you like.
8
9 # (If you use this, don't forget to add pry to your Gemfile!)
10 # require "pry"
11 # Pry.start
12
13 require "irb"
14 IRB.start
1 #!/bin/bash
2 set -euo pipefail
3 IFS=$'\n\t'
4
5 bundle install
6
7 # Do any other automated setup that you need to do here
1 require "airbrussh/configuration"
2 require "airbrussh/formatter"
3 require "airbrussh/version"
4
5 module Airbrussh
6 def self.configuration(options={})
7 return options if options.is_a?(::Airbrussh::Configuration)
8 @configuration ||= Configuration.new
9 @configuration.apply_options(options)
10 end
11
12 def self.configure
13 yield(configuration)
14 end
15 end
1 require "airbrussh/capistrano/tasks"
2
3 tasks = Airbrussh::Capistrano::Tasks.new(self)
4
5 # Hook into Capistrano's init process to set the formatter
6 namespace :load do
7 task :defaults do
8 tasks.load_defaults
9 end
10 end
11
12 # Capistrano failure hook
13 namespace :deploy do
14 task :failed do
15 tasks.deploy_failed
16 end
17 end
1 require "airbrussh"
2 require "airbrussh/colors"
3 require "airbrussh/console"
4 require "forwardable"
5 require "shellwords"
6
7 module Airbrussh
8 module Capistrano
9 # Encapsulates the rake behavior that integrates Airbrussh into Capistrano.
10 # This class allows us to easily test the behavior using a mock to stand in
11 # for the Capistrano DSL.
12 #
13 # See airbrussh/capistrano.rb to see how this class is used.
14 #
15 class Tasks
16 extend Forwardable
17 def_delegators :dsl, :set, :env
18 def_delegators :config, :log_file
19
20 include Airbrussh::Colors
21
22 def initialize(dsl, stderr=$stderr, config=Airbrussh.configuration)
23 @dsl = dsl
24 @stderr = stderr
25 @config = config
26
27 configure
28 warn_if_missing_dsl
29 end
30
31 # Behavior for the rake load:defaults task.
32 def load_defaults
33 require "sshkit/formatter/airbrussh"
34 set :format, :airbrussh
35 end
36
37 # Behavior for the rake deploy:failed task.
38 def deploy_failed
39 return unless airbrussh_is_being_used?
40 return if log_file.nil?
41
42 error_line
43 error_line(red("** DEPLOY FAILED"))
44 error_line(yellow("** Refer to #{log_file} for details. "\
45 "Here are the last 20 lines:"))
46 error_line
47 error_line(tail_log_file)
48 end
49
50 private
51
52 attr_reader :dsl, :stderr, :config
53
54 # Change airbrussh's default configuration to be more appropriate for
55 # capistrano.
56 def configure
57 config.log_file = "log/capistrano.log"
58 config.monkey_patch_rake = true
59 end
60
61 # Verify that capistrano and rake DSLs are present
62 def warn_if_missing_dsl
63 return if %w[set namespace task].all? { |m| dsl.respond_to?(m, true) }
64
65 error_line(
66 red("WARNING: airbrussh/capistrano must be loaded by Capistrano in "\
67 "order to work.\nRequire this gem within your application's "\
68 "Capfile, as described here:\n"\
69 "https://github.com/mattbrictson/airbrussh#installation")
70 )
71 end
72
73 def err_console
74 @err_console ||= begin
75 # Ensure we do not truncate the error output
76 err_config = config.dup
77 err_config.truncate = false
78 Airbrussh::Console.new(stderr, err_config)
79 end
80 end
81
82 def error_line(line="\n")
83 line.each_line(&err_console.method(:print_line))
84 end
85
86 # Returns a String containing the last 20 lines of the log file. Since
87 # this method uses a fixed-size buffer, fewer than 20 lines may be
88 # returned in cases where the lines are extremely long.
89 def tail_log_file
90 open(log_file) do |file|
91 file.seek(-[8192, file.size].min, IO::SEEK_END)
92 file.readlines.last(20).join
93 end
94 end
95
96 def airbrussh_is_being_used?
97 # Obtain the current formatter from the SSHKit backend
98 output = env.backend.config.output
99 output.is_a?(Airbrussh::Formatter)
100 end
101 end
102 end
103 end
1 module Airbrussh
2 # Very basic support for ANSI color, so that we don't have to rely on
3 # any external dependencies.
4 module Colors
5 ANSI_CODES = {
6 :red => 31,
7 :green => 32,
8 :yellow => 33,
9 :blue => 34,
10 :gray => 90
11 }.freeze
12
13 # Define red, green, blue, etc. methods that return a copy of the
14 # String that is wrapped in the corresponding ANSI color escape
15 # sequence.
16 ANSI_CODES.each do |name, code|
17 define_method(name) do |string|
18 "\e[0;#{code};49m#{string}\e[0m"
19 end
20 module_function(name)
21 end
22 end
23 end
1 # encoding: UTF-8
2
3 require "airbrussh/colors"
4 require "delegate"
5 # rubocop:disable Style/AsciiComments
6
7 module Airbrussh
8 # Decorates an SSHKit Command to add string output helpers and the
9 # command's position within currently executing rake task:
10 #
11 # * position - zero-based position of this command in the list of
12 # all commands that have been run in the current rake task; in
13 # some cases this could be nil
14 class CommandFormatter < SimpleDelegator
15 include Airbrussh::Colors
16
17 def initialize(command, position)
18 super(command)
19 @position = position
20 end
21
22 # Prefixes the line with the command number and removes the newline.
23 #
24 # format_output("hello\n") # => "01 hello"
25 #
26 def format_output(line)
27 "#{number} #{line.chomp}"
28 end
29
30 # Returns the abbreviated command (in yellow) with the number prefix.
31 #
32 # start_message # => "01 echo hello"
33 #
34 def start_message
35 "#{number} #{yellow(abbreviated)}"
36 end
37
38 # Returns a green (success) or red (failure) message depending on the
39 # exit status.
40 #
41 # exit_message # => "✔ 01 user@host 0.084s"
42 # exit_message # => "✘ 01 user@host 0.084s"
43 #
44 def exit_message
45 message = if failure?
46 red(failure_message)
47 else
48 green(success_message)
49 end
50 message << " #{runtime}"
51 end
52
53 private
54
55 def user_at_host
56 user_str = host.user || (host.ssh_options || {})[:user]
57 host_str = host.hostname
58 [user_str, host_str].compact.join("@")
59 end
60
61 def runtime
62 format("%5.3fs", super)
63 end
64
65 def abbreviated
66 to_s.sub(%r{^/usr/bin/env }, "")
67 end
68
69 def number
70 format("%02d", @position.to_i + 1)
71 end
72
73 def success_message
74 "✔ #{number} #{user_at_host}"
75 end
76
77 def failure_message
78 "✘ #{number} #{user_at_host}"
79 end
80 end
81 end
1 require "airbrussh/colors"
2 require "airbrussh/console_formatter"
3 require "airbrussh/log_file_formatter"
4
5 module Airbrussh
6 class Configuration
7 attr_accessor :log_file, :monkey_patch_rake, :color, :truncate, :banner,
8 :command_output, :task_prefix, :context
9
10 def initialize
11 self.log_file = nil
12 self.monkey_patch_rake = false
13 self.color = :auto
14 self.truncate = :auto
15 self.banner = :auto
16 self.command_output = false
17 self.task_prefix = nil
18 self.context = Airbrussh::Rake::Context
19 end
20
21 def apply_options(options)
22 return self if options.nil?
23
24 options.each do |key, value|
25 if respond_to?(writer = "#{key}=")
26 public_send(writer, value)
27 else
28 warn_unrecognized_key(key)
29 end
30 end
31 self
32 end
33
34 def banner_message
35 return nil unless banner
36 return banner unless banner == :auto
37 msg = "Using airbrussh format."
38 if log_file
39 msg << "\n"
40 msg << "Verbose output is being written to #{Colors.blue(log_file)}."
41 end
42 msg
43 end
44
45 # This returns an array of formatters appropriate for the configuration.
46 # Depending on whether a log file is configured, this could be just the
47 # Airbrussh:ConsoleFormatter, or that plus the LogFileFormatter.
48 def formatters(io)
49 fmts = [Airbrussh::ConsoleFormatter.new(io, self)]
50 fmts.unshift(Airbrussh::LogFileFormatter.new(log_file)) if log_file
51 fmts
52 end
53
54 def show_command_output?(sym)
55 command_output == true || Array(command_output).include?(sym)
56 end
57
58 private
59
60 def warn_unrecognized_key(key)
61 $stderr.puts("Ignoring unrecognized Airbrussh option: #{key}")
62 end
63 end
64 end
1 # encoding: UTF-8
2
3 require "io/console"
4
5 module Airbrussh
6 # Helper class that wraps an IO object and provides methods for truncating
7 # output, assuming the IO object represents a console window.
8 #
9 # This is useful for writing log messages that will typically show up on
10 # an ANSI color-capable console. When a console is not present (e.g. when
11 # running on a CI server) the output will gracefully degrade.
12 class Console
13 attr_reader :output, :config
14
15 def initialize(output, config=Airbrussh.configuration)
16 @output = output
17 @config = config
18 end
19
20 # Writes to the IO after first truncating the output to fit the console
21 # width. If the underlying IO is not a TTY, ANSI colors are removed from
22 # the output. A newline is always added. Color output can be forced by
23 # setting the SSHKIT_COLOR environment variable.
24 def print_line(obj="")
25 string = obj.to_s
26
27 string = truncate_to_console_width(string) if console_width
28 string = strip_ascii_color(string) unless color_enabled?
29
30 write(string + "\n")
31 output.flush
32 end
33
34 # Writes directly through to the IO with no truncation or color logic.
35 # No newline is added.
36 def write(string)
37 output.write(string || "")
38 end
39 alias << write
40
41 def truncate_to_console_width(string)
42 string = (string || "").rstrip
43 ellipsis = utf8_supported?(string) ? "…" : "..."
44 width = console_width
45
46 if strip_ascii_color(string).length > width
47 width -= ellipsis.length
48 string.chop! while strip_ascii_color(string).length > width
49 string << "#{ellipsis}\e[0m"
50 else
51 string
52 end
53 end
54
55 def strip_ascii_color(string)
56 string ||= ""
57 string = to_utf8(string) unless string.valid_encoding?
58
59 string.gsub(/\033\[[0-9;]*m/, "")
60 end
61
62 def console_width
63 width = case (truncate = config.truncate)
64 when :auto
65 IO.console.winsize.last if @output.tty?
66 when Integer
67 truncate
68 end
69
70 width if width.to_i > 0
71 end
72
73 private
74
75 def color_enabled?
76 case config.color
77 when true
78 true
79 when :auto
80 ENV["SSHKIT_COLOR"] || @output.tty?
81 else
82 false
83 end
84 end
85
86 def utf8_supported?(string)
87 string.encode("UTF-8").valid_encoding?
88 rescue Encoding::UndefinedConversionError
89 false
90 end
91
92 def to_utf8(string)
93 string.force_encoding("ASCII-8BIT")
94 .encode("UTF-8", :invalid => :replace, :undef => :replace)
95 end
96 end
97 end
1 require "airbrussh/colors"
2 require "airbrussh/command_formatter"
3 require "airbrussh/console"
4 require "airbrussh/rake/context"
5 require "sshkit"
6
7 module Airbrussh
8 class ConsoleFormatter < SSHKit::Formatter::Abstract
9 include Airbrussh::Colors
10 extend Forwardable
11
12 attr_reader :config, :context
13 def_delegators :context, :current_task_name, :register_new_command
14
15 def initialize(io, config=Airbrussh.configuration)
16 super(io)
17
18 @config = config
19 @context = config.context.new(config)
20 @console = Airbrussh::Console.new(original_output, config)
21
22 write_banner
23 end
24
25 def write_banner
26 print_line(config.banner_message) if config.banner_message
27 end
28
29 def log_command_start(command)
30 return if debug?(command)
31 first_execution = register_new_command(command)
32 command = decorate(command)
33 print_task_if_changed
34 print_indented_line(command.start_message) if first_execution
35 end
36
37 def log_command_data(command, stream_type, string)
38 return if debug?(command)
39 return unless config.show_command_output?(stream_type)
40 command = decorate(command)
41 string.each_line do |line|
42 print_indented_line(command.format_output(line))
43 end
44 end
45
46 def log_command_exit(command)
47 return if debug?(command)
48 command = decorate(command)
49 print_indented_line(command.exit_message, -2)
50 end
51
52 def write(obj)
53 case obj
54 when SSHKit::Command
55 log_command_start(obj)
56 log_and_clear_command_output(obj, :stderr)
57 log_and_clear_command_output(obj, :stdout)
58 log_command_exit(obj) if obj.finished?
59 when SSHKit::LogMessage
60 write_log_message(obj)
61 end
62 end
63 alias << write
64
65 private
66
67 attr_accessor :last_printed_task
68
69 def write_log_message(log_message)
70 return if debug?(log_message)
71 print_task_if_changed
72 print_indented_line(format_log_message(log_message))
73 end
74
75 def format_log_message(log_message)
76 case log_message.verbosity
77 when SSHKit::Logger::WARN
78 "#{yellow('WARN')} #{log_message}"
79 when SSHKit::Logger::ERROR
80 "#{red('ERROR')} #{log_message}"
81 when SSHKit::Logger::FATAL
82 "#{red('FATAL')} #{log_message}"
83 else
84 log_message.to_s
85 end
86 end
87
88 # For SSHKit versions up to and including 1.7.1, the stdout and stderr
89 # output was available as attributes on the Command. Print the data for
90 # the specified command and stream if enabled and clear the stream.
91 # (see Airbrussh::Configuration#command_output).
92 def log_and_clear_command_output(command, stream)
93 output = command.public_send(stream)
94 log_command_data(command, stream, output)
95 command.public_send("#{stream}=", "")
96 end
97
98 def print_task_if_changed
99 return if current_task_name.nil?
100 return if current_task_name == last_printed_task
101
102 self.last_printed_task = current_task_name
103 print_line("#{config.task_prefix}#{clock} #{blue(current_task_name)}")
104 end
105
106 def clock
107 @start_at ||= Time.now
108 duration = Time.now - @start_at
109
110 minutes = (duration / 60).to_i
111 seconds = (duration - minutes * 60).to_i
112
113 format("%02d:%02d", minutes, seconds)
114 end
115
116 def debug?(obj)
117 obj.verbosity <= SSHKit::Logger::DEBUG
118 end
119
120 def decorate(command)
121 Airbrussh::CommandFormatter.new(command, @context.position(command))
122 end
123
124 def print_line(string)
125 @console.print_line(string)
126 end
127
128 def print_indented_line(string, offset=0)
129 indent = " " * (6 + offset)
130 print_line([indent, string].join)
131 end
132 end
133 end
1 require "sshkit"
2
3 module Airbrussh
4 # This class quacks like an SSHKit::Formatter, but when any formatting
5 # methods are called, it simply forwards them to one more more concrete
6 # formatters. This allows us to split out the responsibilities of
7 # ConsoleFormatter and LogFileFormatter into two separate classes, with
8 # DelegatingFormatter forwarding the logging messages to both at once.
9 #
10 class DelegatingFormatter
11 FORWARD_METHODS = %w[
12 fatal error warn info debug log
13 log_command_start log_command_data log_command_exit
14 ].freeze
15 DUP_AND_FORWARD_METHODS = %w[<< write].freeze
16
17 attr_reader :formatters
18
19 def initialize(formatters)
20 @formatters = formatters
21 end
22
23 FORWARD_METHODS.each do |method|
24 define_method(method) do |*args|
25 formatters.map { |f| f.public_send(method, *args) }.last
26 end
27 end
28
29 # For versions of SSHKit up to and including 1.7.1, the LogfileFormatter
30 # and ConsoleFormatter (and all of SSHKit's built in formatters) clear
31 # the stdout and stderr data in the command obj. Therefore, ensure only
32 # one of the formatters (the last one) gets the original command. This is
33 # also the formatter whose return value is passed to the caller.
34 #
35 DUP_AND_FORWARD_METHODS.each do |method|
36 define_method(method) do |command_or_log_message|
37 formatters[0...-1].each do |f|
38 f.public_send(method, command_or_log_message.dup)
39 end
40 formatters.last.public_send(method, command_or_log_message)
41 end
42 end
43 end
44 end
1 require "airbrussh"
2 require "airbrussh/delegating_formatter"
3
4 # This is the formatter class that conforms to the SSHKit Formatter API and
5 # provides the airbrussh functionality to SSHKit. Note however that this class
6 # doesn't do much by itself; instead, it delegates to the ConsoleFormatter
7 # and (optionally) the LogFileFormatter, which handle the bulk of the logic.
8 #
9 module Airbrussh
10 class Formatter < Airbrussh::DelegatingFormatter
11 def initialize(io, options_or_config_object={})
12 config = ::Airbrussh.configuration(options_or_config_object)
13 # Delegate to ConsoleFormatter and (optionally) LogFileFormatter,
14 # based on the configuration.
15 super(config.formatters(io))
16 end
17 end
18 end
1 require "delegate"
2 require "fileutils"
3 require "logger"
4 require "sshkit"
5
6 module Airbrussh
7 # A Pretty formatter that sends its output to a specified log file path.
8 # LogFileFormatter takes care of creating the file (and its parent
9 # directory) if it does not already exist, opens it for appending, and writes
10 # a delimiter message. The file is automatically rotated if it reaches 20 MB.
11 #
12 class LogFileFormatter < SimpleDelegator
13 attr_reader :path
14
15 def initialize(path, formatter_class=SSHKit::Formatter::Pretty)
16 @path = path
17 ensure_directory_exists if path.is_a?(String)
18 super(formatter_class.new(log_file_io))
19 write_delimiter
20 end
21
22 private
23
24 def write_delimiter
25 delimiter = []
26 delimiter << "-" * 75
27 delimiter << "START #{Time.now} cap #{ARGV.join(' ')}"
28 delimiter << "-" * 75
29 delimiter.each do |line|
30 write(SSHKit::LogMessage.new(SSHKit::Logger::INFO, line))
31 end
32 end
33
34 def ensure_directory_exists
35 FileUtils.mkdir_p(File.dirname(path))
36 end
37
38 def log_file_io
39 @io ||= ::Logger.new(path, 1, 20_971_520)
40 end
41 end
42 end
1 module Airbrussh
2 module Rake
3 # Maintains information about what Rake task is currently being invoked,
4 # in order to be able to decorate SSHKit commands with additional
5 # context-sensitive information. Works via a monkey patch to Rake::Task,
6 # which can be disabled via by setting
7 # Airbrussh.configuration.monkey_patch_rake = false.
8 #
9 # Note that this class is not thread-safe. Normally this is not a problem,
10 # but some Capistrano users are known to use `invoke` to switch the Rake
11 # task in the middle of an SSHKit thread, which causes Context to get very
12 # confused. It such scenarios Context is not reliable and may return `nil`
13 # for the `position` of a command.
14 #
15 class Context
16 def initialize(config=Airbrussh.configuration)
17 @history = []
18 @enabled = config.monkey_patch_rake
19 self.class.install_monkey_patch if enabled?
20 end
21
22 # Returns the name of the currently-executing rake task, if it can be
23 # determined. If monkey patching is disabled, this will be nil.
24 def current_task_name
25 return nil unless enabled?
26 self.class.current_task_name
27 end
28
29 # Update the context when a new command starts by:
30 # * Clearing the command history if the rake task has changed
31 # * Recording the command in the history
32 #
33 # Returns whether or not this command was the first execution
34 # of this command in the current rake task
35 def register_new_command(command)
36 reset_history_if_task_changed
37
38 first_execution = !history.include?(command.to_s)
39 history << command.to_s
40 history.uniq!
41 first_execution
42 end
43
44 # The zero-based position of the specified command in the current rake
45 # task. May be `nil` in certain multi-threaded scenarios, so be careful!
46 def position(command)
47 history.index(command.to_s)
48 end
49
50 class << self
51 attr_accessor :current_task_name
52
53 def install_monkey_patch
54 require "rake"
55 return if ::Rake::Task.instance_methods.include?(:_airbrussh_execute)
56
57 ::Rake::Task.class_exec do
58 alias_method :_airbrussh_execute, :execute
59 def execute(args=nil)
60 ::Airbrussh::Rake::Context.current_task_name = name.to_s
61 _airbrussh_execute(args)
62 end
63 end
64 end
65 end
66
67 private
68
69 attr_reader :history
70 attr_accessor :last_task_name
71
72 def reset_history_if_task_changed
73 history.clear if last_task_name != current_task_name
74 self.last_task_name = current_task_name
75 end
76
77 def enabled?
78 @enabled
79 end
80 end
81 end
82 end
1 # frozen_string_literal: true
2
3 module Airbrussh
4 VERSION = "1.4.1".freeze
5 end
1 require "airbrussh/formatter"
2
3 # Capistrano's formatter configuration requires that the formatter class
4 # be in the SSHKit::Formatter namespace. So we declare
5 # SSHKit::Formatter::Airbrussh that simply functions as an alias for
6 # Airbrussh::Formatter.
7 module SSHKit
8 module Formatter
9 class Airbrussh < Airbrussh::Formatter
10 end
11 end
12 end
This diff could not be displayed because it is too large.
1 The MIT License
2
3 Portions copyright (c) 2010-2019 André Arko
4 Portions copyright (c) 2009 Engine Yard
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in
14 all copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 THE SOFTWARE.
1 [![Version ](https://img.shields.io/gem/v/bundler.svg?style=flat)](https://rubygems.org/gems/bundler)
2 [![Slack ](https://bundler-slackin.herokuapp.com/badge.svg)](https://bundler-slackin.herokuapp.com)
3
4 # Bundler: a gem to bundle gems
5
6 Bundler makes sure Ruby applications run the same code on every machine.
7
8 It does this by managing the gems that the application depends on. Given a list of gems, it can automatically download and install those gems, as well as any other gems needed by the gems that are listed. Before installing gems, it checks the versions of every gem to make sure that they are compatible, and can all be loaded at the same time. After the gems have been installed, Bundler can help you update some or all of them when new versions become available. Finally, it records the exact versions that have been installed, so that others can install the exact same gems.
9
10 ### Installation and usage
11
12 To install (or update to the latest version):
13
14 ```
15 gem install bundler
16 ```
17
18 To install a prerelease version (if one is available), run `gem install bundler --pre`. To uninstall Bundler, run `gem uninstall bundler`.
19
20 Bundler is most commonly used to manage your application's dependencies. For example, these commands will allow you to use Bundler to manage the `rspec` gem for your application:
21
22 ```
23 bundle init
24 bundle add rspec
25 bundle install
26 bundle exec rspec
27 ```
28
29 See [bundler.io](https://bundler.io) for the full documentation.
30
31 ### Troubleshooting
32
33 For help with common problems, see [TROUBLESHOOTING](doc/TROUBLESHOOTING.md).
34
35 Still stuck? Try [filing an issue](https://github.com/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md).
36
37 ### Other questions
38
39 To see what has changed in recent versions of Bundler, see the [CHANGELOG](CHANGELOG.md).
40
41 To get in touch with the Bundler core team and other Bundler users, please see [getting help](doc/contributing/GETTING_HELP.md).
42
43 ### Contributing
44
45 If you'd like to contribute to Bundler, that's awesome, and we <3 you. We've put together [the Bundler contributor guide](https://github.com/rubygems/rubygems/blob/master/bundler/doc/contributing/README.md) with all of the information you need to get started.
46
47 If you'd like to request a substantial change to Bundler or its documentation, refer to the [Bundler RFC process](https://github.com/bundler/rfcs) for more information.
48
49 While some Bundler contributors are compensated by Ruby Together, the project maintainers make decisions independent of Ruby Together. As a project, we welcome contributions regardless of the author's affiliation with Ruby Together.
50
51 ### Supporting
52
53 <a href="https://rubytogether.org/"><img src="https://rubytogether.org/images/rubies.svg" width="150"></a><br>
54 <a href="https://rubytogether.org/">Ruby Together</a> pays some Bundler maintainers for their ongoing work. As a grassroots initiative committed to supporting the critical Ruby infrastructure you rely on, Ruby Together is funded entirely by the Ruby community. Contribute today <a href="https://rubytogether.org/developers">as an individual</a> or (better yet) <a href="https://rubytogether.org/companies">as a company</a> to ensure that Bundler, RubyGems, and other shared tooling is around for years to come.
55
56 ### Code of Conduct
57
58 Everyone interacting in the Bundler project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [Bundler code of conduct](https://github.com/rubygems/rubygems/blob/master/CODE_OF_CONDUCT.md).
59
60 ### License
61
62 Bundler is available under an [MIT License](https://github.com/rubygems/rubygems/blob/master/bundler/LICENSE.md).
1 # frozen_string_literal: true
2
3 begin
4 require_relative "lib/bundler/version"
5 rescue LoadError
6 # for Ruby core repository
7 require_relative "version"
8 end
9
10 Gem::Specification.new do |s|
11 s.name = "bundler"
12 s.version = Bundler::VERSION
13 s.license = "MIT"
14 s.authors = [
15 "André Arko", "Samuel Giddins", "Colby Swandale", "Hiroshi Shibata",
16 "David Rodríguez", "Grey Baker", "Stephanie Morillo", "Chris Morris", "James Wen", "Tim Moore",
17 "André Medeiros", "Jessica Lynn Suttles", "Terence Lee", "Carl Lerche",
18 "Yehuda Katz"
19 ]
20 s.email = ["team@bundler.io"]
21 s.homepage = "https://bundler.io"
22 s.summary = "The best way to manage your application's dependencies"
23 s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably"
24
25 if s.respond_to?(:metadata=)
26 s.metadata = {
27 "bug_tracker_uri" => "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler",
28 "changelog_uri" => "https://github.com/rubygems/rubygems/blob/master/bundler/CHANGELOG.md",
29 "homepage_uri" => "https://bundler.io/",
30 "source_code_uri" => "https://github.com/rubygems/rubygems/",
31 }
32 end
33
34 s.required_ruby_version = ">= 2.3.0"
35 s.required_rubygems_version = ">= 2.5.2"
36
37 s.files = Dir.glob("lib/bundler{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
38
39 # include the gemspec itself because warbler breaks w/o it
40 s.files += %w[bundler.gemspec]
41
42 s.files += %w[CHANGELOG.md LICENSE.md README.md]
43 s.bindir = "exe"
44 s.executables = %w[bundle bundler]
45 s.require_paths = ["lib"]
46 end
1 #!/usr/bin/env ruby
2 # frozen_string_literal: true
3
4 # Exit cleanly from an early interrupt
5 Signal.trap("INT") do
6 Bundler.ui.debug("\n#{caller.join("\n")}") if defined?(Bundler)
7 exit 1
8 end
9
10 base_path = File.expand_path("../lib", __dir__)
11
12 if File.exist?(base_path)
13 require_relative "../lib/bundler"
14 else
15 require "bundler"
16 end
17
18 # Workaround for non-activated bundler spec due to missing https://github.com/rubygems/rubygems/commit/4e306d7bcdee924b8d80ca9db6125aa59ee4e5a3
19 gem "bundler", Bundler::VERSION if Gem.rubygems_version < Gem::Version.new("2.6.2")
20
21 if Gem.rubygems_version < Gem::Version.new("3.2.3") && Gem.ruby_version < Gem::Version.new("2.6.a") && !ENV["BUNDLER_NO_OLD_RUBYGEMS_WARNING"]
22 Bundler.ui.warn \
23 "Your RubyGems version (#{Gem::VERSION}) has a bug that prevents " \
24 "`required_ruby_version` from working for Bundler. Any scripts that use " \
25 "`gem install bundler` will break as soon as Bundler drops support for " \
26 "your Ruby version. Please upgrade RubyGems to avoid future breakage " \
27 "and silence this warning by running `gem update --system 3.2.3`"
28 end
29
30 if File.exist?(base_path)
31 require_relative "../lib/bundler/friendly_errors"
32 else
33 require "bundler/friendly_errors"
34 end
35
36 Bundler.with_friendly_errors do
37 if File.exist?(base_path)
38 require_relative "../lib/bundler/cli"
39 else
40 require "bundler/cli"
41 end
42
43 # Allow any command to use --help flag to show help for that command
44 help_flags = %w[--help -h]
45 help_flag_used = ARGV.any? {|a| help_flags.include? a }
46 args = help_flag_used ? Bundler::CLI.reformatted_help_args(ARGV) : ARGV
47
48 Bundler::CLI.start(args, :debug => true)
49 end
1 #!/usr/bin/env ruby
2 # frozen_string_literal: true
3
4 load File.expand_path("bundle", __dir__)
1 # frozen_string_literal: true
2
3 module Bundler
4 # Represents metadata from when the Bundler gem was built.
5 module BuildMetadata
6 # begin ivars
7 @built_at = "2022-08-24".freeze
8 @git_commit_sha = "d54be5fdd8".freeze
9 @release = true
10 # end ivars
11
12 # A hash representation of the build metadata.
13 def self.to_h
14 {
15 "Built At" => built_at,
16 "Git SHA" => git_commit_sha,
17 "Released Version" => release?,
18 }
19 end
20
21 # A string representing the date the bundler gem was built.
22 def self.built_at
23 @built_at ||= Time.now.utc.strftime("%Y-%m-%d").freeze
24 end
25
26 # The SHA for the git commit the bundler gem was built from.
27 def self.git_commit_sha
28 return @git_commit_sha if instance_variable_defined? :@git_commit_sha
29
30 # If Bundler has been installed without its .git directory and without a
31 # commit instance variable then we can't determine its commits SHA.
32 git_dir = File.expand_path("../../../.git", __dir__)
33 if File.directory?(git_dir)
34 return @git_commit_sha = Dir.chdir(git_dir) { `git rev-parse --short HEAD`.strip.freeze }
35 end
36
37 @git_commit_sha ||= "unknown"
38 end
39
40 # Whether this is an official release build of Bundler.
41 def self.release?
42 @release
43 end
44 end
45 end
1 # frozen_string_literal: true
2
3 require_relative "shared_helpers"
4 Bundler::SharedHelpers.major_deprecation 2,
5 "The Bundler task for Capistrano. Please use https://github.com/capistrano/bundler"
6
7 # Capistrano task for Bundler.
8 #
9 # Add "require 'bundler/capistrano'" in your Capistrano deploy.rb, and
10 # Bundler will be activated after each new deployment.
11 require_relative "deployment"
12 require "capistrano/version"
13
14 if defined?(Capistrano::Version) && Gem::Version.new(Capistrano::Version).release >= Gem::Version.new("3.0")
15 raise "For Capistrano 3.x integration, please use https://github.com/capistrano/bundler"
16 end
17
18 Capistrano::Configuration.instance(:must_exist).load do
19 before "deploy:finalize_update", "bundle:install"
20 Bundler::Deployment.define_task(self, :task, :except => { :no_release => true })
21 set :rake, lambda { "#{fetch(:bundle_cmd, "bundle")} exec rake" }
22 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Add
5 attr_reader :gems, :options, :version
6
7 def initialize(options, gems)
8 @gems = gems
9 @options = options
10 @options[:group] = options[:group].split(",").map(&:strip) unless options[:group].nil?
11 @version = options[:version].split(",").map(&:strip) unless options[:version].nil?
12 end
13
14 def run
15 validate_options!
16 inject_dependencies
17 perform_bundle_install unless options["skip-install"]
18 end
19
20 private
21
22 def perform_bundle_install
23 Installer.install(Bundler.root, Bundler.definition)
24 Bundler.load.cache if Bundler.app_cache.exist?
25 end
26
27 def inject_dependencies
28 dependencies = gems.map {|g| Bundler::Dependency.new(g, version, options) }
29
30 Injector.inject(dependencies,
31 :conservative_versioning => options[:version].nil?, # Perform conservative versioning only when version is not specified
32 :optimistic => options[:optimistic],
33 :strict => options[:strict])
34 end
35
36 def validate_options!
37 raise InvalidOption, "You can not specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic]
38
39 # raise error when no gems are specified
40 raise InvalidOption, "Please specify gems to add." if gems.empty?
41
42 version.to_a.each do |v|
43 raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN =~ v.to_s
44 end
45 end
46 end
47 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Binstubs
5 attr_reader :options, :gems
6 def initialize(options, gems)
7 @options = options
8 @gems = gems
9 end
10
11 def run
12 Bundler.definition.validate_runtime!
13 path_option = options["path"]
14 path_option = nil if path_option && path_option.empty?
15 Bundler.settings.set_command_option :bin, path_option if options["path"]
16 Bundler.settings.set_command_option_if_given :shebang, options["shebang"]
17 installer = Installer.new(Bundler.root, Bundler.definition)
18
19 installer_opts = {
20 :force => options[:force],
21 :binstubs_cmd => true,
22 :all_platforms => options["all-platforms"],
23 }
24
25 if options[:all]
26 raise InvalidOption, "Cannot specify --all with specific gems" unless gems.empty?
27 @gems = Bundler.definition.specs.map(&:name)
28 installer_opts.delete(:binstubs_cmd)
29 elsif gems.empty?
30 Bundler.ui.error "`bundle binstubs` needs at least one gem to run."
31 exit 1
32 end
33
34 gems.each do |gem_name|
35 spec = Bundler.definition.specs.find {|s| s.name == gem_name }
36 unless spec
37 raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(
38 gem_name, Bundler.definition.specs
39 )
40 end
41
42 if options[:standalone]
43 next Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") if gem_name == "bundler"
44 Bundler.settings.temporary(:path => (Bundler.settings[:path] || Bundler.root)) do
45 installer.generate_standalone_bundler_executable_stubs(spec, installer_opts)
46 end
47 else
48 installer.generate_bundler_executable_stubs(spec, installer_opts)
49 end
50 end
51 end
52 end
53 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Cache
5 attr_reader :options
6
7 def initialize(options)
8 @options = options
9 end
10
11 def run
12 Bundler.ui.level = "warn" if options[:quiet]
13 Bundler.settings.set_command_option_if_given :path, options[:path]
14 Bundler.settings.set_command_option_if_given :cache_path, options["cache-path"]
15
16 setup_cache_all
17 install
18
19 # TODO: move cache contents here now that all bundles are locked
20 custom_path = Bundler.settings[:path] if options[:path]
21
22 Bundler.settings.temporary(:cache_all_platforms => options["all-platforms"]) do
23 Bundler.load.cache(custom_path)
24 end
25 end
26
27 private
28
29 def install
30 require_relative "install"
31 options = self.options.dup
32 options["local"] = false if Bundler.settings[:cache_all_platforms]
33 options["no-cache"] = true
34 Bundler::CLI::Install.new(options).run
35 end
36
37 def setup_cache_all
38 all = options.fetch(:all, Bundler.feature_flag.cache_all? || nil)
39
40 Bundler.settings.set_command_option_if_given :cache_all, all
41 end
42 end
43 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Check
5 attr_reader :options
6
7 def initialize(options)
8 @options = options
9 end
10
11 def run
12 Bundler.settings.set_command_option_if_given :path, options[:path]
13
14 definition = Bundler.definition
15 definition.validate_runtime!
16
17 begin
18 definition.resolve_only_locally!
19 not_installed = definition.missing_specs
20 rescue GemNotFound, VersionConflict
21 Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies."
22 Bundler.ui.warn "Install missing gems with `bundle install`."
23 exit 1
24 end
25
26 if not_installed.any?
27 Bundler.ui.error "The following gems are missing"
28 not_installed.each {|s| Bundler.ui.error " * #{s.name} (#{s.version})" }
29 Bundler.ui.warn "Install missing gems with `bundle install`"
30 exit 1
31 elsif !Bundler.default_lockfile.file? && Bundler.frozen_bundle?
32 Bundler.ui.error "This bundle has been frozen, but there is no #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} present"
33 exit 1
34 else
35 Bundler.load.lock(:preserve_unknown_sections => true) unless options[:"dry-run"]
36 Bundler.ui.info "The Gemfile's dependencies are satisfied"
37 end
38 end
39 end
40 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Clean
5 attr_reader :options
6
7 def initialize(options)
8 @options = options
9 end
10
11 def run
12 require_path_or_force unless options[:"dry-run"]
13 Bundler.load.clean(options[:"dry-run"])
14 end
15
16 protected
17
18 def require_path_or_force
19 return unless Bundler.use_system_gems? && !options[:force]
20 raise InvalidOption, "Cleaning all the gems on your system is dangerous! " \
21 "If you're sure you want to remove every system gem not in this " \
22 "bundle, run `bundle clean --force`."
23 end
24 end
25 end
1 # frozen_string_literal: true
2
3 module Bundler
4 module CLI::Common
5 def self.output_post_install_messages(messages)
6 return if Bundler.settings["ignore_messages"]
7 messages.to_a.each do |name, msg|
8 print_post_install_message(name, msg) unless Bundler.settings["ignore_messages.#{name}"]
9 end
10 end
11
12 def self.print_post_install_message(name, msg)
13 Bundler.ui.confirm "Post-install message from #{name}:"
14 Bundler.ui.info msg
15 end
16
17 def self.output_fund_metadata_summary
18 return if Bundler.settings["ignore_funding_requests"]
19 definition = Bundler.definition
20 current_dependencies = definition.requested_dependencies
21 current_specs = definition.specs
22
23 count = current_dependencies.count {|dep| current_specs[dep.name].first.metadata.key?("funding_uri") }
24
25 return if count.zero?
26
27 intro = count > 1 ? "#{count} installed gems you directly depend on are" : "#{count} installed gem you directly depend on is"
28 message = "#{intro} looking for funding.\n Run `bundle fund` for details"
29 Bundler.ui.info message
30 end
31
32 def self.output_without_groups_message(command)
33 return if Bundler.settings[:without].empty?
34 Bundler.ui.confirm without_groups_message(command)
35 end
36
37 def self.without_groups_message(command)
38 command_in_past_tense = command == :install ? "installed" : "updated"
39 groups = Bundler.settings[:without]
40 "Gems in the #{verbalize_groups(groups)} were not #{command_in_past_tense}."
41 end
42
43 def self.verbalize_groups(groups)
44 groups.map! {|g| "'#{g}'" }
45 group_list = [groups[0...-1].join(", "), groups[-1..-1]].
46 reject {|s| s.to_s.empty? }.join(" and ")
47 group_str = groups.size == 1 ? "group" : "groups"
48 "#{group_str} #{group_list}"
49 end
50
51 def self.select_spec(name, regex_match = nil)
52 specs = []
53 regexp = Regexp.new(name) if regex_match
54
55 Bundler.definition.specs.each do |spec|
56 return spec if spec.name == name
57 specs << spec if regexp && spec.name =~ regexp
58 end
59
60 case specs.count
61 when 0
62 dep_in_other_group = Bundler.definition.current_dependencies.find {|dep|dep.name == name }
63
64 if dep_in_other_group
65 raise GemNotFound, "Could not find gem '#{name}', because it's in the #{verbalize_groups(dep_in_other_group.groups)}, configured to be ignored."
66 else
67 raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies)
68 end
69 when 1
70 specs.first
71 else
72 ask_for_spec_from(specs)
73 end
74 rescue RegexpError
75 raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies)
76 end
77
78 def self.ask_for_spec_from(specs)
79 specs.each_with_index do |spec, index|
80 Bundler.ui.info "#{index.succ} : #{spec.name}", true
81 end
82 Bundler.ui.info "0 : - exit -", true
83
84 num = Bundler.ui.ask("> ").to_i
85 num > 0 ? specs[num - 1] : nil
86 end
87
88 def self.gem_not_found_message(missing_gem_name, alternatives)
89 require_relative "../similarity_detector"
90 message = "Could not find gem '#{missing_gem_name}'."
91 alternate_names = alternatives.map {|a| a.respond_to?(:name) ? a.name : a }
92 suggestions = SimilarityDetector.new(alternate_names).similar_word_list(missing_gem_name)
93 message += "\nDid you mean #{suggestions}?" if suggestions
94 message
95 end
96
97 def self.ensure_all_gems_in_lockfile!(names, locked_gems = Bundler.locked_gems)
98 return unless locked_gems
99
100 locked_names = locked_gems.specs.map(&:name).uniq
101 names.-(locked_names).each do |g|
102 raise GemNotFound, gem_not_found_message(g, locked_names)
103 end
104 end
105
106 def self.configure_gem_version_promoter(definition, options)
107 patch_level = patch_level_options(options)
108 patch_level << :patch if patch_level.empty? && Bundler.settings[:prefer_patch]
109 raise InvalidOption, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1
110
111 definition.gem_version_promoter.tap do |gvp|
112 gvp.level = patch_level.first || :major
113 gvp.strict = options[:strict] || options["filter-strict"]
114 end
115 end
116
117 def self.patch_level_options(options)
118 [:major, :minor, :patch].select {|v| options.keys.include?(v.to_s) }
119 end
120
121 def self.clean_after_install?
122 clean = Bundler.settings[:clean]
123 return clean unless clean.nil?
124 clean ||= Bundler.feature_flag.auto_clean_without_path? && Bundler.settings[:path].nil?
125 clean &&= !Bundler.use_system_gems?
126 clean
127 end
128 end
129 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Config < Thor
5 class_option :parseable, :type => :boolean, :banner => "Use minimal formatting for more parseable output"
6
7 def self.scope_options
8 method_option :global, :type => :boolean, :banner => "Only change the global config"
9 method_option :local, :type => :boolean, :banner => "Only change the local config"
10 end
11 private_class_method :scope_options
12
13 desc "base NAME [VALUE]", "The Bundler 1 config interface", :hide => true
14 scope_options
15 method_option :delete, :type => :boolean, :banner => "delete"
16 def base(name = nil, *value)
17 new_args =
18 if ARGV.size == 1
19 ["config", "list"]
20 elsif ARGV.include?("--delete")
21 ARGV.map {|arg| arg == "--delete" ? "unset" : arg }
22 elsif ARGV.include?("--global") || ARGV.include?("--local") || ARGV.size == 3
23 ["config", "set", *ARGV[1..-1]]
24 else
25 ["config", "get", ARGV[1]]
26 end
27
28 SharedHelpers.major_deprecation 3,
29 "Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle #{new_args.join(" ")}` instead."
30
31 Base.new(options, name, value, self).run
32 end
33
34 desc "list", "List out all configured settings"
35 def list
36 Base.new(options, nil, nil, self).run
37 end
38
39 desc "get NAME", "Returns the value for the given key"
40 def get(name)
41 Base.new(options, name, nil, self).run
42 end
43
44 desc "set NAME VALUE", "Sets the given value for the given key"
45 scope_options
46 def set(name, value, *value_)
47 Base.new(options, name, value_.unshift(value), self).run
48 end
49
50 desc "unset NAME", "Unsets the value for the given key"
51 scope_options
52 def unset(name)
53 options[:delete] = true
54 Base.new(options, name, nil, self).run
55 end
56
57 default_task :base
58
59 class Base
60 attr_reader :name, :value, :options, :scope, :thor
61
62 def initialize(options, name, value, thor)
63 @options = options
64 @name = name
65 value = Array(value)
66 @value = value.empty? ? nil : value.join(" ")
67 @thor = thor
68 validate_scope!
69 end
70
71 def run
72 unless name
73 warn_unused_scope "Ignoring --#{scope}"
74 confirm_all
75 return
76 end
77
78 if options[:delete]
79 if !explicit_scope? || scope != "global"
80 Bundler.settings.set_local(name, nil)
81 end
82 if !explicit_scope? || scope != "local"
83 Bundler.settings.set_global(name, nil)
84 end
85 return
86 end
87
88 if value.nil?
89 warn_unused_scope "Ignoring --#{scope} since no value to set was given"
90
91 if options[:parseable]
92 if value = Bundler.settings[name]
93 Bundler.ui.info("#{name}=#{value}")
94 end
95 return
96 end
97
98 confirm(name)
99 return
100 end
101
102 Bundler.ui.info(message) if message
103 Bundler.settings.send("set_#{scope}", name, new_value)
104 end
105
106 def confirm_all
107 if @options[:parseable]
108 thor.with_padding do
109 Bundler.settings.all.each do |setting|
110 val = Bundler.settings[setting]
111 Bundler.ui.info "#{setting}=#{val}"
112 end
113 end
114 else
115 Bundler.ui.confirm "Settings are listed in order of priority. The top value will be used.\n"
116 Bundler.settings.all.each do |setting|
117 Bundler.ui.confirm setting
118 show_pretty_values_for(setting)
119 Bundler.ui.confirm ""
120 end
121 end
122 end
123
124 def confirm(name)
125 Bundler.ui.confirm "Settings for `#{name}` in order of priority. The top value will be used"
126 show_pretty_values_for(name)
127 end
128
129 def new_value
130 pathname = Pathname.new(value)
131 if name.start_with?("local.") && pathname.directory?
132 pathname.expand_path.to_s
133 else
134 value
135 end
136 end
137
138 def message
139 locations = Bundler.settings.locations(name)
140 if @options[:parseable]
141 "#{name}=#{new_value}" if new_value
142 elsif scope == "global"
143 if !locations[:local].nil?
144 "Your application has set #{name} to #{locations[:local].inspect}. " \
145 "This will override the global value you are currently setting"
146 elsif locations[:env]
147 "You have a bundler environment variable for #{name} set to " \
148 "#{locations[:env].inspect}. This will take precedence over the global value you are setting"
149 elsif !locations[:global].nil? && locations[:global] != value
150 "You are replacing the current global value of #{name}, which is currently " \
151 "#{locations[:global].inspect}"
152 end
153 elsif scope == "local" && !locations[:local].nil? && locations[:local] != value
154 "You are replacing the current local value of #{name}, which is currently " \
155 "#{locations[:local].inspect}"
156 end
157 end
158
159 def show_pretty_values_for(setting)
160 thor.with_padding do
161 Bundler.settings.pretty_values_for(setting).each do |line|
162 Bundler.ui.info line
163 end
164 end
165 end
166
167 def explicit_scope?
168 @explicit_scope
169 end
170
171 def warn_unused_scope(msg)
172 return unless explicit_scope?
173 return if options[:parseable]
174
175 Bundler.ui.warn(msg)
176 end
177
178 def validate_scope!
179 @explicit_scope = true
180 scopes = %w[global local].select {|s| options[s] }
181 case scopes.size
182 when 0
183 @scope = inside_app? ? "local" : "global"
184 @explicit_scope = false
185 when 1
186 @scope = scopes.first
187 else
188 raise InvalidOption,
189 "The options #{scopes.join " and "} were specified. Please only use one of the switches at a time."
190 end
191 end
192
193 private
194
195 def inside_app?
196 Bundler.root
197 true
198 rescue GemfileNotFound
199 false
200 end
201 end
202 end
203 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Console
5 attr_reader :options, :group
6 def initialize(options, group)
7 @options = options
8 @group = group
9 end
10
11 def run
12 Bundler::SharedHelpers.major_deprecation 2, "bundle console will be replaced " \
13 "by `bin/console` generated by `bundle gem <name>`"
14
15 group ? Bundler.require(:default, *group.split(" ").map!(&:to_sym)) : Bundler.require
16 ARGV.clear
17
18 console = get_console(Bundler.settings[:console] || "irb")
19 console.start
20 end
21
22 def get_console(name)
23 require name
24 get_constant(name)
25 rescue LoadError
26 Bundler.ui.error "Couldn't load console #{name}, falling back to irb"
27 require "irb"
28 get_constant("irb")
29 end
30
31 def get_constant(name)
32 const_name = {
33 "pry" => :Pry,
34 "ripl" => :Ripl,
35 "irb" => :IRB,
36 }[name]
37 Object.const_get(const_name)
38 rescue NameError
39 Bundler.ui.error "Could not find constant #{const_name}"
40 exit 1
41 end
42 end
43 end
1 # frozen_string_literal: true
2
3 require "rbconfig"
4 require "shellwords"
5 require "fiddle"
6
7 module Bundler
8 class CLI::Doctor
9 DARWIN_REGEX = /\s+(.+) \(compatibility /.freeze
10 LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/.freeze
11
12 attr_reader :options
13
14 def initialize(options)
15 @options = options
16 end
17
18 def otool_available?
19 Bundler.which("otool")
20 end
21
22 def ldd_available?
23 Bundler.which("ldd")
24 end
25
26 def dylibs_darwin(path)
27 output = `/usr/bin/otool -L #{path.shellescape}`.chomp
28 dylibs = output.split("\n")[1..-1].map {|l| l.match(DARWIN_REGEX).captures[0] }.uniq
29 # ignore @rpath and friends
30 dylibs.reject {|dylib| dylib.start_with? "@" }
31 end
32
33 def dylibs_ldd(path)
34 output = `/usr/bin/ldd #{path.shellescape}`.chomp
35 output.split("\n").map do |l|
36 match = l.match(LDD_REGEX)
37 next if match.nil?
38 match.captures[0]
39 end.compact
40 end
41
42 def dylibs(path)
43 case RbConfig::CONFIG["host_os"]
44 when /darwin/
45 return [] unless otool_available?
46 dylibs_darwin(path)
47 when /(linux|solaris|bsd)/
48 return [] unless ldd_available?
49 dylibs_ldd(path)
50 else # Windows, etc.
51 Bundler.ui.warn("Dynamic library check not supported on this platform.")
52 []
53 end
54 end
55
56 def bundles_for_gem(spec)
57 Dir.glob("#{spec.full_gem_path}/**/*.bundle")
58 end
59
60 def check!
61 require_relative "check"
62 Bundler::CLI::Check.new({}).run
63 end
64
65 def run
66 Bundler.ui.level = "warn" if options[:quiet]
67 Bundler.settings.validate!
68 check!
69
70 definition = Bundler.definition
71 broken_links = {}
72
73 definition.specs.each do |spec|
74 bundles_for_gem(spec).each do |bundle|
75 bad_paths = dylibs(bundle).select do |f|
76 begin
77 Fiddle.dlopen(f)
78 false
79 rescue Fiddle::DLError
80 true
81 end
82 end
83 if bad_paths.any?
84 broken_links[spec] ||= []
85 broken_links[spec].concat(bad_paths)
86 end
87 end
88 end
89
90 permissions_valid = check_home_permissions
91
92 if broken_links.any?
93 message = "The following gems are missing OS dependencies:"
94 broken_links.map do |spec, paths|
95 paths.uniq.map do |path|
96 "\n * #{spec.name}: #{path}"
97 end
98 end.flatten.sort.each {|m| message += m }
99 raise ProductionError, message
100 elsif !permissions_valid
101 Bundler.ui.info "No issues found with the installed bundle"
102 end
103 end
104
105 private
106
107 def check_home_permissions
108 require "find"
109 files_not_readable_or_writable = []
110 files_not_rw_and_owned_by_different_user = []
111 files_not_owned_by_current_user_but_still_rw = []
112 broken_symlinks = []
113 Find.find(Bundler.bundle_path.to_s).each do |f|
114 if !File.exist?(f)
115 broken_symlinks << f
116 elsif !File.writable?(f) || !File.readable?(f)
117 if File.stat(f).uid != Process.uid
118 files_not_rw_and_owned_by_different_user << f
119 else
120 files_not_readable_or_writable << f
121 end
122 elsif File.stat(f).uid != Process.uid
123 files_not_owned_by_current_user_but_still_rw << f
124 end
125 end
126
127 ok = true
128
129 if broken_symlinks.any?
130 Bundler.ui.warn "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{broken_symlinks.join("\n - ")}"
131
132 ok = false
133 end
134
135 if files_not_owned_by_current_user_but_still_rw.any?
136 Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
137 "user, but are still readable/writable. These files are:\n - #{files_not_owned_by_current_user_but_still_rw.join("\n - ")}"
138
139 ok = false
140 end
141
142 if files_not_rw_and_owned_by_different_user.any?
143 Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
144 "user, and are not readable/writable. These files are:\n - #{files_not_rw_and_owned_by_different_user.join("\n - ")}"
145
146 ok = false
147 end
148
149 if files_not_readable_or_writable.any?
150 Bundler.ui.warn "Files exist in the Bundler home that are not " \
151 "readable/writable by the current user. These files are:\n - #{files_not_readable_or_writable.join("\n - ")}"
152
153 ok = false
154 end
155
156 ok
157 end
158 end
159 end
1 # frozen_string_literal: true
2
3 require_relative "../current_ruby"
4
5 module Bundler
6 class CLI::Exec
7 attr_reader :options, :args, :cmd
8
9 TRAPPED_SIGNALS = %w[INT].freeze
10
11 def initialize(options, args)
12 @options = options
13 @cmd = args.shift
14 @args = args
15 @args << { :close_others => !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby?
16 end
17
18 def run
19 validate_cmd!
20 SharedHelpers.set_bundle_environment
21 if bin_path = Bundler.which(cmd)
22 if !Bundler.settings[:disable_exec_load] && ruby_shebang?(bin_path)
23 return kernel_load(bin_path, *args)
24 end
25 kernel_exec(bin_path, *args)
26 else
27 # exec using the given command
28 kernel_exec(cmd, *args)
29 end
30 end
31
32 private
33
34 def validate_cmd!
35 return unless cmd.nil?
36 Bundler.ui.error "bundler: exec needs a command to run"
37 exit 128
38 end
39
40 def kernel_exec(*args)
41 Kernel.exec(*args)
42 rescue Errno::EACCES, Errno::ENOEXEC
43 Bundler.ui.error "bundler: not executable: #{cmd}"
44 exit 126
45 rescue Errno::ENOENT
46 Bundler.ui.error "bundler: command not found: #{cmd}"
47 Bundler.ui.warn "Install missing gem executables with `bundle install`"
48 exit 127
49 end
50
51 def kernel_load(file, *args)
52 args.pop if args.last.is_a?(Hash)
53 ARGV.replace(args)
54 $0 = file
55 Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle)
56 require_relative "../setup"
57 TRAPPED_SIGNALS.each {|s| trap(s, "DEFAULT") }
58 Kernel.load(file)
59 rescue SystemExit, SignalException
60 raise
61 rescue Exception # rubocop:disable Lint/RescueException
62 Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})"
63 Bundler::FriendlyErrors.disable!
64 raise
65 end
66
67 def process_title(file, args)
68 "#{file} #{args.join(" ")}".strip
69 end
70
71 def ruby_shebang?(file)
72 possibilities = [
73 "#!/usr/bin/env ruby\n",
74 "#!/usr/bin/env jruby\n",
75 "#!/usr/bin/env truffleruby\n",
76 "#!#{Gem.ruby}\n",
77 ]
78
79 if File.zero?(file)
80 Bundler.ui.warn "#{file} is empty"
81 return false
82 end
83
84 first_line = File.open(file, "rb") {|f| f.read(possibilities.map(&:size).max) }
85 possibilities.any? {|shebang| first_line.start_with?(shebang) }
86 end
87 end
88 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Fund
5 attr_reader :options
6
7 def initialize(options)
8 @options = options
9 end
10
11 def run
12 Bundler.definition.validate_runtime!
13
14 groups = Array(options[:group]).map(&:to_sym)
15
16 deps = if groups.any?
17 Bundler.definition.dependencies_for(groups)
18 else
19 Bundler.definition.current_dependencies
20 end
21
22 fund_info = deps.each_with_object([]) do |dep, arr|
23 spec = Bundler.definition.specs[dep.name].first
24 if spec.metadata.key?("funding_uri")
25 arr << "* #{spec.name} (#{spec.version})\n Funding: #{spec.metadata["funding_uri"]}"
26 end
27 end
28
29 if fund_info.empty?
30 Bundler.ui.info "None of the installed gems you directly depend on are looking for funding."
31 else
32 Bundler.ui.info fund_info.join("\n")
33 end
34 end
35 end
36 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Info
5 attr_reader :gem_name, :options
6 def initialize(options, gem_name)
7 @options = options
8 @gem_name = gem_name
9 end
10
11 def run
12 Bundler.ui.silence do
13 Bundler.definition.validate_runtime!
14 Bundler.load.lock
15 end
16
17 spec = spec_for_gem(gem_name)
18
19 if spec
20 return print_gem_path(spec) if @options[:path]
21 return print_gem_version(spec) if @options[:version]
22 print_gem_info(spec)
23 end
24 end
25
26 private
27
28 def spec_for_gem(gem_name)
29 spec = Bundler.definition.specs.find {|s| s.name == gem_name }
30 spec || default_gem_spec(gem_name) || Bundler::CLI::Common.select_spec(gem_name, :regex_match)
31 end
32
33 def default_gem_spec(gem_name)
34 return unless Gem::Specification.respond_to?(:find_all_by_name)
35 gem_spec = Gem::Specification.find_all_by_name(gem_name).last
36 return gem_spec if gem_spec && gem_spec.respond_to?(:default_gem?) && gem_spec.default_gem?
37 end
38
39 def spec_not_found(gem_name)
40 raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(gem_name, Bundler.definition.dependencies)
41 end
42
43 def print_gem_version(spec)
44 Bundler.ui.info spec.version.to_s
45 end
46
47 def print_gem_path(spec)
48 name = spec.name
49 if name == "bundler"
50 path = File.expand_path("../../..", __dir__)
51 else
52 path = spec.full_gem_path
53 if spec.deleted_gem?
54 return Bundler.ui.warn "The gem #{name} has been deleted. It was installed at: #{path}"
55 end
56 end
57
58 Bundler.ui.info path
59 end
60
61 def print_gem_info(spec)
62 metadata = spec.metadata
63 name = spec.name
64 gem_info = String.new
65 gem_info << " * #{name} (#{spec.version}#{spec.git_version})\n"
66 gem_info << "\tSummary: #{spec.summary}\n" if spec.summary
67 gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage
68 gem_info << "\tDocumentation: #{metadata["documentation_uri"]}\n" if metadata.key?("documentation_uri")
69 gem_info << "\tSource Code: #{metadata["source_code_uri"]}\n" if metadata.key?("source_code_uri")
70 gem_info << "\tFunding: #{metadata["funding_uri"]}\n" if metadata.key?("funding_uri")
71 gem_info << "\tWiki: #{metadata["wiki_uri"]}\n" if metadata.key?("wiki_uri")
72 gem_info << "\tChangelog: #{metadata["changelog_uri"]}\n" if metadata.key?("changelog_uri")
73 gem_info << "\tBug Tracker: #{metadata["bug_tracker_uri"]}\n" if metadata.key?("bug_tracker_uri")
74 gem_info << "\tMailing List: #{metadata["mailing_list_uri"]}\n" if metadata.key?("mailing_list_uri")
75 gem_info << "\tPath: #{spec.full_gem_path}\n"
76 gem_info << "\tDefault Gem: yes\n" if spec.respond_to?(:default_gem?) && spec.default_gem?
77 gem_info << "\tReverse Dependencies: \n\t\t#{gem_dependencies.join("\n\t\t")}" if gem_dependencies.any?
78
79 if name != "bundler" && spec.deleted_gem?
80 return Bundler.ui.warn "The gem #{name} has been deleted. Gemspec information is still available though:\n#{gem_info}"
81 end
82
83 Bundler.ui.info gem_info
84 end
85
86 def gem_dependencies
87 @gem_dependencies ||= Bundler.definition.specs.map do |spec|
88 dependency = spec.dependencies.find {|dep| dep.name == gem_name }
89 next unless dependency
90 "#{spec.name} (#{spec.version}) depends on #{gem_name} (#{dependency.requirements_list.join(", ")})"
91 end.compact.sort
92 end
93 end
94 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Init
5 attr_reader :options
6 def initialize(options)
7 @options = options
8 end
9
10 def run
11 if File.exist?(gemfile)
12 Bundler.ui.error "#{gemfile} already exists at #{File.expand_path(gemfile)}"
13 exit 1
14 end
15
16 unless File.writable?(Dir.pwd)
17 Bundler.ui.error "Can not create #{gemfile} as the current directory is not writable."
18 exit 1
19 end
20
21 if options[:gemspec]
22 gemspec = File.expand_path(options[:gemspec])
23 unless File.exist?(gemspec)
24 Bundler.ui.error "Gem specification #{gemspec} doesn't exist"
25 exit 1
26 end
27
28 spec = Bundler.load_gemspec_uncached(gemspec)
29
30 File.open(gemfile, "wb") do |file|
31 file << "# Generated from #{gemspec}\n"
32 file << spec.to_gemfile
33 end
34 else
35 FileUtils.cp(File.expand_path("../templates/#{gemfile}", __dir__), gemfile)
36 end
37
38 puts "Writing new #{gemfile} to #{SharedHelpers.pwd}/#{gemfile}"
39 end
40
41 private
42
43 def gemfile
44 @gemfile ||= Bundler.preferred_gemfile_name
45 end
46 end
47 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Inject
5 attr_reader :options, :name, :version, :group, :source, :gems
6 def initialize(options, name, version)
7 @options = options
8 @name = name
9 @version = version || last_version_number
10 @group = options[:group].split(",") unless options[:group].nil?
11 @source = options[:source]
12 @gems = []
13 end
14
15 def run
16 # The required arguments allow Thor to give useful feedback when the arguments
17 # are incorrect. This adds those first two arguments onto the list as a whole.
18 gems.unshift(source).unshift(group).unshift(version).unshift(name)
19
20 # Build an array of Dependency objects out of the arguments
21 deps = []
22 # when `inject` support addition of more than one gem, then this loop will
23 # help. Currently this loop is running once.
24 gems.each_slice(4) do |gem_name, gem_version, gem_group, gem_source|
25 ops = Gem::Requirement::OPS.map {|key, _val| key }
26 has_op = ops.any? {|op| gem_version.start_with? op }
27 gem_version = "~> #{gem_version}" unless has_op
28 deps << Bundler::Dependency.new(gem_name, gem_version, "group" => gem_group, "source" => gem_source)
29 end
30
31 added = Injector.inject(deps, options)
32
33 if added.any?
34 Bundler.ui.confirm "Added to Gemfile:"
35 Bundler.ui.confirm(added.map do |d|
36 name = "'#{d.name}'"
37 requirement = ", '#{d.requirement}'"
38 group = ", :group => #{d.groups.inspect}" if d.groups != Array(:default)
39 source = ", :source => '#{d.source}'" unless d.source.nil?
40 %(gem #{name}#{requirement}#{group}#{source})
41 end.join("\n"))
42 else
43 Bundler.ui.confirm "All gems were already present in the Gemfile"
44 end
45 end
46
47 private
48
49 def last_version_number
50 definition = Bundler.definition(true)
51 definition.resolve_remotely!
52 specs = definition.index[name].sort_by(&:version)
53 unless options[:pre]
54 specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? }
55 end
56 spec = specs.last
57 spec.version.to_s
58 end
59 end
60 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Install
5 attr_reader :options
6 def initialize(options)
7 @options = options
8 end
9
10 def run
11 Bundler.ui.level = "warn" if options[:quiet]
12
13 warn_if_root
14
15 Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed
16
17 Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Bundler::FREEBSD
18
19 # Disable color in deployment mode
20 Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment]
21
22 check_for_options_conflicts
23
24 check_trust_policy
25
26 if options[:deployment] || options[:frozen] || Bundler.frozen_bundle?
27 unless Bundler.default_lockfile.exist?
28 flag = "--deployment flag" if options[:deployment]
29 flag ||= "--frozen flag" if options[:frozen]
30 flag ||= "deployment setting"
31 raise ProductionError, "The #{flag} requires a #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}. Please make " \
32 "sure you have checked your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} into version control " \
33 "before deploying."
34 end
35
36 options[:local] = true if Bundler.app_cache.exist?
37
38 Bundler.settings.set_command_option :deployment, true if options[:deployment]
39 Bundler.settings.set_command_option :frozen, true if options[:frozen]
40 end
41
42 # When install is called with --no-deployment, disable deployment mode
43 if options[:deployment] == false
44 Bundler.settings.set_command_option :frozen, nil
45 options[:system] = true
46 end
47
48 normalize_settings
49
50 Bundler::Fetcher.disable_endpoint = options["full-index"]
51
52 if options["binstubs"]
53 Bundler::SharedHelpers.major_deprecation 2,
54 "The --binstubs option will be removed in favor of `bundle binstubs --all`"
55 end
56
57 Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins?
58
59 definition = Bundler.definition
60 definition.validate_runtime!
61
62 installer = Installer.install(Bundler.root, definition, options)
63
64 Bundler.settings.temporary(:cache_all_platforms => options[:local] ? false : Bundler.settings[:cache_all_platforms]) do
65 Bundler.load.cache(nil, options[:local]) if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.frozen_bundle?
66 end
67
68 Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}."
69 Bundler::CLI::Common.output_without_groups_message(:install)
70
71 if Bundler.use_system_gems?
72 Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed."
73 else
74 relative_path = Bundler.configured_bundle_path.base_path_relative_to_pwd
75 Bundler.ui.confirm "Bundled gems are installed into `#{relative_path}`"
76 end
77
78 Bundler::CLI::Common.output_post_install_messages installer.post_install_messages
79
80 warn_ambiguous_gems
81
82 if CLI::Common.clean_after_install?
83 require_relative "clean"
84 Bundler::CLI::Clean.new(options).run
85 end
86
87 Bundler::CLI::Common.output_fund_metadata_summary
88 rescue Gem::InvalidSpecificationException
89 Bundler.ui.warn "You have one or more invalid gemspecs that need to be fixed."
90 raise
91 end
92
93 private
94
95 def warn_if_root
96 return if Bundler.settings[:silence_root_warning] || Gem.win_platform? || !Process.uid.zero?
97 Bundler.ui.warn "Don't run Bundler as root. Bundler can ask for sudo " \
98 "if it is needed, and installing your bundle as root will break this " \
99 "application for all non-root users on this machine.", :wrap => true
100 end
101
102 def dependencies_count_for(definition)
103 count = definition.dependencies.count
104 "#{count} Gemfile #{count == 1 ? "dependency" : "dependencies"}"
105 end
106
107 def gems_installed_for(definition)
108 count = definition.specs.count
109 "#{count} #{count == 1 ? "gem" : "gems"} now installed"
110 end
111
112 def check_for_group_conflicts_in_cli_options
113 conflicting_groups = Array(options[:without]) & Array(options[:with])
114 return if conflicting_groups.empty?
115 raise InvalidOption, "You can't list a group in both with and without." \
116 " The offending groups are: #{conflicting_groups.join(", ")}."
117 end
118
119 def check_for_options_conflicts
120 if (options[:path] || options[:deployment]) && options[:system]
121 error_message = String.new
122 error_message << "You have specified both --path as well as --system. Please choose only one option.\n" if options[:path]
123 error_message << "You have specified both --deployment as well as --system. Please choose only one option.\n" if options[:deployment]
124 raise InvalidOption.new(error_message)
125 end
126 end
127
128 def check_trust_policy
129 trust_policy = options["trust-policy"]
130 unless Bundler.rubygems.security_policies.keys.unshift(nil).include?(trust_policy)
131 raise InvalidOption, "RubyGems doesn't know about trust policy '#{trust_policy}'. " \
132 "The known policies are: #{Bundler.rubygems.security_policies.keys.join(", ")}."
133 end
134 Bundler.settings.set_command_option_if_given :"trust-policy", trust_policy
135 end
136
137 def normalize_groups
138 check_for_group_conflicts_in_cli_options
139
140 # need to nil them out first to get around validation for backwards compatibility
141 Bundler.settings.set_command_option :without, nil
142 Bundler.settings.set_command_option :with, nil
143 Bundler.settings.set_command_option :without, options[:without]
144 Bundler.settings.set_command_option :with, options[:with]
145 end
146
147 def normalize_settings
148 Bundler.settings.set_command_option :path, nil if options[:system]
149 Bundler.settings.set_command_option_if_given :path, options[:path]
150
151 if options["standalone"] && Bundler.settings[:path].nil? && !options["local"]
152 Bundler.settings.temporary(:path_relative_to_cwd => false) do
153 Bundler.settings.set_command_option :path, "bundle"
154 end
155 end
156
157 bin_option = options["binstubs"]
158 bin_option = nil if bin_option && bin_option.empty?
159 Bundler.settings.set_command_option :bin, bin_option if options["binstubs"]
160
161 Bundler.settings.set_command_option_if_given :shebang, options["shebang"]
162
163 Bundler.settings.set_command_option_if_given :jobs, options["jobs"]
164
165 Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"]
166
167 Bundler.settings.set_command_option_if_given :no_install, options["no-install"]
168
169 Bundler.settings.set_command_option_if_given :clean, options["clean"]
170
171 normalize_groups if options[:without] || options[:with]
172
173 options[:force] = options[:redownload]
174 end
175
176 def warn_ambiguous_gems
177 # TODO: remove this when we drop Bundler 1.x support
178 Installer.ambiguous_gems.to_a.each do |name, installed_from_uri, *also_found_in_uris|
179 Bundler.ui.warn "Warning: the gem '#{name}' was found in multiple sources."
180 Bundler.ui.warn "Installed from: #{installed_from_uri}"
181 Bundler.ui.warn "Also found in:"
182 also_found_in_uris.each {|uri| Bundler.ui.warn " * #{uri}" }
183 Bundler.ui.warn "You should add a source requirement to restrict this gem to your preferred source."
184 Bundler.ui.warn "For example:"
185 Bundler.ui.warn " gem '#{name}', :source => '#{installed_from_uri}'"
186 Bundler.ui.warn "Then uninstall the gem '#{name}' (or delete all bundled gems) and then install again."
187 end
188 end
189 end
190 end
1 # frozen_string_literal: true
2
3 require "rbconfig"
4
5 module Bundler
6 class CLI::Issue
7 def run
8 Bundler.ui.info <<-EOS.gsub(/^ {8}/, "")
9 Did you find an issue with Bundler? Before filing a new issue,
10 be sure to check out these resources:
11
12 1. Check out our troubleshooting guide for quick fixes to common issues:
13 https://github.com/rubygems/rubygems/blob/master/bundler/doc/TROUBLESHOOTING.md
14
15 2. Instructions for common Bundler uses can be found on the documentation
16 site: https://bundler.io/
17
18 3. Information about each Bundler command can be found in the Bundler
19 man pages: https://bundler.io/man/bundle.1.html
20
21 Hopefully the troubleshooting steps above resolved your problem! If things
22 still aren't working the way you expect them to, please let us know so
23 that we can diagnose and help fix the problem you're having, by filling
24 in the new issue form located at
25 https://github.com/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md,
26 and copy and pasting the information below.
27
28 EOS
29
30 Bundler.ui.info Bundler::Env.report
31
32 Bundler.ui.info "\n## Bundle Doctor"
33 doctor
34 end
35
36 def doctor
37 require_relative "doctor"
38 Bundler::CLI::Doctor.new({}).run
39 end
40 end
41 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::List
5 def initialize(options)
6 @options = options
7 @without_group = options["without-group"].map(&:to_sym)
8 @only_group = options["only-group"].map(&:to_sym)
9 end
10
11 def run
12 raise InvalidOption, "The `--only-group` and `--without-group` options cannot be used together" if @only_group.any? && @without_group.any?
13
14 raise InvalidOption, "The `--name-only` and `--paths` options cannot be used together" if @options["name-only"] && @options[:paths]
15
16 specs = if @only_group.any? || @without_group.any?
17 filtered_specs_by_groups
18 else
19 begin
20 Bundler.load.specs
21 rescue GemNotFound => e
22 Bundler.ui.error e.message
23 Bundler.ui.warn "Install missing gems with `bundle install`."
24 exit 1
25 end
26 end.reject {|s| s.name == "bundler" }.sort_by(&:name)
27
28 return Bundler.ui.info "No gems in the Gemfile" if specs.empty?
29
30 return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"]
31 return specs.each {|s| Bundler.ui.info s.full_gem_path } if @options["paths"]
32
33 Bundler.ui.info "Gems included by the bundle:"
34
35 specs.each {|s| Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" }
36
37 Bundler.ui.info "Use `bundle info` to print more detailed information about a gem"
38 end
39
40 private
41
42 def verify_group_exists(groups)
43 (@without_group + @only_group).each do |group|
44 raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group)
45 end
46 end
47
48 def filtered_specs_by_groups
49 definition = Bundler.definition
50 groups = definition.groups
51
52 verify_group_exists(groups)
53
54 show_groups =
55 if @without_group.any?
56 groups.reject {|g| @without_group.include?(g) }
57 elsif @only_group.any?
58 groups.select {|g| @only_group.include?(g) }
59 else
60 groups
61 end.map(&:to_sym)
62
63 definition.specs_for(show_groups)
64 end
65 end
66 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Lock
5 attr_reader :options
6
7 def initialize(options)
8 @options = options
9 end
10
11 def run
12 unless Bundler.default_gemfile
13 Bundler.ui.error "Unable to find a Gemfile to lock"
14 exit 1
15 end
16
17 print = options[:print]
18 ui = Bundler.ui
19 Bundler.ui = UI::Silent.new if print
20
21 Bundler::Fetcher.disable_endpoint = options["full-index"]
22
23 update = options[:update]
24 conservative = options[:conservative]
25
26 if update.is_a?(Array) # unlocking specific gems
27 Bundler::CLI::Common.ensure_all_gems_in_lockfile!(update)
28 update = { :gems => update, :conservative => conservative }
29 elsif update
30 update = { :conservative => conservative } if conservative
31 end
32 definition = Bundler.definition(update)
33
34 Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) if options[:update]
35
36 options["remove-platform"].each do |platform|
37 definition.remove_platform(platform)
38 end
39
40 options["add-platform"].each do |platform_string|
41 platform = Gem::Platform.new(platform_string)
42 if platform.to_s == "unknown"
43 Bundler.ui.warn "The platform `#{platform_string}` is unknown to RubyGems " \
44 "and adding it will likely lead to resolution errors"
45 end
46 definition.add_platform(platform)
47 end
48
49 if definition.platforms.empty?
50 raise InvalidOption, "Removing all platforms from the bundle is not allowed"
51 end
52
53 definition.resolve_remotely! unless options[:local]
54
55 if print
56 puts definition.to_lock
57 else
58 file = options[:lockfile]
59 file = file ? File.expand_path(file) : Bundler.default_lockfile
60 puts "Writing lockfile to #{file}"
61 definition.lock(file)
62 end
63
64 Bundler.ui = ui
65 end
66 end
67 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Open
5 attr_reader :options, :name
6 def initialize(options, name)
7 @options = options
8 @name = name
9 end
10
11 def run
12 editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }
13 return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor
14 return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match)
15 if spec.default_gem?
16 Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist."
17 else
18 path = spec.full_gem_path
19 Dir.chdir(path) do
20 require "shellwords"
21 command = Shellwords.split(editor) + [path]
22 Bundler.with_original_env do
23 system(*command)
24 end || Bundler.ui.info("Could not run '#{command.join(" ")}'")
25 end
26 end
27 end
28 end
29 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Outdated
5 attr_reader :options, :gems, :options_include_groups, :filter_options_patch, :sources, :strict
6 attr_accessor :outdated_gems
7
8 def initialize(options, gems)
9 @options = options
10 @gems = gems
11 @sources = Array(options[:source])
12
13 @filter_options_patch = options.keys & %w[filter-major filter-minor filter-patch]
14
15 @outdated_gems = []
16
17 @options_include_groups = [:group, :groups].any? do |v|
18 options.keys.include?(v.to_s)
19 end
20
21 # the patch level options imply strict is also true. It wouldn't make
22 # sense otherwise.
23 @strict = options["filter-strict"] || Bundler::CLI::Common.patch_level_options(options).any?
24 end
25
26 def run
27 check_for_deployment_mode!
28
29 gems.each do |gem_name|
30 Bundler::CLI::Common.select_spec(gem_name)
31 end
32
33 Bundler.definition.validate_runtime!
34 current_specs = Bundler.ui.silence { Bundler.definition.resolve }
35
36 current_dependencies = Bundler.ui.silence do
37 Bundler.load.dependencies.map {|dep| [dep.name, dep] }.to_h
38 end
39
40 definition = if gems.empty? && sources.empty?
41 # We're doing a full update
42 Bundler.definition(true)
43 else
44 Bundler.definition(:gems => gems, :sources => sources)
45 end
46
47 Bundler::CLI::Common.configure_gem_version_promoter(
48 Bundler.definition,
49 options.merge(:strict => @strict)
50 )
51
52 definition_resolution = proc do
53 options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely!
54 end
55
56 if options[:parseable]
57 Bundler.ui.silence(&definition_resolution)
58 else
59 definition_resolution.call
60 end
61
62 Bundler.ui.info ""
63
64 # Loop through the current specs
65 gemfile_specs, dependency_specs = current_specs.partition do |spec|
66 current_dependencies.key? spec.name
67 end
68
69 specs = if options["only-explicit"]
70 gemfile_specs
71 else
72 gemfile_specs + dependency_specs
73 end
74
75 specs.sort_by(&:name).uniq(&:name).each do |current_spec|
76 next unless gems.empty? || gems.include?(current_spec.name)
77
78 active_spec = retrieve_active_spec(definition, current_spec)
79 next unless active_spec
80
81 next unless filter_options_patch.empty? || update_present_via_semver_portions(current_spec, active_spec, options)
82
83 gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version)
84 next unless gem_outdated || (current_spec.git_version != active_spec.git_version)
85
86 dependency = current_dependencies[current_spec.name]
87 groups = ""
88 if dependency && !options[:parseable]
89 groups = dependency.groups.join(", ")
90 end
91
92 outdated_gems << {
93 :active_spec => active_spec,
94 :current_spec => current_spec,
95 :dependency => dependency,
96 :groups => groups,
97 }
98 end
99
100 if outdated_gems.empty?
101 unless options[:parseable]
102 Bundler.ui.info(nothing_outdated_message)
103 end
104 else
105 if options_include_groups
106 relevant_outdated_gems = outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems|
107 contains_group = groups.split(", ").include?(options[:group])
108 next unless options[:groups] || contains_group
109
110 gems
111 end.compact
112
113 if options[:parseable]
114 relevant_outdated_gems.each do |gems|
115 print_gems(gems)
116 end
117 else
118 print_gems_table(relevant_outdated_gems)
119 end
120 elsif options[:parseable]
121 print_gems(outdated_gems)
122 else
123 print_gems_table(outdated_gems)
124 end
125
126 exit 1
127 end
128 end
129
130 private
131
132 def loaded_from_for(spec)
133 return unless spec.respond_to?(:loaded_from)
134
135 spec.loaded_from
136 end
137
138 def groups_text(group_text, groups)
139 "#{group_text}#{groups.split(",").size > 1 ? "s" : ""} \"#{groups}\""
140 end
141
142 def nothing_outdated_message
143 if filter_options_patch.any?
144 display = filter_options_patch.map do |o|
145 o.sub("filter-", "")
146 end.join(" or ")
147
148 "No #{display} updates to display.\n"
149 else
150 "Bundle up to date!\n"
151 end
152 end
153
154 def retrieve_active_spec(definition, current_spec)
155 active_spec = definition.resolve.find_by_name_and_platform(current_spec.name, current_spec.platform)
156 return unless active_spec
157
158 return active_spec if strict
159
160 active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.match_platform(current_spec.platform) }.sort_by(&:version)
161 if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1
162 active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? }
163 end
164 active_specs.last
165 end
166
167 def print_gems(gems_list)
168 gems_list.each do |gem|
169 print_gem(
170 gem[:current_spec],
171 gem[:active_spec],
172 gem[:dependency],
173 gem[:groups],
174 )
175 end
176 end
177
178 def print_gems_table(gems_list)
179 data = gems_list.map do |gem|
180 gem_column_for(
181 gem[:current_spec],
182 gem[:active_spec],
183 gem[:dependency],
184 gem[:groups],
185 )
186 end
187
188 print_indented([table_header] + data)
189 end
190
191 def print_gem(current_spec, active_spec, dependency, groups)
192 spec_version = "#{active_spec.version}#{active_spec.git_version}"
193 if Bundler.ui.debug?
194 loaded_from = loaded_from_for(active_spec)
195 spec_version += " (from #{loaded_from})" if loaded_from
196 end
197 current_version = "#{current_spec.version}#{current_spec.git_version}"
198
199 if dependency && dependency.specific?
200 dependency_version = %(, requested #{dependency.requirement})
201 end
202
203 spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \
204 "installed #{current_version}#{dependency_version})"
205
206 output_message = if options[:parseable]
207 spec_outdated_info.to_s
208 elsif options_include_groups || groups.empty?
209 " * #{spec_outdated_info}"
210 else
211 " * #{spec_outdated_info} in #{groups_text("group", groups)}"
212 end
213
214 Bundler.ui.info output_message.rstrip
215 end
216
217 def gem_column_for(current_spec, active_spec, dependency, groups)
218 current_version = "#{current_spec.version}#{current_spec.git_version}"
219 spec_version = "#{active_spec.version}#{active_spec.git_version}"
220 dependency = dependency.requirement if dependency
221
222 ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s]
223 ret_val << loaded_from_for(active_spec).to_s if Bundler.ui.debug?
224 ret_val
225 end
226
227 def check_for_deployment_mode!
228 return unless Bundler.frozen_bundle?
229 suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any?
230 "bundle config unset frozen"
231 elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any?
232 "bundle config unset deployment"
233 end
234 raise ProductionError, "You are trying to check outdated gems in " \
235 "deployment mode. Run `bundle outdated` elsewhere.\n" \
236 "\nIf this is a development machine, remove the " \
237 "#{Bundler.default_gemfile} freeze" \
238 "\nby running `#{suggested_command}`."
239 end
240
241 def update_present_via_semver_portions(current_spec, active_spec, options)
242 current_major = current_spec.version.segments.first
243 active_major = active_spec.version.segments.first
244
245 update_present = false
246 update_present = active_major > current_major if options["filter-major"]
247
248 if !update_present && (options["filter-minor"] || options["filter-patch"]) && current_major == active_major
249 current_minor = get_version_semver_portion_value(current_spec, 1)
250 active_minor = get_version_semver_portion_value(active_spec, 1)
251
252 update_present = active_minor > current_minor if options["filter-minor"]
253
254 if !update_present && options["filter-patch"] && current_minor == active_minor
255 current_patch = get_version_semver_portion_value(current_spec, 2)
256 active_patch = get_version_semver_portion_value(active_spec, 2)
257
258 update_present = active_patch > current_patch
259 end
260 end
261
262 update_present
263 end
264
265 def get_version_semver_portion_value(spec, version_portion_index)
266 version_section = spec.version.segments[version_portion_index, 1]
267 version_section.to_a[0].to_i
268 end
269
270 def print_indented(matrix)
271 header = matrix[0]
272 data = matrix[1..-1]
273
274 column_sizes = Array.new(header.size) do |index|
275 matrix.max_by {|row| row[index].length }[index].length
276 end
277
278 Bundler.ui.info justify(header, column_sizes)
279
280 data.sort_by! {|row| row[0] }
281
282 data.each do |row|
283 Bundler.ui.info justify(row, column_sizes)
284 end
285 end
286
287 def table_header
288 header = ["Gem", "Current", "Latest", "Requested", "Groups"]
289 header << "Path" if Bundler.ui.debug?
290 header
291 end
292
293 def justify(row, sizes)
294 row.each_with_index.map do |element, index|
295 element.ljust(sizes[index])
296 end.join(" ").strip + "\n"
297 end
298 end
299 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Platform
5 attr_reader :options
6 def initialize(options)
7 @options = options
8 end
9
10 def run
11 platforms, ruby_version = Bundler.ui.silence do
12 locked_ruby_version = Bundler.locked_gems && Bundler.locked_gems.ruby_version&.gsub(/p\d+\Z/, "")
13 gemfile_ruby_version = Bundler.definition.ruby_version && Bundler.definition.ruby_version.single_version_string
14 [Bundler.definition.platforms.map {|p| "* #{p}" },
15 locked_ruby_version || gemfile_ruby_version]
16 end
17 output = []
18
19 if options[:ruby]
20 if ruby_version
21 output << ruby_version
22 else
23 output << "No ruby version specified"
24 end
25 else
26 output << "Your platform is: #{Gem::Platform.local}"
27 output << "Your app has gems that work on these platforms:\n#{platforms.join("\n")}"
28
29 if ruby_version
30 output << "Your Gemfile specifies a Ruby version requirement:\n* #{ruby_version}"
31
32 begin
33 Bundler.definition.validate_runtime!
34 output << "Your current platform satisfies the Ruby version requirement."
35 rescue RubyVersionMismatch => e
36 output << e.message
37 end
38 else
39 output << "Your Gemfile does not specify a Ruby version requirement."
40 end
41 end
42
43 Bundler.ui.info output.join("\n\n")
44 end
45 end
46 end
1 # frozen_string_literal: true
2
3 require_relative "../vendored_thor"
4 module Bundler
5 class CLI::Plugin < Thor
6 desc "install PLUGINS", "Install the plugin from the source"
7 long_desc <<-D
8 Install plugins either from the rubygems source provided (with --source option) or from a git source provided with --git (for remote repos) or --local_git (for local repos). If no sources are provided, it uses Gem.sources
9 D
10 method_option "source", :type => :string, :default => nil, :banner =>
11 "URL of the RubyGems source to fetch the plugin from"
12 method_option "version", :type => :string, :default => nil, :banner =>
13 "The version of the plugin to fetch"
14 method_option "git", :type => :string, :default => nil, :banner =>
15 "URL of the git repo to fetch from"
16 method_option "local_git", :type => :string, :default => nil, :banner =>
17 "Path of the local git repo to fetch from"
18 method_option "branch", :type => :string, :default => nil, :banner =>
19 "The git branch to checkout"
20 method_option "ref", :type => :string, :default => nil, :banner =>
21 "The git revision to check out"
22 def install(*plugins)
23 Bundler::Plugin.install(plugins, options)
24 end
25
26 desc "uninstall PLUGINS", "Uninstall the plugins"
27 long_desc <<-D
28 Uninstall given list of plugins. To uninstall all the plugins, use -all option.
29 D
30 method_option "all", :type => :boolean, :default => nil, :banner =>
31 "Uninstall all the installed plugins. If no plugin is installed, then it does nothing."
32 def uninstall(*plugins)
33 Bundler::Plugin.uninstall(plugins, options)
34 end
35
36 desc "list", "List the installed plugins and available commands"
37 def list
38 Bundler::Plugin.list
39 end
40 end
41 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Pristine
5 def initialize(gems)
6 @gems = gems
7 end
8
9 def run
10 CLI::Common.ensure_all_gems_in_lockfile!(@gems)
11 definition = Bundler.definition
12 definition.validate_runtime!
13 installer = Bundler::Installer.new(Bundler.root, definition)
14
15 Bundler.load.specs.each do |spec|
16 next if spec.name == "bundler" # Source::Rubygems doesn't install bundler
17 next if !@gems.empty? && !@gems.include?(spec.name)
18
19 gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})"
20 gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY
21
22 case source = spec.source
23 when Source::Rubygems
24 cached_gem = spec.cache_file
25 unless File.exist?(cached_gem)
26 Bundler.ui.error("Failed to pristine #{gem_name}. Cached gem #{cached_gem} does not exist.")
27 next
28 end
29
30 FileUtils.rm_rf spec.full_gem_path
31 when Source::Git
32 if source.local?
33 Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is locally overridden.")
34 next
35 end
36
37 source.remote!
38 if extension_cache_path = source.extension_cache_path(spec)
39 FileUtils.rm_rf extension_cache_path
40 end
41 FileUtils.rm_rf spec.extension_dir
42 FileUtils.rm_rf spec.full_gem_path
43 else
44 Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.")
45 next
46 end
47
48 Bundler::GemInstaller.new(spec, installer, false, 0, true).install_from_spec
49 end
50 end
51 end
52 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Remove
5 def initialize(gems, options)
6 @gems = gems
7 @options = options
8 end
9
10 def run
11 raise InvalidOption, "Please specify gems to remove." if @gems.empty?
12
13 Injector.remove(@gems, {})
14 Installer.install(Bundler.root, Bundler.definition)
15 end
16 end
17 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Show
5 attr_reader :options, :gem_name, :latest_specs
6 def initialize(options, gem_name)
7 @options = options
8 @gem_name = gem_name
9 @verbose = options[:verbose] || options[:outdated]
10 @latest_specs = fetch_latest_specs if @verbose
11 end
12
13 def run
14 Bundler.ui.silence do
15 Bundler.definition.validate_runtime!
16 Bundler.load.lock
17 end
18
19 if gem_name
20 if gem_name == "bundler"
21 path = File.expand_path("../../..", __dir__)
22 else
23 spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match)
24 return unless spec
25 path = spec.full_gem_path
26 unless File.directory?(path)
27 return Bundler.ui.warn "The gem #{gem_name} has been deleted. It was installed at: #{path}"
28 end
29 end
30 return Bundler.ui.info(path)
31 end
32
33 if options[:paths]
34 Bundler.load.specs.sort_by(&:name).map do |s|
35 Bundler.ui.info s.full_gem_path
36 end
37 else
38 Bundler.ui.info "Gems included by the bundle:"
39 Bundler.load.specs.sort_by(&:name).each do |s|
40 desc = " * #{s.name} (#{s.version}#{s.git_version})"
41 if @verbose
42 latest = latest_specs.find {|l| l.name == s.name }
43 Bundler.ui.info <<-END.gsub(/^ +/, "")
44 #{desc}
45 \tSummary: #{s.summary || "No description available."}
46 \tHomepage: #{s.homepage || "No website available."}
47 \tStatus: #{outdated?(s, latest) ? "Outdated - #{s.version} < #{latest.version}" : "Up to date"}
48 END
49 else
50 Bundler.ui.info desc
51 end
52 end
53 end
54 end
55
56 private
57
58 def fetch_latest_specs
59 definition = Bundler.definition(true)
60 if options[:outdated]
61 Bundler.ui.info "Fetching remote specs for outdated check...\n\n"
62 Bundler.ui.silence { definition.resolve_remotely! }
63 else
64 definition.resolve_with_cache!
65 end
66 Bundler.reset!
67 definition.specs
68 end
69
70 def outdated?(current, latest)
71 return false unless latest
72 Gem::Version.new(current.version) < Gem::Version.new(latest.version)
73 end
74 end
75 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Update
5 attr_reader :options, :gems
6 def initialize(options, gems)
7 @options = options
8 @gems = gems
9 end
10
11 def run
12 Bundler.ui.level = "warn" if options[:quiet]
13
14 update_bundler = options[:bundler]
15
16 Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler
17
18 Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins?
19
20 sources = Array(options[:source])
21 groups = Array(options[:group]).map(&:to_sym)
22
23 full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !update_bundler
24
25 if full_update && !options[:all]
26 if Bundler.feature_flag.update_requires_all_flag?
27 raise InvalidOption, "To update everything, pass the `--all` flag."
28 end
29 SharedHelpers.major_deprecation 3, "Pass --all to `bundle update` to update everything"
30 elsif !full_update && options[:all]
31 raise InvalidOption, "Cannot specify --all along with specific options."
32 end
33
34 conservative = options[:conservative]
35
36 if full_update
37 if conservative
38 Bundler.definition(:conservative => conservative)
39 else
40 Bundler.definition(true)
41 end
42 else
43 unless Bundler.default_lockfile.exist?
44 raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \
45 "Run `bundle install` to update and install the bundled gems."
46 end
47 Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems)
48
49 if groups.any?
50 deps = Bundler.definition.dependencies.select {|d| (d.groups & groups).any? }
51 gems.concat(deps.map(&:name))
52 end
53
54 Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby],
55 :conservative => conservative,
56 :bundler => update_bundler)
57 end
58
59 Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options)
60
61 Bundler::Fetcher.disable_endpoint = options["full-index"]
62
63 opts = options.dup
64 opts["update"] = true
65 opts["local"] = options[:local]
66
67 Bundler.settings.set_command_option_if_given :jobs, opts["jobs"]
68
69 Bundler.definition.validate_runtime!
70
71 if locked_gems = Bundler.definition.locked_gems
72 previous_locked_info = locked_gems.specs.reduce({}) do |h, s|
73 h[s.name] = { :spec => s, :version => s.version, :source => s.source.identifier }
74 h
75 end
76 end
77
78 installer = Installer.install Bundler.root, Bundler.definition, opts
79 Bundler.load.cache if Bundler.app_cache.exist?
80
81 if CLI::Common.clean_after_install?
82 require_relative "clean"
83 Bundler::CLI::Clean.new(options).run
84 end
85
86 if locked_gems
87 gems.each do |name|
88 locked_info = previous_locked_info[name]
89 next unless locked_info
90
91 locked_spec = locked_info[:spec]
92 new_spec = Bundler.definition.specs[name].first
93 unless new_spec
94 unless locked_spec.match_platform(Bundler.local_platform)
95 Bundler.ui.warn "Bundler attempted to update #{name} but it was not considered because it is for a different platform from the current one"
96 end
97
98 next
99 end
100
101 locked_source = locked_info[:source]
102 new_source = new_spec.source.identifier
103 next if locked_source != new_source
104
105 new_version = new_spec.version
106 locked_version = locked_info[:version]
107 if new_version < locked_version
108 Bundler.ui.warn "Note: #{name} version regressed from #{locked_version} to #{new_version}"
109 elsif new_version == locked_version
110 Bundler.ui.warn "Bundler attempted to update #{name} but its version stayed the same"
111 end
112 end
113 end
114
115 Bundler.ui.confirm "Bundle updated!"
116 Bundler::CLI::Common.output_without_groups_message(:update)
117 Bundler::CLI::Common.output_post_install_messages installer.post_install_messages
118
119 Bundler::CLI::Common.output_fund_metadata_summary
120 end
121 end
122 end
1 # frozen_string_literal: true
2
3 module Bundler
4 class CLI::Viz
5 attr_reader :options, :gem_name
6 def initialize(options)
7 @options = options
8 end
9
10 def run
11 # make sure we get the right `graphviz`. There is also a `graphviz`
12 # gem we're not built to support
13 gem "ruby-graphviz"
14 require "graphviz"
15
16 options[:without] = options[:without].join(":").tr(" ", ":").split(":")
17 output_file = File.expand_path(options[:file])
18
19 graph = Graph.new(Bundler.load, output_file, options[:version], options[:requirements], options[:format], options[:without])
20 graph.viz
21 rescue LoadError => e
22 Bundler.ui.error e.inspect
23 Bundler.ui.warn "Make sure you have the graphviz ruby gem. You can install it with:"
24 Bundler.ui.warn "`gem install ruby-graphviz`"
25 rescue StandardError => e
26 raise unless e.message =~ /GraphViz not installed or dot not in PATH/
27 Bundler.ui.error e.message
28 Bundler.ui.warn "Please install GraphViz. On a Mac with Homebrew, you can run `brew install graphviz`."
29 end
30 end
31 end
1 # frozen_string_literal: true
2
3 require "pathname"
4 require "set"
5
6 module Bundler
7 class CompactIndexClient
8 DEBUG_MUTEX = Thread::Mutex.new
9 def self.debug
10 return unless ENV["DEBUG_COMPACT_INDEX"]
11 DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") }
12 end
13
14 class Error < StandardError; end
15
16 require_relative "compact_index_client/cache"
17 require_relative "compact_index_client/updater"
18
19 attr_reader :directory
20
21 def initialize(directory, fetcher)
22 @directory = Pathname.new(directory)
23 @updater = Updater.new(fetcher)
24 @cache = Cache.new(@directory)
25 @endpoints = Set.new
26 @info_checksums_by_name = {}
27 @parsed_checksums = false
28 @mutex = Thread::Mutex.new
29 end
30
31 def execution_mode=(block)
32 Bundler::CompactIndexClient.debug { "execution_mode=" }
33 @endpoints = Set.new
34
35 @execution_mode = block
36 end
37
38 # @return [Lambda] A lambda that takes an array of inputs and a block, and
39 # maps the inputs with the block in parallel.
40 #
41 def execution_mode
42 @execution_mode || sequentially
43 end
44
45 def sequential_execution_mode!
46 self.execution_mode = sequentially
47 end
48
49 def sequentially
50 @sequentially ||= lambda do |inputs, &blk|
51 inputs.map(&blk)
52 end
53 end
54
55 def names
56 Bundler::CompactIndexClient.debug { "/names" }
57 update(@cache.names_path, "names")
58 @cache.names
59 end
60
61 def versions
62 Bundler::CompactIndexClient.debug { "/versions" }
63 update(@cache.versions_path, "versions")
64 versions, @info_checksums_by_name = @cache.versions
65 versions
66 end
67
68 def dependencies(names)
69 Bundler::CompactIndexClient.debug { "dependencies(#{names})" }
70 execution_mode.call(names) do |name|
71 update_info(name)
72 @cache.dependencies(name).map {|d| d.unshift(name) }
73 end.flatten(1)
74 end
75
76 def update_and_parse_checksums!
77 Bundler::CompactIndexClient.debug { "update_and_parse_checksums!" }
78 return @info_checksums_by_name if @parsed_checksums
79 update(@cache.versions_path, "versions")
80 @info_checksums_by_name = @cache.checksums
81 @parsed_checksums = true
82 end
83
84 private
85
86 def update(local_path, remote_path)
87 Bundler::CompactIndexClient.debug { "update(#{local_path}, #{remote_path})" }
88 unless synchronize { @endpoints.add?(remote_path) }
89 Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" }
90 return
91 end
92 @updater.update(local_path, url(remote_path))
93 end
94
95 def update_info(name)
96 Bundler::CompactIndexClient.debug { "update_info(#{name})" }
97 path = @cache.info_path(name)
98 checksum = @updater.checksum_for_file(path)
99 unless existing = @info_checksums_by_name[name]
100 Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since it is missing from versions" }
101 return
102 end
103 if checksum == existing
104 Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since the versions checksum matches the local checksum" }
105 return
106 end
107 Bundler::CompactIndexClient.debug { "updating info for #{name} since the versions checksum #{existing} != the local checksum #{checksum}" }
108 update(path, "info/#{name}")
109 end
110
111 def url(path)
112 path
113 end
114
115 def synchronize
116 @mutex.synchronize { yield }
117 end
118 end
119 end
1 # frozen_string_literal: true
2
3 require_relative "gem_parser"
4
5 module Bundler
6 class CompactIndexClient
7 class Cache
8 attr_reader :directory
9
10 def initialize(directory)
11 @directory = Pathname.new(directory).expand_path
12 info_roots.each do |dir|
13 SharedHelpers.filesystem_access(dir) do
14 FileUtils.mkdir_p(dir)
15 end
16 end
17 end
18
19 def names
20 lines(names_path)
21 end
22
23 def names_path
24 directory.join("names")
25 end
26
27 def versions
28 versions_by_name = Hash.new {|hash, key| hash[key] = [] }
29 info_checksums_by_name = {}
30
31 lines(versions_path).each do |line|
32 name, versions_string, info_checksum = line.split(" ", 3)
33 info_checksums_by_name[name] = info_checksum || ""
34 versions_string.split(",").each do |version|
35 if version.start_with?("-")
36 version = version[1..-1].split("-", 2).unshift(name)
37 versions_by_name[name].delete(version)
38 else
39 version = version.split("-", 2).unshift(name)
40 versions_by_name[name] << version
41 end
42 end
43 end
44
45 [versions_by_name, info_checksums_by_name]
46 end
47
48 def versions_path
49 directory.join("versions")
50 end
51
52 def checksums
53 checksums = {}
54
55 lines(versions_path).each do |line|
56 name, _, checksum = line.split(" ", 3)
57 checksums[name] = checksum
58 end
59
60 checksums
61 end
62
63 def dependencies(name)
64 lines(info_path(name)).map do |line|
65 parse_gem(line)
66 end
67 end
68
69 def info_path(name)
70 name = name.to_s
71 if name =~ /[^a-z0-9_-]/
72 name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}"
73 info_roots.last.join(name)
74 else
75 info_roots.first.join(name)
76 end
77 end
78
79 private
80
81 def lines(path)
82 return [] unless path.file?
83 lines = SharedHelpers.filesystem_access(path, :read, &:read).split("\n")
84 header = lines.index("---")
85 header ? lines[header + 1..-1] : lines
86 end
87
88 def parse_gem(line)
89 @dependency_parser ||= GemParser.new
90 @dependency_parser.parse(line)
91 end
92
93 def info_roots
94 [
95 directory.join("info"),
96 directory.join("info-special-characters"),
97 ]
98 end
99 end
100 end
101 end