diff --git a/detect/detect_test.go b/detect/detect_test.go index 7fec1d7..74ab89c 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -704,6 +704,100 @@ func TestPythonProject(t *testing.T) { } } +func TestPythonRustNativeExtensionProjects(t *testing.T) { + tests := []struct { + name string + fixture string + tool string + }{ + {name: "Maturin", fixture: "../testdata/maturin-project", tool: "Maturin"}, + {name: "setuptools-rust", fixture: "../testdata/setuptools-rust-project", tool: "setuptools-rust"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := runOn(t, tt.fixture) + assertToolDetectedWithConfidence(t, r, "native_extension", tt.tool, brief.ConfidenceHigh) + }) + } +} + +func TestPythonRustNativeExtensionSignals(t *testing.T) { + tests := []struct { + name string + path string + content string + tool string + }{ + { + name: "Maturin build backend", + path: "pyproject.toml", + content: "[build-system]\nbuild-backend = \"maturin\"\n", + tool: "Maturin", + }, + { + name: "Maturin tool table", + path: "pyproject.toml", + content: "[tool.maturin]\nbindings = \"pyo3\"\n", + tool: "Maturin", + }, + { + name: "Maturin parsed dependency", + path: "pyproject.toml", + content: "[project]\nname = \"example\"\nversion = \"0.1.0\"\ndependencies = [\"maturin>=1.0\"]\n", + tool: "Maturin", + }, + { + name: "setuptools-rust parsed dependency", + path: "pyproject.toml", + content: "[project]\nname = \"example\"\nversion = \"0.1.0\"\ndependencies = [\"setuptools-rust>=1.10\"]\n", + tool: "setuptools-rust", + }, + { + name: "setuptools-rust extension table", + path: "pyproject.toml", + content: "[[tool.setuptools-rust.ext-modules]]\ntarget = \"example._lib\"\n", + tool: "setuptools-rust", + }, + { + name: "setuptools-rust binary table", + path: "pyproject.toml", + content: "[[tool.setuptools-rust.bins]]\ntarget = \"example\"\n", + tool: "setuptools-rust", + }, + { + name: "setuptools-rust setup.py", + path: "setup.py", + content: "from setuptools_rust import RustExtension\n\nextension = RustExtension(\"example._lib\")\n", + tool: "setuptools-rust", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + writeProjectFile(t, dir, tt.path, tt.content) + r := runOn(t, dir) + assertToolDetectedWithConfidence(t, r, "native_extension", tt.tool, brief.ConfidenceHigh) + }) + } +} + +func TestPythonRustNativeExtensionNearMiss(t *testing.T) { + dir := t.TempDir() + writeProjectFile(t, dir, "pyproject.toml", "[project]\nname = \"near-miss\"\nversion = \"0.1.0\"\n") + writeProjectFile(t, dir, "README.md", "This compares maturin with setuptools-rust.\n") + + r := runOn(t, dir) + for _, name := range []string{"Maturin", "setuptools-rust"} { + if slices.ContainsFunc(r.Tools["native_extension"], func(d brief.Detection) bool { + return d.Name == name + }) { + t.Errorf("README mention should not detect %s", name) + } + } +} + func TestPerlProject(t *testing.T) { r := runOn(t, "../testdata/perl-project") @@ -1347,3 +1441,23 @@ func assertToolDetected(t *testing.T, r *brief.Report, category, name string) { } t.Errorf("expected %s in %s category", name, category) } + +func assertToolDetectedWithConfidence( + t *testing.T, + r *brief.Report, + category string, + name string, + confidence brief.Confidence, +) { + t.Helper() + for _, tool := range r.Tools[category] { + if tool.Name != name { + continue + } + if tool.Confidence != confidence { + t.Errorf("%s confidence = %q, want %q", name, tool.Confidence, confidence) + } + return + } + t.Errorf("expected %s in %s category", name, category) +} diff --git a/knowledge/python/maturin.toml b/knowledge/python/maturin.toml new file mode 100644 index 0000000..2ba67d5 --- /dev/null +++ b/knowledge/python/maturin.toml @@ -0,0 +1,24 @@ +[tool] +name = "Maturin" +category = "native_extension" +homepage = "https://www.maturin.rs/" +docs = "https://www.maturin.rs/" +repo = "https://github.com/PyO3/maturin" +description = "Builds Python packages containing Rust extensions or executables" + +[detect] +dependencies = ["maturin"] +ecosystems = ["python"] + +[detect.file_contains] +"pyproject.toml" = ["build-backend = \"maturin\"", "[tool.maturin]"] + +[commands] +run = "maturin develop" +alternatives = ["maturin build", "python -m build"] + +[config] +files = ["pyproject.toml", "Cargo.toml"] + +[taxonomy] +role = ["native-extension"] diff --git a/knowledge/python/setuptools-rust.toml b/knowledge/python/setuptools-rust.toml new file mode 100644 index 0000000..461a950 --- /dev/null +++ b/knowledge/python/setuptools-rust.toml @@ -0,0 +1,28 @@ +[tool] +name = "setuptools-rust" +category = "native_extension" +homepage = "https://setuptools-rust.readthedocs.io/" +docs = "https://setuptools-rust.readthedocs.io/" +repo = "https://github.com/PyO3/setuptools-rust" +description = "Setuptools plugin for building Python extensions and executables written in Rust" + +[detect] +dependencies = ["setuptools-rust"] +ecosystems = ["python"] + +[detect.file_contains] +"pyproject.toml" = [ + "[[tool.setuptools-rust.ext-modules]]", + "[[tool.setuptools-rust.bins]]", +] +"setup.py" = ["RustExtension("] + +[commands] +run = "python -m build" +alternatives = ["pip install -e ."] + +[config] +files = ["pyproject.toml", "setup.py", "Cargo.toml"] + +[taxonomy] +role = ["native-extension"] diff --git a/testdata/maturin-project/Cargo.toml b/testdata/maturin-project/Cargo.toml new file mode 100644 index 0000000..453d8c2 --- /dev/null +++ b/testdata/maturin-project/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "maturin-project" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] diff --git a/testdata/maturin-project/pyproject.toml b/testdata/maturin-project/pyproject.toml new file mode 100644 index 0000000..c90e2a1 --- /dev/null +++ b/testdata/maturin-project/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "maturin-project" +version = "0.1.0" + +[tool.maturin] +bindings = "pyo3" diff --git a/testdata/maturin-project/src/lib.rs b/testdata/maturin-project/src/lib.rs new file mode 100644 index 0000000..ace84d3 --- /dev/null +++ b/testdata/maturin-project/src/lib.rs @@ -0,0 +1 @@ +pub fn fixture() {} diff --git a/testdata/setuptools-rust-project/Cargo.toml b/testdata/setuptools-rust-project/Cargo.toml new file mode 100644 index 0000000..3eabd81 --- /dev/null +++ b/testdata/setuptools-rust-project/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "setuptools-rust-project" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] diff --git a/testdata/setuptools-rust-project/pyproject.toml b/testdata/setuptools-rust-project/pyproject.toml new file mode 100644 index 0000000..6e8fb26 --- /dev/null +++ b/testdata/setuptools-rust-project/pyproject.toml @@ -0,0 +1,11 @@ +[build-system] +requires = ["setuptools>=77", "setuptools-rust>=1.10"] +build-backend = "setuptools.build_meta" + +[project] +name = "setuptools-rust-project" +version = "0.1.0" + +[[tool.setuptools-rust.ext-modules]] +target = "setuptools_rust_project._lib" +path = "Cargo.toml" diff --git a/testdata/setuptools-rust-project/src/lib.rs b/testdata/setuptools-rust-project/src/lib.rs new file mode 100644 index 0000000..ace84d3 --- /dev/null +++ b/testdata/setuptools-rust-project/src/lib.rs @@ -0,0 +1 @@ +pub fn fixture() {}