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")