guix_mirror_bot pushed a commit to branch master
in repository guix.

commit 811ee1ab9fb585130fe0c27df03f10dc21b1e7f7
Author: Danny Milosavljevic <[email protected]>
AuthorDate: Fri Jan 9 23:57:19 2026 +0100

    import: nuget: Add tests and documentation.
    
    * guix/import/nuget.scm: Prevent optimizing small functions away completely.
    * tests/import/nuget.scm: New file.
    * doc/guix.texi (nuget): Document it.
    * Makefile.am (SCM_TESTS): Add reference to it.
    
    Fixes: guix/guix#5483
    Change-Id: Id58932fe404a11a03e61a91d3b6177b39548f1bc
---
 Makefile.am                   |   1 +
 doc/guix.texi                 |  44 +++++++
 guix/import/nuget.scm         |  33 ++++--
 guix/scripts/import/nuget.scm |   6 -
 tests/import/nuget.scm        | 270 ++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 341 insertions(+), 13 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index dabceddf2a..e71b2d2ed5 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -577,6 +577,7 @@ SCM_TESTS =                                 \
   tests/import/hexpm.scm                       \
   tests/import/luanti.scm                      \
   tests/import/npm-binary.scm                  \
+  tests/import/nuget.scm                       \
   tests/import/opam.scm                                \
   tests/import/print.scm                       \
   tests/import/pypi.scm                                \
diff --git a/doc/guix.texi b/doc/guix.texi
index a6d6d655fb..a22230e153 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -14709,6 +14709,50 @@ and generate package expressions for all those 
packages that are not yet
 in Guix.
 @end table
 
+@item nuget
+@cindex nuget
+@cindex .NET
+Import metadata from @uref{https://www.nuget.org/, NuGet}, the package
+manager for .NET.  Information is taken from the JSON-formatted metadata
+provided through NuGet's v3 API at @code{api.nuget.org} and includes
+most relevant information, including package dependencies.
+There are some caveats, however.  The metadata does not always include
+repository information, in which case the importer attempts to extract
+it from the symbol package (@file{.snupkg}) if available. 
+Additionally, dependencies are grouped by target framework in NuGet,
+but the importer flattens all dependency groups into a single list.
+
+The command below imports metadata for the @code{Avalonia} .NET package:
+
+@example
+guix import nuget Avalonia
+@end example
+
+You can also recursively import all dependencies:
+
+@example
+guix import nuget -r Avalonia
+@end example
+
+@table @code
+@item --archive=@var{repo}
+@itemx -a @var{repo}
+Specify the archive repository.  Currently only @code{nuget} is supported,
+which uses the official NuGet package repository at @code{nuget.org}.
+
+@item --recursive
+@itemx -r
+Traverse the dependency graph of the given upstream package recursively
+and generate package expressions for all those packages that are not yet
+in Guix.
+
+@item --license-prefix=@var{prefix}
+@itemx -p @var{prefix}
+Add a custom prefix to license identifiers in the generated package
+definitions.  This can be useful when license identifiers need to be
+qualified with a module name.
+@end table
+
 @item minetest
 @cindex minetest
 @cindex ContentDB
diff --git a/guix/import/nuget.scm b/guix/import/nuget.scm
index d540e6817f..a8060b2730 100644
--- a/guix/import/nuget.scm
+++ b/guix/import/nuget.scm
@@ -37,6 +37,7 @@
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-34) ; For catch
   #:use-module (srfi srfi-37)
+  #:use-module (srfi srfi-39) ; parameters
   #:use-module (srfi srfi-71) ; multi-value let
   #:use-module (sxml simple)
   #:use-module (sxml match)
@@ -60,7 +61,10 @@
   #:use-module (guix packages)
   #:use-module (guix upstream)
   #:use-module (guix http-client)
-  #:export (nuget->guix-package
+  #:export (%nuget-v3-registration-url
+            %nuget-v3-package-versions-url
+            %nuget-symbol-packages-url
+            nuget->guix-package
             nuget-recursive-import))
 
 ;; copy from guix/import/pypi.scm
@@ -84,9 +88,12 @@
 ;; Example: 
