appraisal2 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 1.latest | ✅ |
8
+
9
+ ## Security contact information
10
+
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
13
+ Tidelift will coordinate the fix and disclosure.
14
+
15
+ ## Additional Support
16
+
17
+ If you are interested in support for versions older than the latest release,
18
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
+ or find other sponsorship links in the [README].
20
+
21
+ [README]: README.md
data/exe/appraisal ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+ require "appraisal2"
7
+ require "appraisal/cli"
8
+
9
+ begin
10
+ Appraisal::CLI.start
11
+ rescue Appraisal::AppraisalsNotFound => e
12
+ puts e.message
13
+ exit(127)
14
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Std Lib
4
+ require "fileutils"
5
+ require "pathname"
6
+
7
+ # This gem
8
+ require "appraisal/gemfile"
9
+ require "appraisal/command"
10
+ require "appraisal/customize"
11
+ require "appraisal/utils"
12
+
13
+ module Appraisal
14
+ # Represents one appraisal and its dependencies
15
+ class Appraisal
16
+ DEFAULT_INSTALL_OPTIONS = {"jobs" => 1}.freeze
17
+
18
+ attr_reader :name, :gemfile
19
+
20
+ def initialize(name, source_gemfile)
21
+ @name = name
22
+ @gemfile = source_gemfile.dup
23
+ end
24
+
25
+ def eval_gemfile(*args)
26
+ gemfile.eval_gemfile(*args)
27
+ end
28
+
29
+ def gem(*args)
30
+ gemfile.gem(*args)
31
+ end
32
+
33
+ def remove_gem(*args)
34
+ gemfile.remove_gem(*args)
35
+ end
36
+
37
+ def source(*args, &block)
38
+ gemfile.source(*args, &block)
39
+ end
40
+
41
+ def ruby(*args)
42
+ gemfile.ruby(*args)
43
+ end
44
+
45
+ def git(*args, &block)
46
+ gemfile.git(*args, &block)
47
+ end
48
+
49
+ def path(*args, &block)
50
+ gemfile.path(*args, &block)
51
+ end
52
+
53
+ def group(*args, &block)
54
+ gemfile.group(*args, &block)
55
+ end
56
+
57
+ def install_if(*args, &block)
58
+ gemfile.install_if(*args, &block)
59
+ end
60
+
61
+ def platforms(*args, &block)
62
+ gemfile.platforms(*args, &block)
63
+ end
64
+
65
+ def gemspec(options = {})
66
+ gemfile.gemspec(options)
67
+ end
68
+
69
+ def git_source(*args, &block)
70
+ gemfile.git_source(*args, &block)
71
+ end
72
+
73
+ def write_gemfile
74
+ File.open(gemfile_path, "w") do |file|
75
+ signature =
76
+ Customize.heading(self) || "This file was generated by Appraisal2"
77
+ file.puts([comment_lines(signature), quoted_gemfile].join("\n\n"))
78
+ end
79
+ end
80
+
81
+ def install(options = {})
82
+ commands = [install_command(options).join(" ")]
83
+
84
+ commands.unshift(check_command.join(" ")) if options["without"].nil? || options["without"].empty?
85
+
86
+ command = commands.join(" || ")
87
+
88
+ if Bundler.settings[:path]
89
+ env = {"BUNDLE_DISABLE_SHARED_GEMS" => "1"}
90
+ Command.new(command, :env => env).run
91
+ else
92
+ Command.new(command).run
93
+ end
94
+ end
95
+
96
+ def update(gems = [])
97
+ Command.new(update_command(gems), :gemfile => gemfile_path).run
98
+ end
99
+
100
+ def gemfile_path
101
+ gemfile_root.mkdir unless gemfile_root.exist?
102
+
103
+ gemfile_root.join(gemfile_name).to_s
104
+ end
105
+
106
+ def relative_gemfile_path
107
+ File.join("gemfiles", gemfile_name)
108
+ end
109
+
110
+ def relativize
111
+ current_directory = Pathname.new(Dir.pwd)
112
+ relative_path = current_directory.relative_path_from(gemfile_root).cleanpath
113
+ lockfile_content = File.read(lockfile_path)
114
+
115
+ File.open(lockfile_path, "w") do |file|
116
+ file.write(lockfile_content.gsub(
117
+ / #{current_directory}/,
118
+ " #{relative_path}",
119
+ ))
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def check_command
126
+ gemfile_option = "--gemfile='#{gemfile_path}'"
127
+ ["bundle", "check", gemfile_option]
128
+ end
129
+
130
+ def install_command(options = {})
131
+ gemfile_option = "--gemfile='#{gemfile_path}'"
132
+ ["bundle", "install", gemfile_option, bundle_options(options)].compact
133
+ end
134
+
135
+ def update_command(gems)
136
+ ["bundle", "update", *gems].compact
137
+ end
138
+
139
+ def gemfile_root
140
+ project_root + "gemfiles"
141
+ end
142
+
143
+ def project_root
144
+ Pathname.new(Dir.pwd)
145
+ end
146
+
147
+ def gemfile_name
148
+ "#{clean_name}.gemfile"
149
+ end
150
+
151
+ def lockfile_path
152
+ "#{gemfile_path}.lock"
153
+ end
154
+
155
+ def clean_name
156
+ name.gsub(/[^\w.]/, "_")
157
+ end
158
+
159
+ def bundle_options(options)
160
+ full_options = DEFAULT_INSTALL_OPTIONS.dup.merge(options)
161
+ options_strings = []
162
+ jobs = full_options.delete("jobs")
163
+ if jobs > 1
164
+ if Utils.support_parallel_installation?
165
+ options_strings << "--jobs=#{jobs}"
166
+ else
167
+ warn("Your current version of Bundler does not support parallel installation. Please " \
168
+ "upgrade Bundler to version >= 1.4.0, or invoke `appraisal` without `--jobs` option.")
169
+ end
170
+ end
171
+
172
+ path = full_options.delete("path")
173
+ if path
174
+ relative_path = project_root.join(options["path"])
175
+ options_strings << "--path #{relative_path}"
176
+ end
177
+
178
+ full_options.each do |flag, val|
179
+ options_strings << "--#{flag} #{val}"
180
+ end
181
+
182
+ options_strings.join(" ") if options_strings != []
183
+ end
184
+
185
+ def comment_lines(heading)
186
+ heading.lines.map do |line|
187
+ if line.lstrip.empty?
188
+ line
189
+ else
190
+ "# #{line}"
191
+ end
192
+ end.join
193
+ end
194
+
195
+ def quoted_gemfile
196
+ return gemfile.to_s unless Customize.single_quotes
197
+
198
+ gemfile.to_s.tr('"', "'")
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appraisal/appraisal"
4
+ require "appraisal/customize"
5
+ require "appraisal/errors"
6
+ require "appraisal/gemfile"
7
+
8
+ module Appraisal
9
+ # Loads and parses Appraisals file
10
+ class AppraisalFile
11
+ attr_reader :appraisals, :gemfile
12
+
13
+ def self.each(&block)
14
+ new.each(&block)
15
+ end
16
+
17
+ def initialize
18
+ @appraisals = []
19
+ @gemfile = Gemfile.new
20
+ @gemfile.load(ENV["BUNDLE_GEMFILE"] || "Gemfile")
21
+
22
+ raise AppraisalsNotFound unless File.exist?(path)
23
+
24
+ run(File.read(path))
25
+ end
26
+
27
+ def each(&block)
28
+ appraisals.each(&block)
29
+ end
30
+
31
+ def appraise(name, &block)
32
+ appraisal = Appraisal.new(name, gemfile)
33
+ appraisal.instance_eval(&block)
34
+ @appraisals << appraisal
35
+ end
36
+
37
+ def customize_gemfiles(&_block)
38
+ args = yield
39
+ Customize.new(args)
40
+ end
41
+
42
+ private
43
+
44
+ def run(definitions)
45
+ instance_eval(definitions, __FILE__, __LINE__)
46
+ end
47
+
48
+ def path
49
+ "Appraisals"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appraisal/dependency_list"
4
+ require "appraisal/ordered_hash"
5
+
6
+ module Appraisal
7
+ class BundlerDSL
8
+ attr_reader :dependencies
9
+
10
+ PARTS = %w[
11
+ source
12
+ ruby_version
13
+ gits
14
+ paths
15
+ dependencies
16
+ groups
17
+ platforms
18
+ source_blocks
19
+ install_if
20
+ gemspec
21
+ eval_gemfile
22
+ ]
23
+
24
+ def initialize
25
+ @sources = []
26
+ @ruby_version = nil
27
+ @dependencies = DependencyList.new
28
+ @gemspecs = []
29
+ @groups = OrderedHash.new
30
+ @platforms = OrderedHash.new
31
+ @gits = OrderedHash.new
32
+ @paths = OrderedHash.new
33
+ @source_blocks = OrderedHash.new
34
+ @git_sources = {}
35
+ @install_if = {}
36
+ @eval_gemfile = []
37
+ end
38
+
39
+ def run(&block)
40
+ instance_exec(&block)
41
+ end
42
+
43
+ def eval_gemfile(path, contents = nil)
44
+ @eval_gemfile << [path, contents]
45
+ end
46
+
47
+ def gem(name, *requirements)
48
+ @dependencies.add(name, substitute_git_source(requirements))
49
+ end
50
+
51
+ def remove_gem(name)
52
+ @dependencies.remove(name)
53
+ end
54
+
55
+ def group(*names, &block)
56
+ @groups[names] ||=
57
+ Group.new(names).tap { |g| g.git_sources = @git_sources.dup }
58
+ @groups[names].run(&block)
59
+ end
60
+
61
+ def install_if(condition, &block)
62
+ @install_if[condition] ||=
63
+ Conditional.new(condition).tap { |g| g.git_sources = @git_sources.dup }
64
+ @install_if[condition].run(&block)
65
+ end
66
+
67
+ def platforms(*names, &block)
68
+ @platforms[names] ||=
69
+ Platform.new(names).tap { |g| g.git_sources = @git_sources.dup }
70
+ @platforms[names].run(&block)
71
+ end
72
+
73
+ alias_method :platform, :platforms
74
+
75
+ def source(source, &block)
76
+ if block_given?
77
+ @source_blocks[source] ||=
78
+ Source.new(source).tap { |g| g.git_sources = @git_sources.dup }
79
+ @source_blocks[source].run(&block)
80
+ else
81
+ @sources << source
82
+ end
83
+ end
84
+
85
+ def ruby(ruby_version)
86
+ @ruby_version = ruby_version
87
+ end
88
+
89
+ def git(source, options = {}, &block)
90
+ @gits[source] ||=
91
+ Git.new(source, options).tap { |g| g.git_sources = @git_sources.dup }
92
+ @gits[source].run(&block)
93
+ end
94
+
95
+ def path(source, options = {}, &block)
96
+ @paths[source] ||=
97
+ Path.new(source, options).tap { |g| g.git_sources = @git_sources.dup }
98
+ @paths[source].run(&block)
99
+ end
100
+
101
+ def to_s
102
+ Utils.join_parts(PARTS.map { |part| send(:"#{part}_entry") })
103
+ end
104
+
105
+ def for_dup
106
+ Utils.join_parts(PARTS.map { |part| send(:"#{part}_entry_for_dup") })
107
+ end
108
+
109
+ def gemspec(options = {})
110
+ @gemspecs << Gemspec.new(options)
111
+ end
112
+
113
+ def git_source(source, &block)
114
+ @git_sources[source] = block
115
+ end
116
+
117
+ protected
118
+
119
+ attr_writer :git_sources
120
+
121
+ private
122
+
123
+ def eval_gemfile_entry
124
+ @eval_gemfile.map { |(p, c)| "eval_gemfile(#{p.inspect}#{", #{c.inspect}" if c})" } * "\n\n"
125
+ end
126
+
127
+ alias_method :eval_gemfile_entry_for_dup, :eval_gemfile_entry
128
+
129
+ def source_entry
130
+ @sources.uniq.map { |source| "source #{source.inspect}" }.join("\n")
131
+ end
132
+
133
+ alias_method :source_entry_for_dup, :source_entry
134
+
135
+ def ruby_version_entry
136
+ return unless @ruby_version
137
+
138
+ case @ruby_version
139
+ when String then "ruby #{@ruby_version.inspect}"
140
+ else "ruby(#{Utils.format_string(@ruby_version)})"
141
+ end
142
+ end
143
+
144
+ alias_method :ruby_version_entry_for_dup, :ruby_version_entry
145
+
146
+ def gemspec_entry
147
+ @gemspecs.map(&:to_s).join("\n")
148
+ end
149
+
150
+ def gemspec_entry_for_dup
151
+ @gemspecs.map(&:for_dup).join("\n")
152
+ end
153
+
154
+ def dependencies_entry
155
+ @dependencies.to_s
156
+ end
157
+
158
+ def dependencies_entry_for_dup
159
+ @dependencies.for_dup
160
+ end
161
+
162
+ [:gits, :paths, :platforms, :groups, :source_blocks, :install_if].each do |method_name|
163
+ class_eval <<-METHODS, __FILE__, __LINE__ + 1
164
+ private
165
+
166
+ def #{method_name}_entry
167
+ @#{method_name}.values.map(&:to_s).join("\n\n")
168
+ end
169
+
170
+ def #{method_name}_entry_for_dup
171
+ @#{method_name}.values.map(&:for_dup).join("\n\n")
172
+ end
173
+ METHODS
174
+ end
175
+
176
+ def indent(string)
177
+ indent_by = ENV.fetch("APPRAISAL_INDENTER", "lookaround")
178
+ if indent_by == "lookaround"
179
+ # Default indenter for Appraisal v3
180
+ # Uses a "look-around" of the "look-behind" variety to indent lines that are more than just empty space.
181
+ # In other words, retain existing indentation, and indent the line again, but not on empty lines.
182
+ string.
183
+ # NOTES:
184
+ # (?![\r\n]) - Negative Look Behind which requires that the following pattern,
185
+ # which is (\s*) in this case, *not* be followed by a new line character
186
+ # (\s*) - Captures whitespace at beginning of the line
187
+ # Learn more here: https://learnbyexample.github.io/Ruby_Regexp/lookarounds.html
188
+ gsub(/^(?![\r\n])(\s*)/, ' \0').
189
+ rstrip
190
+ elsif indent_by == "capture"
191
+ # Original indentation regex for Appraisal < v3
192
+ string.gsub(/^(.+)$/, ' \1').rstrip
193
+ else
194
+ string
195
+ end
196
+ end
197
+
198
+ def substitute_git_source(requirements)
199
+ requirements.each do |requirement|
200
+ next unless requirement.is_a?(Hash)
201
+
202
+ (requirement.keys & @git_sources.keys).each do |matching_source|
203
+ value = requirement.delete(matching_source)
204
+ requirement[:git] = @git_sources[matching_source].call(value)
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "fileutils"
5
+
6
+ module Appraisal
7
+ class CLI < Thor
8
+ default_task :install
9
+ map ["-v", "--version"] => "version"
10
+
11
+ class << self
12
+ # Override help command to print out usage
13
+ def help(shell, subcommand = false)
14
+ shell.say(strip_heredoc(<<-HELP))
15
+ Appraisal: Find out what your Ruby gems are worth.
16
+
17
+ Usage:
18
+ appraisal [APPRAISAL_NAME] EXTERNAL_COMMAND
19
+
20
+ If APPRAISAL_NAME is given, only run that EXTERNAL_COMMAND against the given
21
+ appraisal, otherwise it runs the EXTERNAL_COMMAND against all appraisals.
22
+ HELP
23
+
24
+ if File.exist?("Appraisals")
25
+ shell.say
26
+ shell.say("Available Appraisal(s):")
27
+
28
+ AppraisalFile.each do |appraisal|
29
+ shell.say(" - #{appraisal.name}")
30
+ end
31
+ end
32
+
33
+ shell.say
34
+
35
+ super
36
+ end
37
+
38
+ def exit_on_failure?
39
+ true
40
+ end
41
+
42
+ private
43
+
44
+ def strip_heredoc(string)
45
+ indent = string.scan(/^[ \t]*(?=\S)/).min.size || 0
46
+ string.gsub(/^[ \t]{#{indent}}/, "")
47
+ end
48
+ end
49
+
50
+ desc "install", "Resolve and install dependencies for each appraisal"
51
+ method_option "jobs",
52
+ :aliases => "j",
53
+ :type => :numeric,
54
+ :default => 1,
55
+ :banner => "SIZE",
56
+ :desc => "Install gems in parallel using the given number of workers."
57
+ method_option "retry",
58
+ :type => :numeric,
59
+ :default => 1,
60
+ :desc => "Retry network and git requests that have failed"
61
+ method_option "without",
62
+ :banner => "GROUP_NAMES",
63
+ :desc => "A space-separated list of groups referencing gems to skip " \
64
+ "during installation. Bundler will remember this option."
65
+ method_option "full-index",
66
+ :type => :boolean,
67
+ :desc => "Run bundle install with the " \
68
+ "full-index argument."
69
+ method_option "path",
70
+ :type => :string,
71
+ :desc => "Install gems in the specified directory. " \
72
+ "Bundler will remember this option."
73
+
74
+ def install
75
+ invoke(:generate, [], {})
76
+
77
+ AppraisalFile.each do |appraisal|
78
+ appraisal.install(options)
79
+ appraisal.relativize
80
+ end
81
+ end
82
+
83
+ desc "generate", "Generate a gemfile for each appraisal"
84
+ def generate
85
+ AppraisalFile.each do |appraisal|
86
+ appraisal.write_gemfile
87
+ end
88
+ end
89
+
90
+ desc "clean", "Remove all generated gemfiles and lockfiles from gemfiles folder"
91
+ def clean
92
+ FileUtils.rm_f(Dir["gemfiles/*.{gemfile,gemfile.lock}"])
93
+ end
94
+
95
+ desc "update [LIST_OF_GEMS]", "Remove all generated gemfiles and lockfiles, resolve, and install dependencies again"
96
+ def update(*gems)
97
+ invoke(:generate, [])
98
+
99
+ AppraisalFile.each do |appraisal|
100
+ appraisal.update(gems)
101
+ end
102
+ end
103
+
104
+ desc "list", "List the names of the defined appraisals"
105
+ def list
106
+ AppraisalFile.new.appraisals.each { |appraisal| puts appraisal.name }
107
+ end
108
+
109
+ desc "version", "Display the version and exit"
110
+ def version
111
+ puts "Appraisal2 #{VERSION}"
112
+ end
113
+
114
+ private
115
+
116
+ def method_missing(name, *args)
117
+ matching_appraisal = AppraisalFile.new.appraisals.detect do |appraisal|
118
+ appraisal.name == name.to_s
119
+ end
120
+
121
+ if matching_appraisal
122
+ Command.new(args, :gemfile => matching_appraisal.gemfile_path).run
123
+ else
124
+ AppraisalFile.each do |appraisal|
125
+ Command.new(ARGV, :gemfile => appraisal.gemfile_path).run
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end