aboutsummaryrefslogtreecommitdiffstats
path: root/lib/rubygems/specification_policy.rb
diff options
context:
space:
mode:
authorhsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2018-05-30 13:01:35 +0000
committerhsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2018-05-30 13:01:35 +0000
commit8da8d4b043c37b53a69803c71ff36b478d4776d0 (patch)
tree7c8cec15645e74f19c88e4eb5b210b96174c7d03 /lib/rubygems/specification_policy.rb
parentc5cb386eba6d9a2d9a8e6ffa8c30137d0c4660c1 (diff)
downloadruby-8da8d4b043c37b53a69803c71ff36b478d4776d0.tar.gz
Merge RubyGems 3.0.0.beta1.
* It drop to support < Ruby 2.2 * Cleanup deprecated methods and classes. * Mark obsoleted methods to deprecate. * and other enhancements. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@63528 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/rubygems/specification_policy.rb')
-rw-r--r--lib/rubygems/specification_policy.rb410
1 files changed, 410 insertions, 0 deletions
diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb
new file mode 100644
index 0000000000..a05edcd5c9
--- /dev/null
+++ b/lib/rubygems/specification_policy.rb
@@ -0,0 +1,410 @@
+require 'delegate'
+require 'uri'
+
+class Gem::SpecificationPolicy < SimpleDelegator
+ VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc:
+
+ VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z} # :nodoc:
+
+ METADATA_LINK_KEYS = %w[
+ bug_tracker_uri
+ changelog_uri
+ documentation_uri
+ homepage_uri
+ mailing_list_uri
+ source_code_uri
+ wiki_uri
+ ] # :nodoc:
+
+ ##
+ # If set to true, run packaging-specific checks, as well.
+
+ attr_accessor :packaging
+
+ ##
+ # Checks that the specification contains all required fields, and does a
+ # very basic sanity check.
+ #
+ # Raises InvalidSpecificationException if the spec does not pass the
+ # checks.
+
+ def validate
+ validate_nil_attributes
+
+ validate_rubygems_version
+
+ validate_required_attributes
+
+ validate_name
+
+ validate_require_paths
+
+ keep_only_files_and_directories
+
+ validate_non_files
+
+ validate_self_inclusion_in_files_list
+
+ validate_specification_version
+
+ validate_platform
+
+ validate_array_attributes
+
+ validate_authors_field
+
+ validate_metadata
+
+ validate_licenses
+
+ validate_permissions
+
+ validate_lazy_metadata
+
+ validate_values
+
+ validate_dependencies
+ true
+ end
+
+ ##
+ # Implementation for Specification#validate_metadata
+
+ def validate_metadata
+ unless Hash === metadata then
+ raise Gem::InvalidSpecificationException,
+ 'metadata must be a hash'
+ end
+
+ metadata.each do |key, value|
+ if !key.kind_of?(String) then
+ raise Gem::InvalidSpecificationException,
+ "metadata keys must be a String"
+ end
+
+ if key.size > 128 then
+ raise Gem::InvalidSpecificationException,
+ "metadata key too large (#{key.size} > 128)"
+ end
+
+ if !value.kind_of?(String) then
+ raise Gem::InvalidSpecificationException,
+ "metadata values must be a String"
+ end
+
+ if value.size > 1024 then
+ raise Gem::InvalidSpecificationException,
+ "metadata value too large (#{value.size} > 1024)"
+ end
+
+ if METADATA_LINK_KEYS.include? key then
+ if value !~ VALID_URI_PATTERN then
+ raise Gem::InvalidSpecificationException,
+ "metadata['#{key}'] has invalid link: #{value.inspect}"
+ end
+ end
+ end
+ end
+
+ ##
+ # Implementation for Specification#validate_dependencies
+
+ def validate_dependencies # :nodoc:
+ # NOTE: see REFACTOR note in Gem::Dependency about types - this might be brittle
+ seen = Gem::Dependency::TYPES.inject({}) { |types, type| types.merge({ type => {}}) }
+
+ error_messages = []
+ warning_messages = []
+ dependencies.each do |dep|
+ if prev = seen[dep.type][dep.name] then
+ error_messages << <<-MESSAGE
+duplicate dependency on #{dep}, (#{prev.requirement}) use:
+ add_#{dep.type}_dependency '#{dep.name}', '#{dep.requirement}', '#{prev.requirement}'
+ MESSAGE
+ end
+
+ seen[dep.type][dep.name] = dep
+
+ prerelease_dep = dep.requirements_list.any? do |req|
+ Gem::Requirement.new(req).prerelease?
+ end
+
+ warning_messages << "prerelease dependency on #{dep} is not recommended" if
+ prerelease_dep && !version.prerelease?
+
+ overly_strict = dep.requirement.requirements.length == 1 &&
+ dep.requirement.requirements.any? do |op, version|
+ op == '~>' and
+ not version.prerelease? and
+ version.segments.length > 2 and
+ version.segments.first != 0
+ end
+
+ if overly_strict then
+ _, dep_version = dep.requirement.requirements.first
+
+ base = dep_version.segments.first 2
+ upper_bound = dep_version.segments.first(dep_version.segments.length - 1)
+ upper_bound[-1] += 1
+
+ warning_messages << <<-WARNING
+pessimistic dependency on #{dep} may be overly strict
+ if #{dep.name} is semantically versioned, use:
+ add_#{dep.type}_dependency '#{dep.name}', '~> #{base.join '.'}', '>= #{dep_version}'
+ if #{dep.name} is not semantically versioned, you can bypass this warning with:
+ add_#{dep.type}_dependency '#{dep.name}', '>= #{dep_version}', '< #{upper_bound.join '.'}.a'
+ WARNING
+ end
+
+ open_ended = dep.requirement.requirements.all? do |op, version|
+ not version.prerelease? and (op == '>' or op == '>=')
+ end
+
+ if open_ended then
+ op, dep_version = dep.requirement.requirements.first
+
+ base = dep_version.segments.first 2
+
+ bugfix = if op == '>' then
+ ", '> #{dep_version}'"
+ elsif op == '>=' and base != dep_version.segments then
+ ", '>= #{dep_version}'"
+ end
+
+ warning_messages << <<-WARNING
+open-ended dependency on #{dep} is not recommended
+ if #{dep.name} is semantically versioned, use:
+ add_#{dep.type}_dependency '#{dep.name}', '~> #{base.join '.'}'#{bugfix}
+ WARNING
+ end
+ end
+ if error_messages.any? then
+ raise Gem::InvalidSpecificationException, error_messages.join
+ end
+ if warning_messages.any? then
+ warning_messages.each { |warning_message| warning warning_message }
+ end
+ end
+
+ ##
+ # Issues a warning for each file to be packaged which is world-readable.
+ #
+ # Implementation for Specification#validate_permissions
+
+ def validate_permissions
+ return if Gem.win_platform?
+
+ files.each do |file|
+ next unless File.file?(file)
+ next if File.stat(file).mode & 0444 == 0444
+ warning "#{file} is not world-readable"
+ end
+
+ executables.each do |name|
+ exec = File.join bindir, name
+ next unless File.file?(exec)
+ next if File.stat(exec).executable?
+ warning "#{exec} is not executable"
+ end
+ end
+
+ private
+
+ def validate_nil_attributes
+ nil_attributes = Gem::Specification.non_nil_attributes.select do |attrname|
+ __getobj__.instance_variable_get("@#{attrname}").nil?
+ end
+ return if nil_attributes.empty?
+ raise Gem::InvalidSpecificationException,
+ "#{nil_attributes.join ', '} must not be nil"
+ end
+
+ def validate_rubygems_version
+ return unless packaging
+ return if rubygems_version == Gem::VERSION
+
+ raise Gem::InvalidSpecificationException,
+ "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}"
+ end
+
+ def validate_required_attributes
+ Gem::Specification.required_attributes.each do |symbol|
+ unless send symbol then
+ raise Gem::InvalidSpecificationException,
+ "missing value for attribute #{symbol}"
+ end
+ end
+ end
+
+ def validate_name
+ if !name.is_a?(String) then
+ raise Gem::InvalidSpecificationException,
+ "invalid value for attribute name: \"#{name.inspect}\" must be a string"
+ elsif name !~ /[a-zA-Z]/ then
+ raise Gem::InvalidSpecificationException,
+ "invalid value for attribute name: #{name.dump} must include at least one letter"
+ elsif name !~ VALID_NAME_PATTERN then
+ raise Gem::InvalidSpecificationException,
+ "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores"
+ end
+ end
+
+ def validate_require_paths
+ return unless raw_require_paths.empty?
+
+ raise Gem::InvalidSpecificationException,
+ 'specification must have at least one require_path'
+ end
+
+ def validate_non_files
+ return unless packaging
+ non_files = files.reject {|x| File.file?(x) || File.symlink?(x)}
+
+ unless non_files.empty? then
+ raise Gem::InvalidSpecificationException,
+ "[\"#{non_files.join "\", \""}\"] are not files"
+ end
+ end
+
+ def validate_self_inclusion_in_files_list
+ return unless files.include?(file_name)
+
+ raise Gem::InvalidSpecificationException,
+ "#{full_name} contains itself (#{file_name}), check your files list"
+ end
+
+ def validate_specification_version
+ return if specification_version.is_a?(Integer)
+
+ raise Gem::InvalidSpecificationException,
+ 'specification_version must be an Integer (did you mean version?)'
+ end
+
+ def validate_platform
+ case platform
+ when Gem::Platform, Gem::Platform::RUBY then # ok
+ else
+ raise Gem::InvalidSpecificationException,
+ "invalid platform #{platform.inspect}, see Gem::Platform"
+ end
+ end
+
+ def validate_array_attributes
+ Gem::Specification.array_attributes.each do |field|
+ validate_array_attribute(field)
+ end
+ end
+
+ def validate_array_attribute(field)
+ val = self.send(field)
+ klass = case field
+ when :dependencies then
+ Gem::Dependency
+ else
+ String
+ end
+
+ unless Array === val and val.all? {|x| x.kind_of?(klass)} then
+ raise(Gem::InvalidSpecificationException,
+ "#{field} must be an Array of #{klass}")
+ end
+ end
+
+ def validate_authors_field
+ return unless authors.empty?
+
+ raise Gem::InvalidSpecificationException,
+ "authors may not be empty"
+ end
+
+ def validate_licenses
+ licenses.each { |license|
+ if license.length > 64 then
+ raise Gem::InvalidSpecificationException,
+ "each license must be 64 characters or less"
+ end
+
+ if !Gem::Licenses.match?(license) then
+ suggestions = Gem::Licenses.suggestions(license)
+ message = <<-warning
+license value '#{license}' is invalid. Use a license identifier from
+http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license.
+ warning
+ message += "Did you mean #{suggestions.map { |s| "'#{s}'"}.join(', ')}?\n" unless suggestions.nil?
+ warning(message)
+ end
+ }
+
+ warning <<-warning if licenses.empty?
+licenses is empty, but is recommended. Use a license identifier from
+http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license.
+ warning
+ end
+
+ LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, '')
+ LAZY_PATTERN = /FI XME|TO DO/x
+ HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i
+
+ def validate_lazy_metadata
+ unless authors.grep(LAZY_PATTERN).empty? then
+ raise Gem::InvalidSpecificationException, "#{LAZY} is not an author"
+ end
+
+ unless Array(email).grep(LAZY_PATTERN).empty? then
+ raise Gem::InvalidSpecificationException, "#{LAZY} is not an email"
+ end
+
+ if description =~ LAZY_PATTERN then
+ raise Gem::InvalidSpecificationException, "#{LAZY} is not a description"
+ end
+
+ if summary =~ LAZY_PATTERN then
+ raise Gem::InvalidSpecificationException, "#{LAZY} is not a summary"
+ end
+
+ # Make sure a homepage is valid HTTP/HTTPS URI
+ if homepage and not homepage.empty?
+ begin
+ homepage_uri = URI.parse(homepage)
+ unless [URI::HTTP, URI::HTTPS].member? homepage_uri.class
+ raise Gem::InvalidSpecificationException, "\"#{homepage}\" is not a valid HTTP URI"
+ end
+ rescue URI::InvalidURIError
+ raise Gem::InvalidSpecificationException, "\"#{homepage}\" is not a valid HTTP URI"
+ end
+ end
+ end
+
+ def validate_values
+ %w[author homepage summary files].each do |attribute|
+ validate_attribute_present(attribute)
+ end
+
+ if description == summary then
+ warning "description and summary are identical"
+ end
+
+ # TODO: raise at some given date
+ warning "deprecated autorequire specified" if autorequire
+
+ executables.each do |executable|
+ validate_shebang_line_in(executable)
+ end
+
+ files.select { |f| File.symlink?(f) }.each do |file|
+ warning "#{file} is a symlink, which is not supported on all platforms"
+ end
+ end
+
+ def validate_attribute_present(attribute)
+ value = self.send attribute
+ warning("no #{attribute} specified") if value.nil? || value.empty?
+ end
+
+ def validate_shebang_line_in(executable)
+ executable_path = File.join(bindir, executable)
+ return if File.read(executable_path, 2) == '#!'
+
+ warning "#{executable_path} is missing #! line"
+ end
+end