Skip to content
1 change: 1 addition & 0 deletions Manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ bundler/lib/bundler/templates/newgem/bin/setup.tt
bundler/lib/bundler/templates/newgem/circleci/config.yml.tt
bundler/lib/bundler/templates/newgem/exe/newgem.tt
bundler/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt
bundler/lib/bundler/templates/newgem/ext/newgem/build.rs.tt
bundler/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt
bundler/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt
bundler/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt
Expand Down
1 change: 1 addition & 0 deletions bundler/lib/bundler/cli/gem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def run
templates.merge!(
"Cargo.toml.tt" => "Cargo.toml",
"ext/newgem/Cargo.toml.tt" => "ext/#{name}/Cargo.toml",
"ext/newgem/build.rs.tt" => "ext/#{name}/build.rs",
"ext/newgem/extconf-rust.rb.tt" => "ext/#{name}/extconf.rb",
"ext/newgem/src/lib.rs.tt" => "ext/#{name}/src/lib.rs",
)
Expand Down
6 changes: 6 additions & 0 deletions bundler/lib/bundler/templates/newgem/Cargo.toml.tt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@
[workspace]
members = ["./ext/<%= config[:name] %>"]
resolver = "2"

[profile.release]
# By default, debug symbols are stripped from the final binary which makes it
# harder to debug if something goes wrong. It's recommended to keep debug
# symbols in the release build so that you can debug the final binary if needed.
debug = true
3 changes: 0 additions & 3 deletions bundler/lib/bundler/templates/newgem/Gemfile.tt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ gem "rake", "~> 13.0"
<%- if config[:ext] -%>

gem "rake-compiler"
<%- if config[:ext] == 'rust' -%>
gem "rb_sys", "~> 0.9.63"
<%- end -%>
<%- end -%>
<%- if config[:test] -%>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ publish = false
crate-type = ["cdylib"]

[dependencies]
magnus = { version = "0.6.2" }
magnus = { version = "0.6.3" }
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, we support ruby-head out of the box. This allows users to run development/unstable rubies in production without friction


[build-dependencies]
rb-sys-env = "0.1.2"

[dev-dependencies]
rb-sys-test-helpers = { version = "0.2.0" }
5 changes: 5 additions & 0 deletions bundler/lib/bundler/templates/newgem/ext/newgem/build.rs.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = rb_sys_env::activate()?;

Ok(())
}
15 changes: 13 additions & 2 deletions bundler/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use magnus::{function, prelude::*, Error, Ruby};

fn hello(subject: String) -> String {
format!("Hello from Rust, {subject}!")
pub fn hello(subject: String) -> String {
format!("Hello {subject}, from Rust!")
}

#[magnus::init]
Expand All @@ -10,3 +10,14 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
module.define_singleton_method("hello", function!(hello, 1))?;
Ok(())
}

#[cfg(test)]
mod tests {
use rb_sys_test_helpers::ruby_test;
use super::hello;

#[ruby_test]
fn test_hello() {
assert_eq!("Hello world, from Rust!", hello("world".to_string()));
}
}
9 changes: 8 additions & 1 deletion bundler/lib/bundler/templates/newgem/lib/newgem.rb.tt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

require_relative "<%= File.basename(config[:namespaced_path]) %>/version"
<%- if config[:ext] -%>
require_relative "<%= File.basename(config[:namespaced_path]) %>/<%= config[:underscored_name] %>"
# Attempt to load a versioned extension based on the Ruby version.
# Fall back to loading the non-versioned extension if version-specific loading fails.
begin
RUBY_VERSION =~ /(\d+\.\d+)/
require "#{Regexp.last_match(1)}/<%= File.basename(config[:namespaced_path]) %>/<%= config[:underscored_name] %>"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something users frequently stumble on when trying to precompile gems, so let's just use the recommended pattern by rake-compiler as a default: https://github.com/rake-compiler/rake-compiler?tab=readme-ov-file#cross-compilation---the-future-is-now

rescue LoadError
require "<%= File.basename(config[:namespaced_path]) %>/<%= config[:underscored_name] %>"
end
<%- end -%>

