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