388 lines
17 KiB
Scheme
388 lines
17 KiB
Scheme
(module util mzscheme
|
|
|
|
(require "config.ss"
|
|
"planet-archives.ss"
|
|
|
|
"private/planet-shared.ss"
|
|
"private/linkage.ss"
|
|
"resolver.ss"
|
|
(lib "url.ss" "net")
|
|
(lib "xml.ss" "xml")
|
|
(lib "contract.ss")
|
|
(lib "file.ss")
|
|
(lib "list.ss")
|
|
(lib "pack.ss" "setup")
|
|
(lib "plt-single-installer.ss" "setup")
|
|
(lib "getinfo.ss" "setup"))
|
|
|
|
#| The util collection provides a number of useful functions for interacting with the PLaneT system. |#
|
|
|
|
(provide
|
|
|
|
current-cache-contents
|
|
current-linkage
|
|
make-planet-archive
|
|
get-installed-planet-archives
|
|
get-hard-linked-packages
|
|
remove-pkg
|
|
unlink-all
|
|
lookup-package-by-keys
|
|
|
|
resolve-planet-path)
|
|
|
|
(provide/contract
|
|
[download/install-pkg
|
|
(-> string? string? natural-number/c natural-number/c (union pkg? false/c))]
|
|
[add-hard-link
|
|
(-> string? string? natural-number/c natural-number/c path? void?)]
|
|
[remove-hard-link
|
|
(-> string? string? natural-number/c natural-number/c void?)]
|
|
[erase-pkg
|
|
(-> string? string? natural-number/c natural-number/c boolean?)])
|
|
|
|
;; download/install-pkg : string string nat nat -> pkg | #f
|
|
(define (download/install-pkg owner name maj min)
|
|
(let* ([pspec (pkg-spec->full-pkg-spec (list owner name maj min) #f)]
|
|
[upkg (get-package-from-server pspec)])
|
|
(cond
|
|
[(uninstalled-pkg? upkg)
|
|
(pkg-promise->pkg upkg)]
|
|
[else #f])))
|
|
|
|
;; current-cache-contents : -> ((string ((string ((nat (nat ...)) ...)) ...)) ...)
|
|
;; returns the packages installed in the local PLaneT cache
|
|
(define (current-cache-contents)
|
|
(cdr (tree->list (repository-tree))))
|
|
|
|
;; just so it will be provided
|
|
(define unlink-all remove-all-linkage!)
|
|
|
|
;; to remove:
|
|
;; -- setup-plt -c the package
|
|
;; -- remove relevant infodomain cache entries
|
|
;; -- delete files from cache directory
|
|
;; -- remove any existing linkage for package
|
|
;; returns #t if the removal worked; #f if no package existed.
|
|
(define (remove-pkg owner name maj min)
|
|
(let ((p (get-installed-package owner name maj min)))
|
|
(and p
|
|
(let ((path (pkg-path p)))
|
|
(with-logging
|
|
(LOG-FILE)
|
|
(lambda ()
|
|
(printf "\n============= Removing ~a =============\n" (list owner name maj min))
|
|
(clean-planet-package path (list owner name '() maj min))))
|
|
(erase-metadata p)
|
|
(delete-directory/files path)
|
|
(trim-directory (CACHE-DIR) path)
|
|
#t))))
|
|
|
|
;; erase-metadata : pkg -> void
|
|
;; clears out any references to the given package in planet's metadata files
|
|
;; (i.e., linkage and info.ss cache; not hard links which are not considered metadata)
|
|
(define (erase-metadata p)
|
|
(remove-infodomain-entries (pkg-path p))
|
|
(remove-linkage-to! p))
|
|
|
|
;; this really should go somewhere else. But what should setup's behavior be
|
|
;; when a package is cleaned? should it clear info-domain entries out? I think
|
|
;; no; an uncompiled package isn't necessarily not to be indexed and so on.
|
|
;; remove-infodomain-entries : path -> void
|
|
(define (remove-infodomain-entries path)
|
|
(let* ([pathbytes (path->bytes path)]
|
|
[cache-file (build-path (PLANET-DIR) "cache.ss")])
|
|
(when (file-exists? cache-file)
|
|
(let ([cache-lines (with-input-from-file cache-file read)])
|
|
(with-output-to-file cache-file
|
|
(lambda ()
|
|
(if (pair? cache-lines)
|
|
(write (filter (lambda (line) (not (and (pair? line) (equal? (car line) pathbytes)))) cache-lines))
|
|
(printf "\n")))
|
|
'truncate/replace)))))
|
|
|
|
(define (erase-pkg owner name maj min)
|
|
(let* ([uninstalled-pkg-dir
|
|
(build-path (UNINSTALLED-PACKAGE-CACHE) owner name (number->string maj) (number->string min))]
|
|
[uninstalled-pkg-file (build-path uninstalled-pkg-dir name)])
|
|
(let ([removed-something? (remove-pkg owner name maj min)]
|
|
[erased-something?
|
|
(if (file-exists? uninstalled-pkg-file)
|
|
(begin
|
|
(delete-file uninstalled-pkg-file)
|
|
(trim-directory (UNINSTALLED-PACKAGE-CACHE) uninstalled-pkg-dir)
|
|
#t)
|
|
#f)])
|
|
(or removed-something? erased-something?))))
|
|
|
|
;; listof X * listof X -> nonempty listof X
|
|
;; returns de-prefixed version of l2 if l1 is a proper prefix of l2;
|
|
;; signals an error otherwise.
|
|
(define (drop-common-base list1 list2)
|
|
(let loop ((l1 list1) (l2 list2))
|
|
(cond
|
|
[(null? l2)
|
|
(error 'drop-common-base "root ~s is not a prefix of stem ~s" list1 list2)]
|
|
[(null? l1) l2]
|
|
[(not (equal? (car l1) (car l2)))
|
|
(error 'drop-common-base "root ~s is not a prefix of stem ~s" list1 list2)]
|
|
[else (loop (cdr l1) (cdr l2))])))
|
|
|
|
;; pathify-list : path (listof path) -> listof path
|
|
;; given a base and a list of names, interprets each name as a subdirectory
|
|
;; of the previous, starting with base, and returns a list. (This list
|
|
;; is in reverse order, so the deepest subdirectory is returned first)
|
|
(define (pathify-list root dirs)
|
|
(let loop ((base root) (dirs dirs) (acc '()))
|
|
(cond
|
|
[(null? dirs) acc]
|
|
[else
|
|
(let ((new (build-path base (car dirs))))
|
|
(loop new (cdr dirs) (cons new acc)))])))
|
|
|
|
;; directory-empty? path -> bool
|
|
;; #t iff the given directory contains no subdirectories of files
|
|
(define (directory-empty? dir)
|
|
(null? (directory-list dir)))
|
|
|
|
;; trim-directory path path -> void
|
|
;; deletes nonempty directories starting with stem and working down to root
|
|
(define (trim-directory root stem)
|
|
(let* ([rootl (explode-path root)]
|
|
[steml (explode-path stem)]
|
|
[extras (cdr (pathify-list root (drop-common-base rootl steml)))])
|
|
(let loop ((dirs extras))
|
|
(cond
|
|
[(null? dirs) (void)]
|
|
[(directory-empty? (car dirs))
|
|
(delete-directory (car dirs))
|
|
(loop (cdr dirs))]
|
|
[else (void)]))))
|
|
|
|
;; current-linkage : -> ((symbol (package-name nat nat) ...) ...)
|
|
;; gives the current "linkage table"; a table that links modules to particular versions
|
|
;; of planet requires that satisfy those linkages
|
|
(define (current-linkage)
|
|
(let* ((links
|
|
(if (file-exists? (LINKAGE-FILE))
|
|
(with-input-from-file (LINKAGE-FILE) read-all)
|
|
'()))
|
|
(buckets (categorize caar links)))
|
|
(map
|
|
(lambda (x) (cons (car x) (map (lambda (y) (drop-last (cadr y))) (cdr x))))
|
|
buckets)))
|
|
|
|
;; regexp->filter : (string | regexp) -> (path -> bool)
|
|
;; computes a filter that accepts paths that match the given regexps and rejects other paths
|
|
(define (regexp->filter re-s)
|
|
(let ([re (cond
|
|
[(string? re-s) (regexp re-s)]
|
|
[(regexp? re-s) re-s]
|
|
[else (error 'regexp->filter "not a regular expression")])])
|
|
(lambda (p) (regexp-match re (path->bytes p)))))
|
|
|
|
|
|
;; make-planet-archive: directory [file] -> file
|
|
;; Makes a .plt archive file suitable for PLaneT whose contents are
|
|
;; all files in the given directory and returns that file's name.
|
|
;; If the optional filename argument is provided, that filename will
|
|
;; be used as the output file's name.
|
|
(define make-planet-archive
|
|
(case-lambda
|
|
[(dir)
|
|
(let-values ([(path name must-be-dir?) (split-path dir)])
|
|
(make-planet-archive
|
|
dir
|
|
(build-path (normalize-path (current-directory))
|
|
(string-append (path->string name) ".plt"))))]
|
|
[(dir archive-name)
|
|
(parameterize ((current-directory dir))
|
|
(let ([announcements '()]
|
|
[warnings '()]
|
|
[critical-errors '()])
|
|
(check-info.ss-sanity
|
|
dir
|
|
(λ (msg . args) (set! announcements (cons (apply format msg args) announcements)))
|
|
(λ (bad) (set! warnings (cons bad warnings)))
|
|
(λ (err) (set! critical-errors (cons err critical-errors))))
|
|
|
|
(unless (null? critical-errors) (error '|PLaneT packager| "~a Refusing to continue packaging." (car critical-errors)))
|
|
|
|
(pack archive-name
|
|
"archive"
|
|
(list ".")
|
|
null
|
|
(if (PLANET-ARCHIVE-FILTER)
|
|
(regexp->filter (PLANET-ARCHIVE-FILTER))
|
|
std-filter)
|
|
#t
|
|
'file
|
|
#f
|
|
#f)
|
|
|
|
(for-each display (reverse announcements))
|
|
(newline)
|
|
(for-each
|
|
(λ (s) (fprintf (current-error-port) "WARNING:\n\t~a\n" s))
|
|
(reverse warnings)))
|
|
|
|
(normalize-path archive-name))]))
|
|
|
|
;; check-info.ss-sanity : path (string -> void) (string -> void) (string -> void) -> void
|
|
;; gets all the info.ss fields that planet will use (using the info.ss file
|
|
;; from the current directory) and calls the announce, warn, and fail functions with strings
|
|
;; that describe how PLaneT sees the info.ss file. NOTA BENE: if this function calls fail, it may
|
|
;; also warn on the same field, and the warning may not make sense. This is based on the
|
|
;; assumption that errors will be turned into some kind of critical failure that obliterates
|
|
;; all the other information produced.
|
|
(define (check-info.ss-sanity dir announce warn fail)
|
|
(with-handlers ([exn:fail:read?
|
|
(λ (e) (fail (format "Package has an unreadable info.ss file. ~a" (exn-message e))))]
|
|
[exn:fail:syntax?
|
|
(λ (e) (fail (format "Package's info.ss has an syntactically ill-formed info.ss file: ~a" (exn-message e))))])
|
|
(let ([i* (get-info/full dir)])
|
|
(cond
|
|
[(not i*)
|
|
(warn "Package has no info.ss file. This means it will not have a description or documentation on the PLaneT web site.")]
|
|
[else
|
|
(let ([i (λ (field) (i* field (λ () #f)))])
|
|
(checkinfo i fail
|
|
[name ; field name
|
|
string? ; check
|
|
(announce "Name: ~a\n" name) ; success action
|
|
(warn "Package's info.ss file has no name field. Without a name, PLT Scheme will not compile your package.") ;failure action
|
|
]
|
|
[blurb
|
|
(λ (b) (and (list? b) (andmap xexpr? b)))
|
|
(announce "Blurb: ~s\n" blurb)
|
|
(unless blurb
|
|
(warn "Package's info.ss does not contain a blurb field. Without a blurb field, the package will have no description on planet.plt-scheme.org."))]
|
|
[categories
|
|
(λ (s) (and (list? s) (andmap symbol? s)))
|
|
(cond
|
|
[(ormap illegal-category categories)
|
|
=>
|
|
(λ (bad-cat)
|
|
(fail (format "Package's info.ss file contains illegal category \"~a\". The legal categories are: ~a\n"
|
|
bad-cat
|
|
legal-categories)))]
|
|
[else (announce "Categories: ~a\n" categories)])
|
|
(unless categories
|
|
(warn "Package's info.ss file does not contain a category listing. It will be placed in the Miscellaneous category."))]
|
|
[doc.txt
|
|
string?
|
|
(announce "doc.txt file: ~a\n" doc.txt)
|
|
(unless doc.txt
|
|
(warn "Package's info.ss does not contain a doc.txt entry. Without a doc.txt entry, the package will not have any documentation on planet.plt-scheme.org."))]
|
|
[html-docs
|
|
(lambda (s) (and (list? s) (andmap string? s)))
|
|
(announce "HTML documentation: yes\n")]
|
|
[homepage
|
|
string?
|
|
(cond
|
|
[(url-string? homepage)
|
|
(announce "Home page: ~a\n" homepage)]
|
|
[else
|
|
(fail (format "The value of the package's info.ss homepage field, ~s, does not appear to be a legal URL." homepage))])]
|
|
[primary-file
|
|
string?
|
|
(begin
|
|
(unless (file-in-current-directory? primary-file)
|
|
(warn (format "Package's info.ss primary-file field is ~s, a file that does not exist in the package." primary-file)))
|
|
(announce "Primary file: ~a\n" primary-file))
|
|
(unless primary-file
|
|
(warn "Package's info.ss does not contain a primary-file field. The package's listing on planet.plt-scheme.org will not have a valid require line for your package."))]
|
|
[required-core-version
|
|
core-version?
|
|
(announce "Required mzscheme version: ~a\n" required-core-version)]
|
|
[version
|
|
string?
|
|
(announce "Version description: ~a\n" version)]))]))))
|
|
|
|
;; legal-categories : (listof symbol)
|
|
(define legal-categories
|
|
'(devtools net media xml datastructures io scientific
|
|
system ui metaprogramming planet misc))
|
|
|
|
;; legal-category : symbol -> boolean
|
|
;; determine if the given symbol is a legal category
|
|
(define (legal-category? x) (memq x legal-categories))
|
|
|
|
;; illegal-category : symbol -> (union symbol false)
|
|
;; returns #f if the symbol is a legal category, or the symbol itself if it isn't
|
|
(define (illegal-category s) (if (legal-category? s) #f s))
|
|
|
|
;; url-string? : string -> boolean
|
|
;; determines if the given string is a reasonable homepage url
|
|
(define (url-string? s)
|
|
(and (string? s)
|
|
(let ([u (string->url s)])
|
|
(and (url-scheme u)
|
|
(url-host u)))))
|
|
|
|
;; file-in-current-directory? : string -> boolean
|
|
;; determines if the given string represents a file in the current directory
|
|
(define (file-in-current-directory? f)
|
|
(and (string? f) (file-exists? f)))
|
|
|
|
;; core-version : string -> boolean
|
|
;; determines if the given string is something that (version) could've produced
|
|
(define (core-version? s)
|
|
(and (string? s)
|
|
(regexp-match #rx"^[0-9]+(\\.[0-9]*)?$" s)))
|
|
|
|
;; checkinfo: syntax
|
|
;; given an info.ss function, a failure function, and a bunch of fields to check,
|
|
;; goes through the checklist calling either the success or the failure branch
|
|
;; of each check as appropriate
|
|
(define-syntax checkinfo
|
|
(syntax-rules ()
|
|
[(checkinfo fn fail clauses ...)
|
|
(let ([fn* fn] [fail* fail])
|
|
(checkinfo* () fn* fail* clauses ...))]))
|
|
|
|
(define-syntax checkinfo*
|
|
(syntax-rules ()
|
|
[(checkinfo* () fn fail) (void)]
|
|
[(checkinfo* (handler1 handler ...) fn fail) (begin handler1 handler ...)]
|
|
[(checkinfo* (handler ...) fn fail [id check on-success] clauses ...)
|
|
(checkinfo* (handler ...) fn fail [id check on-success void] clauses ...)]
|
|
[(checkinfo* (handler ...) fn fail [id check on-success on-fail] clauses ...)
|
|
(checkinfo*
|
|
(handler ...
|
|
(let ([id (fn 'id)])
|
|
(cond
|
|
[id
|
|
(let ([checked (check id)])
|
|
(unless checked
|
|
on-fail
|
|
(fail (format "Package's info.ss contained a malformed ~a field." 'id)))
|
|
on-success)]
|
|
[else on-fail])))
|
|
fn fail clauses ...)]))
|
|
|
|
;; ============================================================
|
|
;; HARD LINKS (aka development links)
|
|
|
|
;; add-hard-link : string string num num path -> void
|
|
;; adds an entry in the hard-links table associating the given
|
|
;; require spec to the given path
|
|
(define (add-hard-link owner pkg-name maj min path)
|
|
(unless (directory-exists? path)
|
|
(if (file-exists? path)
|
|
(error 'add-hard-link "Hard links must point to directories, not files")
|
|
(fprintf (current-error-port)
|
|
"Warning: directory ~a does not exist\n"
|
|
(path->string path))))
|
|
(add-hard-link! pkg-name (list owner) maj min path))
|
|
|
|
;; remove-hard-link : string string num num -> void
|
|
;; removes any development association from the given package spec
|
|
(define (remove-hard-link owner pkg-name maj min)
|
|
(filter-link-table!
|
|
(lambda (row)
|
|
(not (points-to? row pkg-name (list owner) maj min)))
|
|
(lambda (row)
|
|
(let ([p (row->package row)])
|
|
(when p
|
|
(erase-metadata p)))))))
|