<https://api.nuget.org/v3/registration5-semver1/newtonsoft.json/index.json>.
 ;; List of all packages.  You get a lot of references to @type CatalogPage out.
 (define %nuget-v3-feed-catalog-url 
"https://api.nuget.org/v3/catalog0/index.json";)
-(define %nuget-v3-registration-url 
"https://api.nuget.org/v3/registration5-semver1/";)
-(define %nuget-v3-package-versions-url 
"https://api.nuget.org/v3-flatcontainer/";)
-(define %nuget-symbol-packages-url 
"https://globalcdn.nuget.org/symbol-packages/";)
+(define %nuget-v3-registration-url
+  (make-parameter "https://api.nuget.org/v3/registration5-semver1/";))
+(define %nuget-v3-package-versions-url
+  (make-parameter "https://api.nuget.org/v3-flatcontainer/";))
+(define %nuget-symbol-packages-url
+  (make-parameter "https://globalcdn.nuget.org/symbol-packages/";))
 
 (define %nuget-nuspec 
"http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd";)
 ;;; Version index https://api.nuget.org/v3-flatcontainer/{id-lower}/index.json
@@ -150,6 +157,9 @@ primitives suitable for the 'semver-range' constructor."
                       (warning (G_ "Unrecognized NuGet range format: '~a'.~%") 
str)
                       '()))))))))
 
+;; Make this testable.
+(set! parse-nuget-range->primitives parse-nuget-range->primitives)
+
 (define (nuget->semver-range range-str)
   (semver-range (parse-nuget-range->primitives range-str)))
 
