diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 2cae05b43ae8..85dd460e47fa 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -26,7 +26,11 @@ def initialize options[:output] = value end - add_ruby_abi_option("build", " (builds a content addressable gem)") + add_ruby_abi_option("build") + + add_option "--content-addressable", "build a content-addressable gem" do |_value, options| + options[:content_addressable] = true + end end def arguments # :nodoc: @@ -54,10 +58,19 @@ def description # :nodoc: $ gem build my_gem-1.0.gemspec --output=release.gem -Platform gems can be built for a single Ruby ABI with the --ruby-abi option: +Use the --ruby-abi option to set a specific Ruby ABI for the gem being built: $ gem build my_gem-1.0.gemspec --ruby-abi=3.4 +Use the --content-addressable option to build a content-addressable gem using +the platform and required_ruby_version declared in the gemspec: + + $ gem build my_gem-1.0.gemspec --content-addressable + +The options can be combined to set the Ruby ABI explicitly: + + $ gem build my_gem-1.0.gemspec --ruby-abi=3.4 --content-addressable + EOF end @@ -95,12 +108,25 @@ def build_package(gemspec) spec.platform = Gem::Platform.local end + ruby_abi = options[:ruby_abi] + if ruby_abi + requirement = Gem::ContentAddress.ruby_abi_requirement(ruby_abi) + existing = spec.required_ruby_version + + unless existing.none? || existing == requirement + alert_warning "required_ruby_version was changed from \"#{existing}\" to \"#{requirement}\" for this build " \ + "because --ruby-abi #{ruby_abi} was given." + end + + spec.required_ruby_version = requirement + end + Gem::Package.build( spec, options[:force], options[:strict], options[:output], - options[:ruby_abi] + content_addressable: options[:content_addressable] ) else alert_error "Error loading gemspec. Aborting." diff --git a/lib/rubygems/content_address.rb b/lib/rubygems/content_address.rb index e815c45191c0..0f3dcaa34f57 100644 --- a/lib/rubygems/content_address.rb +++ b/lib/rubygems/content_address.rb @@ -90,6 +90,23 @@ def self.eligible?(spec, validate_ruby_abi: true) !ruby_abi_for(spec.required_ruby_version).nil? end + ## + # Raises ArgumentError unless +spec+ is eligible for content addressing + + def self.eligible!(spec) + unless platform_eligible?(spec.platform) + raise ArgumentError, + "Cannot build a content-addressable gem as no platform or a Ruby platform has been set" + end + + return unless ruby_abi_for(spec.required_ruby_version).nil? + + raise ArgumentError, + "Cannot build a content-addressable gem because required_ruby_version is set to " \ + "#{spec.required_ruby_version}. Please set required_ruby_version to \"~> X.Y.0\" " \ + "so that it identifies a single Ruby ABI." + end + ## # Whether +spec+ is content-addressed: it is eligible for content # addressing and has a valid content address set. See eligible? for @@ -114,20 +131,6 @@ def self.content_addressed_row?(suffix, platform, required_ruby_version = nil, v !ruby_abi_for(required_ruby_version).nil? end - ## - # Whether +spec+'s required_ruby_version permits building for +ruby_abi+: - # an unset or default requirement can still be pinned to the ABI, and - # anything else must already pin exactly that ABI. Used at build time, - # before the requirement is injected, where eligible? would be - # premature. - - def self.ruby_abi_compatible?(spec, ruby_abi) - required_ruby_version = spec.required_ruby_version - return true if required_ruby_version.nil? || required_ruby_version.none? - - ruby_abi_for(required_ruby_version) == ruby_abi - end - ## # Generates the content address for +bytes+: the first +length+ # characters of the hexadecimal SHA256 digest of the contents. diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 035f63e312cd..51928bdbe95a 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -137,18 +137,20 @@ class TarInvalidError < Error; end MINIMUM_RUBYGEMS_VERSION = ">= 4.1.0.a" ## - # Builds the gem described by +spec+ and returns the built file name; - # passing +ruby_abi+ ("X.Y") builds a content-addressable gem named by the - # SHA-256 of its contents, updates +spec.required_ruby_version+ to - # "~> X.Y.0", and constrains +spec.required_rubygems_version+ to at least - # MINIMUM_RUBYGEMS_VERSION (incompatible with - # +file_name+). - - def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil, ruby_abi = nil) - if ruby_abi && file_name - raise ArgumentError, "Cannot specify both a Ruby ABI and an output file name because content addressable gems must use the generated file name." - end - if ruby_abi + # Builds the gem described by +spec+ and returns the built file name. + # + # When +content_addressable+ is true, builds a gem named by the SHA-256 + # digest of its contents. The spec must declare a non-Ruby platform and a + # +required_ruby_version+ identifying a single Ruby ABI. Its + # +required_rubygems_version+ is constrained to at least + # MINIMUM_RUBYGEMS_VERSION. Content-addressable builds are incompatible + # with +file_name+. + + def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil, content_addressable: false) + if content_addressable && file_name + raise ArgumentError, "Cannot specify an output file name for a content-addressable gem as these gems must use the generated file name." + end + if content_addressable require "digest" require "stringio" @@ -157,9 +159,8 @@ def self.build(spec, skip_validation = false, strict_validation = false, file_na package = new io package.spec = spec.dup - gem_file = package.build_content_addressable_file ruby_abi, skip_validation, strict_validation + gem_file = package.build_content_addressable_file skip_validation, strict_validation - spec.required_ruby_version = package.spec.required_ruby_version spec.required_rubygems_version = package.spec.required_rubygems_version else gem_file = file_name || spec.file_name @@ -375,20 +376,18 @@ def build(skip_validation = false, strict_validation = false) end ## - # Builds this package scoped to +ruby_abi+ ("X.Y"), then writes it to a - # content-addressable file name derived from the SHA-256 digest of the gem - # contents, e.g. "example-1.0-01234567.gem". Returns the file name of the - # written gem. + # Builds this package and writes it to a content-addressable file name + # derived from the SHA-256 digest of the gem contents, for example + # "example-1.0-01234567.gem". Returns the written file name. # - # The spec is validated for an ABI-scoped build and its - # +required_ruby_version+ and +required_rubygems_version+ are constrained - # before building, so every gem this method produces is eligible for - # content addressing. + # The spec is validated for content addressing. Its + # +required_ruby_version+ determines the Ruby ABI, while its + # +required_rubygems_version+ is constrained before building. - def build_content_addressable_file(ruby_abi, skip_validation = false, strict_validation = false) - validate_ruby_abi ruby_abi + def build_content_addressable_file(skip_validation = false, strict_validation = false) + Gem::ContentAddress.eligible!(@spec) + ruby_abi = Gem::ContentAddress.ruby_abi_for(@spec.required_ruby_version) @spec.required_rubygems_version = normalized_required_rubygems_version(ruby_abi) - @spec.required_ruby_version = Gem::ContentAddress.ruby_abi_requirement(ruby_abi) build skip_validation, strict_validation @@ -789,22 +788,6 @@ def satisfies_rubygems_floor?(requirement, floor) end end - ## - # Validates that the spec can be built as a content-addressable gem scoped - # to +ruby_abi+ ("X.Y"): the ABI must be well-formed, the spec must declare - # a non-Ruby platform, and any existing +required_ruby_version+ must match - # the ABI. - - def validate_ruby_abi(ruby_abi) - if !Gem::ContentAddress.valid_ruby_abi?(ruby_abi) - raise ArgumentError, "Ruby ABI must be in X.Y format" - elsif !Gem::ContentAddress.platform_eligible?(@spec.platform) - raise ArgumentError, "Cannot build a gem scoped to a single Ruby ABI as no platform or a Ruby platform has been set" - elsif !Gem::ContentAddress.ruby_abi_compatible?(@spec, ruby_abi) - raise ArgumentError, "Cannot build gem for Ruby ABI #{ruby_abi} because required_ruby_version is set to #{@spec.required_ruby_version}. Please set required_ruby_version to \"~> #{ruby_abi}.0\"." - end - end - ## # Returns the full path for installing +filename+ into +destination_dir+, # which must already be resolved with File.realpath by the caller. diff --git a/lib/rubygems/package_task.rb b/lib/rubygems/package_task.rb index 398f7820ad77..fa746137ce1c 100644 --- a/lib/rubygems/package_task.rb +++ b/lib/rubygems/package_task.rb @@ -65,6 +65,10 @@ class Gem::PackageTask < Rake::PackageTask attr_accessor :gem_spec + ## + # Indicates whether this package should be built as a content-addressable gem. + attr_accessor :content_addressable + ## # Create a Gem Package task library. Automatically define the gem if a # block is given. If no block is supplied, then #define needs to be called @@ -82,6 +86,7 @@ def initialize(gem_spec) def init(gem) super gem.full_name, :noversion @gem_spec = gem + @content_addressable = false @package_files += gem_spec.files if gem_spec.files @fileutils_output = $stdout end @@ -91,33 +96,97 @@ def init(gem) # (+define+ is automatically called if a block is given to +new+). def define + Gem::ContentAddress.eligible!(gem_spec) if content_addressable + super gem_file = File.basename gem_spec.cache_file gem_path = File.join package_dir, gem_file - gem_dir = File.join package_dir, gem_spec.full_name + gem_dir = package_dir_path + + if content_addressable + build_target = stamp_file + gem_description = "content-addressable gem #{gem_spec.name}-#{gem_spec.version} " \ + "(Platform: #{gem_spec.platform}, Ruby ABI: #{content_addressable_ruby_abi})" + + discard_stale_stamp_file + else + build_target = gem_path + gem_description = "gem file #{gem_file}" + end task package: [:gem] directory package_dir directory gem_dir - desc "Build the gem file #{gem_file}" - task gem: [gem_path] + desc "Build the #{gem_description}" + task gem: [build_target] trace = Rake.application.options.trace Gem.configuration.verbose = trace - file gem_path => [package_dir, gem_dir] + @gem_spec.files do + file build_target => [package_dir, gem_dir] + @gem_spec.files do + previous_gem = recorded_gem_path if content_addressable + built_gem_path = nil + chdir(gem_dir) do - when_writing "Creating #{gem_spec.file_name}" do - built_gem_file = Gem::Package.build gem_spec + when_writing "Creating #{gem_description}" do + built_gem_file = Gem::Package.build gem_spec, content_addressable: content_addressable verbose trace do mv built_gem_file, ".." end + + if content_addressable + built_gem_path = File.join(package_dir, built_gem_file) + File.write File.join("..", File.basename(stamp_file)), built_gem_path + end + end + end + + if previous_gem && built_gem_path && previous_gem != built_gem_path + verbose trace do + rm_f previous_gem end end end end + + ## + # The name of this package. Content-addressable builds append the Ruby ABI + # so per-ABI builds of the same +full_name+ do not share a staging + # directory. + + def package_name + name = super + ruby_abi = content_addressable_ruby_abi + ruby_abi ? "#{name}-#{ruby_abi}" : name + end + + private + + def content_addressable_ruby_abi + return unless content_addressable + + Gem::ContentAddress.ruby_abi_for(gem_spec.required_ruby_version) + end + + def stamp_file + File.join package_dir, "#{package_name}.gem-built" + end + + def recorded_gem_path + return unless File.file?(stamp_file) + + path = File.read(stamp_file).strip + path unless path.empty? + end + + def discard_stale_stamp_file + return unless File.file?(stamp_file) + + path = recorded_gem_path + File.delete(stamp_file) unless path && File.file?(path) + end end diff --git a/spec/commands/clean_spec.rb b/spec/commands/clean_spec.rb index 5e9cac75c4e7..3ed8f0a2216d 100644 --- a/spec/commands/clean_spec.rb +++ b/spec/commands/clean_spec.rb @@ -391,7 +391,7 @@ def should_not_have_gems(*gems) expect(vendored_gems("bin/myrackup")).not_to exist end - it "removes orphaned gemspecs from ABI-scoped specification dirs", rubygems: ">= 4.1.0.dev" do + it "removes orphaned gemspecs from ABI-scoped specification dirs", rubygems: ">= 4.1.0.a" do gemfile <<-G source "https://gem.repo1" @@ -412,10 +412,12 @@ def should_not_have_gems(*gems) should_have_gems "foo-1.0" end - it "does not remove gemspecs for content-addressed gems in the bundle", :compact_index, rubygems: ">= 4.1.0.dev" do + it "does not remove gemspecs for content-addressed gems in the bundle", :compact_index, rubygems: ">= 4.1.0.a" do skip "Gem::ContentAddress not available" if ruby_core? + skip "ABI-scoped requirements do not match prerelease Ruby versions" if Gem.ruby_version.prerelease? simulate_platform "x86_64-linux" do + current_abi = "#{Gem.ruby_version.segments[0]}.#{Gem.ruby_version.segments[1]}" build_repo2 do build_gem "mygem", "1.0" do |s| s.platform = Gem::Platform.new("x86_64-linux") @@ -423,9 +425,9 @@ def should_not_have_gems(*gems) end end - build_gem "mygem", "1.0", ruby_abi: Gem.ruby_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") - s.required_ruby_version = "~> #{Gem.ruby_abi}.0" + s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" end diff --git a/spec/install/gemfile/content_addressable_spec.rb b/spec/install/gemfile/content_addressable_spec.rb index 8b0a472413fc..fa7077833be9 100644 --- a/spec/install/gemfile/content_addressable_spec.rb +++ b/spec/install/gemfile/content_addressable_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -RSpec.describe "bundle install with content-addressable gems", :compact_index, rubygems: ">= 4.1.0.dev" do +RSpec.describe "bundle install with content-addressable gems", :compact_index, rubygems: ">= 4.1.0.a" do before do skip "Gem::ContentAddress not available" if ruby_core? + skip "ABI-scoped requirements do not match prerelease Ruby versions" if Gem.ruby_version.prerelease? end let(:current_abi) { "#{Gem.ruby_version.segments[0]}.#{Gem.ruby_version.segments[1]}" } @@ -17,7 +18,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -65,7 +66,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -99,7 +100,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{mismatched_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -143,7 +144,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("arm64-darwin") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -163,13 +164,13 @@ simulate_platform "x86_64-linux" do build_repo2 - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_linux'" end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("arm64-darwin") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_darwin'" @@ -194,7 +195,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("arm64-darwin") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -218,7 +219,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{mismatched_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -238,13 +239,13 @@ simulate_platform "x86_64-linux" do build_repo2 - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_matching_abi'" end - build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{mismatched_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_mismatched_abi'" @@ -269,7 +270,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -294,14 +295,14 @@ end end - build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{mismatched_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_mismatched_abi_1'" end second_mismatched_abi = "#{Gem.ruby_version.segments[0] + 2}.0" - build_gem "mygem", "1.0", ruby_abi: second_mismatched_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{second_mismatched_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_mismatched_abi_2'" @@ -323,7 +324,7 @@ build_gem "other", "1.0" end - build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{mismatched_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -350,7 +351,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -381,13 +382,13 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" end - build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{mismatched_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 mismatched'" @@ -444,7 +445,7 @@ build_gem "othergem", "1.0" end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -485,7 +486,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" @@ -523,7 +524,7 @@ end end - build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + build_gem "mygem", "1.0", content_addressable: true, path: gem_repo2("gems") do |s| s.platform = Gem::Platform.new("x86_64-linux") s.required_ruby_version = "~> #{current_abi}.0" s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" diff --git a/spec/support/builders.rb b/spec/support/builders.rb index 427205b44f7a..5275b25eb4cf 100644 --- a/spec/support/builders.rb +++ b/spec/support/builders.rb @@ -662,8 +662,8 @@ def _build(opts) FileUtils.mkdir_p(lib_path.join(destination)) built_gem = - if opts[:ruby_abi] - Dir.chdir(lib_path) { Gem::Package.build(@spec, false, false, nil, opts[:ruby_abi]) } + if opts[:content_addressable] + Dir.chdir(lib_path) { Gem::Package.build(@spec, content_addressable: true) } elsif [:yaml, false].include?(opts[:gemspec]) Dir.chdir(lib_path) do Bundler.rubygems.build(@spec, opts[:skip_validation]) diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 71bd38af655a..5cbaad427755 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -894,8 +894,13 @@ def util_build_gem(spec, ruby_abi: nil) end built_gem_name = nil + content_addressable = false + if ruby_abi + spec.required_ruby_version = Gem::ContentAddress.ruby_abi_requirement(ruby_abi) + content_addressable = true + end use_ui Gem::MockGemUi.new do - built_gem_name = Gem::Package.build spec, false, false, nil, ruby_abi + built_gem_name = Gem::Package.build spec, content_addressable: content_addressable end cache = File.join File.dirname(spec.cache_file), File.basename(built_gem_name) diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index f804bc8d1b52..f0e4c69f63e4 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -37,43 +37,6 @@ def test_handle_options_force_strict_platform assert_includes Gem.platforms, Gem::Platform.local end - def test_options_ruby_abi - gem = util_spec "platformed_gem" do |s| - s.license = "AGPL-3.0-only" - s.files = ["README.md"] - s.platform = "arm64-darwin" - s.required_ruby_version = "~> 3.4.0" - end - - gemspec_file = File.join(@tempdir, gem.spec_name) - - File.open gemspec_file, "w" do |gs| - gs.write gem.to_ruby - end - - @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] - assert_equal "3.4", @cmd.options[:ruby_abi] - - use_ui @ui do - Dir.chdir @tempdir do - @cmd.execute - end - end - - files = Dir[File.join(@tempdir, "platformed_gem-2-*.gem")] - assert_equal 1, files.size - assert_match(/\Aplatformed_gem-2-[0-9a-f]{8}\.gem\z/, File.basename(files.first)) - - output = @ui.output.split "\n" - assert_equal " Successfully built RubyGem", output.shift - assert_equal " Name: platformed_gem", output.shift - assert_equal " Version: 2", output.shift - assert_match(/\A File: platformed_gem-2-[0-9a-f]{8}\.gem\z/, output.shift) - assert_equal " Platform: arm64-darwin", output.shift - assert_equal " Ruby ABI: 3.4", output.shift - assert_equal [], output - end - def test_options_filename gemspec_file = File.join(@tempdir, @gem.spec_name) @@ -108,6 +71,7 @@ def test_handle_options_defaults refute @cmd.options[:strict] assert_nil @cmd.options[:output] assert_nil @cmd.options[:ruby_abi] + refute @cmd.options[:content_addressable] end def test_execute @@ -122,10 +86,11 @@ def test_execute util_test_build_gem @gem end - def test_ruby_abi_rejects_ruby_platform + def test_content_addressable_rejects_ruby_platform gem = util_spec "some_gem" do |s| s.license = "AGPL-3.0-only" s.files = ["README.md"] + s.platform = Gem::Platform::RUBY end gemspec_file = File.join(@tempdir, gem.spec_name) @@ -133,7 +98,7 @@ def test_ruby_abi_rejects_ruby_platform gs.write gem.to_ruby end - @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + @cmd.handle_options [gemspec_file, "--content-addressable"] error = assert_raise(ArgumentError) do use_ui @ui do Dir.chdir @tempdir do @@ -144,12 +109,12 @@ def test_ruby_abi_rejects_ruby_platform assert_match(/no platform or a Ruby platform has been set/, error.message) end - def test_ruby_abi_rejects_mismatched_required_ruby_version + def test_content_addressable_rejects_ineligible_required_ruby_version gem = util_spec "platformed_gem" do |s| s.license = "AGPL-3.0-only" s.files = ["README.md"] s.platform = "arm64-darwin" - s.required_ruby_version = "~> 3.3.0" + s.required_ruby_version = ">= 3.3" end gemspec_file = File.join(@tempdir, gem.spec_name) @@ -157,7 +122,7 @@ def test_ruby_abi_rejects_mismatched_required_ruby_version gs.write gem.to_ruby end - @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + @cmd.handle_options [gemspec_file, "--content-addressable"] error = assert_raise(ArgumentError) do use_ui @ui do Dir.chdir @tempdir do @@ -165,10 +130,10 @@ def test_ruby_abi_rejects_mismatched_required_ruby_version end end end - assert_match(/Cannot build gem for Ruby ABI 3\.4 because required_ruby_version/, error.message) + assert_match(/required_ruby_version/, error.message) end - def test_ruby_abi_rejects_conflicting_required_rubygems_version + def test_content_addressable_rejects_conflicting_required_rubygems_version gem = util_spec "platformed_gem" do |s| s.license = "AGPL-3.0-only" s.files = ["README.md"] @@ -182,7 +147,7 @@ def test_ruby_abi_rejects_conflicting_required_rubygems_version gs.write gem.to_ruby end - @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + @cmd.handle_options [gemspec_file, "--content-addressable"] error = assert_raise(ArgumentError) do use_ui @ui do Dir.chdir @tempdir do @@ -193,11 +158,41 @@ def test_ruby_abi_rejects_conflicting_required_rubygems_version assert_match(/Cannot build gem for Ruby ABI 3\.4 because required_rubygems_version/, error.message) end - def test_ruby_abi_defaults_required_ruby_version_when_unset + def test_ruby_abi_sets_required_ruby_version + gem = util_spec "platformed_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + s.platform = "arm64-darwin" + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + assert_equal "3.4", @cmd.options[:ruby_abi] + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + files = Dir[File.join(@tempdir, "platformed_gem-2-*.gem")] + assert_equal ["platformed_gem-2-arm64-darwin.gem"], files.map {|f| File.basename(f) } + spec = Gem::Package.new(files.first).spec + assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version + + refute_match(/required_ruby_version was changed/, @ui.error) + end + + def test_ruby_abi_overrides_existing_required_ruby_version gem = util_spec "platformed_gem" do |s| s.license = "AGPL-3.0-only" s.files = ["README.md"] s.platform = "arm64-darwin" + s.required_ruby_version = "~> 3.3.0" end gemspec_file = File.join(@tempdir, gem.spec_name) @@ -212,13 +207,53 @@ def test_ruby_abi_defaults_required_ruby_version_when_unset end end + assert_match( + /WARNING: required_ruby_version was changed from "~> 3\.3\.0" to "~> 3\.4\.0" for this build because --ruby-abi 3\.4 was given\./, + @ui.error + ) + + files = Dir[File.join(@tempdir, "platformed_gem-2-arm64-darwin.gem")] + assert_equal 1, files.size + spec = Gem::Package.new(files.first).spec + assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version + end + + def test_ruby_abi_and_content_addressable_build_content_addressed_gem + gem = util_spec "platformed_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + s.platform = "arm64-darwin" + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4", "--content-addressable"] + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + files = Dir[File.join(@tempdir, "platformed_gem-2-*.gem")] assert_equal 1, files.size + assert_match(/\Aplatformed_gem-2-[0-9a-f]{8}\.gem\z/, File.basename(files.first)) + spec = Gem::Package.new(files.first).spec assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version + + output = @ui.output.split "\n" + assert_equal " Successfully built RubyGem", output.shift + assert_equal " Name: platformed_gem", output.shift + assert_equal " Version: 2", output.shift + assert_match(/\A File: platformed_gem-2-[0-9a-f]{8}\.gem\z/, output.shift) + assert_equal " Platform: arm64-darwin", output.shift + assert_equal " Ruby ABI: 3.4", output.shift end - def test_ruby_abi_produces_deterministic_content_address + def test_content_addressable_produces_deterministic_content_address gemspec = lambda do gem = util_spec "platformed_gem" do |s| s.license = "AGPL-3.0-only" @@ -232,14 +267,12 @@ def test_ruby_abi_produces_deterministic_content_address gs.write gem.to_ruby end - @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + @cmd.handle_options [gemspec_file, "--content-addressable"] use_ui @ui do Dir.chdir @tempdir do @cmd.execute end end - - Dir[File.join(@tempdir, "platformed_gem-2-*.gem")].first end first_build = gemspec.call @@ -248,7 +281,7 @@ def test_ruby_abi_produces_deterministic_content_address assert_equal File.basename(first_build), File.basename(second_build) end - def test_ruby_abi_with_output_raises + def test_content_addressable_with_output_raises gem = util_spec "platformed_gem" do |s| s.license = "AGPL-3.0-only" s.files = ["README.md"] @@ -261,7 +294,7 @@ def test_ruby_abi_with_output_raises gs.write gem.to_ruby end - @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4", "--output", "test.gem"] + @cmd.handle_options [gemspec_file, "--content-addressable", "--output", "test.gem"] error = assert_raise(ArgumentError) do use_ui @ui do Dir.chdir @tempdir do @@ -269,7 +302,7 @@ def test_ruby_abi_with_output_raises end end end - assert_match(/Cannot specify both a Ruby ABI and an output file name/, error.message) + assert_match(/Cannot specify an output file name for a content-addressable gem as these gems must use the generated file name./, error.message) end def test_execute_platform diff --git a/test/rubygems/test_gem_content_address.rb b/test/rubygems/test_gem_content_address.rb index 868d4ad6866d..71fae8315b44 100644 --- a/test/rubygems/test_gem_content_address.rb +++ b/test/rubygems/test_gem_content_address.rb @@ -216,29 +216,6 @@ def test_content_addressed_row_without_ruby_abi_validation refute Gem::ContentAddress.content_addressed_row?("x86_64-linux", Gem::Platform.new("x86_64-linux"), validate_ruby_abi: false) end - def test_ruby_abi_compatible_with_unset_requirement - spec = Gem::Specification.new "a", 1 - assert Gem::ContentAddress.ruby_abi_compatible?(spec, "3.4") - end - - def test_ruby_abi_compatible_with_matching_abi - spec = Gem::Specification.new "a", 1 - spec.required_ruby_version = "~> 3.4.0" - assert Gem::ContentAddress.ruby_abi_compatible?(spec, "3.4") - end - - def test_ruby_abi_compatible_with_different_abi - spec = Gem::Specification.new "a", 1 - spec.required_ruby_version = "~> 3.3.0" - refute Gem::ContentAddress.ruby_abi_compatible?(spec, "3.4") - end - - def test_ruby_abi_compatible_with_non_abi_shaped_requirement - spec = Gem::Specification.new "a", 1 - spec.required_ruby_version = ">= 3.0" - refute Gem::ContentAddress.ruby_abi_compatible?(spec, "3.4") - end - def test_address_for assert_equal Digest::SHA256.hexdigest("gem bytes")[0, 8], Gem::ContentAddress.address_for("gem bytes") assert Gem::ContentAddress.match?(Gem::ContentAddress.address_for("gem bytes")) diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index b0935693d1c7..bfbcd067b035 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -236,7 +236,7 @@ def test_add_files_symlink assert_equal [{ "lib/code_sym.rb" => "code.rb" }, { "lib/code_sym2.rb" => "../lib/code.rb" }], symlinks end - def test_ruby_abi_creates_content_addressed_file + def test_content_addressable_build_creates_content_addressed_file spec = Gem::Specification.new "platformed", "1" spec.summary = "platformed" spec.authors = "platformed" @@ -250,14 +250,14 @@ def test_ruby_abi_creates_content_addressed_file io.write "# lib/code.rb" end - built_file = Gem::Package.build(spec, false, false, nil, "3.4") + built_file = Gem::Package.build(spec, content_addressable: true) assert_path_not_exist spec.file_name assert_path_exist built_file assert_match(/\Aplatformed-1-[0-9a-f]{8}\.gem\z/, built_file) end - def test_ruby_abi_built_gem_preserves_derived_metadata + def test_content_addressable_build_preserves_derived_metadata spec = Gem::Specification.new "platformed", "1" spec.summary = "platformed" spec.authors = "platformed" @@ -271,7 +271,7 @@ def test_ruby_abi_built_gem_preserves_derived_metadata io.write "# lib/code.rb" end - built_file = Gem::Package.build(spec, false, false, nil, "3.4") + built_file = Gem::Package.build(spec, content_addressable: true) loaded_spec = Gem::Package.new(built_file).spec assert_equal "platformed", loaded_spec.name @@ -282,7 +282,7 @@ def test_ruby_abi_built_gem_preserves_derived_metadata assert_equal "3.4", loaded_spec.ruby_abi end - def test_required_rubygems_version_is_set_by_ruby_abi_if_default + def test_required_rubygems_version_is_set_for_content_addressable_build_if_default spec = Gem::Specification.new "platformed", "1" spec.summary = "platformed" spec.authors = "platformed" @@ -298,7 +298,7 @@ def test_required_rubygems_version_is_set_by_ruby_abi_if_default assert_equal Gem::Requirement.default, spec.required_rubygems_version - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) assert_equal Gem::Requirement.new(">= 4.1.0.a"), spec.required_rubygems_version end @@ -320,7 +320,7 @@ def test_required_rubygems_version_untouched_when_floor_already_satisfied ui = Gem::MockGemUi.new built_file = use_ui ui do - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) end assert_equal Gem::Requirement.new(">= 4.2"), spec.required_rubygems_version @@ -345,7 +345,7 @@ def test_required_rubygems_version_weaker_lower_bound_is_raised_to_floor_with_wa ui = Gem::MockGemUi.new built_file = use_ui ui do - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) end assert_equal Gem::Requirement.new(">= 4.1.0.a"), spec.required_rubygems_version @@ -371,7 +371,7 @@ def test_required_rubygems_version_upper_bound_above_minimum_is_preserved ui = Gem::MockGemUi.new use_ui ui do - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) end assert_equal Gem::Requirement.new(["< 5.0", ">= 4.1.0.a"]), spec.required_rubygems_version @@ -404,7 +404,7 @@ def test_raise_if_required_rubygems_version_conflicts_with_content_addressing spec.required_rubygems_version = Gem::Requirement.new(conflicting) e = assert_raise ArgumentError do - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) end assert_match "Cannot build gem for Ruby ABI 3.4 because required_rubygems_version is set to #{Gem::Requirement.new(conflicting)}", e.message @@ -429,7 +429,7 @@ def test_required_rubygems_version_is_not_duplicated_if_already_present io.write "# lib/code.rb" end - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) assert_equal [">= 4.1.0.a"], spec.required_rubygems_version.as_list end @@ -448,7 +448,7 @@ def test_required_rubygems_version_is_not_modified_if_build_fails end assert_raise Gem::InvalidSpecificationException do - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) end assert_equal Gem::Requirement.default, spec.required_rubygems_version @@ -470,13 +470,13 @@ def test_required_ruby_version_unchanged_after_successful_matching_build original_rrv = spec.required_ruby_version - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) assert_equal original_rrv, spec.required_ruby_version assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version end - def test_ruby_abi_not_passed_does_not_create_content_addressed_file + def test_default_build_does_not_create_content_addressed_file spec = Gem::Specification.new "platformed", "1" spec.summary = "platformed" spec.authors = "platformed" @@ -498,33 +498,12 @@ def test_ruby_abi_not_passed_does_not_create_content_addressed_file assert_equal Gem::Requirement.default, Gem::Package.new(built_file).spec.required_rubygems_version end - def test_required_ruby_version_is_set_by_ruby_abi_if_default - spec = Gem::Specification.new "platformed", "1" - spec.summary = "platformed" - spec.authors = "platformed" - spec.files = ["lib/code.rb"] - spec.platform = "arm64-darwin" - spec.required_ruby_version = Gem::Requirement.default - - FileUtils.mkdir "lib" - - File.open "lib/code.rb", "w" do |io| - io.write "# lib/code.rb" - end - - built_file = Gem::Package.build(spec, false, false, nil, "3.4") - - assert_path_exist built_file - assert_match(/\Aplatformed-1-[0-9a-f]{8}\.gem\z/, built_file) - assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version - end - def test_required_ruby_version_is_not_modified_if_build_fails spec = Gem::Specification.new "platformed", "1" spec.summary = "platformed" spec.files = ["lib/code.rb"] spec.platform = "arm64-darwin" - spec.required_ruby_version = Gem::Requirement.default + spec.required_ruby_version = Gem::Requirement.new("~> 3.4.0") FileUtils.mkdir "lib" @@ -534,42 +513,19 @@ def test_required_ruby_version_is_not_modified_if_build_fails # missing authors makes validation during the build raise assert_raise Gem::InvalidSpecificationException do - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) end - assert_equal Gem::Requirement.default, spec.required_ruby_version - end - - def test_raise_if_required_ruby_version_conflicts_with_ruby_abi - spec = Gem::Specification.new "platformed", "1" - spec.summary = "platformed" - spec.authors = "platformed" - spec.files = ["lib/code.rb"] - spec.platform = "arm64-darwin" - spec.required_ruby_version = Gem::Requirement.new("~> 3.5.0") - - FileUtils.mkdir "lib" - - File.open "lib/code.rb", "w" do |io| - io.write "# lib/code.rb" - end - - e = assert_raise ArgumentError do - Gem::Package.build(spec, false, false, nil, "3.4") - end - - assert_match "Cannot build gem for Ruby ABI 3.4 because required_ruby_version is set to ~> 3.5.0", e.message - assert_match "Please set required_ruby_version to \"~> 3.4.0\"", e.message - assert_equal Gem::Requirement.new("~> 3.5.0"), spec.required_ruby_version + assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version end - def test_raise_if_ruby_abi_is_not_in_x_y_format + def test_raise_if_required_ruby_version_is_ineligible_for_content_addressing spec = Gem::Specification.new "platformed", "1" spec.summary = "platformed" spec.authors = "platformed" spec.files = ["lib/code.rb"] spec.platform = "arm64-darwin" - spec.required_ruby_version = Gem::Requirement.new("~> 3.4.0") + spec.required_ruby_version = Gem::Requirement.new(">= 3.4") FileUtils.mkdir "lib" @@ -578,18 +534,20 @@ def test_raise_if_ruby_abi_is_not_in_x_y_format end e = assert_raise ArgumentError do - Gem::Package.build(spec, false, false, nil, "3.4.5") + Gem::Package.build(spec, content_addressable: true) end - assert_match "Ruby ABI must be in X.Y format", e.message + assert_match "Cannot build a content-addressable gem because required_ruby_version is set to >= 3.4", e.message + assert_match "Please set required_ruby_version to \"~> X.Y.0\"", e.message + assert_equal Gem::Requirement.new(">= 3.4"), spec.required_ruby_version end - def test_raise_if_spec_is_non_platformed_but_ruby_abi_is_passed + def test_raise_if_spec_is_non_platformed_but_content_addressable_is_requested spec = Gem::Specification.new "non-platformed", "1" spec.summary = "non-platformed" spec.authors = "non-platformed" spec.files = ["lib/code.rb"] - spec.required_ruby_version = Gem::Requirement.new("~> 3.4") + spec.required_ruby_version = Gem::Requirement.new("~> 3.4.0") FileUtils.mkdir "lib" @@ -598,7 +556,7 @@ def test_raise_if_spec_is_non_platformed_but_ruby_abi_is_passed end e = assert_raise ArgumentError do - Gem::Package.build(spec, false, false, nil, "3.4") + Gem::Package.build(spec, content_addressable: true) end assert_match "no platform or a Ruby platform has been set", e.message @@ -624,7 +582,7 @@ def test_explicit_output_keeps_requested_filename assert_equal("explicit-output.gem", built_file) end - def test_explicit_output_and_ruby_abi_raises + def test_explicit_output_and_content_addressable_raises spec = Gem::Specification.new "explicit", "1" spec.summary = "explicit" spec.authors = "explicit" @@ -639,10 +597,10 @@ def test_explicit_output_and_ruby_abi_raises end e = assert_raise ArgumentError do - Gem::Package.build(spec, false, false, "explicit-output.gem", "3.4") + Gem::Package.build(spec, false, false, "explicit-output.gem", content_addressable: true) end - assert_match "Cannot specify both a Ruby ABI and an output file name", e.message + assert_match "Cannot specify an output file name for a content-addressable gem", e.message assert_equal Gem::Requirement.default, spec.required_ruby_version assert_path_not_exist "explicit-output.gem" end diff --git a/test/rubygems/test_gem_package_task.rb b/test/rubygems/test_gem_package_task.rb index e180cc5d4ce7..a57feea07327 100644 --- a/test/rubygems/test_gem_package_task.rb +++ b/test/rubygems/test_gem_package_task.rb @@ -38,12 +38,14 @@ def test_gem_package Rake.application["package"].invoke assert_path_exist "pkg/pkgr-1.2.3.gem" + assert_path_not_exist "pkg/pkgr-1.2.3.gem-built" + assert_equal %w[x y], Dir.children("pkg/pkgr-1.2.3").sort end ensure RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity end - def test_moves_filename_returned_by_build + def test_builds_content_addressable_gem_and_records_it_in_the_stamp_file original_rake_fileutils_verbosity = RakeFileUtils.verbose_flag RakeFileUtils.verbose_flag = false @@ -58,32 +60,304 @@ def test_moves_filename_returned_by_build g.summary = "summary" end - Rake.application = Rake::Application.new + Dir.chdir @tempdir do + FileUtils.touch "x" + FileUtils.touch "y" - pkg = Gem::PackageTask.new(gem) do |p| - p.package_files << "y" + Rake.application = Rake::Application.new + + pkg = Gem::PackageTask.new(gem) do |p| + p.package_files << "y" + p.content_addressable = true + end + + assert_equal %w[x y], pkg.package_files + + Rake.application["package"].invoke + + built_files = Dir["pkg/pkgr-1.2.3-*.gem"] + assert_equal 1, built_files.length + + built_gem_path = built_files.first + assert_match(%r{\Apkg/pkgr-1\.2\.3-[0-9a-f]{8}\.gem\z}, built_gem_path) + assert_path_not_exist "pkg/pkgr-1.2.3-arm64-darwin.gem" + + stamp_path = "pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built" + assert_path_exist stamp_path + assert_equal built_gem_path, File.read(stamp_path) + + assert_equal %w[x y], Dir.children("pkg/pkgr-1.2.3-arm64-darwin-3.4").sort + assert_path_not_exist "pkg/pkgr-1.2.3-arm64-darwin" + + built_spec = Gem::Package.new(built_gem_path).spec + assert_equal "3.4", built_spec.ruby_abi + assert_equal Gem::Requirement.new("~> 3.4.0"), built_spec.required_ruby_version end + ensure + RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity + end - assert_equal %w[x y], pkg.package_files + def test_rebuilds_content_addressable_gem_when_stamp_points_to_missing_gem + original_rake_fileutils_verbosity = RakeFileUtils.verbose_flag + RakeFileUtils.verbose_flag = false + + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.platform = "arm64-darwin" + g.required_ruby_version = "~> 3.4.0" + + g.authors = %w[author] + g.files = %w[x] + g.summary = "summary" + end Dir.chdir @tempdir do FileUtils.touch "x" - FileUtils.touch "y" + FileUtils.mkdir_p "pkg" - built_gem_file = "pkgr-1.2.3-01234567.gem" + stamp_path = "pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built" + missing_gem_path = "pkg/pkgr-1.2.3-deadbeef.gem" + File.write stamp_path, missing_gem_path - Gem::Package.stub :build, ->(_) { - FileUtils.touch built_gem_file - built_gem_file - } do - Rake.application["package"].invoke + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true end + assert_path_not_exist stamp_path + + Rake.application["package"].invoke + built_files = Dir["pkg/pkgr-1.2.3-*.gem"] + assert_equal 1, built_files.length + + built_gem_path = built_files.first + assert_match(%r{\Apkg/pkgr-1\.2\.3-[0-9a-f]{8}\.gem\z}, built_gem_path) + assert_path_exist stamp_path + assert_equal built_gem_path, File.read(stamp_path) + end + ensure + RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity + end + + def test_discards_empty_content_addressable_stamp_file + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.platform = "arm64-darwin" + g.required_ruby_version = "~> 3.4.0" + + g.authors = %w[author] + g.files = %w[x] + g.summary = "summary" + end + + Dir.chdir @tempdir do + FileUtils.touch "x" + FileUtils.mkdir_p "pkg" + + stamp_path = "pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built" + FileUtils.touch stamp_path + out, err = capture_output do + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + end + + assert_path_not_exist stamp_path + assert_empty out + assert_empty err + end + end + + def test_does_not_rebuild_content_addressable_gem_when_stamp_is_up_to_date + original_rake_fileutils_verbosity = RakeFileUtils.verbose_flag + RakeFileUtils.verbose_flag = false + + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.platform = "arm64-darwin" + g.required_ruby_version = "~> 3.4.0" + + g.authors = %w[author] + g.files = %w[x] + g.summary = "summary" + end + + Dir.chdir @tempdir do + FileUtils.touch "x" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + Rake.application["package"].invoke + + built_files = Dir["pkg/pkgr-1.2.3-*.gem"] assert_equal 1, built_files.length - assert_equal "pkg/pkgr-1.2.3-01234567.gem", built_files.first - assert_path_not_exist "pkg/pkgr-1.2.3-arm64-darwin.gem" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + + Gem::Package.stub :build, ->(*) { flunk "expected the gem not to be rebuilt" } do + Rake.application["package"].invoke + end + + assert_equal built_files, Dir["pkg/pkgr-1.2.3-*.gem"] + assert_equal built_files.first, File.read("pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built") + end + ensure + RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity + end + + def test_rebuilds_content_addressable_gem_and_removes_previous_build_when_files_change + original_rake_fileutils_verbosity = RakeFileUtils.verbose_flag + RakeFileUtils.verbose_flag = false + + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.platform = "arm64-darwin" + g.required_ruby_version = "~> 3.4.0" + + g.authors = %w[author] + g.files = %w[x] + g.summary = "summary" + end + + Dir.chdir @tempdir do + stamp_path = "pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built" + + FileUtils.touch "x" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + Rake.application["package"].invoke + + first_gem_path = File.read(stamp_path) + assert_path_exist first_gem_path + + File.write "x", "changed" + newer = File.mtime(stamp_path) + 10 + File.utime newer, newer, "x" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + Rake.application["package"].invoke + + second_gem_path = File.read(stamp_path) + refute_equal first_gem_path, second_gem_path + + assert_path_not_exist first_gem_path + assert_path_exist second_gem_path + assert_equal [second_gem_path], Dir["pkg/pkgr-1.2.3-*.gem"] + end + ensure + RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity + end + + def test_keeps_previous_content_addressable_gem_when_rebuild_fails + original_rake_fileutils_verbosity = RakeFileUtils.verbose_flag + RakeFileUtils.verbose_flag = false + + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.platform = "arm64-darwin" + g.required_ruby_version = "~> 3.4.0" + + g.authors = %w[author] + g.files = %w[x] + g.summary = "summary" + end + + Dir.chdir @tempdir do + stamp_path = "pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built" + + FileUtils.touch "x" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + Rake.application["package"].invoke + + first_gem_path = File.read(stamp_path) + assert_path_exist first_gem_path + + File.write "x", "changed" + newer = File.mtime(stamp_path) + 10 + File.utime newer, newer, "x" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + + Gem::Package.stub :build, ->(*) { raise Gem::InvalidSpecificationException, "boom" } do + assert_raise(Gem::InvalidSpecificationException) do + Rake.application["package"].invoke + end + end + + assert_path_exist first_gem_path + assert_equal first_gem_path, File.read(stamp_path) + assert_equal [first_gem_path], Dir["pkg/pkgr-1.2.3-*.gem"] + end + ensure + RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity + end + + def test_keeps_content_addressable_gem_when_rebuild_produces_same_content + original_rake_fileutils_verbosity = RakeFileUtils.verbose_flag + RakeFileUtils.verbose_flag = false + + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.platform = "arm64-darwin" + g.required_ruby_version = "~> 3.4.0" + + g.authors = %w[author] + g.files = %w[x] + g.summary = "summary" + end + + Dir.chdir @tempdir do + stamp_path = "pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built" + + FileUtils.touch "x" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + Rake.application["package"].invoke + + first_gem_path = File.read(stamp_path) + + newer = File.mtime(stamp_path) + 10 + File.utime newer, newer, "x" + + Rake.application = Rake::Application.new + Gem::PackageTask.new(gem) do |package| + package.content_addressable = true + end + Rake.application["package"].invoke + + assert_equal first_gem_path, File.read(stamp_path) + assert_path_exist first_gem_path + assert_equal [first_gem_path], Dir["pkg/pkgr-1.2.3-*.gem"] end ensure RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity @@ -157,4 +431,93 @@ def test_package_dir_path assert_equal "pkg/nokogiri-1.5.0-java", pkg.package_dir_path end + + def test_package_dir_path_with_content_addressable + gem = Gem::Specification.new do |g| + g.name = "nokogiri" + g.version = "1.5.0" + g.platform = "x86_64-linux" + g.required_ruby_version = "~> 3.4.0" + end + + pkg = Gem::PackageTask.new gem + pkg.content_addressable = true + pkg.define + + assert_equal "nokogiri-1.5.0-x86_64-linux-3.4", pkg.package_name + assert_equal "pkg/nokogiri-1.5.0-x86_64-linux-3.4", pkg.package_dir_path + assert_equal "nokogiri-1.5.0-x86_64-linux-3.4.tgz", pkg.tgz_file + end + + def test_content_addressable_package_with_need_tar + original_rake_fileutils_verbosity = RakeFileUtils.verbose_flag + RakeFileUtils.verbose_flag = false + + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.platform = "arm64-darwin" + g.required_ruby_version = "~> 3.4.0" + + g.authors = %w[author] + g.files = %w[x] + g.summary = "summary" + end + + Dir.chdir @tempdir do + FileUtils.touch "x" + + Rake.application = Rake::Application.new + + Gem::PackageTask.new(gem) do |p| + p.content_addressable = true + p.need_tar = true + end + + Rake.application["package"].invoke + + built_files = Dir["pkg/pkgr-1.2.3-*.gem"] + assert_equal 1, built_files.length + assert_equal built_files.first, File.read("pkg/pkgr-1.2.3-arm64-darwin-3.4.gem-built") + assert_path_exist "pkg/pkgr-1.2.3-arm64-darwin-3.4.tgz" + end + ensure + RakeFileUtils.verbose_flag = original_rake_fileutils_verbosity + end + + def test_content_addressable_raises_when_required_ruby_version_does_not_identify_single_abi + gem = Gem::Specification.new do |g| + g.name = "nokogiri" + g.version = "1.5.0" + g.platform = "x86_64-linux" + g.required_ruby_version = ">= 3.4" + end + + Rake.application = Rake::Application.new + + pkg = Gem::PackageTask.new gem + pkg.content_addressable = true + + error = assert_raise(ArgumentError) { pkg.define } + assert_match(/required_ruby_version is set to >= 3.4/, error.message) + assert_match(/identifies a single Ruby ABI/, error.message) + assert_empty Rake.application.tasks + end + + def test_content_addressable_raises_when_platform_is_ruby + gem = Gem::Specification.new do |g| + g.name = "pkgr" + g.version = "1.2.3" + g.required_ruby_version = "~> 3.4.0" + end + + Rake.application = Rake::Application.new + + pkg = Gem::PackageTask.new gem + pkg.content_addressable = true + + error = assert_raise(ArgumentError) { pkg.define } + assert_match(/no platform or a Ruby platform has been set/, error.message) + assert_empty Rake.application.tasks + end end if defined?(Rake::PackageTask)