Commit 0943954d authored by Ludovic Courtès's avatar Ludovic Courtès

Switch to upstream hpcguix-web.

See <https://github.com/UMCUGenetics/hpcguix-web>. * hpcweb-config.scm: New file. * nginx-locations.conf: Rewrite /browse URLs. Add 'location' blocks for /static. * browse: Remove.
parent 85069118
# Copyright © 2017 Roel Janssen <roel@gnu.org>
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program 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
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
include guile.am
moddir=$(prefix)/share/guile/site/2.2
godir=$(libdir)/guile/2.2/ccache
SOURCES = \
web-interface.scm \
www/config.scm \
www/util.scm \
www/pages.scm \
www/pages/package.scm \
www/pages/error.scm \
www/pages/browse.scm
WWW_STATIC_RESOURCES = \
static/css/main.css \
static/fonts/FiraMono-Regular.ttf \
static/fonts/Roboto-Bold.ttf \
static/fonts/Roboto-LightItalic.ttf \
static/fonts/Roboto-Light.ttf \
static/fonts/OFL.txt \
static/graphs/index.html \
static/images/g16.png \
static/images/grid.png \
static/images/grid.svg \
static/images/sort_asc.png \
static/images/sort_asc_disabled.png \
static/images/sort_both.png \
static/images/sort_desc.png \
static/images/logo.png \
static/images/cubes.png \
static/highlight/highlight.pack.js \
static/highlight/LICENSE \
static/highlight/styles/androidstudio.css \
static/highlight/styles/github.css \
static/datatables.min.css \
static/datatables.min.js
EXTRA_DIST += env.in $(WWW_STATIC_RESOURCES)
This diff is collapsed.
dnl Copyright © 2017 Roel Janssen <roel@gnu.org>
dnl
dnl This program is free software: you can redistribute it and/or modify
dnl it under the terms of the GNU General Public License as published by
dnl the Free Software Foundation, either version 3 of the License, or
dnl (at your option) any later version.
dnl
dnl This program is distributed in the hope that it will be useful,
dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
dnl GNU General Public License for more details.
dnl
dnl You should have received a copy of the GNU General Public License
dnl along with this program. If not, see <http://www.gnu.org/licenses/>.
dnl -*- Autoconf -*-
AC_INIT(hpcguix-web, 0.0.1)
AC_CONFIG_AUX_DIR([build-aux])
AM_INIT_AUTOMAKE([color-tests -Wall -Wno-portability foreign])
AM_SILENT_RULES([yes])
GUILE_PROGS
if $GUILE_TOOLS | grep -q compile; then
true
else
AC_MSG_ERROR([Guile 2.2 required.])
fi
AC_CONFIG_FILES([Makefile])
AC_CONFIG_FILES([pre-inst-env], [chmod +x pre-inst-env])
AC_OUTPUT
(use-modules (guix packages)
(guix licenses)
(guix build-system gnu)
(gnu packages autotools)
(gnu packages guile)
(gnu packages package-management))
(package
(name "hpcguix-web")
(version "0.1")
(source ".")
(build-system gnu-build-system)
(native-inputs
`(("autoconf" ,autoconf)
("automake" ,automake)))
(inputs
`(("guile" ,guile-2.2)
("guile-json" ,guile-json)
("guix" ,guix)))
(synopsis "Support web interface for GNU Guix on a cluster")
(description "This package provides a web interface for GNU Guix to
search packages, how-to instructions, examples and usage recommendations.")
(home-page "https://gitorious.org/guix-web/guix-web")
(license agpl3+))
GOBJECTS = $(SOURCES:%.scm=%.go)
nobase_mod_DATA = $(SOURCES) $(NOCOMP_SOURCES)
nobase_go_DATA = $(GOBJECTS)
# Make sure source files are installed first, so that the mtime of
# installed compiled files is greater than that of installed source
# files. See
# <http://lists.gnu.org/archive/html/guile-devel/2010-07/msg00125.html>
# for details.
guile_install_go_files = install-nobase_goDATA
$(guile_install_go_files): install-nobase_modDATA
CLEANFILES = $(GOBJECTS)
EXTRA_DIST = $(SOURCES) $(NOCOMP_SOURCES)
GUILE_WARNINGS = -Wunbound-variable -Warity-mismatch -Wformat
SUFFIXES = .scm .go
.scm.go:
$(AM_V_GEN)$(top_builddir)/pre-inst-env $(GUILE_TOOLS) compile $(GUILE_WARNINGS) -o "$@" "$<"
#!/bin/sh
# gwl - Workflow management extension for GNU Guix
# Copyright © 2014 David Thompson <davet@gnu.org>
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program 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
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
abs_top_srcdir="`cd "@abs_top_srcdir@" > /dev/null; pwd`"
abs_top_builddir="`cd "@abs_top_builddir@" > /dev/null; pwd`"
GUILE_LOAD_COMPILED_PATH="$abs_top_builddir${GUILE_LOAD_COMPILED_PATH:+:}$GUILE_LOAD_COMPILED_PATH"
GUILE_LOAD_PATH="$abs_top_builddir:$abs_top_srcdir${GUILE_LOAD_PATH:+:}:$GUILE_LOAD_PATH"
export GUILE_LOAD_COMPILED_PATH GUILE_LOAD_PATH
PATH="$abs_top_builddir/scripts:$PATH"
export PATH
exec "$@"
;;; Copyright © 2016, 2017 Roel Janssen <roel@gnu.org>
;;; Copyright © 2017 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This program is free software: you can redistribute it and/or
;;; modify it under the terms of the GNU Affero General Public License
;;; as published by the Free Software Foundation, either version 3 of
;;; the License, or (at your option) any later version.
;;;
;;; This program 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
;;; Affero General Public License for more details.
;;;
;;; You should have received a copy of the GNU Affero General Public
;;; License along with this program. If not, see
;;; <http://www.gnu.org/licenses/>.
(define-module (web-interface)
#:use-module (web server)
#:use-module (web request)
#:use-module (web response)
#:use-module (web uri)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 match)
#:use-module (rnrs bytevectors)
#:use-module (rnrs io ports)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-19)
#:use-module (sxml simple)
#:use-module (guix utils)
#:use-module (guix packages)
#:use-module (guix licenses)
#:use-module (gnu packages)
#:use-module (json)
#:use-module (www util)
#:use-module (www config)
#:use-module (www pages)
#:use-module (www pages error)
#:use-module (www pages package)
#:export (run-web-interface))
(define %package-blacklist '())
;; ----------------------------------------------------------------------------
;; HANDLERS
;; ----------------------------------------------------------------------------
;;
;; The way a request is handled varies upon the nature of the request. It can
;; be as simple as serving a pre-existing file, or as complex as finding a
;; Scheme module to use for handling the request.
;;
;; In this section, the different handlers are implemented.
;;
(define (request-packages-json-handler request)
(let* ((packages-file (string-append %www-root "/packages.json"))
(cache-timeout-file (string-append %www-root "/cache.timeout"))
(cache-timeout-exists? (access? cache-timeout-file F_OK)))
;; Write the packages JSON to disk to speed up the page load.
;; This caching mechanism prevents new packages from propagating
;; into the search. For this, we can manually create a file
;; "cache.timeout" in the %www-root.
(when (or (not (access? packages-file F_OK))
cache-timeout-exists?)
(let ((all-packages (fold-packages cons '()))
(package->json (lambda (package)
(json (object
("name" ,(package-name package))
("version" ,(package-version package))
("synopsis" ,(package-synopsis package))
("homepage" ,(package-home-page package)))))))
(with-atomic-file-output packages-file
(lambda (port)
(scm->json (map package->json
(remove (lambda (package)
(member (package-name package)
%package-blacklist))
all-packages))
port)))
(when cache-timeout-exists?
(delete-file cache-timeout-file))))
(request-file-handler request)))
(define (request-file-handler request)
"This handler takes data from a file and sends that as a response."
(define (response-content-type path)
"This function returns the content type of a file based on its extension."
(let ((extension (substring path (1+ (string-rindex path #\.)))))
(cond [(string= extension "css") '(text/css)]
[(string= extension "js") '(application/javascript)]
[(string= extension "json") '(application/javascript)]
[(string= extension "html") '(text/html)]
[(string= extension "png") '(image/png)]
[(string= extension "svg") '(image/svg+xml)]
[(string= extension "ico") '(image/x-icon)]
[(string= extension "pdf") '(application/pdf)]
[(string= extension "ttf") '(application/font-sfnt)]
[(#t '(text/plain))])))
(define path
(uri-path (request-uri request)))
(let* ((full-path (string-append %www-root "/" path))
(file-stat (stat full-path #f))
(modified (and file-stat
(make-time time-utc
0 (stat:mtime file-stat)))))
(define (send-file)
;; Do not handle files larger than %maximum-file-size.
;; Please increase the file size if your server can handle it.
(if (> (stat:size file-stat) %www-max-file-size)
(values `((content-type . (text/html))
(last-modified . ,(time-utc->date modified)))
(with-output-to-string
(lambda _ (sxml->xml (page-error-filesize path)))))
(values `((content-type . ,(response-content-type full-path))
(last-modified . ,(time-utc->date modified)))
(with-input-from-file full-path
(lambda _
(get-bytevector-all (current-input-port)))))))
(cond ((not file-stat)
(values '((content-type . (text/html)))
(with-output-to-string
(lambda _
(sxml->xml (page-error-404 path))))))
((assoc-ref (request-headers request) 'if-modified-since)
=>
(lambda (client-date)
;; For /packages.json, which is quite big, it's a good idea to
;; honor 'If-Modified-Since'.
(if (time>? modified (date->time-utc client-date))
(send-file)
(values (build-response #:code 304) ;"Not Modified"
#f))))
(else
(send-file)))))
(define (request-package-handler request-path)
(values '((content-type . (text/html)))
(call-with-output-string
(lambda (port)
(sxml->xml (page-package request-path) port)))))
(define (request-scheme-page-handler request request-body request-path)
(define (module-path prefix elements)
"Returns the module path so it can be loaded."
(if (> (length elements) 1)
(module-path
(append prefix (list (string->symbol (car elements))))
(cdr elements))
(append prefix (list (string->symbol (car elements))))))
(values '((content-type . (text/html)))
(call-with-output-string
(lambda (port)
(set-port-encoding! port "utf8")
(format port "<!DOCTYPE html>~%")
(let* ((function-symbol (string->symbol
(string-map
(lambda (x)
(if (eq? x #\/) #\- x))
(substring request-path 1))))
(module (resolve-module
(module-path
'(www pages)
(string-split (substring request-path 1) #\/))
#:ensure #f))
(page-symbol (symbol-append 'page- function-symbol)))
(if module
(let ((display-function
(module-ref module page-symbol)))
(if (eq? (request-method request) 'POST)
(sxml->xml (display-function
request-path
#:post-data
(utf8->string
request-body)) port)
(sxml->xml (display-function request-path) port)))
(sxml->xml (page-error-404 request-path) port)))))))
;; ----------------------------------------------------------------------------
;; ROUTING & HANDLERS
;; ----------------------------------------------------------------------------
;;
;; Requests can have different handlers.
;; * Static objects (images, stylesheet, javascript files) have their own
;; handler.
;; * Package pages are generated dynamically, so they have their own handler.
;; * The 'regular' Scheme pages have their own handler that resolves the
;; module dynamically.
;;
;; Feel free to add your own handler whenever that is necessary.
;;
;; ----------------------------------------------------------------------------
(define (request-handler request request-body)
(let ((request-path (uri-path (request-uri request))))
(format #t "~a ~a~%" (request-method request) request-path)
(cond
((string= request-path "/packages.json")
(request-packages-json-handler request))
((and (> (string-length request-path) 7)
(string= (string-take request-path 8) "/static/"))
(request-file-handler request))
((and (> (string-length request-path) 8)
(string= (string-take request-path 9) "/package/"))
(request-package-handler request-path))
(else
(request-scheme-page-handler request request-body request-path)))))
;; ----------------------------------------------------------------------------
;; RUNNER
;; ----------------------------------------------------------------------------
;;
;; This code runs the web server.
(define (run-web-interface)
(run-server request-handler 'http
`(#:port ,%www-listen-port
#:addr ,INADDR_ANY)))
(run-web-interface)
;;; Copyright © 2016, 2017 Roel Janssen <roel@gnu.org>
;;;
;;; This program is free software: you can redistribute it and/or
;;; modify it under the terms of the GNU Affero General Public License
;;; as published by the Free Software Foundation, either version 3 of
;;; the License, or (at your option) any later version.
;;;
;;; This program 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
;;; Affero General Public License for more details.
;;;
;;; You should have received a copy of the GNU Affero General Public
;;; License along with this program. If not, see
;;; <http://www.gnu.org/licenses/>.
(define-module (www config)
#:export (%www-root
%www-max-file-size
%www-listen-port
%www-static-root))
(define %www-root (string-append (dirname (search-path %load-path "web-interface.scm")) "/.."))
(define %www-static-root (string-append %www-root "/static"))
(define %www-max-file-size 250000000)
(define %www-listen-port 5000)
;;; Copyright © 2016, 2017 Roel Janssen <roel@gnu.org>
;;;
;;; This program is free software: you can redistribute it and/or
;;; modify it under the terms of the GNU Affero General Public License
;;; as published by the Free Software Foundation, either version 3 of
;;; the License, or (at your option) any later version.
;;;
;;; This program 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
;;; Affero General Public License for more details.
;;;
;;; You should have received a copy of the GNU Affero General Public
;;; License along with this program. If not, see
;;; <http://www.gnu.org/licenses/>.
(define-module (www pages)
#:use-module (srfi srfi-1)
#:export (page-root-template))
(define page-title-prefix "hpcguix | ")
(define pages
'(("/about" "About")
("/browse" "Browse")
("/blog" "Blog")
("/blog/feed.xml" "Atom feed" "/static/images/feed.png")))
(define (page-partial-main-menu request-path)
`(ul
,(map
(lambda (item)
(cond
;((string= (substring (car item) 1) (car (string-split (substring request-path 1) #\/)))
; `(li (@ (class "active")) ,(cadr item)))
((and (string= "package" (car (string-split (substring request-path 1) #\/)))
(string= (car item) "/browse"))
`(li (@ (class "active"))
(a (@ (href "/")
(onclick "history.go(-1); return false;"))
"← back to search")))
(else
(if (> (length item) 2)
`(li (a (@ (href ,(car item)))
(img (@ (alt ,(cadr item))
(src ,(list-ref item 2))))))
`(li (a (@ (href ,(car item))) ,(cadr item)))))))
pages)))
(define* (page-root-template title request-path content-tree #:key (dependencies '(test)))
`((html (@ (lang "en"))
(head
(title ,(string-append page-title-prefix title))
(meta (@ (http-equiv "Content-Type") (content "text/html; charset=utf-8")))
(link (@ (rel "icon")
(type "image/x-icon")
(href "/static/images/favicon.png")))
,(if (memq 'highlight dependencies)
`((link (@ (rel "stylesheet") (href "/static/highlight/styles/github.css")))
(script (@ (src "/static/highlight/highlight.pack.js")) "")
(script "hljs.initHighlightingOnLoad();"))
`())
,(if (memq 'datatables dependencies)
`((link (@ (rel "stylesheet") (type "text/css") (href "/static/datatables.min.css")))
(script (@ (type "text/javascript") (src "/static/datatables.min.js")) ""))
`())
(link (@ (rel "stylesheet")
(href "/static/css/main.css")
(type "text/css")
(media "screen"))))
(body
(div (@ (id "header"))
(div (@ (id "header-inner")
(class "width-control"))
(a (@ (href "/"))
(img (@ (class "logo")
(src "/static/images/logo-small.png")
(alt "GuixHPC"))))
(div (@ (class "baseline"))
"Reproducible software deployment for high-performance computing.")))
(div (@ (id "menubar")
(class "width-control"))
,(page-partial-main-menu request-path))
(div (@ (id "content")
(class "width-control"))
,content-tree)
(div (@ (id "footer-box")
(class "width-control"))
(p "Made with λ by the GNU Guix community — Copyright © 2017. "
(a (@ (href "https://github.com/UMCUGenetics/hpcguix-web"))
"Download the source code of this page") "."))))))
;;; Copyright © 2017 Roel Janssen <roel@gnu.org>
;;;
;;; This program is free software: you can redistribute it and/or
;;; modify it under the terms of the GNU Affero General Public License
;;; as published by the Free Software Foundation, either version 3 of
;;; the License, or (at your option) any later version.
;;;
;;; This program 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
;;; Affero General Public License for more details.
;;;
;;; You should have received a copy of the GNU Affero General Public
;;; License along with this program. If not, see
;;; <http://www.gnu.org/licenses/>.
(define-module (www pages browse)
#:use-module (www pages)
#:export (page-welcome))
(define (page-browse request-path)
(page-root-template "Browse" request-path
`((h2 "Find software packages")
(form
(input (@ (type "search")
(id "search-field")
(class "search-field")
(aria-controls "packages-table")
(placeholder "Search"))))
(hr)
(div (@ (id "stand-by")) (p "Please wait for the package data to load..."))
(div (@ (id "packages-table-wrapper"))
(table (@ (id "packages-table")
(class "display"))
(thead
(tr
(th "Name")
(th "Version")
(th "Synopsis")
(th (@ (style "width: 250pt")) "Homepage")))))
(script (@ (type "text/javascript")) "
function feed_table(packages) {
var dt = $('#packages-table').DataTable({
sDom: 'lrtip',
data: packages,
processing: true,
createdRow: function (row, data, index) {
$('#stand-by').hide();
$('#packages-table').show();
},
columns: [
{ data: 'name',
mRender: function (data, type, full) {
return '" (a (@ (href "package/' + data + '")) "' + data + '") "';
}
},
{ data: 'version' },
{ data: 'synopsis' },
{ data: 'homepage',
mRender: function (data, type, full) {
return '" (a (@ (href "' + data + '")) "' + data + '") "';
}
}
]});
$('#search-field').on('keyup', function() {
dt.search(this.value).draw();
});
$('#search-field').on('keydown', function(event) {
if (event.which == 13) return false;
});
}
$(document).ready(function() {
$('#packages-table').hide();
$.getJSON('/packages.json', feed_table);
});
"))
#:dependencies '(datatables)))
;;; Copyright © 2016, 2017 Roel Janssen <roel@gnu.org>
;;;
;;; This program is free software: you can redistribute it and/or
;;; modify it under the terms of the GNU Affero General Public License
;;; as published by the Free Software Foundation, either version 3 of
;;; the License, or (at your option) any later version.
;;;
;;; This program 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
;;; Affero General Public License for more details.
;;;
;;; You should have received a copy of the GNU Affero General Public
;;; License along with this program. If not, see
;;; <http://www.gnu.org/licenses/>.
(define-module (www pages error)
#:use-module (www pages)
#:use-module (www config)
#:export (page-error-404
page-error-filesize
page-error))
(define (page-error-404 request-path)
(page-root-template "Oops!" request-path
`(p "The page you tried to reach cannot be found.")))
(define (page-error-filesize request-path)
(page-root-template "Oops!" request-path
`(p ,(format #f "The maximum file size has been set to ~a megabytes."
(/ %www-max-file-size 1000000)))))
(define page-error page-error-404)
;;; Copyright © 2016, 2017 Roel Janssen <roel@gnu.org>
;;;
;;; This program is free software: you can redistribute it and/or
;;; modify it under the terms of the GNU Affero General Public License
;;; as published by the Free Software Foundation, either version 3 of
;;; the License, or (at your option) any later version.
;;;
;;; This program 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
;;; Affero General Public License for more details.
;;;
;;; You should have received a copy of the GNU Affero General Public
;;; License along with this program. If not, see
;;; <http://www.gnu.org/licenses/>.
(define-module (www pages package)
#:use-module (www pages)
#:use-module (www config)
#:use-module (gnu packages)
#:use-module (guix packages)
#:use-module (guix utils)
#:use-module (guix discovery)
#:use-module (guix memoization)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 match)
#:use-module (ice-9 control)
#:use-module (texinfo)
#:use-module (texinfo html)
#:export (page-package))
(define package->variable-name
(mlambdaq (package)
"Return the name of the variable that defines PACKAGE, a package object,
or #f if we failed to find it."
(let/ec return
(let loop ((modules (all-modules (%package-module-path))))
(match modules
(() #f)
((module . rest)
(module-map (lambda (symbol variable)
(let ((value (false-if-exception
(variable-ref variable))))
(and (eq? value package)
(return symbol))))
module)
(loop rest)))))))
(define (package-description-shtml package)
"Return an SXML representation of PACKAGE description field with HTML
vocabulary."
;; 'texi-fragment->stexi' uses 'call-with-input-string', so make sure
;; those string ports are Unicode-capable.
(with-fluids ((%default-port-encoding "UTF-8"))
(and=> (package-description package)
(compose stexi->shtml texi-fragment->stexi))))
(define (page-package request-path)
(let* ((name (list-ref (string-split request-path #\/) 2))
(packages (find-packages-by-name name)))
(if (eqv? packages '())
(page-root-template "Oops!" request-path
`((h2 "Uh-oh...")
(p "The package is gone!")))
(page-root-template (string-append "Details for " name) request-path
`((h2 "Package details of " (code (@ (class "h2-title")) ,name))
(p ,(package-description-shtml (car packages)))
(p "There " ,(if (> (length packages) 1) "are " "is ") ,(length packages) " version"
,(if (> (length packages) 1) "s" "") " available for this package.")
(hr)
,(map (lambda (instance)
(let ((location (package-location instance)))
`((table (@ (style "width: 100%"))
(tr
(td (strong "Version"))
(td ,(package-version instance)))
(tr
(td (strong "Defined at"))
(td (code (@ (class "nobg"))
,(string-append (location-file location) ":"
(number->string
(location-line location))))))
(tr
(td (strong "Symbol name"))
(td (code (@ (class "nobg"))
,(package->variable-name instance))))
(tr
(td (@ (style "width: 150pt")) (strong "Installation command"))
(td (pre (code (@ (class "bash"))
(string-append "guix package -i "
,name ,(if (> (length packages) 1)
(string-append
"@" (package-version instance)) ""))))))
(tr
(td (strong "Homepage"))
(td (a (@ (href ,(package-home-page instance))) ,(package-home-page instance)))))
(hr))))
packages)