---
 Makefile.am                       |   2 +
 guix/build-system/conan.scm       | 128 +++++++++++++++++
 guix/build/conan-build-system.scm | 232 ++++++++++++++++++++++++++++++
 3 files changed, 362 insertions(+)
 create mode 100644 guix/build-system/conan.scm
 create mode 100644 guix/build/conan-build-system.scm

diff --git a/Makefile.am b/Makefile.am
index f2f4a9643e..25ae004fc5 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -157,6 +157,7 @@ MODULES =                                   \
   guix/build-system/chicken.scm                \
   guix/build-system/clojure.scm                \
   guix/build-system/cmake.scm                  \
+  guix/build-system/conan.scm                  \
   guix/build-system/copy.scm                   \
   guix/build-system/composer.scm               \
   guix/build-system/dub.scm                    \
@@ -218,6 +219,7 @@ MODULES =                                   \
   guix/build/chicken-build-system.scm          \
   guix/build/cmake-build-system.scm            \
   guix/build/composer-build-system.scm         \
+  guix/build/conan-build-system.scm            \
   guix/build/dub-build-system.scm              \
   guix/build/dune-build-system.scm             \
   guix/build/elm-build-system.scm              \
diff --git a/guix/build-system/conan.scm b/guix/build-system/conan.scm
new file mode 100644
index 0000000000..a02ff4cf58
--- /dev/null
+++ b/guix/build-system/conan.scm
@@ -0,0 +1,128 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Nicolas Graves <ngra...@ngraves.fr>
+;;;
+;;; 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 (guix build-system conan)
+  #:use-module (guix store)
+  #:use-module (guix utils)
+  #:use-module (guix gexp)
+  #:use-module (guix monads)
+  #:use-module (guix packages)
+  #:use-module (guix search-paths)
+  #:use-module (guix build-system)
+  #:use-module (guix build-system cmake)
+  #:use-module (guix build-system gnu)
+  #:use-module ((guix build-system pyproject) #:prefix pyproject:)
+  #:export (%conan-build-system-modules
+            conan-build
+            conan-build-system))
+
+(define %conan-build-system-modules
+  ;; Build-side modules imported by default.
+  `((guix build conan-build-system)
+    ,@%cmake-build-system-modules))
+
+(define (default-conan)
+  "Return the default Conan package."
+  (let ((module (resolve-interface '(gnu packages package-management))))
+    (module-ref module 'conan)))
+
+(define (default-python-pyyaml)
+  "Return the default python-pyyaml package."
+  (let ((module (resolve-interface '(gnu packages python-xyz))))
+    (module-ref module 'python-pyyaml)))
+
+(define* (lower name
+                #:key source inputs native-inputs outputs system target
+                (conan (default-conan))
+                (python (pyproject:default-python))
+                (python-pyyaml (default-python-pyyaml))
+                #:allow-other-keys
+                #:rest arguments)
+  "Return a bag for NAME."
+  (define private-keywords
+    '(#:target #:conan #:inputs #:native-inputs #:python #:python-pyyaml))
+
+  (and (not target)                               ;XXX: no cross-compilation
+       (bag
+         (name name)
+         (system system)
+         (host-inputs `(,@(if source
+                              `(("source" ,source))
+                              '())
+                        ,@inputs
+
+                        ;; Keep the standard inputs of 'gnu-build-system'.
+                        ,@(standard-packages)))
+         (build-inputs `(("conan" ,conan)
+                         ("python" ,python)
+                         ("python-pyyaml" ,python-pyyaml)
+                         ,@native-inputs))
+         (outputs outputs)
+         (build conan-build)
+         (arguments (strip-keyword-arguments private-keywords arguments)))))
+
+(define* (conan-build name inputs
+                      #:key
+                      (source #f)
+                      (outputs '("out"))
+                      (conan (default-conan))
+                      (build-flags ''())
+                      (phases '%standard-phases)
+                      (search-paths '())
+                      (system (%current-system))
+                      (validate-runpath? #t)
+                      (tests? #t)
+                      (guile #f)
+                      (imported-modules %conan-build-system-modules)
+                      (modules '((guix build conan-build-system)
+                                 (guix build utils))))
+  "Build the given package using Conan."
+  (define builder
+    (with-imported-modules imported-modules
+      #~(begin
+          (use-modules #$@(sexp->gexp modules))
+          #$(with-build-variables inputs outputs
+              #~(conan-build #:name #$name
+                             #:source #$source
+                             #:build-flags #$build-flags
+                             #:system #$system
+                             #:phases #$(if (pair? phases)
+                                            (sexp->gexp phases)
+                                            phases)
+                             #:outputs %outputs
+                             #:inputs %build-inputs
+                             #:search-paths
+                             '#$(sexp->gexp
+                                 (map search-path-specification->sexp
+                                      search-paths))
+                             #:validate-runpath? #$validate-runpath?
+                             #:tests? #$tests?)))))
+
+  (mlet %store-monad ((guile (package->derivation (or guile (default-guile))
+                                                  system #:graft? #f)))
+    (gexp->derivation name builder
+                      #:system system
+                      #:guile-for-build guile)))
+
+(define conan-build-system
+  (build-system
+    (name 'conan)
+    (description "The Conan build system")
+    (lower lower)))
+
+;;; conan.scm ends here
diff --git a/guix/build/conan-build-system.scm 
b/guix/build/conan-build-system.scm
new file mode 100644
index 0000000000..523a1a88d2
--- /dev/null
+++ b/guix/build/conan-build-system.scm
@@ -0,0 +1,232 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Nicolas Graves <ngra...@ngraves.fr>
+;;;
+;;; 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 (guix build conan-build-system)
+  #:use-module ((guix build cmake-build-system) #:prefix cmake:)
+  #:use-module (guix build utils)
+  #:use-module (ice-9 ftw)
+  #:use-module (ice-9 match)
+  #:use-module ((ice-9 rdelim) #:select (read-line))
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-71)
+  #:export (%standard-phases
+            conan-install-guix-input
+            conan-build))
+
+;; Generic conanfile to export_pkg from a Guix input.  Uses
+;; GUIX_STORE_PATH to know which store item to handle.
+(define conanfile "\
+from conan import ConanFile
+from conan.tools.files import collect_libs, copy
+import os
+
+class GuixPackageConanFile(ConanFile):
+    def package(self):
+        for item in os.listdir(self.source_folder):
+            item_path = os.path.join(self.source_folder, item)
+            if os.path.islink(item_path):
+                link_target = os.readlink(item_path)
+                dst_path = os.path.join(self.package_folder, item)
+                os.symlink(link_target, dst_path)
+
+    def package_info(self):
+        self.cpp_info.libs = collect_libs(self)
+        self.cpp_info.bindirs = ['bin'] if 
os.path.exists(os.path.join(self.package_folder, 'bin')) else []
+        self.cpp_info.includedirs = ['include'] if 
os.path.exists(os.path.join(self.package_folder, 'include')) else []
+        self.cpp_info.libdirs = ['lib'] if 
os.path.exists(os.path.join(self.package_folder, 'lib')) else []")
+
+(define* (patch-conanfile #:key outputs #:allow-other-keys)
+  "Some Conan configuration don't play well with Guix.  Handle them here."
+  ;; Avoid trying revision from git, but hash instead.
+  (substitute* "conanfile.py"
+    ((".*revision_mode[ ]*=.*") "")
+    (("ConanFile\\):")
+     "ConanFile):
+    revision_mode=\"hash\""))
+  ;; An explicit version is required to run export-pkg
+  ;; Putting it at top of Conanfile allows both later rewrites
+  ;; and is more robust.
+  (let* ((out (assoc-ref outputs "out"))
+         (_ version (package-name->name+version
+                     (strip-store-file-name out))))
+    (unless (file-exists? "conandata.yml")
+      (substitute* "conanfile.py"
+        (("ConanFile\\):")
+         (format #f "ConanFile):
+    version=~s" version))))))
+
+(define* (pre-configure #:rest args)
+  "Pre-configure Conan: set variables, ensure home existence."
+  (let ((conan-home (string-append (getcwd) "/.conan2")))
+    (setenv "HOME" (getcwd))
+    (setenv "CONAN_HOME" conan-home)
+    (setenv "CONAN_NO_REMOTE" "1")
+    (mkdir-p conan-home)))
+
+(define* (configure #:rest args)
+  "Configure Conan: Create default profile."
+  (invoke "conan" "profile" "detect"))
+
+(define (conan-install-guix-input name version path)
+  "Install guix input in Conan cache."
+  (let ((dir (mkdtemp (string-append (getcwd) "/conan.XXXXXX"))))
+
+    (call-with-output-file (string-append dir "/conanfile.py")
+      (lambda (port)
+        (format port conanfile)))
+    (for-each
+     (match-lambda
+       ((store-dir . conan-dir)
+        (let ((guix-dir (string-append path "/" store-dir)))
+          (when (directory-exists? guix-dir)
+            (symlink guix-dir (string-append dir "/" conan-dir))))))
+     '(("bin" . "bin")
+       ("lib" . "lib")
+       ("include" . "include")
+       ("share/include" . "include")))
+    (invoke "conan" "export-pkg" dir
+            "--name" name
+            "--version" version
+            "--skip-install")
+    (delete-file-recursively dir)))
+
+(define* (conan-install-guix-inputs #:key inputs #:allow-other-keys)
+  ;; Step 1: Make requirements accessible in Guile.
+  (let ((target
+         (cond
+          ((file-exists? "conanfile.py")  "conanfile.py")
+          ((file-exists? "conanfile.txt") "conanfile.txt")
+          (else (error "No usable conanfile.~%")))))
+    (system* "python3" "-c" "\
+import os
+import sys
+import yaml
+from conan import ConanFile
+
+path,g=sys.argv[1],{}
+exec(open(path).read(), g)
+cls=[v for v in g.values() if isinstance(v, type) and issubclass(v, ConanFile) 
and v!=ConanFile][0]
+o,_requires=cls(None),[]
+if hasattr(o, 'requirements'):
+    
o.settings={'os':'Linux','compiler':'gcc','build_type':'Release','arch':'x86_64'}
+    o.requires = lambda req, **kwargs: _requires.append(req)
+    o.conan_data={}
+    conandata_path=os.path.join(os.path.dirname(path), 'conandata.yml')
+    if os.path.exists(conandata_path):
+        with open(conandata_path, 'r') as f:
+            o.conan_data=yaml.safe_load(f) or {}
+    o.requirements()
+open('.requires', 'w').write(\"\\n\".join(_requires))
+" target))
+  ;; Step 2: Inject intersection of inputs and requirements as conan packages.
+  (for-each
+   (match-lambda
+     ((name version . ())
+      (let ((path (assoc-ref inputs name)))
+        (and path
+             (let ((_ found-version (package-name->name+version
+                                     (strip-store-file-name path))))
+               ;; conveniency: warn about version mismatch
+               (and (unless (string= version found-version)
+                      (format (current-error-port) "\
+warning: ~a: Expected version ~a, passing found version ~a.~%"
+                              name version found-version))
+                    (conan-install-guix-input name found-version path)))))))
+   (call-with-input-file ".requires"
+     (lambda (port)
+       (let loop ((line (read-line port))
+                  (lines '()))
+         (if (eof-object? line)
+             lines
+             (let ((line+ (match (string-split line #\@)
+                            ((single . ())       (string-split line #\/))
+                            ((first second . ()) (string-split first #\/)))))
+               (loop (read-line port)
+                     (cons* line+ lines)))))))))
+
+(define* (conan-generate-profile #:rest args)
+  (let ((conan-home (string-append (getcwd) "/.conan2")))
+    (invoke "conan"  "new"
+            (string-append conan-home "/profiles")
+            "--force")))
+
+(define* (build #:key build-flags inputs #:allow-other-keys)
+  (apply invoke "conan" "install" "." build-flags)
+  ;; TODO Like in pyproject.toml, we'll probably need a match
+  ;; based on build-system to handle other cases than cmake.
+  ;; Also for this reason do not provide cmake by default,
+  ;; it has to be passed in native-inputs (just like python-setuptools).
+  (when (assoc-ref inputs "cmake")
+    (invoke "cmake" "--preset" "conan-release")
+    (invoke "cmake" "--build" "--preset" "conan-release")
+    (chdir "../source")))
+
+(define* (install #:key outputs #:allow-other-keys)
+  ;; XXX: --output-folder #$output doesn't install what we want.
+  ;; Install in cache and copy what's in the cache instead.
+  ;; Additionally, there doesn't seem to be a native easy way
+  ;; to get precisely in which directory the package has been
+  ;; installed.  Find out through directory changes.
+  (let* ((out (assoc-ref outputs "out"))
+         (conan-binary-cache (string-append (getcwd) "/.conan2/p/b/"))
+         (former-tree (scandir conan-binary-cache))
+         (_ (invoke "conan" "export-pkg" "."
+                    ;; Do no run tests now.
+                    "--test-folder" ""))
+         (new-tree (scandir conan-binary-cache)))
+    (match (or (and former-tree
+                    (lset-difference string= new-tree former-tree))
+               (list (last new-tree)))
+      ((install-dir . ())
+       (with-directory-excursion
+           (string-append conan-binary-cache install-dir "/p")
+         (for-each
+          (lambda (dir)
+            (when (directory-exists? dir)
+              (copy-recursively dir (string-append out "/" dir))))
+          '("bin" "lib" "include"))))
+      (_
+       (format #t "\
+Install failed: Unable to get the internal cache installation directory.")))))
+
+(define* (check #:key name outputs tests? #:allow-other-keys)
+  (if tests?
+      (let* ((out (assoc-ref outputs "out"))
+             (_ version (package-name->name+version
+                         (strip-store-file-name out))))
+        (invoke "conan" "test" "test_package"
+                (string-append name "/" version)))
+      (format #t "Test suite not run.~%")))
+
+(define %standard-phases
+  (modify-phases cmake:%standard-phases
+    (replace 'configure configure)
+    (add-before 'configure 'pre-configure pre-configure)
+    (add-after 'pre-configure 'patch-conanfile patch-conanfile)
+    (add-after 'configure 'conan-install-guix-inputs conan-install-guix-inputs)
+    (add-after 'configure 'conan-generate-profile conan-generate-profile)
+    (replace 'build build)
+    (replace 'install install)
+    (replace 'check check)))
+
+(define* (conan-build #:key inputs (phases %standard-phases)
+                      #:allow-other-keys #:rest args)
+  "Build the given Conan package, applying all of PHASES in order."
+  (apply cmake:cmake-build #:inputs inputs #:phases phases args))
+
+;;; conan-build-system.scm ends here
-- 
2.49.0



-- 
Best regards,
Nicolas Graves

Reply via email to