@@ -158,7 +168,7 @@ primitives suitable for the 'semver-range' constructor."
   version that satisfies the range. This version correctly handles list
   creation and filtering to avoid type errors."
   (let* ((name-lower (string-downcase name))
-         (versions-url (string-append %nuget-v3-package-versions-url 
name-lower "/index.json")))
+         (versions-url (string-append (%nuget-v3-package-versions-url) 
name-lower "/index.json")))
     (let ((versions-json (json-fetch versions-url)))
       (if versions-json
           (let* ((available-versions (vector->list (or (assoc-ref 
versions-json "versions")
@@ -193,6 +203,9 @@ primitives suitable for the 'semver-range' constructor."
             (warning (G_ "Failed to fetch version list for ~a~%") name)
             #f)))))
 
+;; Make this testable.
+(set! nuget-find-best-version-for-range nuget-find-best-version-for-range)
+
 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;
 ;;; Part 2: Core Data Fetching and Package Generation
@@ -202,7 +215,7 @@ primitives suitable for the 'semver-range' constructor."
   "Fetch the full 'catalogEntry' JSON object for a specific package version,
   correctly handling the paginated structure of the registration index."
   (let* ((name-lower (string-downcase name))
-         (index-url (string-append %nuget-v3-registration-url name-lower 
"/index.json"))
+         (index-url (string-append (%nuget-v3-registration-url) name-lower 
"/index.json"))
          (index-json (json-fetch index-url)))
     (if index-json
         (let loop ((pages-to-check
@@ -242,6 +255,9 @@ primitives suitable for the 'semver-range' constructor."
           (warning (G_ "Failed to fetch registration index for ~a~%") name)
           #f))))
 
+;; Make this testable.
+(set! nuget-fetch-catalog-entry nuget-fetch-catalog-entry)
+
 (define (car-safe lst)
   (if (null? lst)
       '()
@@ -253,7 +269,7 @@ file using the system 'unzip' command, and parse it to find 
the repository URL
 and commit.  Returns an association list with 'url' and 'commit' keys on
 success, or #f on failure."
   (let* ((name (string-append (string-downcase package-name) "." version 
".snupkg"))
-         (snupkg-url (string-append %nuget-symbol-packages-url name)))
+         (snupkg-url (string-append (%nuget-symbol-packages-url) name)))
     (format (current-error-port)
             "~%;; Source repository not found in NuGet catalog entry.~%;; ~
              Attempting to find it in symbol package: ~a~%"
@@ -311,6 +327,9 @@ success, or #f on failure."
 (define (nuget-name->guix-name name)
   (string-append "dotnet-" (snake-case name)))
 
+;; Make this testable.
+(set! nuget-name->guix-name nuget-name->guix-name)
+
 (define nuget->guix-package
   (memoize
    (lambda* (package-name #:key (repo 'nuget) (version #f) (license-prefix 
identity) #:allow-other-keys)
diff --git a/guix/scripts/import/nuget.scm b/guix/scripts/import/nuget.scm
index 8338adf3eb..8bb870cd9e 100644
--- a/guix/scripts/import/nuget.scm
+++ b/guix/scripts/import/nuget.scm
@@ -51,8 +51,6 @@ Import and convert the NuGet package for PACKAGE-NAME.\n"))
   (display (G_ "
   -r, --recursive        import packages recursively"))
   (display (G_ "
-  -s, --style=STYLE      choose output style, either specification or 
variable"))
-  (display (G_ "
   -p, --license-prefix=PREFIX
                          add custom prefix to licenses"))
   (display (G_ "
@@ -73,10 +71,6 @@ Import and convert the NuGet package for PACKAGE-NAME.\n"))
                  (lambda (opt name arg result)
                    (alist-cons 'repo (string->symbol arg)
                                (alist-delete 'repo result))))
-         (option '(#\s "style") #t #f
-                 (lambda (opt name arg result)
-                   (alist-cons 'style (string->symbol arg)
-                               (alist-delete 'style result))))
          (option '(#\p "license-prefix") #t #f
                  (lambda (opt name arg result)
                    (alist-cons 'license-prefix arg
diff --git a/tests/import/nuget.scm b/tests/import/nuget.scm
new file mode 100644
index 0000000000..cbbffed539
--- /dev/null
+++ b/tests/import/nuget.scm
@@ -0,0 +1,270 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Danny Milosavljevic <[email protected]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (test-nuget)
+  #:use-module (guix import nuget)
+  #:use-module (guix tests)
+  #:use-module (guix tests http)
+  #:use-module (guix http-client)
+  #:use-module (json)
+  #:use-module (semver)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-34)
+  #:use-module (srfi srfi-64)
+  #:use-module (srfi srfi-71)
+  #:use-module (web uri))
+
+(define (make-versions-json versions)
+  "Generate a NuGet package versions index JSON string."
+  (scm->json-string
+   `((versions . ,(list->vector versions)))))
+
+(define (make-catalog-entry id version description summary home-page license
+                            repo-url repo-commit dependencies)
+  "Generate a NuGet catalog entry alist."
+  `((id . ,id)
+    (version . ,version)
+    (description . ,description)
+    (summary . ,summary)
+    (projectUrl . ,home-page)
+    (licenseExpression . ,license)
+    ,@(if repo-url
+          `((repository . ((url . ,repo-url)
+                           (commit . ,repo-commit))))
+          '())
+    (dependencyGroups
+     . ,(list->vector
+         (if (null? dependencies)
+             '()
+             `(((targetFramework . "net6.0")
+                (dependencies
+                 . ,(list->vector
+                     (map (lambda (dep)
+                            `((id . ,(car dep))
+                              (range . ,(cdr dep))))
+                          dependencies))))))))))
+
+(define (make-registration-index-json catalog-entry)
+  "Generate a NuGet registration index JSON string."
+  (scm->json-string
+   `((items
+      . #(((items
+            . #(((catalogEntry . ,catalog-entry))))))))))
+
+(define test-avalonia-versions-json
+  (make-versions-json '("0.10.0" "0.10.1" "11.0.0" "11.0.1" "11.1.0")))
+
+(define test-avalonia-catalog-entry
+  (make-catalog-entry "Avalonia" "11.1.0"
+                      "A cross-platform UI framework for .NET"
+                      "Avalonia UI Framework"
+                      "https://avaloniaui.net/";
+                      "MIT"
+                      "https://github.com/AvaloniaUI/Avalonia.git";
+                      "abc123def456"
+                      '(("System.Text.Json" . "[6.0.0, )"))))
+
+(define test-avalonia-index-json
+  (make-registration-index-json test-avalonia-catalog-entry))
+
+(define test-system-text-json-versions-json
+  (make-versions-json '("6.0.0" "6.0.1" "7.0.0" "8.0.0")))
+
+(define test-system-text-json-catalog-entry
+  (make-catalog-entry "System.Text.Json" "8.0.0"
+                      "Provides high-performance JSON APIs"
+                      "JSON library"
+                      "https://dot.net/";
+                      "MIT"
+                      "https://github.com/dotnet/runtime.git";
+                      "def789abc012"
+                      '()))
+
+(define test-system-text-json-index-json
+  (make-registration-index-json test-system-text-json-catalog-entry))
+
+(define test-package-no-repo-versions-json
+  (make-versions-json '("1.0.0")))
+
+(define test-package-no-repo-catalog-entry
+  (make-catalog-entry "TestPackage" "1.0.0"
+                      "Test package without repository"
+                      #f
+                      "https://example.com/";
+                      "MIT"
+                      #f #f
+                      '()))
+
+(define test-package-no-repo-index-json
+  (make-registration-index-json test-package-no-repo-catalog-entry))
+
+(define-syntax-rule (with-nuget responses body ...)
+  (with-http-server responses
+    (parameterize ((%nuget-v3-package-versions-url
+                    (%local-url #:path "/versions/"))
+                   (%nuget-v3-registration-url
+                    (%local-url #:path "/registration/"))
+                   (%nuget-symbol-packages-url
+                    (%local-url #:path "/symbols/")))
+      body ...)))
+
+(test-begin "nuget")
+
+(test-assert "nuget->guix-package"
+  ;; Replace network resources with sample data.
+  (with-nuget `(("/versions/avalonia/index.json" 200 
,test-avalonia-versions-json)
+                ("/registration/avalonia/index.json" 200 
,test-avalonia-index-json))
+    (let ((package-sexp dependencies (nuget->guix-package "Avalonia")))
+      (match package-sexp
+        (`(package
+            (name "dotnet-avalonia")
+            (version "11.1.0")
+            (source
+             (origin
+               (method git-fetch)
+               (uri (git-reference
+                     (url "https://github.com/AvaloniaUI/Avalonia.git";)
+                     (commit "abc123def456")))
+               (file-name (git-file-name name version))
+               (sha256 (base32 ,(?  string?)))))
+            (build-system mono-build-system)
+            (inputs (list dotnet-system-text-json))
+            (home-page "https://avaloniaui.net/";)
+            (synopsis ,(?  string?))
+            (description ,(?  string?))
+            (license ,?))
+         (equal? dependencies '("System.Text.Json")))
+        (x
+         (pk 'fail x #f))))))
+
+(test-assert "nuget-name->guix-name"
+  (and (string=? ((@@ (guix import nuget) nuget-name->guix-name) "Avalonia")
+                 "dotnet-avalonia")
+       (string=? ((@@ (guix import nuget) nuget-name->guix-name) 
"System.Text.Json")
+                 "dotnet-system-text-json")))
+
+(test-assert "nuget-recursive-import"
+  ;; Replace network resources with sample data.
+  ;; recursive-import returns a list of package s-expressions in topological 
order.
+  (with-nuget `(("/versions/avalonia/index.json" 200 
,test-avalonia-versions-json)
+                ("/registration/avalonia/index.json" 200 
,test-avalonia-index-json)
+                ("/versions/system.text.json/index.json" 200
+                 ,test-system-text-json-versions-json)
+                ("/registration/system.text.json/index.json" 200
+                 ,test-system-text-json-index-json))
+    (let ((packages (nuget-recursive-import "Avalonia")))
+      (match packages
+        ((first second)
+         ;; Check that we got two packages
+         (and (match first
+                (`(package (name ,name1) . ,_)
+                 (or (string=? name1 "dotnet-system-text-json")
+                     (string=? name1 "dotnet-avalonia"))))
+              (match second
+                (`(package (name ,name2) . ,_)
+                 (or (string=? name2 "dotnet-system-text-json")
+                     (string=? name2 "dotnet-avalonia"))))))
+        (x
+         (pk 'fail-recursive-count x #f))))))
+
+(test-assert "parse-nuget-range->primitives: exact version"
+  (let ((result ((@@ (guix import nuget) parse-nuget-range->primitives) 
"[1.0.0]")))
+    (match result
+      (((('= slice)))
+       (equal? slice '(1 0 0 0 () ())))
+      (_ #f))))
+
+(test-assert "parse-nuget-range->primitives: minimum version"
+  (let ((result ((@@ (guix import nuget) parse-nuget-range->primitives) 
"1.0.0")))
+    (match result
+      ((('>= slice))
+       (equal? slice '(1 0 0 0 () ())))
+      (_ #f))))
+
+(test-assert "parse-nuget-range->primitives: range with brackets"
+  (let ((result ((@@ (guix import nuget) parse-nuget-range->primitives) 
"[1.0.0,2.0.0]")))
+    (match result
+      ((('>= sv1) ('<= sv2))
+       (and (semver? sv1)
+            (semver? sv2)
+            (string=? (semver->string sv1) "1.0.0")
+            (string=? (semver->string sv2) "2.0.0")))
+      (_ #f))))
+
+(test-assert "parse-nuget-range->primitives: range with parens"
+  (let ((result ((@@ (guix import nuget) parse-nuget-range->primitives) 
"(1.0.0,2.0.0)")))
+    (match result
+      ((('> sv1) ('< sv2))
+       (and (semver? sv1)
+            (semver? sv2)
+            (string=? (semver->string sv1) "1.0.0")
+            (string=? (semver->string sv2) "2.0.0")))
+      (_ #f))))
+
+(test-assert "parse-nuget-range->primitives: open-ended range"
+  (let ((result ((@@ (guix import nuget) parse-nuget-range->primitives) 
"[1.0.0, )")))
+    (match result
+      ((('>= sv))
+       (and (semver? sv)
+            (string=? (semver->string sv) "1.0.0")))
+      (_ #f))))
+
+(test-assert "nuget-find-best-version-for-range: stable version"
+  ;; Test that it finds the highest stable version matching a range
+  (with-nuget `(("/versions/avalonia/index.json" 200 
,test-avalonia-versions-json))
+    (let ((version ((@@ (guix import nuget) nuget-find-best-version-for-range)
+                    "Avalonia" "[11.0.0,)")))
+      (string=? version "11.1.0"))))
+
+(test-assert "nuget-find-best-version-for-range: closed range"
+  (with-nuget `(("/versions/avalonia/index.json" 200 
,test-avalonia-versions-json))
+    (let ((version ((@@ (guix import nuget) nuget-find-best-version-for-range)
+                    "Avalonia" "[11.0.0,12.0.0]")))
+      (string=? version "11.1.0"))))
+
+(test-assert "nuget-fetch-catalog-entry: finds specific version"
+  (with-nuget `(("/registration/avalonia/index.json" 200 
,test-avalonia-index-json))
+    (let ((entry ((@@ (guix import nuget) nuget-fetch-catalog-entry)
+                  "Avalonia" "11.1.0")))
+      (and entry
+           (string=? (assoc-ref entry "version") "11.1.0")
+           (string=? (assoc-ref entry "id") "Avalonia")))))
+
+(test-assert "nuget->guix-package: package without repository"
+  ;; Test package with no repository info (source should have FIXME)
+  (with-nuget `(("/versions/testpackage/index.json" 200
+                 ,test-package-no-repo-versions-json)
+                ("/registration/testpackage/index.json" 200
+                 ,test-package-no-repo-index-json)
+                ("/symbols/testpackage.1.0.0.snupkg" 404 ""))
+    (let ((package-sexp dependencies (nuget->guix-package "TestPackage")))
+      (match package-sexp
+        (`(package
+            (name "dotnet-testpackage")
+            (version "1.0.0")
+            (source
+             (origin
+               (method url-fetch)
+               (uri "FIXME: No source URL found.")
+               . ,_))
+            . ,_)
+         (equal? dependencies '()))
+        (x
+         (pk 'fail-no-repo x #f))))))
+
+(test-end "nuget")

Reply via email to