<%- config[:constant_array].each_with_index do |c, i| -%>
Expand Down
6 changes: 1 addition & 5 deletions bundler/lib/bundler/templates/newgem/newgem.gemspec.tt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ Gem::Specification.new do |spec|
spec.license = "MIT"
<%- end -%>
spec.required_ruby_version = ">= <%= config[:required_ruby_version] %>"
<%- if config[:ext] == 'rust' -%>
spec.required_rubygems_version = ">= <%= config[:rust_builder_required_rubygems_version] %>"
<%- end -%>

spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"

spec.metadata["homepage_uri"] = spec.homepage
Expand All @@ -41,7 +37,7 @@ Gem::Specification.new do |spec|
spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"]
<%- end -%>
<%- if config[:ext] == 'rust' -%>
spec.extensions = ["ext/<%= config[:underscored_name] %>/Cargo.toml"]
spec.add_dependency "rb_sys", "~> 0.9"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CargoBuilder was a good first start, but it's much less flexible and not the best default IMO. rb-sys + create_rust_makefile is much more robust these days, so let's use it

<%- end -%>

# Uncomment to register a new dependency of your gem
Expand Down
8 changes: 8 additions & 0 deletions bundler/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ RSpec.describe <%= config[:constant_name] %> do
expect(<%= config[:constant_name] %>::VERSION).not_to be nil
end

<%- if config[:ext] == 'rust' -%>
it "can call into Rust" do
result = Testing.hello("world")

expect(result).to be("Hello earth, from Rust!")
end
<%- else -%>
it "does something useful" do
expect(false).to eq(true)
end
<%- end -%>
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ class <%= config[:minitest_constant_name] %> < Minitest::Test
refute_nil ::<%= config[:constant_name] %>::VERSION
end

<%- if config[:ext] == 'rust' -%>
def test_hello_world
assert_equal "Hello earth, from Rust!", Testing.hello("world")
end
<%- else -%>
def test_it_does_something_useful
assert false
end
<%- end -%>
end
41 changes: 26 additions & 15 deletions bundler/spec/commands/newgem_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1450,18 +1450,6 @@ def create_temporary_dir(dir)
end
end

context "--ext parameter set with rust and old RubyGems" do
it "fails in friendly way" do
if ::Gem::Version.new("3.3.11") <= ::Gem.rubygems_version
skip "RubyGems compatible with Rust builder"
end

expect do
bundle ["gem", gem_name, "--ext=rust"].compact.join(" ")
end.to raise_error(RuntimeError, /too old to build Rust extension/)
end
end

context "--ext parameter set with rust" do
let(:flags) { "--ext=rust" }

Expand All @@ -1480,12 +1468,11 @@ def create_temporary_dir(dir)
expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist
expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist
expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist
end

it "includes rake-compiler, rb_sys gems and required_rubygems_version constraint" do
it "includes rake-compiler constraint" do
expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"')
expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rb_sys"')
expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.required_rubygems_version = ">= ')
end

it "depends on compile task for build" do
Expand All @@ -1508,6 +1495,30 @@ def create_temporary_dir(dir)

expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
end

it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do
env = setup_rust_env
gem_path = bundled_app(gem_name)
result = sys_exec("cargo test", env: env, dir: gem_path)

expect(result).to include("1 passed")
end

def setup_rust_env
skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw")

env = {
"CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")),
"RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")),
"RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"),
}

system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out])
skip "cargo not present" unless $?.success?
# Hermetic Cargo setup
RbConfig::CONFIG.each { |k, v| env["RBCONFIG_#{k}"] = v }
env
end
end
end

Expand Down
1 change: 1 addition & 0 deletions bundler/spec/support/filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def inspect
config.filter_run_excluding jruby_only: RUBY_ENGINE != "jruby"
config.filter_run_excluding truffleruby_only: RUBY_ENGINE != "truffleruby"
config.filter_run_excluding man: Gem.win_platform?
config.filter_run_excluding mri_only: RUBY_ENGINE != "ruby"

config.filter_run_when_matching :focus unless ENV["CI"]
end