;;;; Generate WiX XML Source, from which we eventually generate the .MSI ;;;; This software is part of the SBCL system. See the README file for ;;;; more information. ;;;; ;;;; This software is derived from the CMU CL system, which was ;;;; written at Carnegie Mellon University and released into the ;;;; public domain. The software is in the public domain and is ;;;; provided with absolutely no warranty. See the COPYING and CREDITS ;;;; files for more information. ;;;; Restore internal-features. Needed to package libsbcl files if ;;;; sb-linkable-runtime is enabled. (eval-when (:compile-toplevel :load-toplevel :execute) (setq *features* (union *features* sb-impl:+internal-features+))) ;;;; XML generation (defvar *indent-level* 0) (defvar *sbcl-source-root* (truename (merge-pathnames (make-pathname :directory (list :relative :up)) (make-pathname :name nil :type nil :defaults *load-truename*)))) (defun print-xml (sexp &optional (stream *standard-output*)) (destructuring-bind (tag &optional attributes &body children) sexp (when attributes (assert (evenp (length attributes)))) (format stream "~V@T<~A~{ ~A='~A'~}~@[/~]>~%" *indent-level* tag attributes (not children)) (let ((*indent-level* (+ *indent-level* 3))) (dolist (child children) (unless (listp child) (error "Malformed child: ~S in ~S" child children)) (print-xml child stream))) (when children (format stream "~V@T~%" *indent-level* tag)))) (defun xml-1.0 (pathname sexp) (with-open-file (xml pathname :direction :output :if-exists :supersede :external-format :ascii) (format xml "~%") (print-xml sexp xml))) (defun application-name () "Steel Bank Common Lisp") (defun application-name/version+machine-type () (format nil "~A ~A (~A)" (application-name) (lisp-implementation-version) (machine-type))) (defun manufacturer-name () "http://www.sbcl.org") (defun upgrade-code () "BFF1D4CA-0153-4AAC-BB21-06DC4B8EAD7D") (defun version-digits (&optional (horrible-thing (lisp-implementation-version))) "Turns something like 0.pre7.14.flaky4.13 (see version.lisp-expr) into an acceptable form for WIX (up to four dot-separated numbers)." (with-output-to-string (output) (loop with position = 0 for separator = "" then "." for next-digit = (position-if #'digit-char-p horrible-thing :start position) repeat 4 while next-digit do (multiple-value-bind (number end) (parse-integer horrible-thing :start next-digit :junk-allowed t) (format output "~A~D" separator number) (setf position end))))) ;;;; GUID generation ;;;; ;;;; Apparently this willy-nilly regeneration of GUIDs is a bad thing, and ;;;; we should probably have a single GUID per release / Component, so ;;;; that no matter by whom the .MSI is built the GUIDs are the same. ;;;; ;;;; Something to twiddle on a rainy day, I think. (load-shared-object "OLE32.DLL") (define-alien-type uuid (struct uuid (data1 unsigned-int) (data2 unsigned-short) (data3 unsigned-short) (data4 (array unsigned-char 8)))) (define-alien-routine ("CoCreateGuid" co-create-guid) int (guid (* uuid))) (defun uuid-string (uuid) (declare (type (alien (* uuid)) uuid)) (let ((data4 (slot uuid 'data4))) (format nil "~8,'0X-~4,'0X-~4,'0X-~2,'0X~2,'0X-~{~2,'0X~}" (slot uuid 'data1) (slot uuid 'data2) (slot uuid 'data3) (deref data4 0) (deref data4 1) (loop for i from 2 upto 7 collect (deref data4 i))))) (defun make-guid () (let (guid) (unwind-protect (progn (setf guid (make-alien (struct uuid))) (co-create-guid guid) (uuid-string guid)) (free-alien guid)))) (defvar *id-char-substitutions* '((#\\ . #\_) (#\/ . #\_) (#\: . #\.) (#\- . #\.))) (defun id (string) ;; Mangle a string till it can be used as an Id. A-Z, a-z, 0-9, and ;; _ are ok, nothing else is. (map 'string (lambda (c) (or (cdr (assoc c *id-char-substitutions*)) c)) string)) (defun directory-id (name) (id (format nil "Directory_~A" (enough-namestring name *sbcl-source-root*)))) (defun file-id (pathname) (id (format nil "File_~A" (enough-namestring pathname *sbcl-source-root*)))) (defparameter *ignored-directories* '("CVS" ".svn")) (defparameter *pathname-type-abbrevs* '(("lisp" . "lsp") ("fasl" . "fas") ("SBCL" . "txt") ; README.SBCL -> README.txt ("texinfo" . "tfo") ("lisp-temp" . "lmp") ("html" . "htm"))) (defparameter *components* nil) (defun component-id (pathname) (let ((id (id (format nil "Contrib_~A" (enough-namestring pathname *sbcl-source-root*))))) (push id *components*) id)) (defun ref-all-components () (prog1 (mapcar (lambda (id) `("ComponentRef" ("Id" ,id))) *components*) (setf *components* nil))) (defun collect-1-component (root) `("Directory" ("Name" ,(car (last (pathname-directory root))) "Id" ,(directory-id root)) ("Component" ("Id" ,(component-id root) "Guid" ,(make-guid) "DiskId" 1 #+64-bit "Win64" #+64-bit "yes") ,@(loop for file in (directory (make-pathname :name :wild :type :wild :defaults root)) when (or (pathname-name file) (pathname-type file)) collect `("File" ("Name" ,(file-namestring file) "Id" ,(file-id file) "Source" ,(enough-namestring file))))))) (defun directory-empty-p (dir) (null (directory (make-pathname :name :wild :type :wild :defaults dir)))) (defun collect-components (root) (append (unless (directory-empty-p root) (list (collect-1-component root))) (loop for directory in (directory (merge-pathnames (make-pathname :directory '(:relative :wild) :name nil :type nil) root)) unless (member (car (last (pathname-directory directory))) *ignored-directories* :test #'equal) append (collect-components directory)))) (defun collect-contrib-components () (collect-components "../obj/sbcl-home/contrib/")) (defun split-string-on-spaces (list-string) "Takes a string of space separated tokens and returns them as a list of strings." (let ((next-space (position #\Space list-string))) (if next-space (list* (subseq list-string 0 next-space) (split-string-on-spaces (subseq list-string (1+ next-space)))) (list list-string)))) (defun read-libsbcl-value-from-sbcl.mk () "Parse sbcl.mk, looking for the LIBSBCL= line and return its value." (with-open-file (s "../src/runtime/sbcl.mk") (loop for line = (read-line s nil nil) while line for match-location = (search "LIBSBCL=" line) when (and match-location (zerop match-location)) do (return (subseq line 8))))) (defun collect-libsbcl-files () (let ((filenames (list* "sbcl.mk" (split-string-on-spaces (read-libsbcl-value-from-sbcl.mk))))) (loop for filename in filenames collecting `("File" ("Name" ,filename "Source" ,(concatenate 'string "../src/runtime/" filename)))))) (defun make-extension (type mime) `("Extension" ("Id" ,type "ContentType" ,mime) ("Verb" ("Id" ,(format nil "load_~A" type) "Argument" "--core \"[#sbcl.core]\" --load \"%1\"" "Command" "Load with SBCL" "Target" "[#sbcl.exe]")))) (defun write-wxs (pathname) ;; both :INVERT and :PRESERVE could be used here, but this seemed ;; better at the time (xml-1.0 pathname `("Wix" ("xmlns" "http://schemas.microsoft.com/wix/2006/wi") ("Product" ("Id" "*" "Name" ,(application-name/version+machine-type) "Version" ,(version-digits) "Manufacturer" ,(manufacturer-name) "UpgradeCode" ,(upgrade-code) "Language" 1033) ("Package" ("Id" "*" "Manufacturer" ,(manufacturer-name) "InstallerVersion" 500 "Compressed" "yes" #+(or arm64 x86-64) "Platform" #+x86-64 "x64" #+arm64 "arm64" "InstallScope" "perMachine")) ("Media" ("Id" 1 "Cabinet" "sbcl.cab" "EmbedCab" "yes")) ("Property" ("Id" "PREVIOUSVERSIONSINSTALLED" "Secure" "yes")) ("Upgrade" ("Id" ,(upgrade-code)) ("UpgradeVersion" ("Minimum" "1.0.0" "Maximum" "99.0.0" "Property" "PREVIOUSVERSIONSINSTALLED" "IncludeMinimum" "yes" "IncludeMaximum" "no"))) ("InstallExecuteSequence" () ("RemoveExistingProducts" ("After" "InstallInitialize"))) ("Directory" ("Id" "TARGETDIR" "Name" "SourceDir") ("Directory" ("Id" "ProgramMenuFolder") ("Directory" ("Id" "ProgramMenuDir" "Name" ,(application-name/version+machine-type)) ("Component" ("Id" "ProgramMenuDir" "Guid" ,(make-guid)) ("RemoveFolder" ("Id" "ProgramMenuDir" "On" "uninstall")) ("RegistryValue" ("Root" "HKCU" "Key" "Software\\[Manufacturer]\\[ProductName]" "Type" "string" "Value" "" "KeyPath" "yes"))))) ("Directory" ("Id" #+x86 "ProgramFilesFolder" #+64-bit "ProgramFiles64Folder" "Name" "PFiles") ("Directory" ("Id" "BaseFolder" "Name" ,(application-name)) ("Directory" ("Id" "INSTALLDIR") ("Component" ("Id" "SBCL_SetPATH" "Guid" ,(make-guid) "DiskId" 1 #+64-bit "Win64" #+64-bit "yes") ("CreateFolder") ("Environment" ("Id" "Env_PATH" "System" "yes" "Action" "set" "Name" "PATH" "Part" "last" "Value" "[INSTALLDIR]"))) ("Component" ("Id" "SBCL_Base" "Guid" ,(make-guid) "DiskId" 1 #+64-bit "Win64" #+64-bit "yes") ;; If we want to associate files with SBCL, this ;; is how it's done -- but doing this by default ;; and without asking the user for permission Is ;; Bad. Before this is enabled we need to figure out ;; how to make WiX ask for permission for this... ;; ,(make-extension "fasl" "application/x-lisp-fasl") ;; ,(make-extension "lisp" "text/x-lisp-source") ("File" ("Name" "sbcl.core" "Source" "sbcl.core")) #+sb-linkable-runtime ,@(collect-libsbcl-files) ("File" ("Name" "sbcl.exe" "Source" "../src/runtime/sbcl.exe" "KeyPath" "yes") ("Shortcut" ("Id" "sbcl.lnk" "Advertise" "yes" "Name" ,(application-name/version+machine-type) "Directory" "ProgramMenuDir" "Arguments" "--core \"[#sbcl.core]\"")))) ,@(collect-contrib-components))))) ("Feature" ("Id" "Minimal" "Title" "SBCL Executable" "ConfigurableDirectory" "INSTALLDIR" "Level" 1) ("ComponentRef" ("Id" "SBCL_Base")) ("ComponentRef" ("Id" "ProgramMenuDir")) ("Feature" ("Id" "Contrib" "Level" 1 "Title" "Contributed Modules") ,@(ref-all-components)) ("Feature" ("Id" "SetPath" "Level" 1 "Title" "Set Environment Variable: PATH") ("ComponentRef" ("Id" "SBCL_SetPATH")))) ("WixVariable" ("Id" "WixUILicenseRtf" "Value" "License.rtf")) ("Property" ("Id" "WIXUI_INSTALLDIR" "Value" "INSTALLDIR")) ("UIRef" ("Id" "WixUI_FeatureTree"))))))