Here is a patch that will make CFFI play better with ASDF bundles. This allows delivery with a single .so and/or .a file for all the wrappers in a system, which can be automatically linked into ECL (and in the future, in SBCL or other implementations, for application delivery).
Also, report for a bug, not fixed in the attached patch: compiling, linking, etc., should be atomic, by using temporary pathnames; otherwise, there is a race condition when multiple processes try to execute a script that depends on a CFFI library. Recent-enough ASDF 3.1.2 provides with-temporary-file to help, and 3.1.5 uses it internally for its own targets, but some implementations still lag behind with ASDF 3.0.x, and ASDF can't backwards-compatibly use with-temporary-file for you, so you must use it yourself. This patch also drops support for ASDF 2 or earlier, because backwards compatibility is hard, and not necessary: today, all implementations provide ASDF 3. —♯ƒ • François-René ÐVB Rideau •Reflection&Cybernethics• http://fare.tunes.org The least deviation from truth will be multiplied later. — Aristotle
From 8f3dc5b46a487e4a3d884525d4f12458e395c9d6 Mon Sep 17 00:00:00 2001 From: Francois-Rene Rideau <tu...@google.com> Date: Wed, 16 Sep 2015 23:17:05 -0400 Subject: [PATCH] Make CFFI-grovel play well with ASDF 3.1.5.12. Create linkable object files and include them in output-files for compile-op, so that they can be found and linked by a recent-enough ASDF (3.1.5.12 or later), allowing for delivery of a system with a single static and/or dynamic library, and in the future, delivery of a standalone executable that statically links required extensions. --- cffi-grovel.asd | 7 +-- grovel/asdf.lisp | 159 ++++++++++++++++++++++++++--------------------------- grovel/grovel.lisp | 114 +++++++++++++++++++++++++------------- grovel/invoke.lisp | 149 ------------------------------------------------- 4 files changed, 158 insertions(+), 271 deletions(-) delete mode 100644 grovel/invoke.lisp diff --git a/cffi-grovel.asd b/cffi-grovel.asd index 11ac4a1..f1ddf8a 100644 --- a/cffi-grovel.asd +++ b/cffi-grovel.asd @@ -25,17 +25,16 @@ ;;; DEALINGS IN THE SOFTWARE. ;;; -(asdf:defsystem cffi-grovel +(defsystem "cffi-grovel" :description "The CFFI Groveller" :author "Dan Knapp <dan...@accela.net>" - :depends-on (cffi alexandria) + :depends-on ("cffi" "alexandria" (:version "asdf" "3")) :licence "MIT" :components - ((:module grovel + ((:module "grovel" :serial t :components ((:file "package") - (:file "invoke") (:static-file "common.h") (:file "grovel") (:file "asdf"))))) diff --git a/grovel/asdf.lisp b/grovel/asdf.lisp index 506cfba..3490eab 100644 --- a/grovel/asdf.lisp +++ b/grovel/asdf.lisp @@ -30,139 +30,136 @@ (in-package #:cffi-grovel) -(defun ensure-pathname (thing) - (if (typep thing 'logical-pathname) - (translate-logical-pathname thing) - (pathname thing))) - (defclass cc-flags-mixin () ((cc-flags :initform nil :accessor cc-flags-of :initarg :cc-flags))) (defmethod asdf:perform :around ((op asdf:compile-op) (file cc-flags-mixin)) - (declare (ignorable op)) (let ((*cc-flags* (append (ensure-list (cc-flags-of file)) *cc-flags*))) (call-next-method))) -(eval-when (:compile-toplevel :load-toplevel :execute) - (defclass process-op (#-asdf3 asdf:operation - #+asdf3 asdf:downward-operation) - () - (:documentation "This ASDF operation performs the steps necessary +(defclass process-op (asdf:downward-operation) + () + (:documentation "This ASDF operation performs the steps necessary to generate a compilable and loadable lisp file from a PROCESS-OP-INPUT component.")) - (defclass process-op-input (asdf:cl-source-file) - ((generated-lisp-file-type - :initarg :generated-lisp-file-type - :accessor generated-lisp-file-type - :documentation "The :TYPE argument to use for the generated lisp file.")) - (:default-initargs - :generated-lisp-file-type "generated-lisp-file") - (:documentation "This ASDF component represents a file that is +(defclass process-op-input (asdf:cl-source-file) + ((generated-lisp-file-type + :initarg :generated-lisp-file-type + :accessor generated-lisp-file-type + :documentation "The :TYPE argument to use for the generated lisp file.")) + (:default-initargs + :generated-lisp-file-type "generated-lisp-file") + (:documentation "This ASDF component represents a file that is used as input to a function that generates lisp source file. This component acts as if it is a CL-SOURCE-FILE by applying the COMPILE-OP and LOAD-SOURCE-OP operations to the file generated by - PROCESS-OP."))) + PROCESS-OP.")) (defmethod asdf:input-files ((op process-op) (c process-op-input)) (list (asdf:component-pathname c))) +(defmethod asdf:input-files ((op asdf:compile-op) (c process-op-input)) + (list (first (asdf:output-files 'process-op c)))) + (defmethod asdf:component-depends-on ((op process-op) (c process-op-input)) - `(#-asdf3 (asdf:load-op ,@(asdf::component-load-dependencies c)) - #+asdf3 (asdf:prepare-op ,c) - ,@(call-next-method))) + `((asdf:prepare-op ,c) ,@(call-next-method))) (defmethod asdf:component-depends-on ((op asdf:compile-op) (c process-op-input)) - (declare (ignorable op)) - `((process-op ,(asdf:component-name c)) - ,@(call-next-method))) + `((process-op ,c) ,@(call-next-method))) (defmethod asdf:component-depends-on ((op asdf:load-source-op) (c process-op-input)) - (declare (ignorable op)) - `((process-op ,(asdf:component-name c)) - ,@(call-next-method))) - -(defmethod asdf:perform ((op asdf:compile-op) (c process-op-input)) - (let ((generated-lisp-file (first (asdf:output-files (make-instance 'process-op) c)))) - (asdf:perform op (make-instance 'asdf:cl-source-file - :name (asdf:component-name c) - :parent (asdf:component-parent c) - :pathname generated-lisp-file)))) - -(defmethod asdf:perform ((op asdf:load-source-op) (c process-op-input)) - (let ((generated-lisp-file (first (asdf:output-files (make-instance 'process-op) c)))) - (asdf:perform op (make-instance 'asdf:cl-source-file - :name (asdf:component-name c) - :parent (asdf:component-parent c) - :pathname generated-lisp-file)))) + `((process-op ,c) ,@(call-next-method))) ;;;# ASDF component: GROVEL-FILE -(eval-when (:compile-toplevel :load-toplevel :execute) - (defclass grovel-file (process-op-input cc-flags-mixin) - () - (:default-initargs - :generated-lisp-file-type "processed-grovel-file") - (:documentation - "This ASDF component represents an input file that is processed - by PROCESS-GROVEL-FILE."))) +(defclass grovel-file (process-op-input cc-flags-mixin) + () + (:default-initargs + :generated-lisp-file-type "processed-grovel-file") + (:documentation + "This ASDF component represents an input file that is processed + by PROCESS-GROVEL-FILE.")) (defmethod asdf:output-files ((op process-op) (c grovel-file)) - (let* ((input-file (asdf:component-pathname c)) + (let* ((input-file (first (asdf:input-files op c))) (output-file (make-pathname :type (generated-lisp-file-type c) :defaults input-file)) - (c-file (make-c-file-name output-file))) + (c-file (make-c-file-name output-file "__grovel"))) (list output-file c-file (exe-filename c-file)))) (defmethod asdf:perform ((op process-op) (c grovel-file)) - (let ((output-file (first (asdf:output-files op c))) - (input-file (asdf:component-pathname c))) - (ensure-directories-exist (directory-namestring output-file)) - (let ((tmp-file (process-grovel-file input-file output-file))) - (unwind-protect - (alexandria:copy-file tmp-file output-file :if-to-exists :supersede) - (delete-file tmp-file))))) + (let* ((output-file (first (asdf:output-files op c))) + (input-file (first (asdf:input-files op c))) + (tmp-file (process-grovel-file input-file output-file))) + (uiop:rename-file-overwriting-target tmp-file output-file))) + ;;;# ASDF component: WRAPPER-FILE -(eval-when (:compile-toplevel :load-toplevel :execute) - (defclass wrapper-file (process-op-input cc-flags-mixin) - ((soname :initform nil :initarg :soname :accessor soname-of)) - (:default-initargs - :generated-lisp-file-type "processed-wrapper-file") - (:documentation - "This ASDF component represents an input file that is processed - by PROCESS-WRAPPER-FILE. This generates a foreign library and - matching CFFI bindings that are subsequently compiled and - loaded."))) +(defclass wrapper-file (process-op-input cc-flags-mixin) + ((soname :initform nil :initarg :soname :accessor soname-of)) + (:default-initargs + :generated-lisp-file-type "processed-wrapper-file") + (:documentation + "This ASDF component represents an input file that is processed + by PROCESS-WRAPPER-FILE. This generates a foreign library and + matching CFFI bindings that are subsequently compiled and + loaded.")) (defun wrapper-soname (c) (or (soname-of c) (asdf:component-name c))) (defmethod asdf:output-files ((op process-op) (c wrapper-file)) - (let* ((input-file (asdf:component-pathname c)) + (let* ((input-file (first (asdf:input-files op c))) (output-file (make-pathname :type (generated-lisp-file-type c) :defaults input-file)) - (c-file (make-c-file-name output-file)) + (c-file (make-c-file-name output-file "__wrapper")) + (o-file (make-o-file-name output-file "__wrapper")) (lib-soname (wrapper-soname c))) (list output-file + (lib-filename (make-soname lib-soname output-file)) c-file - (lib-filename (make-soname lib-soname output-file))))) + o-file))) + +;;; Declare the .o and .so files as compilation outputs, +;;; so they get picked up by bundle operations. +#.(when (uiop:version<= "3.1.5.12" (asdf:asdf-version)) + '(defmethod asdf:output-files ((op asdf:compile-op) (c wrapper-file)) + (destructuring-bind (generated-lisp lib-file c-file o-file) (asdf:output-files 'process-op c) + (declare (ignore generated-lisp c-file)) + (multiple-value-bind (files translatedp) (call-next-method) + (values (append files (list lib-file o-file)) translatedp))))) (defmethod asdf:perform ((op process-op) (c wrapper-file)) - (let ((output-file (first (asdf:output-files op c))) - (input-file (asdf:component-pathname c))) - (ensure-directories-exist (directory-namestring output-file)) - (let ((tmp-file (process-wrapper-file input-file output-file (wrapper-soname c)))) + (let* ((output-file (first (asdf:output-files op c))) + (input-file (first (asdf:input-files op c))) + (tmp-file (process-wrapper-file + input-file + :output-defaults output-file + :lib-soname (wrapper-soname c)))) (unwind-protect (alexandria:copy-file tmp-file output-file :if-to-exists :supersede) - (delete-file tmp-file))))) + (delete-file tmp-file)))) + ;; Allow for naked :grovel-file and :wrapper-file in asdf definitions. -(eval-when (:compile-toplevel :load-toplevel :execute) - (setf (find-class 'asdf::cffi-grovel-file) (find-class 'grovel-file)) - (setf (find-class 'asdf::cffi-wrapper-file) (find-class 'wrapper-file))) +(setf (find-class 'asdf::cffi-grovel-file) (find-class 'grovel-file)) +(setf (find-class 'asdf::cffi-wrapper-file) (find-class 'wrapper-file)) + +;; Implement link-op on image-based platforms. + +#-(or clasp ecl mkcl) +(defmethod asdf:perform ((o asdf/bundle::link-op) (c asdf:system)) + (let* ((inputs (asdf:input-files o c)) + (output (first (asdf:output-files o c))) + (kind (asdf/bundle::bundle-type o))) + (when output ;; some operations skip any output when there is no input + (ecase kind + (:program (cc-create-exe output inputs)) + ((:lib :static-library) (cc-link-static-library output inputs)) + ((:dll :shared-library) (cc-link-shared-library output inputs)))))) diff --git a/grovel/grovel.lisp b/grovel/grovel.lisp index b821841..fa23a6f 100644 --- a/grovel/grovel.lisp +++ b/grovel/grovel.lisp @@ -37,6 +37,13 @@ for trim = (string-trim '(#\Space #\Tab #\Newline) s) unless (string= "" trim) collect trim)) +(defun invoke (command &rest args) + (when (pathnamep command) + (setf command (cffi-sys:native-namestring command))) + (format *debug-io* "; ~A~{ ~A~}~%" command args) + (uiop:run-program `(,command ,@args) :output :interactive :error-output :interactive)) + + ;;;# Error Conditions (define-condition grovel-error (simple-error) ()) @@ -194,11 +201,21 @@ int main(int argc, char**argv) { (defun header-form-p (form) (member (form-kind form) *header-forms*)) -(defun make-c-file-name (output-defaults) - (make-pathname :type "c" :defaults output-defaults)) +(defun make-c-file-name (output-defaults &optional suffix) + (make-pathname :type "c" + :name (format nil "~A~@[~A~]" (pathname-name output-defaults) suffix) + :defaults output-defaults)) + +(defun o-type () + (uiop:os-cond ((uiop:os-unix-p) "o") ((uiop:os-windows-p) "obj") (t "o"))) + +(defun make-o-file-name (output-defaults &optional suffix) + (make-pathname :type (o-type) + :name (format nil "~A~@[~A~]" (pathname-name output-defaults) suffix) + :defaults output-defaults)) (defun generate-c-file (input-file output-defaults) - (let ((c-file (make-c-file-name output-defaults))) + (let ((c-file (make-c-file-name output-defaults "__grovel"))) (with-open-file (out c-file :direction :output :if-exists :supersede) (with-open-file (in input-file :direction :input) (flet ((read-forms (s) @@ -263,7 +280,7 @@ int main(int argc, char**argv) { #+darwin (list "-I" "/opt/local/include/") #-darwin nil ;; ECL internal flags - #+ecl (list c::*cc-flags*) + #+ecl (remove-if 'uiop:emptyp (uiop:split-string c::*cc-flags* :separator " ")) ;; FreeBSD non-base header files #+freebsd (list "-I" "/usr/local/include/"))) @@ -277,10 +294,9 @@ int main(int argc, char**argv) { (4 (list "-m32")) (8 (list "-m64")))) -(defparameter *platform-library-flags* +(defparameter *shared-library-flags* (list #+darwin "-bundle" - #-darwin "-shared" - #-windows "-fPIC")) + #-darwin "-shared")) (defun host-and-directory-namestring (pathname) (namestring @@ -288,24 +304,44 @@ int main(int argc, char**argv) { :type nil :defaults pathname))) -(defun cc-compile-and-link (input-file output-file &key library) - (let ((arglist - `(,(or (getenv "CC") *cc*) - ,@*cpu-word-size-flags* - ,@*cc-flags* - ;; add the cffi directory to the include path to make common.h visible - ,(format nil "-I~A" - (host-and-directory-namestring - (truename (asdf:system-definition-pathname :cffi-grovel)))) - ,@(when library *platform-library-flags*) - "-o" ,(native-namestring output-file) - ,(native-namestring input-file)))) - (when library - ;; if it's a library that may be used, remove it - ;; so we won't possibly be overwriting the code of any existing process - (ignore-some-conditions (file-error) - (delete-file output-file))) - (apply #'invoke arglist))) +(defun program-argument (x) + (etypecase x + (string x) + (pathname (native-namestring x)))) + +(defun invoke-cc (&rest args) + (apply 'invoke `(,(or (getenv "CC") *cc*) + ,@*cpu-word-size-flags* + ,@*cc-flags* + ,(format nil "-I~A" + (truename (asdf:system-source-directory :cffi-grovel))) + ,@args))) + +(defun cc-compile (output-file input-file) + (invoke-cc + #-windows "-fPIC" + "-o" (native-namestring output-file) + "-c" (native-namestring input-file))) + +(defun cc-create-exe (output-file inputs) + (apply 'invoke-cc + "-o" (native-namestring output-file) + (mapcar 'program-argument inputs))) + +(defun cc-link-static-library (output-file inputs) + (apply 'invoke + `(#+unix ,@`("ar" "rcs" ,(native-namestring output-file)) + #+windows ,@'("lib" "-nologo" ,(format nil "-out:~A" (native-namestring output-file))) + ,@(mapcar 'program-argument inputs)))) + +(defun cc-link-shared-library (output-file inputs) + ;; remove the library so we won't possibly be overwriting + ;; the code of any existing process + (uiop:delete-file-if-exists output-file) + (apply 'invoke-cc + "-o" (native-namestring output-file) + `(,@*shared-library-flags* + ,@(mapcar 'program-argument inputs)))) ;;; *PACKAGE* is rebound so that the IN-PACKAGE form can set it during ;;; *the extent of a given grovel file. @@ -314,7 +350,7 @@ int main(int argc, char**argv) { (let* ((c-file (generate-c-file input-file output-defaults)) (exe-file (exe-filename c-file)) (lisp-file (tmp-lisp-filename c-file))) - (cc-compile-and-link c-file exe-file) + (cc-create-exe exe-file (list c-file)) (invoke exe-file (native-namestring lisp-file)) lisp-file))) @@ -402,9 +438,9 @@ int main(int argc, char**argv) { (ecase type (integer (format out "~& if(_64_BIT_VALUE_FITS_SIGNED_P(~A))~%" c-name) - (format out " fprintf(output, \"%lli\", (int64_t) ~A);" c-name) + (format out " fprintf(output, \"%lli\", (long long signed) ~A);" c-name) (format out "~& else~%") - (format out " fprintf(output, \"%llu\", (uint64_t) ~A);" c-name)) + (format out " fprintf(output, \"%llu\", (long long unsigned) ~A);" c-name)) (double-float (format out "~& fprintf(output, \"%s\", print_double_for_lisp((double)~A));~%" c-name))) (when documentation @@ -519,8 +555,8 @@ int main(int argc, char**argv) { (c-export out slot-lisp-name))) (c-format out "(cffi:defcstruct (") (c-print-symbol out struct-lisp-name t) - (c-printf out " :size %i)" - (format nil "sizeof(~A)" struct-c-name)) + (c-printf out " :size %llu)" + (format nil "(long long unsigned) sizeof(~A)" struct-c-name)) (when documentation (c-format out "~% ~S" documentation)) (dolist (slot slots) @@ -818,7 +854,7 @@ string." (defun generate-c-lib-file (input-file output-defaults) (let ((*lisp-forms* nil) - (c-file (make-c-file-name output-defaults))) + (c-file (make-c-file-name output-defaults "__wrapper"))) (with-open-file (out c-file :direction :output :if-exists :supersede) (with-open-file (in input-file :direction :input) (write-string *header* out) @@ -860,13 +896,17 @@ string." ;;; *PACKAGE* is rebound so that the IN-PACKAGE form can set it during ;;; *the extent of a given wrapper file. -(defun process-wrapper-file (input-file output-defaults lib-soname) +(defun process-wrapper-file (input-file + &key + (output-defaults (make-pathname :defaults input-file :type "processed")) + lib-soname) (with-standard-io-syntax - (let ((lib-file - (lib-filename (make-soname lib-soname output-defaults)))) - (multiple-value-bind (c-file lisp-forms) - (generate-c-lib-file input-file output-defaults) - (cc-compile-and-link c-file lib-file :library t) + (multiple-value-bind (c-file lisp-forms) + (generate-c-lib-file input-file output-defaults) + (let ((lib-file (lib-filename (make-soname lib-soname output-defaults))) + (o-file (make-o-file-name output-defaults "__wrapper"))) + (cc-compile o-file c-file) + (cc-link-shared-library lib-file (list o-file)) ;; FIXME: hardcoded library path. (values (generate-bindings-file lib-file lib-soname lisp-forms output-defaults) lib-file))))) diff --git a/grovel/invoke.lisp b/grovel/invoke.lisp deleted file mode 100644 index 11fac80..0000000 --- a/grovel/invoke.lisp +++ /dev/null @@ -1,149 +0,0 @@ -;;;; -*- Mode: lisp; indent-tabs-mode: nil -*- -;;; -;;; invoke.lisp --- Half-baked portable run-program. -;;; -;;; Copyright (C) 2005-2006, Dan Knap <dan...@accela.net> -;;; Copyright (C) 2005-2006, Emily Backes <lu...@accela.net> -;;; Copyright (C) 2007, Stelian Ionescu <sione...@cddr.org> -;;; Copyright (C) 2007, Luis Oliveira <lolive...@common-lisp.net> -;;; -;;; Permission is hereby granted, free of charge, to any person -;;; obtaining a copy of this software and associated documentation -;;; files (the "Software"), to deal in the Software without -;;; restriction, including without limitation the rights to use, copy, -;;; modify, merge, publish, distribute, sublicense, and/or sell copies -;;; of the Software, and to permit persons to whom the Software is -;;; furnished to do so, subject to the following conditions: -;;; -;;; The above copyright notice and this permission notice shall be -;;; included in all copies or substantial portions of the Software. -;;; -;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -;;; DEALINGS IN THE SOFTWARE. -;;; - -(in-package #:cffi-grovel) - -;;;# Shell Execution - -#-(or abcl allegro clisp cmu ecl lispworks openmcl sbcl scl) -(grovel-error "%INVOKE is unimplemented for this Lisp. Patches welcome.") - -;; FIXME: doesn't do shell quoting -#+abcl -(defun %invoke (command arglist) - (let ((cmdline (reduce (lambda (str1 str2) - (concatenate 'string str1 " " str2)) - arglist :initial-value command)) - (stream (make-string-output-stream))) - (values (ext:run-shell-command cmdline :output stream) - (get-output-stream-string stream)))) - -(defun process-output (process-stream) - (with-open-stream (process-stream process-stream) - (with-output-to-string (str) - (loop for char = (read-char process-stream nil) - while char - do (write-char char str))))) - -#+clisp -(defun %invoke (command arglist) - (let* ((output-file - (make-pathname - :name (format nil "clisp-cffi-invoke-~A-~A.tmp" - (get-universal-time) - (random 1000)))) - (ret (ext:run-program command - :arguments arglist - :output output-file))) - (with-open-file (stream output-file :direction :input) - (multiple-value-prog1 - (values (etypecase ret - ((eql nil) 0) - ((eql t) 1) - (integer ret)) - (process-output stream)) - (delete-file output-file))))) - -#+ecl -(defun %invoke (command arglist) - (multiple-value-bind (output-stream exit-code) - (ext:run-program "/bin/sh" - (list "-c" - (format nil "~A~{ ~A~}" command arglist)) - :wait t :output :stream :input nil :error nil) - (values exit-code - (process-output output-stream)))) - -#+(or cmu scl) -(defun %invoke (command arglist) - (let* ((process (ext:run-program command arglist - :output :stream - :wait nil - :error :output)) - (output (process-output (ext:process-output process)))) - (ext:process-wait process) - (values (ext:process-exit-code process) output))) - -#+sbcl -(defun %invoke (command arglist) - (let* ((process (sb-ext:run-program command arglist - :output :stream - :wait nil - :error :output - :search t)) - (output (process-output (sb-ext:process-output process)))) - (sb-ext:process-wait process) - (values (sb-ext:process-exit-code process) output))) - -#+openmcl -(defun %invoke (command arglist) - (let* ((exit-code) - (output (with-output-to-string (s) - (let ((process (ccl:run-program command arglist - :output s - :error :output))) - (setq exit-code (nth-value 1 (ccl:external-process-status process))))))) - (values exit-code output))) - -#+allegro -(eval-when (:compile-toplevel :load-toplevel :execute) - (require '#:osi)) - -#+allegro -(defun %invoke (command arglist) - (let ((cmd #-mswindows (concatenate 'vector (list command command) arglist) - #+mswindows (format nil "~A~{ ~A~}" command arglist))) - (multiple-value-bind (output error-output exit-code) - (excl.osi:command-output cmd :whole t) - (declare (ignore error-output)) - (values exit-code output)))) - -;;; FIXME: Runs shell, and arguments are unquoted. -#+lispworks -(defun %invoke (command arglist) - (let ((s (make-string-output-stream))) - (values (sys:call-system-showing-output - (format nil "~A~{ ~A~}" command arglist) - :output-stream s :prefix "" :show-cmd nil) - (get-output-stream-string s)))) - -;;; Do we really want to suppress the output by default? -(defun invoke (command &rest args) - (when (pathnamep command) - (setf command (cffi-sys:native-namestring command))) - (format *debug-io* "; ~A~{ ~A~}~%" command args) - (multiple-value-bind (exit-code output) - (%invoke command args) - (unless (zerop exit-code) - (grovel-error "External process exited with code ~S.~@ - Command was: ~S~{ ~S~}~@ - Output was:~%~A" - exit-code command args output)) - output)) -- 2.6.0.rc0.131.gf624c3d