diff --git a/detect/detect_test.go b/detect/detect_test.go index 74ab89c..d1848e7 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -68,6 +68,129 @@ func TestRubyTools(t *testing.T) { assertToolDetected(t, r, "lint", "RuboCop") } +func TestRubyAndBEAMRustIntegrations(t *testing.T) { + t.Run("projects", func(t *testing.T) { + ruby := runOn(t, "../testdata/ruby-rust-project") + assertToolDetectedWithConfidence(t, ruby, "native_extension", "rb-sys", brief.ConfidenceHigh) + assertToolDetectedWithConfidence(t, ruby, "library", "Magnus", brief.ConfidenceHigh) + assertToolDetected(t, ruby, "native_extension", "mkmf") + + elixir := runOn(t, "../testdata/rustler-project") + assertToolDetectedWithConfidence(t, elixir, "native_extension", "Rustler", brief.ConfidenceHigh) + }) + + t.Run("signals", func(t *testing.T) { + tests := []struct { + name string + files map[string]string + category string + tool string + }{ + { + name: "rb-sys Ruby dependency", + files: map[string]string{ + "Gemfile": "source \"https://rubygems.org\"\ngem \"rb_sys\"\n", + }, + category: "native_extension", + tool: "rb-sys", + }, + { + name: "rb-sys Cargo dependency", + files: map[string]string{ + "Gemfile": "source \"https://rubygems.org\"\n", + "Cargo.toml": "[package]\nname = \"example\"\nversion = \"0.1.0\"\n[dependencies.rb-sys]\nversion = \"0.9\"\n", + }, + category: "native_extension", + tool: "rb-sys", + }, + { + name: "Magnus Cargo dependency", + files: map[string]string{ + "Gemfile": "source \"https://rubygems.org\"\n", + "Cargo.toml": "[package]\nname = \"example\"\nversion = \"0.1.0\"\n[dependencies]\nmagnus = \"0.8\"\n", + }, + category: "library", + tool: "Magnus", + }, + { + name: "Rustler Mix dependency", + files: map[string]string{ + "mix.exs": "defmodule Example.MixProject do\n use Mix.Project\n def project, do: [deps: [{:rustler, \"~> 0.38\"}]]\nend\n", + }, + category: "native_extension", + tool: "Rustler", + }, + { + name: "Rustler Cargo dependency", + files: map[string]string{ + "mix.exs": "defmodule Example.MixProject do\n use Mix.Project\n def project, do: [deps: []]\nend\n", + "Cargo.toml": "[package]\nname = \"example\"\nversion = \"0.1.0\"\n[dependencies.rustler]\nversion = \"0.38\"\n", + }, + category: "native_extension", + tool: "Rustler", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + for path, content := range tt.files { + writeProjectFile(t, dir, path, content) + } + assertToolDetectedWithConfidence(t, runOn(t, dir), tt.category, tt.tool, brief.ConfidenceHigh) + }) + } + }) + + t.Run("plain extconf", func(t *testing.T) { + dir := t.TempDir() + writeProjectFile(t, dir, "Gemfile", "source \"https://rubygems.org\"\n") + writeProjectFile(t, dir, "ext/example/extconf.rb", "require \"mkmf\"\ncreate_makefile(\"example\")\n") + writeProjectFile( + t, + dir, + "Cargo.toml", + "[package]\nname = \"near-miss\"\nversion = \"0.1.0\"\n[dependencies]\nother-rb-sys = \"1\"\nsuper-magnus = \"1\"\n", + ) + + r := runOn(t, dir) + assertToolDetected(t, r, "native_extension", "mkmf") + if slices.ContainsFunc(r.Tools["native_extension"], func(d brief.Detection) bool { + return d.Name == "rb-sys" + }) { + t.Error("plain extconf.rb should not detect rb-sys") + } + if slices.ContainsFunc(r.Tools["library"], func(d brief.Detection) bool { + return d.Name == "Magnus" + }) { + t.Error("plain extconf.rb should not detect Magnus") + } + }) + + t.Run("plain Mix project", func(t *testing.T) { + dir := t.TempDir() + writeProjectFile( + t, + dir, + "mix.exs", + "defmodule Example.MixProject do\n use Mix.Project\n def project, do: [deps: []]\nend\n", + ) + writeProjectFile( + t, + dir, + "README.md", + "Rustler can be added later if this project needs a native extension.\n", + ) + + r := runOn(t, dir) + if slices.ContainsFunc(r.Tools["native_extension"], func(d brief.Detection) bool { + return d.Name == "Rustler" + }) { + t.Error("plain Mix project should not detect Rustler") + } + }) +} + func TestRubyTaxonomyPassesThrough(t *testing.T) { r := rubyReport(t) diff --git a/knowledge/elixir/rustler.toml b/knowledge/elixir/rustler.toml new file mode 100644 index 0000000..43b6571 --- /dev/null +++ b/knowledge/elixir/rustler.toml @@ -0,0 +1,26 @@ +[tool] +name = "Rustler" +category = "native_extension" +homepage = "https://github.com/rusterlium/rustler" +docs = "https://hexdocs.pm/rustler/" +repo = "https://github.com/rusterlium/rustler" +description = "Builds safe Erlang NIFs written in Rust" + +# Match the Mix package in mix.exs and the Cargo crate in Cargo.toml rather than +# relying on the global dependency cache shared by all manifest ecosystems. +[detect] +ecosystems = ["elixir"] + +[detect.file_contains] +"mix.exs" = ["{:rustler,"] +"Cargo.toml" = ["\nrustler =", "\nrustler=", "[dependencies.rustler]"] + +[commands] +run = "mix compile" +alternatives = ["mix compile --force"] + +[config] +files = ["mix.exs", "Cargo.toml", "native/**/Cargo.toml"] + +[taxonomy] +role = ["native-extension"] diff --git a/knowledge/ruby/magnus.toml b/knowledge/ruby/magnus.toml new file mode 100644 index 0000000..3e17c11 --- /dev/null +++ b/knowledge/ruby/magnus.toml @@ -0,0 +1,21 @@ +[tool] +name = "Magnus" +category = "library" +homepage = "https://github.com/matsadler/magnus" +docs = "https://docs.rs/magnus/" +repo = "https://github.com/matsadler/magnus" +description = "Rust bindings for the Ruby API" + +# Match the Cargo crate in Cargo.toml rather than through the global dependency +# cache so a same-named dependency from another ecosystem does not trigger it. +[detect] +ecosystems = ["ruby"] + +[detect.file_contains] +"Cargo.toml" = ["\nmagnus =", "\nmagnus=", "[dependencies.magnus]"] + +[config] +files = ["Cargo.toml"] + +[taxonomy] +role = ["library"] diff --git a/knowledge/ruby/rb-sys.toml b/knowledge/ruby/rb-sys.toml new file mode 100644 index 0000000..9ff6488 --- /dev/null +++ b/knowledge/ruby/rb-sys.toml @@ -0,0 +1,26 @@ +[tool] +name = "rb-sys" +category = "native_extension" +homepage = "https://oxidize-rb.github.io/rb-sys/" +docs = "https://oxidize-rb.github.io/rb-sys/" +repo = "https://github.com/oxidize-rb/rb-sys" +description = "Builds Ruby native extensions written in Rust" + +# Match the Cargo crate in Cargo.toml rather than through the global dependency +# cache so a same-named dependency from another ecosystem does not trigger it. +[detect] +dependencies = ["rb_sys"] +ecosystems = ["ruby"] + +[detect.file_contains] +"Cargo.toml" = ["\nrb-sys =", "\nrb-sys=", "[dependencies.rb-sys]"] + +[commands] +run = "bundle exec rake compile" +alternatives = ["rake compile"] + +[config] +files = ["Gemfile", "Cargo.toml", "extconf.rb", "ext/**/extconf.rb"] + +[taxonomy] +role = ["native-extension"] diff --git a/testdata/ruby-rust-project/Cargo.toml b/testdata/ruby-rust-project/Cargo.toml new file mode 100644 index 0000000..a1954f3 --- /dev/null +++ b/testdata/ruby-rust-project/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ruby-rust-project" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +magnus = "0.8" +rb-sys = "0.9" diff --git a/testdata/ruby-rust-project/Gemfile b/testdata/ruby-rust-project/Gemfile new file mode 100644 index 0000000..5b17fe8 --- /dev/null +++ b/testdata/ruby-rust-project/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "rb_sys" diff --git a/testdata/ruby-rust-project/ext/ruby_rust_project/extconf.rb b/testdata/ruby-rust-project/ext/ruby_rust_project/extconf.rb new file mode 100644 index 0000000..d0438b5 --- /dev/null +++ b/testdata/ruby-rust-project/ext/ruby_rust_project/extconf.rb @@ -0,0 +1,4 @@ +require "mkmf" +require "rb_sys/mkmf" + +create_rust_makefile("ruby_rust_project/ruby_rust_project") diff --git a/testdata/ruby-rust-project/src/lib.rs b/testdata/ruby-rust-project/src/lib.rs new file mode 100644 index 0000000..ace84d3 --- /dev/null +++ b/testdata/ruby-rust-project/src/lib.rs @@ -0,0 +1 @@ +pub fn fixture() {} diff --git a/testdata/rustler-project/mix.exs b/testdata/rustler-project/mix.exs new file mode 100644 index 0000000..dbbc0f6 --- /dev/null +++ b/testdata/rustler-project/mix.exs @@ -0,0 +1,21 @@ +defmodule RustlerProject.MixProject do + use Mix.Project + + def project do + [ + app: :rustler_project, + version: "0.1.0", + deps: deps() + ] + end + + def application do + [extra_applications: [:logger]] + end + + defp deps do + [ + {:rustler, "~> 0.38"} + ] + end +end diff --git a/testdata/rustler-project/native/rustler_project/Cargo.toml b/testdata/rustler-project/native/rustler_project/Cargo.toml new file mode 100644 index 0000000..53b3bd7 --- /dev/null +++ b/testdata/rustler-project/native/rustler_project/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rustler_project" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +rustler = "0.38" diff --git a/testdata/rustler-project/native/rustler_project/src/lib.rs b/testdata/rustler-project/native/rustler_project/src/lib.rs new file mode 100644 index 0000000..ace84d3 --- /dev/null +++ b/testdata/rustler-project/native/rustler_project/src/lib.rs @@ -0,0 +1 @@ +pub fn fixture() {}