From d7771da1e8a5dcf6192d292a24e7f05bc68271dd Mon Sep 17 00:00:00 2001 From: Jonathan Barquero Date: Fri, 15 Aug 2025 09:14:50 -0600 Subject: [PATCH] fix: install_extension_in_lib option will add wrapper file logic to use ext/ path --- lib/rubygems/ext/cargo_builder.rb | 72 ++++++++ lib/rubygems/ext/ext_conf_builder.rb | 72 ++++++++ test/rubygems/test_gem_ext_cargo_builder.rb | 148 ++++++++++++++++ .../rubygems/test_gem_ext_ext_conf_builder.rb | 162 ++++++++++++++++++ 4 files changed, 454 insertions(+) diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb index 21b50f394d55..49958b44fd9c 100644 --- a/lib/rubygems/ext/cargo_builder.rb +++ b/lib/rubygems/ext/cargo_builder.rb @@ -54,6 +54,9 @@ def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = D nested_lib_dir = File.join(lib_dir, nesting) FileUtils.mkdir_p nested_lib_dir FileUtils.cp_r dlext_path, nested_lib_dir, remove_destination: true + + # Create wrapper files for backwards compatibility + create_wrapper_files(nested_lib_dir, nested_dest_path, [dlext_path], gem_name) end # move to final destination @@ -347,4 +350,73 @@ def initialize(dir) MSG end end + + def self.detect_gem_name_from_path(cargo_dir) + # Try to detect gem name from the cargo directory path + # Look for patterns like /path/to/gem_name/ext/extension_name + path_parts = cargo_dir.split(File::SEPARATOR) + + # Find the gem name by looking for the parent of 'ext' directory + ext_index = path_parts.rindex('ext') + return nil unless ext_index && ext_index > 0 + + gem_name = path_parts[ext_index - 1] + return nil if gem_name.nil? || gem_name.empty? + + gem_name + end + + def self.create_wrapper_files(lib_dir, dest_path, entries, gem_name) + return unless gem_name + + # Find native extensions in the entries + native_extensions = entries.select do |entry| + File.file?(entry) && native_extension?(entry) + end + + native_extensions.each do |extension_path| + extension_name = File.basename(extension_path) + wrapper_path = File.join(lib_dir, "#{extension_name}.rb") + + # Create wrapper file that loads from ext/ and shows deprecation warning + create_wrapper_file(wrapper_path, extension_name, gem_name) + end + end + + def self.native_extension?(file_path) + # Check if file is a native extension based on platform + case RbConfig::CONFIG["host_os"] + when /darwin|mac os/ + File.extname(file_path) == ".bundle" + when /mswin|mingw|cygwin/ + File.extname(file_path) == ".dll" + else + File.extname(file_path) == ".so" + end + end + + # Creates a wrapper file that loads the extension from ext/ and shows deprecation warning + # it can be removed when ffi-compiler is updated to use the new path + def self.create_wrapper_file(wrapper_path, extension_name, gem_name) + wrapper_content = <<~RUBY + # frozen_string_literal: true + + # DEPRECATED: This extension is loaded from lib/ directory + # The extension has been moved to ext/ directory for better organization + # + # To fix this deprecation warning, update your code to load from ext/: + # require_relative '../ext/#{extension_name}' + # + # Or set install_extension_in_lib: true in your .gemrc to maintain current behavior + + warn "DEPRECATED: Gem '#{gem_name}' is loading native extension '#{extension_name}' from lib/ directory. " \ + "Consider updating your code to load from ext/ directory instead. " \ + "Set install_extension_in_lib: true in your .gemrc to maintain current behavior." + + # Load the actual extension from ext/ directory + require_relative "../ext/#{extension_name}" + RUBY + + File.write(wrapper_path, wrapper_content) + end end diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index 8aa15962a0f3..313818fd271a 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -53,6 +53,9 @@ def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_di entries = Dir.entries(full_tmp_dest) - %w[. ..] entries = entries.map {|entry| File.join full_tmp_dest, entry } FileUtils.cp_r entries, lib_dir, remove_destination: true + + # Create wrapper files for backwards compatibility + create_wrapper_files(lib_dir, dest_path, entries, gem_name) end FileUtils::Entry_.new(full_tmp_dest).traverse do |ent| @@ -78,4 +81,73 @@ def self.get_relative_path(path, base) path[0..base.length - 1] = "." if path.start_with?(base) path end + + def self.detect_gem_name_from_path(extension_dir) + # Try to detect gem name from the extension directory path + # Look for patterns like /path/to/gem_name/ext/extension_name + path_parts = extension_dir.split(File::SEPARATOR) + + # Find the gem name by looking for the parent of 'ext' directory + ext_index = path_parts.rindex('ext') + return nil unless ext_index && ext_index > 0 + + gem_name = path_parts[ext_index - 1] + return nil if gem_name.nil? || gem_name.empty? + + gem_name + end + + def self.create_wrapper_files(lib_dir, dest_path, entries, gem_name) + return unless gem_name + + # Find native extensions in the entries + native_extensions = entries.select do |entry| + File.file?(entry) && native_extension?(entry) + end + + native_extensions.each do |extension_path| + extension_name = File.basename(extension_path) + wrapper_path = File.join(lib_dir, "#{extension_name}.rb") + + # Create wrapper file that loads from ext/ and shows deprecation warning + create_wrapper_file(wrapper_path, extension_name, gem_name) + end + end + + def self.native_extension?(file_path) + # Check if file is a native extension based on platform + case RbConfig::CONFIG["host_os"] + when /darwin|mac os/ + File.extname(file_path) == ".bundle" + when /mswin|mingw|cygwin/ + File.extname(file_path) == ".dll" + else + File.extname(file_path) == ".so" + end + end + + # Creates a wrapper file that loads the extension from ext/ and shows deprecation warning + # it can be removed when ffi-compiler is updated to use the new path + def self.create_wrapper_file(wrapper_path, extension_name, gem_name) + wrapper_content = <<~RUBY + # frozen_string_literal: true + + # DEPRECATED: This extension is loaded from lib/ directory + # The extension has been moved to ext/ directory for better organization + # + # To fix this deprecation warning, update your code to load from ext/: + # require_relative '../ext/#{extension_name}' + # + # Or set install_extension_in_lib: true in your .gemrc to maintain current behavior + + warn "DEPRECATED: Gem '#{gem_name}' is loading native extension '#{extension_name}' from lib/ directory. " \ + "Consider updating your code to load from ext/ directory instead. " \ + "Set install_extension_in_lib: true in your .gemrc to maintain current behavior." + + # Load the actual extension from ext/ directory + require_relative "../ext/#{extension_name}" + RUBY + + File.write(wrapper_path, wrapper_content) + end end diff --git a/test/rubygems/test_gem_ext_cargo_builder.rb b/test/rubygems/test_gem_ext_cargo_builder.rb index b970e442c250..8a263e90d689 100644 --- a/test/rubygems/test_gem_ext_cargo_builder.rb +++ b/test/rubygems/test_gem_ext_cargo_builder.rb @@ -193,6 +193,154 @@ def test_linker_args_with_cachetools_and_options RbConfig::MAKEFILE_CONFIG["CC"] = orig_cc end + def test_extension_in_lib_with_wrapper_file_when_enabled + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + # Mock the install_extension_in_lib setting + Gem.stub(:install_extension_in_lib, true) do + # Create a mock Rust extension file + extension_file = File.join(@ext, "test_rust_extension.#{RbConfig::CONFIG["DLEXT"]}") + File.write(extension_file, "fake rust extension content") + + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + # Test the wrapper file creation + entries = [extension_file] + gem_name = "test_rust_gem" + + Gem::Ext::CargoBuilder.create_wrapper_files(lib_dir, @dest_path, entries, gem_name) + + # Verify wrapper file was created + wrapper_path = File.join(lib_dir, "test_rust_extension.#{RbConfig::CONFIG["DLEXT"]}.rb") + assert_path_exist wrapper_path + + # Verify wrapper content and path to ext/ + wrapper_content = File.read(wrapper_path) + assert_includes wrapper_content, "DEPRECATED: Gem 'test_rust_gem'" + assert_includes wrapper_content, "require_relative \"../ext/test_rust_extension.#{RbConfig::CONFIG["DLEXT"]}\"" + end + end + + def test_extension_in_lib_with_wrapper_file_when_disabled + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + # Mock the install_extension_in_lib setting to false + Gem.stub(:install_extension_in_lib, false) do + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + extension_file = File.join(@ext, "test_extension.#{RbConfig::CONFIG["DLEXT"]}") + File.write(extension_file, "fake extension content") + + entries = [extension_file] + gem_name = "test_gem" + + # This should not create wrapper files when install_extension_in_lib is false + # The wrapper creation is only called when install_extension_in_lib is true + # So we're testing the integration point + refute_path_exist File.join(lib_dir, "test_extension.#{RbConfig::CONFIG["DLEXT"]}.rb") + end + end + + def test_extension_in_lib_with_wrapper_file_when_enabled_with_nested_lib_directory + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + Gem.stub(:install_extension_in_lib, true) do + # Test with nested lib directory structure (like cargo builder uses) + nested_lib_dir = File.join(@dest_path, "lib", "nested") + FileUtils.mkdir_p(nested_lib_dir) + + extension_file = File.join(@ext, "test_nested_extension.#{RbConfig::CONFIG["DLEXT"]}") + File.write(extension_file, "fake nested extension content") + + entries = [extension_file] + gem_name = "test_nested_gem" + + Gem::Ext::CargoBuilder.create_wrapper_files(nested_lib_dir, @dest_path, entries, gem_name) + + # Verify wrapper file was created in nested directory + wrapper_path = File.join(nested_lib_dir, "test_nested_extension.#{RbConfig::CONFIG["DLEXT"]}.rb") + assert_path_exist wrapper_path + + # Verify wrapper content points to correct ext/ location + wrapper_content = File.read(wrapper_path) + assert_includes wrapper_content, "require_relative \"../ext/test_nested_extension.#{RbConfig::CONFIG["DLEXT"]}\"" + end + end + + def test_extension_in_lib_detection_os + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + # Test operative system specific extension detection + case RbConfig::CONFIG["host_os"] + when /darwin|mac os/ + extension_file = File.join(@ext, "test_extension.bundle") + expected_wrapper = "test_extension.bundle.rb" + when /mswin|mingw|cygwin/ + extension_file = File.join(@ext, "test_extension.dll") + expected_wrapper = "test_extension.dll.rb" + else + extension_file = File.join(@ext, "test_extension.so") + expected_wrapper = "test_extension.so.rb" + end + + File.write(extension_file, "fake extension content") + + entries = [extension_file] + gem_name = "test_platform_gem" + + Gem::Ext::CargoBuilder.create_wrapper_files(lib_dir, @dest_path, entries, gem_name) + + # Verify wrapper file was created with correct extension + wrapper_path = File.join(lib_dir, expected_wrapper) + assert_path_exist wrapper_path + + # Verify wrapper content + wrapper_content = File.read(wrapper_path) + assert_includes wrapper_content, "DEPRECATED: Gem 'test_platform_gem'" + end + + def test_extension_in_lib_with_wrapper_file_when_enabled_with_multiple_extensions + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + Gem.stub(:install_extension_in_lib, true) do + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + # Create multiple extension files + extension1 = File.join(@ext, "extension1.#{RbConfig::CONFIG["DLEXT"]}") + extension2 = File.join(@ext, "extension2.#{RbConfig::CONFIG["DLEXT"]}") + extension3 = File.join(@ext, "extension3.#{RbConfig::CONFIG["DLEXT"]}") + + [extension1, extension2, extension3].each do |ext_file| + File.write(ext_file, "fake extension content") + end + + entries = [extension1, extension2, extension3] + gem_name = "test_multi_gem" + + Gem::Ext::CargoBuilder.create_wrapper_files(lib_dir, @dest_path, entries, gem_name) + + # Verify all wrapper files were created + assert_path_exist File.join(lib_dir, "extension1.#{RbConfig::CONFIG["DLEXT"]}.rb") + assert_path_exist File.join(lib_dir, "extension2.#{RbConfig::CONFIG["DLEXT"]}.rb") + assert_path_exist File.join(lib_dir, "extension3.#{RbConfig::CONFIG["DLEXT"]}.rb") + + # Verify wrapper content for each + [extension1, extension2, extension3].each do |ext_file| + extension_name = File.basename(ext_file) + wrapper_path = File.join(lib_dir, "#{extension_name}.rb") + wrapper_content = File.read(wrapper_path) + assert_includes wrapper_content, "DEPRECATED: Gem 'test_multi_gem'" + assert_includes wrapper_content, "require_relative \"../ext/#{extension_name}\"" + end + end + end + private def skip_unsupported_platforms! diff --git a/test/rubygems/test_gem_ext_ext_conf_builder.rb b/test/rubygems/test_gem_ext_ext_conf_builder.rb index bc383e5540a9..62d8889def14 100644 --- a/test/rubygems/test_gem_ext_ext_conf_builder.rb +++ b/test/rubygems/test_gem_ext_ext_conf_builder.rb @@ -220,4 +220,166 @@ def configure_args(args = nil) RbConfig::CONFIG.delete "configure_args" end end + + def test_install_extension_in_lib_with_wrapper_file_when_enabled + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + # Mock the install_extension_in_lib setting + Gem.stub(:install_extension_in_lib, true) do + # Create a mock extension file + extension_file = File.join(@ext, "test_extension.#{RbConfig::CONFIG["DLEXT"]}") + File.write(extension_file, "fake extension content") + + # Mock the build method to test wrapper creation + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + # Test the wrapper file creation + entries = [extension_file] + gem_name = "test_gem" + + Gem::Ext::ExtConfBuilder.create_wrapper_files(lib_dir, @dest_path, entries, gem_name) + + # Verify wrapper file was created + wrapper_path = File.join(lib_dir, "test_extension.#{RbConfig::CONFIG["DLEXT"]}.rb") + assert_path_exist wrapper_path + + # Verify wrapper content + wrapper_content = File.read(wrapper_path) + assert_includes wrapper_content, "DEPRECATED: Gem 'test_gem'" + assert_includes wrapper_content, "require_relative \"../ext/test_extension.#{RbConfig::CONFIG["DLEXT"]}\"" + end + end + + def test_install_extension_in_lib_with_wrapper_file_without_gem_name + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + # Test that no wrapper is created when gem_name is nil + extension_file = File.join(@ext, "test_extension.#{RbConfig::CONFIG["DLEXT"]}") + File.write(extension_file, "fake extension content") + + entries = [extension_file] + + Gem::Ext::ExtConfBuilder.create_wrapper_files(lib_dir, @dest_path, entries, nil) + + # Verify no wrapper file was created + wrapper_path = File.join(lib_dir, "test_extension.#{RbConfig::CONFIG["DLEXT"]}.rb") + refute_path_exist wrapper_path + end + + def test_install_extension_in_lib_detection_os + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + # Test different operative system extensions + case RbConfig::CONFIG["host_os"] + when /darwin|mac os/ + assert Gem::Ext::ExtConfBuilder.native_extension?("test.bundle") + refute Gem::Ext::ExtConfBuilder.native_extension?("test.so") + when /mswin|mingw|cygwin/ + assert Gem::Ext::ExtConfBuilder.native_extension?("test.dll") + refute Gem::Ext::ExtConfBuilder.native_extension?("test.so") + else + assert Gem::Ext::ExtConfBuilder.native_extension?("test.so") + refute Gem::Ext::ExtConfBuilder.native_extension?("test.dll") + end + + # Test non-extension files + refute Gem::Ext::ExtConfBuilder.native_extension?("test.rb") + refute Gem::Ext::ExtConfBuilder.native_extension?("test.txt") + end + + def test_install_extension_in_lib_with_wrapper_file_detects_gem_name_from_path + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + # Test path detection + path = "/path/to/gem_name/ext/extension_name" + gem_name = Gem::Ext::ExtConfBuilder.detect_gem_name_from_path(path) + assert_equal "gem_name", gem_name + + # Test path without ext directory + path = "/path/to/gem_name/lib/extension_name" + gem_name = Gem::Ext::ExtConfBuilder.detect_gem_name_from_path(path) + assert_nil gem_name + + # Test path with multiple ext directories + path = "/path/to/gem_name/ext/subdir/ext/extension_name" + gem_name = Gem::Ext::ExtConfBuilder.detect_gem_name_from_path(path) + assert_equal "subdir", gem_name + end + + def test_extension_in_lib_with_wrapper_file_content_file_structure + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + extension_name = "test_extension.#{RbConfig::CONFIG["DLEXT"]}" + gem_name = "test_gem" + wrapper_path = File.join(lib_dir, "#{extension_name}.rb") + + Gem::Ext::ExtConfBuilder.create_wrapper_file(wrapper_path, extension_name, gem_name) + + # Verify wrapper file exists + assert_path_exist wrapper_path + + # Verify content structure + content = File.read(wrapper_path) + assert_includes content, "# frozen_string_literal: true" + assert_includes content, "# DEPRECATED: This extension is loaded from lib/ directory" + assert_includes content, "warn \"DEPRECATED: Gem 'test_gem'" + assert_includes content, "require_relative \"../ext/#{extension_name}\"" + assert_includes content, "This wrapper will be removed in a future RubyGems version" + end + + def test_extension_in_lib_wont_wrap_non_native_extensions + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + # Create non-native extension files + ruby_file = File.join(@ext, "test_helper.rb") + File.write(ruby_file, "class TestHelper; end") + + text_file = File.join(@ext, "README.txt") + File.write(text_file, "Read me") + + entries = [ruby_file, text_file] + gem_name = "test_gem" + + Gem::Ext::ExtConfBuilder.create_wrapper_files(lib_dir, @dest_path, entries, gem_name) + + # Verify no wrapper files were created + refute_path_exist File.join(lib_dir, "test_helper.rb.rb") + refute_path_exist File.join(lib_dir, "README.txt.rb") + end + + def test_extension_in_lib_will_wrap_native_extensions_only + skip "Wrapper functionality not available" unless Gem.respond_to?(:install_extension_in_lib) + + lib_dir = File.join(@dest_path, "lib") + FileUtils.mkdir_p(lib_dir) + + # Create mixed content: native extension + Ruby file + extension_file = File.join(@ext, "test_extension.#{RbConfig::CONFIG["DLEXT"]}") + File.write(extension_file, "fake extension content") + + ruby_file = File.join(@ext, "test_wrapper.rb") + File.write(ruby_file, "class TestWrapper; end") + + entries = [extension_file, ruby_file] + gem_name = "test_gem" + + Gem::Ext::ExtConfBuilder.create_wrapper_files(lib_dir, @dest_path, entries, gem_name) + + # Verify wrapper file was created only for native extension + wrapper_path = File.join(lib_dir, "test_extension.#{RbConfig::CONFIG["DLEXT"]}.rb") + assert_path_exist wrapper_path + + # Verify no wrapper for Ruby file + refute_path_exist File.join(lib_dir, "test_helper.rb.rb") + end end