racket/collects/compiler/embed-unit.ss
2005-06-28 12:51:18 +00:00

607 lines
19 KiB
Scheme

(module embed-unit mzscheme
(require (lib "unitsig.ss")
(lib "file.ss")
(lib "list.ss")
(lib "etc.ss")
(lib "port.ss")
(lib "moddep.ss" "syntax")
(lib "plist.ss" "xml")
(lib "process.ss")
"embed-sig.ss"
"private/winicon.ss"
"private/winsubsys.ss")
(provide compiler:embed@)
(define compiler:embed@
(unit/sig compiler:embed^
(import)
(define (embedding-executable-is-directory? mred?)
(eq? 'macosx (system-type)))
(define (embedding-executable-put-file-extension+style+filters mred?)
(case (system-type)
[(windows) (values ".exe" null '(("Executable" "*.exe")))]
[(macosx) (values ".app" '(enter-packages) #f)]
[else (values #f null null)]))
(define (embedding-executable-add-suffix path mred?)
(let* ([path (if (string? path)
(string->path path)
path)]
[fixup (lambda (re sfx)
(if (regexp-match re (path->bytes path))
path
(path-replace-suffix path sfx)))])
(case (system-type)
[(windows) (fixup #rx#"[.][eE][xX][eE]$" #".exe")]
[(macosx) (if mred?
(fixup #rx#"[.][aA][pP][pP]$" #".app")
path)]
[else path])))
;; Find executable relative to the "mzlib"
;; collection.
(define (find-exe mred? variant)
(let* ([c-path (collection-path "mzlib")]
[base (build-path c-path 'up 'up)]
[fail
(lambda ()
(error 'make-embedding-executable
"can't find ~a executable"
(if mred? "MrEd" "MzScheme")))]
[variant-suffix (case variant
[(normal) ""]
[(3m) "3m"])])
(let ([exe (build-path
base
(case (system-type)
[(macosx)
(cond
[(not mred?)
;; Need MzScheme:
(build-path "bin" (string-append
"mzscheme"
variant-suffix))]
[mred?
;; Need MrEd:
(build-path (format "MrEd~a.app" variant-suffix)
"Contents" "MacOS"
(format "MrEd~a" variant-suffix))])]
[(windows)
(format "~a~a.exe" (if mred?
"mred"
"mzscheme")
variant-suffix)]
[(unix)
(build-path "bin"
(format "~a~a" (if mred?
"mred"
"mzscheme")
variant-suffix))]
[(macos)
(format "~a~a" (if mred?
"MrEd"
"MzScheme")
variant-suffix)]))])
(unless (or (file-exists? exe)
(directory-exists? exe))
(fail))
exe)))
;; Find the magic point in the binary:
(define (find-cmdline what rx)
(let ([m (regexp-match-positions rx (current-input-port))])
(if m
(caar m)
(error
'make-embedding-executable
(format
"can't find ~a position in executable"
what)))))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (prepare-macosx-mred exec-name dest aux variant)
(let* ([name (let-values ([(base name dir?) (split-path dest)])
(path-replace-suffix name #""))]
[variant-suffix (case variant
[(normal) ""]
[(3m) "3m"])]
[src (build-path (collection-path "launcher")
(format "Starter~a.app" variant-suffix))]
[creator (let ([c (assq 'creator aux)])
(or (and c
(cdr c))
"MrSt"))]
[file-types (let ([m (assq 'file-types aux)])
(and m
(pair? (cdr m))
(cdr m)))]
[resource-files (let ([m (assq 'resource-files aux)])
(and m
(cdr m)))])
(when creator
(unless (and (string? creator) (= 4 (string-length creator)))
(error 'make-executable "creator is not a 4-character string: ~e" creator)))
(when file-types
(unless (and (list? file-types)
(andmap list? file-types)
(andmap (lambda (spec)
(andmap (lambda (p)
(and (list? p)
(= 2 (length p))
(string? (car p))))
spec))
file-types))
(error 'make-executable "bad file-types spec: ~e" file-types)))
(when resource-files
(unless (and (list? resource-files)
(andmap path-string?
resource-files))
(error 'make-executable "resource-files is not a list of paths: ~e" resource-files)))
(when (or (directory-exists? dest)
(file-exists? dest)
(link-exists? dest))
(delete-directory/files dest))
(make-directory* (build-path dest "Contents" "Resources"))
(make-directory* (build-path dest "Contents" "MacOS"))
(copy-file exec-name (build-path dest "Contents" "MacOS" name))
(copy-file (build-path src "Contents" "PkgInfo")
(build-path dest "Contents" "PkgInfo"))
(let ([icon (or (let ([icon (assq 'icns aux)])
(and icon
(cdr icon)))
(build-path src "Contents" "Resources"
(format "Starter~a.icns" variant-suffix)))])
(copy-file icon
(build-path dest "Contents" "Resources"
(format "Starter~a.icns" variant-suffix))))
(let ([orig-plist (call-with-input-file (build-path src
"Contents"
"Info.plist")
read-plist)]
[plist-replace (lambda (plist . l)
(let loop ([plist plist][l l])
(if (null? l)
plist
(let ([key (car l)]
[val (cadr l)])
(loop `(dict
,@(let loop ([c (cdr plist)])
(cond
[(null? c) (list (list 'assoc-pair key val))]
[(string=? (cadar c) key)
(cons (list 'assoc-pair key val)
(cdr c))]
[else
(cons (car c)
(loop (cdr c)))])))
(cddr l))))))])
(let* ([new-plist (plist-replace
orig-plist
"CFBundleExecutable"
(path->string name)
"CFBundleSignature"
creator
"CFBundleIdentifier"
(format "org.plt-scheme.~a" (path->string name)))]
[new-plist (if file-types
(plist-replace
new-plist
"CFBundleDocumentTypes"
(cons 'array
(map (lambda (spec)
(cons
'dict
(map (lambda (p)
(list
'assoc-pair
(car p)
(cadr p)))
spec)))
file-types)))
new-plist)])
(call-with-output-file (build-path dest
"Contents"
"Info.plist")
(lambda (port)
(write-plist new-plist port))
'truncate)))
(call-with-output-file (build-path dest
"Contents"
"PkgInfo")
(lambda (port)
(fprintf port "APPL~a" creator))
'truncate)
(when resource-files
(for-each (lambda (p)
(let-values ([(base name dir?) (split-path p)])
(copy-file p (build-path dest
"Contents"
"Resources"
name))))
resource-files))
(build-path dest "Contents" "MacOS" name)))
(define (finish-osx-mred dest flags exec-name keep-exe?)
(call-with-output-file (build-path dest
"Contents"
"Resources"
"starter-info")
(lambda (port)
(write-plist
`(dict ,@(if keep-exe?
`((assoc-pair "executable name"
,(path->string exec-name)))
null)
(assoc-pair "stored arguments"
(array ,@flags)))
port))
'truncate))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Represent modules with lists starting with the filename, so we
;; can use assoc:
(define (make-mod normal-file-path normal-module-path code name prefix full-name relative-mappings)
(list normal-file-path normal-module-path code
name prefix full-name relative-mappings))
(define (mod-file m) (car m))
(define (mod-mod-path m) (cadr m))
(define (mod-code m) (caddr m))
(define (mod-name m) (list-ref m 3))
(define (mod-prefix m) (list-ref m 4))
(define (mod-full-name m) (list-ref m 5))
(define (mod-mappings m) (list-ref m 6))
(define (generate-prefix)
(format "#%embedded:~a:" (gensym)))
(define (normalize filename)
(simplify-path (expand-path filename)))
;; Loads module code, using .zo if there, compiling from .scm if not
(define (get-code filename module-path codes prefixes verbose?)
(when verbose?
(fprintf (current-error-port) "Getting ~s~n" filename))
(let ([a (assoc filename (unbox codes))])
(if a
;; Already have this module. Make sure that library-referenced
;; modules are consistently referenced through library paths:
(let ([found-lib? (and (pair? (mod-mod-path a))
(eq? 'lib (car (mod-mod-path a))))]
[look-lib? (and (pair? module-path)
(eq? 'lib (car module-path)))])
(cond
[(and found-lib? look-lib?)
'ok]
[(or found-lib? look-lib?)
(error 'find-module
"module referenced both as a library and through a path: ~a"
filename)]
[else 'ok]))
;; First use of the module. Get code and then get code for imports.
(let ([code (get-module-code filename)])
(let-values ([(imports fs-imports ft-imports) (module-compiled-imports code)])
(let ([name (let-values ([(base name dir?) (split-path filename)])
(path->string (path-replace-suffix name #"")))]
[prefix (let ([a (assoc filename prefixes)])
(if a
(cdr a)
(generate-prefix)))]
[all-file-imports (filter (lambda (x) (not (symbol? x)))
(append imports fs-imports ft-imports))])
(let ([sub-files (map (lambda (i) (normalize (resolve-module-path-index i filename)))
all-file-imports)]
[sub-paths (map (lambda (i) (collapse-module-path-index i module-path))
all-file-imports)])
;; Get code for imports:
(for-each (lambda (sub-filename sub-path)
(get-code sub-filename
sub-path
codes
prefixes
verbose?))
sub-files sub-paths)
;; Build up relative module resolutions, relative to this one,
;; that will be requested at run-time.
(let ([mappings (map (lambda (sub-i sub-filename)
(let-values ([(path base) (module-path-index-split sub-i)])
;; Assert: base should refer to this module:
(let-values ([(path2 base2) (module-path-index-split base)])
(when (or path2 base2)
(error 'embed "unexpected nested module path index")))
(let ([m (assoc sub-filename (unbox codes))])
(cons path (mod-full-name m)))))
all-file-imports sub-files)])
;; Record the module
(set-box! codes
(cons (make-mod filename module-path code
name prefix (string->symbol
(format "~a~a" prefix name))
mappings)
(unbox codes)))))))))))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (make-module-name-resolver code-l)
`(let ([orig (current-module-name-resolver)]
[ns (current-namespace)]
[mapping-table (quote
,(map
(lambda (m)
`(,(mod-full-name m)
,(mod-mappings m)))
code-l))]
[library-table (quote
,(filter values
(map (lambda (m)
(let ([path (mod-mod-path m)])
(if (and (pair? path)
(eq? 'lib (car path)))
(cons path (mod-full-name m))
#f)))
code-l)))])
(current-module-name-resolver
(lambda (name rel-to stx)
(if (or (not name)
(not (eq? (current-namespace) ns)))
;; a notification,or wrong namespace
(orig name rel-to stx)
;; Have a relative mapping?
(let ([a (assoc rel-to mapping-table)])
(if a
(let ([a2 (assoc name (cadr a))])
(if a2
(cdr a2)
(error 'embedding-module-name-resolver
"unexpected relative mapping request: ~e in ~e"
name rel-to)))
;; A library mapping that we have?
(let ([a3 (and (pair? name)
(eq? (car name) 'lib)
(ormap (lambda (lib-entry)
(with-handlers ([exn:fail? (lambda (x) #f)])
;; To check equality of library references,
;; we have to consider relative paths in the
;; filename part of the name.
(let loop ([a (build-path
(apply build-path
'same
(cddar lib-entry))
(cadar lib-entry))]
[b (build-path
(apply build-path
'same
(let ([d (cddr name)])
(if (null? d)
'("mzlib")
d)))
(cadr name))])
(if (equal? a b)
lib-entry
(let-values ([(abase aname d?) (split-path a)])
(if (eq? aname 'same)
(loop abase b)
(let-values ([(bbase bname a?) (split-path b)])
(if (eq? bname 'same)
(loop a bbase)
(if (equal? aname bname)
(loop abase bbase)
#f)))))))))
library-table))])
(if a3
;; Have it:
(cdr a3)
;; Let default handler try:
(orig name rel-to stx))))))))))
;; Write a module bundle that can be loaded with 'load' (do not embed it
;; into an executable). The bundle is written to the current output port.
(define (write-module-bundle verbose? modules literal-files literal-expression)
(let* ([module-paths (map cadr modules)]
[files (map
(lambda (mp)
(let ([f (resolve-module-path mp #f)])
(unless f
(error 'write-module-bundle "bad module path: ~e" mp))
(normalize f)))
module-paths)]
[collapsed-mps (map
(lambda (mp)
(collapse-module-path mp "."))
module-paths)]
[prefix-mapping (map (lambda (f m)
(cons f (let ([p (car m)])
(cond
[(symbol? p) (symbol->string p)]
[(eq? p #t) (generate-prefix)]
[(not p) ""]
[else (error
'write-module-bundle
"bad prefix: ~e"
p)]))))
files modules)]
;; Each element is created with `make-mod'.
;; As we descend the module tree, we append to the front after
;; loasing imports, so the list in the right order.
[codes (box null)])
(for-each (lambda (f mp) (get-code f mp codes prefix-mapping verbose?))
files
collapsed-mps)
;; Install a module name resolver that redirects
;; to the embedded modules
(write (make-module-name-resolver (unbox codes)))
(let ([l (unbox codes)])
(for-each
(lambda (nc)
(when verbose?
(fprintf (current-error-port) "Writing module from ~s~n" (mod-file nc)))
(write `(current-module-name-prefix ',(string->symbol (mod-prefix nc))))
(write (mod-code nc)))
l))
(write '(current-module-name-prefix #f))
(newline)
(for-each (lambda (f)
(when verbose?
(fprintf (current-error-port) "Copying from ~s~n" f))
(call-with-input-file*
f
(lambda (i)
(copy-port i (current-output-port)))))
literal-files)
(when literal-expression
(write literal-expression))))
;; Use `write-module-bundle', but figure out how to put it into an executable
(define make-embedding-executable
(opt-lambda (dest mred? verbose?
modules
literal-files literal-expression
cmdline
[aux null]
[launcher? #f]
[variant 'normal])
(define keep-exe? (and launcher?
(let ([m (assq 'forget-exe? aux)])
(or (not m)
(not (cdr m))))))
(define long-cmdline? (or (eq? (system-type) 'windows)
(and mred? (eq? 'macosx (system-type)))))
(unless (or long-cmdline?
((apply + (length cmdline) (map (lambda (s)
(bytes-length (string->bytes/utf-8 s)))
cmdline)) . < . 50))
(error 'make-embedding-executable "command line too long"))
(let ([exe (find-exe mred? variant)])
(when verbose?
(fprintf (current-error-port) "Copying to ~s~n" dest))
(let-values ([(dest-exe osx?)
(if (and mred? (eq? 'macosx (system-type)))
(values (prepare-macosx-mred exe dest aux variant) #t)
(begin
(when (or (file-exists? dest)
(directory-exists? dest)
(link-exists? dest))
;; Delete-file isn't enough if the target
;; is supposed to be a directory. But
;; currently, that happens only for MrEd
;; on Mac OS X, which is handles above.
(delete-file dest))
(copy-file exe dest)
(values dest #f)))])
(with-handlers ([void (lambda (x)
(if osx?
(when (directory-exists? dest)
(delete-directory/files dest))
(when (file-exists? dest)
(delete-file dest)))
(raise x))])
(let ([m (and (eq? 'macosx (system-type))
(assq 'framework-root aux))])
(when m
(for-each (lambda (p)
(system* "/usr/bin/install_name_tool"
"-change"
(format "~a.framework/Versions/~a/~a" p (version) p)
(format "~a~a.framework/Versions/~a/~a"
(cdr m)
p (version) p)
(let ([dest (if mred?
(let-values ([(base name dir?) (split-path dest)])
(build-path dest
"Contents" "MacOS"
(path-replace-suffix name #"")))
dest)])
(if (path? dest)
(path->string dest)
dest))))
(if mred?
'("PLT_MzScheme" "PLT_MrEd")
'("PLT_MzScheme")))))
(let ([start (file-size dest-exe)])
(with-output-to-file dest-exe
(lambda ()
(write-module-bundle verbose? modules literal-files literal-expression))
'append)
(let ([end (file-size dest-exe)])
(when verbose?
(fprintf (current-error-port) "Setting command line~n"))
(let ([start-s (number->string start)]
[end-s (number->string end)])
(let ([full-cmdline (append
(if launcher?
(if (and (eq? 'windows (system-type))
keep-exe?)
(list (path->string exe)) ; argv[0]
null)
(list "-k" start-s end-s))
cmdline)])
(if osx?
(finish-osx-mred dest full-cmdline exe keep-exe?)
(let ([cmdpos (with-input-from-file dest-exe
(lambda () (find-cmdline
"cmdline"
#"\\[Replace me for EXE hack")))]
[anotherpos (and mred?
(eq? 'windows (system-type))
(let ([m (assq 'single-instance? aux)])
(and m (not (cdr m))))
(with-input-from-file dest-exe
(lambda () (find-cmdline
"instance-check"
#"yes, please check for another"))))]
[out (open-output-file dest-exe 'update)])
(dynamic-wind
void
(lambda ()
(when anotherpos
(file-position out anotherpos)
(write-bytes #"no," out))
(if long-cmdline?
;; write cmdline at end:
(file-position out end)
(begin
;; write (short) cmdline in the normal position:
(file-position out cmdpos)
(display "!" out)))
(for-each
(lambda (s)
(fprintf out "~a~a~c"
(integer->integer-bytes
(add1 (bytes-length (string->bytes/utf-8 s)) )
4 #t #f)
s
#\000))
full-cmdline)
(display "\0\0\0\0" out)
(when long-cmdline?
;; cmdline written at the end;
;; now put forwarding information at the normal cmdline pos
(let ([new-end (file-position out)])
(file-position out cmdpos)
(fprintf out "~a...~a~a"
(if keep-exe? "*" "?")
(integer->integer-bytes end 4 #t #f)
(integer->integer-bytes (- new-end end) 4 #t #f)))))
(lambda ()
(close-output-port out)))
(let ([m (and (eq? 'windows (system-type))
(assq 'ico aux))])
(when m
(install-icon dest-exe (cdr m))))
(let ([m (and (eq? 'windows (system-type))
(assq 'subsystem aux))])
(when m
(set-subsystem dest-exe (cdr m))))))))))))))))))