Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions lib/rubygems/ext/cargo_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
72 changes: 72 additions & 0 deletions lib/rubygems/ext/ext_conf_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand All @@ -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
148 changes: 148 additions & 0 deletions test/rubygems/test_gem_ext_cargo_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
Loading