From b9dc8f93b15c3d1fd8572f2c710f6500fce7ab55 Mon Sep 17 00:00:00 2001 From: Jonathan Ringer Date: Wed, 6 May 2026 09:47:46 -0700 Subject: [PATCH 1/3] Add dotnet template --- CHANGELOG.md | 5 +- src/cli.rs | 14 +++- src/detect.rs | 71 +++++++++++++++++++ src/expression.rs | 6 ++ src/interactive.rs | 1 + src/types/mod.rs | 5 ++ src/url.rs | 66 +++++++++++++++++ tests/integration_tests.rs | 44 ++++++++++++ ...egration_tests__dotnet_basic_template.snap | 30 ++++++++ 9 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 tests/snapshots/integration_tests__dotnet_basic_template.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c098b..d5e56d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,16 @@ - All templates now use `finalAttrs` pattern instead of `rec` for better override composition - Additions: - - Modern `finalAttrs` pattern is now default for all package templates (stdenv, Python, Rust, Go, npm, pnpm) + - Modern `finalAttrs` pattern is now default for all package templates (stdenv, Python, Rust, Go, npm, pnpm, dotnet) - Self-references now use `finalAttrs.pname` and `finalAttrs.version` - PyPI fetcher uses `inherit (finalAttrs) pname version;` syntax - Added `npm` template for Node.js packages using buildNpmPackage - Added `pnpm` template for pnpm-based packages using fetchPnpmDeps with stdenv.mkDerivation + - Added `dotnet` template for .NET packages using buildDotnetModule - Dependency hash prefetching now supports npm and pnpm templates (requires package-lock.json/pnpm-lock.yaml in repository) - Auto-detection now recognizes npm and pnpm projects (via pnpm-lock.yaml, package-lock.json, or package.json) + - Auto-detection now recognizes .NET projects (via *.csproj, *.fsproj, or *.sln files) + - Project file inference for dotnet template when using --from-url (automatically detects .csproj, .fsproj, or .sln) ## v0.4.1 diff --git a/src/cli.rs b/src/cli.rs index e414934..3ab4739 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,7 +6,7 @@ use crate::interactive::InteractiveData; use crate::go_deps::infer_go_dependencies; use crate::rust_deps::infer_rust_dependencies; use crate::types::{ExpressionInfo, Fetcher, Template, UserConfig, FAKE_SRI_HASH}; -use crate::url::{prefetch_dependency_hash, read_meta_from_url}; +use crate::url::{infer_dotnet_project_file, prefetch_dependency_hash, read_meta_from_url}; // clap will validate inputs, only use on functions with possible_values defined pub fn arg_to_type(arg: Option<&str>) -> T @@ -282,6 +282,7 @@ pub fn validate_and_serialize_matches( vendor_hash: FAKE_SRI_HASH.to_owned(), npm_deps_hash: FAKE_SRI_HASH.to_owned(), pnpm_deps_hash: FAKE_SRI_HASH.to_owned(), + project_file: "CHANGE".to_owned(), domain: "CHANGE".to_owned(), build_inputs: Vec::new(), native_build_inputs: Vec::new(), @@ -376,6 +377,11 @@ pub fn validate_and_serialize_matches( info.native_build_inputs = native; } } + Template::dotnet => { + if let Some(project_file) = infer_dotnet_project_file(&info) { + info.project_file = project_file; + } + } _ => {} } } @@ -444,6 +450,7 @@ pub fn build_expression_info_from_interactive( vendor_hash: FAKE_SRI_HASH.to_owned(), npm_deps_hash: FAKE_SRI_HASH.to_owned(), pnpm_deps_hash: FAKE_SRI_HASH.to_owned(), + project_file: "CHANGE".to_owned(), domain: "CHANGE".to_owned(), build_inputs: Vec::new(), native_build_inputs: Vec::new(), @@ -479,6 +486,11 @@ pub fn build_expression_info_from_interactive( info.native_build_inputs = native; } } + Template::dotnet => { + if let Some(project_file) = infer_dotnet_project_file(&info) { + info.project_file = project_file; + } + } _ => {} } } diff --git a/src/detect.rs b/src/detect.rs index 48a5d86..9c73746 100644 --- a/src/detect.rs +++ b/src/detect.rs @@ -88,9 +88,44 @@ pub fn detect_template_candidates_from_path(source_path: &Path) -> Vec Option<&'static str> { + use std::fs; + + if let Ok(entries) = fs::read_dir(source_path) { + for entry in entries.flatten() { + if let Some(ext) = entry.path().extension().and_then(|s| s.to_str()) { + match ext { + "csproj" => return Some("*.csproj"), + "fsproj" => return Some("*.fsproj"), + "sln" => return Some("*.sln"), + _ => {} + } + } + } + } + None +} + /// Detect template candidates by materialising a remote source tree. /// /// If the fetcher is PyPI, short-circuits without materialising (we already @@ -321,4 +356,40 @@ mod tests { assert_eq!(candidates[0].template, Template::rust); assert_eq!(candidates[1].template, Template::npm); } + + #[test] + fn detect_dotnet_from_csproj() { + let dir = make_source_dir(&["MyProject.csproj", "Program.cs"]); + let candidates = detect_template_candidates_from_path(dir.path()); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].template, Template::dotnet); + assert_eq!(candidates[0].reason, "*.csproj"); + } + + #[test] + fn detect_dotnet_from_fsproj() { + let dir = make_source_dir(&["MyProject.fsproj", "Program.fs"]); + let candidates = detect_template_candidates_from_path(dir.path()); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].template, Template::dotnet); + assert_eq!(candidates[0].reason, "*.fsproj"); + } + + #[test] + fn detect_dotnet_from_sln() { + let dir = make_source_dir(&["MySolution.sln", "README.md"]); + let candidates = detect_template_candidates_from_path(dir.path()); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].template, Template::dotnet); + assert_eq!(candidates[0].reason, "*.sln"); + } + + #[test] + fn detect_multiple_with_dotnet() { + let dir = make_source_dir(&["Cargo.toml", "MyProject.csproj"]); + let candidates = detect_template_candidates_from_path(dir.path()); + assert_eq!(candidates.len(), 2); + assert_eq!(candidates[0].template, Template::rust); + assert_eq!(candidates[1].template, Template::dotnet); + } } diff --git a/src/expression.rs b/src/expression.rs index 0916f81..d4f4580 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -22,6 +22,7 @@ fn derivation_helper(info: &ExpressionInfo) -> (String, String) { Template::rust => ("rustPlatform", "rustPlatform.buildRustPackage", None), Template::npm => ("buildNpmPackage", "buildNpmPackage", Some("buildNpmPackage")), Template::pnpm => ("stdenv", "stdenv.mkDerivation", Some("stdenvMkDerivation")), + Template::dotnet => ("buildDotnetModule", "buildDotnetModule", Some("buildDotnetModule")), Template::test => ("", "", None), // Tests aren't a normal expression Template::module => ("", "", None), // Modules aren't a normal expression }; @@ -159,6 +160,9 @@ fn build_inputs(info: &ExpressionInfo) -> String { hash = \"@pnpm_deps_hash@\"; };".to_owned() } + Template::dotnet => { + " projectFile = \"@project_file@\";\n nugetDeps = ./deps.json; # Run `nix-build -A package-name.passthru.fetch-deps` to generate".to_owned() + } // stdenv / stdenvNoCC: render `nativeBuildInputs` only when // populated (via --native-build-inputs); always render // `buildInputs` to preserve the existing template's ergonomic @@ -350,6 +354,7 @@ mod tests { vendor_hash: "sha256-vendor".to_owned(), npm_deps_hash: "sha256-npm".to_owned(), pnpm_deps_hash: "sha256-pnpm".to_owned(), + project_file: "Project.csproj".to_owned(), domain: "".to_owned(), build_inputs: Vec::new(), native_build_inputs: Vec::new(), @@ -391,6 +396,7 @@ mod tests { vendor_hash: "".to_owned(), npm_deps_hash: "".to_owned(), pnpm_deps_hash: "".to_owned(), + project_file: "".to_owned(), domain: "".to_owned(), build_inputs: Vec::new(), native_build_inputs: Vec::new(), diff --git a/src/interactive.rs b/src/interactive.rs index 13c0c42..f95e7cd 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -357,6 +357,7 @@ pub fn prompt_template_type(default: Option